From 79671a8acd379aef525be391b3bbb60824596b8d Mon Sep 17 00:00:00 2001 From: Jake Albaugh Date: Fri, 14 Jun 2024 18:05:07 -0500 Subject: [PATCH] cards --- figma.config.json | 7 +- src/compositions/Cards/Cards.figma.tsx | 15 ++- src/compositions/Cards/Cards.tsx | 146 +++++++++++++++++---- src/compositions/Cards/cards.css | 23 ++-- src/layout/Flex/Flex.tsx | 12 +- src/layout/Flex/flex.css | 2 +- src/stories/compositions/Cards.stories.tsx | 110 +++++++++++++++- src/ui/Text/text.css | 2 +- 8 files changed, 264 insertions(+), 53 deletions(-) diff --git a/figma.config.json b/figma.config.json index 6e779be..7ced384 100644 --- a/figma.config.json +++ b/figma.config.json @@ -90,7 +90,12 @@ "": "https://figma.com/design/J0KLPKXiONDRssXD1AX9Oi?node-id=2153-7834", "": "https://figma.com/design/J0KLPKXiONDRssXD1AX9Oi?node-id=2153-7838", "": "https://figma.com/design/J0KLPKXiONDRssXD1AX9Oi?node-id=9762:1135", - "": "https://figma.com/design/J0KLPKXiONDRssXD1AX9Oi?node-id=9762-3088" + "": "https://figma.com/design/J0KLPKXiONDRssXD1AX9Oi?node-id=9762-3088", + "": "https://www.figma.com/design/J0KLPKXiONDRssXD1AX9Oi/SDS?node-id=7753-4465&m=dev", + "": "https://www.figma.com/design/J0KLPKXiONDRssXD1AX9Oi/SDS?node-id=7722-3736&t=zlze7QSzfpWykbLu-11", + "": "https://www.figma.com/design/J0KLPKXiONDRssXD1AX9Oi/SDS?node-id=7717-3946&t=zlze7QSzfpWykbLu-11", + "": "https://www.figma.com/design/J0KLPKXiONDRssXD1AX9Oi/SDS?node-id=2236-15082&t=zlze7QSzfpWykbLu-11", + "": "https://www.figma.com/design/J0KLPKXiONDRssXD1AX9Oi/SDS?node-id=2236-16106&t=zlze7QSzfpWykbLu-11" } } } diff --git a/src/compositions/Cards/Cards.figma.tsx b/src/compositions/Cards/Cards.figma.tsx index cf5d9dc..004a0ea 100644 --- a/src/compositions/Cards/Cards.figma.tsx +++ b/src/compositions/Cards/Cards.figma.tsx @@ -1,6 +1,13 @@ import { figma } from "@figma/code-connect"; import { Image, Text, TextHeading } from "ui"; -import { Card } from "./Cards"; +import { + Card, + PricingCard, + ProductInfoCard, + ReviewCard, + StatsCard, + TestimonialCard, +} from "./Cards"; figma.connect(Card, "", { props: { @@ -28,3 +35,9 @@ figma.connect(Card, "", { ), }); + +figma.connect(ProductInfoCard, ""); +figma.connect(PricingCard, ""); +figma.connect(TestimonialCard, ""); +figma.connect(StatsCard, ""); +figma.connect(ReviewCard, ""); diff --git a/src/compositions/Cards/Cards.tsx b/src/compositions/Cards/Cards.tsx index 6017168..ac2074c 100644 --- a/src/compositions/Cards/Cards.tsx +++ b/src/compositions/Cards/Cards.tsx @@ -1,6 +1,8 @@ import clsx from "clsx"; import { useMediaQuery } from "hooks"; -import { ComponentPropsWithoutRef } from "react"; +import { IconStar } from "icons"; +import { Flex } from "layout"; +import { ComponentPropsWithoutRef, ReactNode } from "react"; import { Avatar, AvatarBlock, @@ -11,12 +13,18 @@ import { TextHeading, TextList, TextListItem, + TextSmall, + TextStrong, TextTitlePage, } from "ui"; import { AnchorOrButton, AnchorOrButtonProps } from "utils"; import "./cards.css"; export type CardProps = ComponentPropsWithoutRef<"div"> & { + /** + * The alignment of the card content. + */ + align?: "start" | "center" | "end"; /** * The initial direction of the card. * All cards become vertical on mobile. @@ -46,6 +54,7 @@ export type CardProps = ComponentPropsWithoutRef<"div"> & { * The basic card generic component that can be used to create vanity card components. */ export function Card({ + align = "start", children, className, direction = "vertical", @@ -58,6 +67,7 @@ export function Card({ const classNames = clsx( className, "card", + `card-align-${align}`, `card-direction-${isMobile ? "vertical" : direction}`, `card-variant-${variant}`, ); @@ -108,19 +118,23 @@ export function PricingCard({ }: PricingCardProps) { return ( - {heading} - - ${price} - / per month - + + {heading} + + ${price} + / per month + + {items.map((item) => ( {item} ))} - - - + + + + + ); } @@ -135,13 +149,9 @@ export type ProductInfoCardProps = Pick & { */ price: number; /** - * The text labeling the action button - */ - action: string; - /** - * The action for the button + * The description of the product */ - onAction: () => void; + description: string; }; /** @@ -151,19 +161,105 @@ export function ProductInfoCard({ asset, heading, price, - action, - onAction, + description, ...props }: ProductInfoCardProps) { return ( - - {heading} - ${price} - - - + + + {heading} + ${price} + {description} + + + ); +} + +export type ReviewCardProps = { + /** + * The number of stars (1-5) + */ + stars: number; + /** + * The title of the review + */ + title: string; + /** + * The review + */ + body: string; + /** + * The name of the reviewer + */ + name: string; + /** + * The date of the review + */ + date: string; + /** + * The avatar src + */ + src?: AvatarProps["src"]; +}; + +/** + * A card demonstrating a statistic or metric + */ +export function ReviewCard({ + stars, + title, + body, + name, + date, + src, + + ...props +}: ReviewCardProps) { + return ( + + {new Array(stars).fill()} + + {title} + {body} + + + + + + ); +} + +export type StatsCardProps = { + /** + * The icon + */ + icon?: ReactNode; + /** + * The stat + */ + stat: string; + /** + * The description + */ + description: string; +}; + +/** + * A card demonstrating a statistic or metric + */ +export function StatsCard({ + icon, + stat, + description, + ...props +}: StatsCardProps) { + return ( + + {icon} + + {stat} + {description && {description}} + ); } diff --git a/src/compositions/Cards/cards.css b/src/compositions/Cards/cards.css index c754078..27d24e3 100644 --- a/src/compositions/Cards/cards.css +++ b/src/compositions/Cards/cards.css @@ -44,20 +44,25 @@ } } + &.card-align-start { + --content-align: start; + } + &.card-align-center { + --content-align: center; + } + &.card-align-end { + --content-align: end; + } + .card-content { + align-items: var(--content-align, start); display: flex; flex-direction: column; - gap: var(--sds-size-space-200); + gap: var(--sds-size-space-600); grid-area: content; - .text-body-base { - color: var(--sds-color-text-default-secondary); - } - - .button-group { - margin-top: var(--sds-size-space-400); - position: relative; - z-index: 2; + > * { + width: 100%; } } } diff --git a/src/layout/Flex/Flex.tsx b/src/layout/Flex/Flex.tsx index b45ab2a..02579d8 100644 --- a/src/layout/Flex/Flex.tsx +++ b/src/layout/Flex/Flex.tsx @@ -6,13 +6,7 @@ export type FlexProps = ComponentPropsWithoutRef<"div"> & { alignPrimary?: "start" | "end" | "center" | "stretch" | "space-between"; alignSecondary?: "start" | "end" | "center" | "stretch" | "space-between"; direction?: "row" | "row-reverse" | "column" | "column-reverse"; - gap?: - | "100" // xs - | "200" // sm - | "300" // md - | "400" // lg - | "600" // xl - | "800"; // xxl; + gap?: "100" | "200" | "300" | "400" | "600" | "800"; type?: "quarter" | "third" | "half" | "auto"; container?: boolean; wrap?: boolean; @@ -52,9 +46,9 @@ export function Flex({ ); } -type FlextItemSize = "full" | "major" | "minor" | "half"; +type FlexItemSize = "full" | "major" | "minor" | "half"; export type FlexItemProps = ComponentPropsWithoutRef<"div"> & { - size?: FlextItemSize; + size?: FlexItemSize; }; export function FlexItem({ className, children, size }: FlexItemProps) { const classNames = clsx( diff --git a/src/layout/Flex/flex.css b/src/layout/Flex/flex.css index 361d3c4..f505d6e 100644 --- a/src/layout/Flex/flex.css +++ b/src/layout/Flex/flex.css @@ -27,7 +27,7 @@ &.flex-direction-column, &.flex-direction-column-reverse { - > * { + &.flex-align-secondary-stretch > * { width: 100%; } } diff --git a/src/stories/compositions/Cards.stories.tsx b/src/stories/compositions/Cards.stories.tsx index 0dd925d..0a5e697 100644 --- a/src/stories/compositions/Cards.stories.tsx +++ b/src/stories/compositions/Cards.stories.tsx @@ -3,9 +3,17 @@ import { Card, PricingCard, ProductInfoCard, + ReviewCard, + StatsCard, TestimonialCard, } from "compositions"; -import { IconActivity } from "icons"; +import { + IconActivity, + IconClock, + IconCode, + IconCompass, + IconSmile, +} from "icons"; import { Flex } from "layout"; import { ComponentProps } from "react"; import { Button, ButtonGroup, Image, Text, TextHeading } from "ui"; @@ -105,20 +113,110 @@ export const BlockPricingCard: StoryObj = { export const BlockProductInfoCard: StoryObj = { render: () => ( - + } heading="Product" price={5} - action="Buy this" - onAction={() => {}} + description="Wow do we have a cool thing for you. What an amazing thing." /> } heading="Product" price={5} - action="Buy this" - onAction={() => {}} + description="Wow do we have a cool thing for you. What an amazing thing." + /> + } + heading="Product" + price={5} + description="Wow do we have a cool thing for you. What an amazing thing." + /> + } + heading="Product" + price={5} + description="Wow do we have a cool thing for you. What an amazing thing." + /> + + ), +}; + +export const BlockReviewCard: StoryObj = { + render: () => ( + + + + + + + ), +}; + +export const BlockStatsCard: StoryObj = { + render: () => ( + + } + stat="400" + description="SDS Hours" + /> + } + stat="15.3k" + description="Lines of TypeScript" + /> + } + stat="8M" + description="Smiles" + /> + } + stat="120.4k" + description="Directions" /> ), diff --git a/src/ui/Text/text.css b/src/ui/Text/text.css index bd68ac7..2571e31 100644 --- a/src/ui/Text/text.css +++ b/src/ui/Text/text.css @@ -87,7 +87,7 @@ &:not(.text-link-list) { color: var(--sds-color-text-default-secondary); - padding: 1rem; + padding-left: 1rem; } } .text-list-item {