Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Metric Card Component #8987

Merged
merged 11 commits into from
Nov 28, 2024
2 changes: 1 addition & 1 deletion sparkle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,4 @@
"unist-util-visit": "^5.0.0"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}
140 changes: 140 additions & 0 deletions sparkle/src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";

import { Spinner } from "@sparkle/components";
import { cn } from "@sparkle/lib/utils";

const CARD_SIZES = ["xs", "sm", "md", "lg"] as const;
export type CardSizeType = (typeof CARD_SIZES)[number];

const cardVariants = cva(
"s-flex s-flex-col s-gap-2 s-rounded-2xl s-bg-structure-50 s-p-4 s-min-h-[128px]",
{
variants: {
size: {
xs: "s-w-[180px]",
sm: "s-w-[240px]",
md: "s-w-[300px]",
lg: "s-w-[360px]",
},
},
defaultVariants: {
size: "sm",
},
}
);

interface CardRootProps extends VariantProps<typeof cardVariants> {
children: React.ReactNode;
className?: string;
}
const Root = ({ size, className, children }: CardRootProps) => (
<div className={cn(cardVariants({ size }), className)}>{children}</div>
);

interface CardHeaderProps {
children: React.ReactNode;
className?: string;
}

const Header = ({ className, children }: CardHeaderProps) => (
<div className={cn("s-space-y-0.5", className)}>{children}</div>
);

interface CardTitleProps {
children: React.ReactNode;
className?: string;
}

const Title = ({ className, children }: CardTitleProps) => (
<div
className={cn("s-text-sm s-font-semibold s-text-element-800", className)}
>
{children}
</div>
);

interface CardSubtitleProps {
children: React.ReactNode;
className?: string;
}

const Subtitle = ({ className, children }: CardSubtitleProps) => (
<div className={cn("s-text-sm s-text-element-700", className)}>
{children}
</div>
);

interface CardContentProps {
children?: React.ReactNode;
className?: string;
isLoading?: boolean;
}

const Content = ({
className,
children,
isLoading = false,
}: CardContentProps) => {
if (isLoading) {
return (
<div className="s-flex s-items-center s-justify-start">
<Spinner size="sm" />
</div>
);
}
return (
<div className={cn("s-flex s-flex-col s-gap-3", className)}>{children}</div>
);
};

interface CardFooterProps {
children: React.ReactNode;
className?: string;
}

const Footer = ({ className, children }: CardFooterProps) => (
<div className={cn("s-flex s-items-center s-gap-2", className)}>
{children}
</div>
);

interface CardProps {
title: string;
subtitle?: string;
content: React.ReactNode;
footer?: React.ReactNode;
isLoading?: boolean;
size?: CardSizeType;
className?: string;
}

export const Card = ({
title,
subtitle,
content,
footer,
isLoading = false,
size = "sm",
className,
}: CardProps) => {
return (
<Root size={size} className={className}>
<Header>
<Title>{title}</Title>
{subtitle && <Subtitle>{subtitle}</Subtitle>}
</Header>
<Content isLoading={isLoading}>{content}</Content>
{footer && <Footer>{footer}</Footer>}
</Root>
);
};

export const ComposableCard = {
Root,
Header,
Title,
Subtitle,
Content,
Footer,
};
1 change: 1 addition & 0 deletions sparkle/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type { ButtonProps } from "./Button";
export type { MetaButtonProps } from "./Button";
export { Button } from "./Button";
export { MetaButton } from "./Button";
export { Card, ComposableCard } from "./Card";
export { CardButton } from "./CardButton";
export type { CheckboxProps } from "./Checkbox";
export {
Expand Down
2 changes: 1 addition & 1 deletion sparkle/src/index_with_tw_base.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import "./styles/global_with_tw_base.css";
export * from "./index";
export * from "./index";
122 changes: 122 additions & 0 deletions sparkle/src/stories/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Meta, StoryObj } from "@storybook/react";
import * as React from "react";

import {
Avatar,
} from "@sparkle/components";
import { HandThumbUpIcon } from "@sparkle/icons/solid";

import { Card,ComposableCard } from "../components/Card";

const meta: Meta<typeof Card> = {
title: "Components/Card",
component: Card,
parameters: {
layout: "padded",
},
argTypes: {
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg'],
},
className: {
control: 'text'
},
},
args: {
size: 'sm',
},
tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<typeof Card>;

export const Basic: Story = {
args: {
title: "Messages",
subtitle: "Monthly activity",
content: (
<div className="s-flex s-items-center s-gap-2">
<div className="s-text-lg s-font-semibold s-text-element-900">
847
</div>
</div>
),
},
};

export const Loading: Story = {
args: {
...Basic.args,
isLoading: true,
},
};

export const WithIcons: Story = {
args: {
title: "Reactions",
content: (
<div className="s-flex s-items-center s-gap-2">
<HandThumbUpIcon className="s-h-4 s-w-4 s-text-element-600" />
<div className="s-text-lg s-font-semibold s-text-element-900">12</div>
</div>
),
},
};

export const Sizes: Story = {
render: (args) => (
<div className="s-flex s-flex-wrap s-gap-4">
{(["xs", "sm", "md"] as const).map((size) => (
<Card
key={size}
{...args}
size={size}
title={`Size: ${size}`}
content={
<div className="s-text-lg s-font-semibold s-text-element-900">
847
</div>
}
/>
))}
</div>
),
};

export const Composable: Story = {
render: () => (
<ComposableCard.Root size="sm">
<ComposableCard.Header>
<ComposableCard.Title>Messages</ComposableCard.Title>
<ComposableCard.Subtitle>Monthly activity</ComposableCard.Subtitle>
</ComposableCard.Header>
<ComposableCard.Content>
<div className="s-flex s-items-center s-gap-2">
<HandThumbUpIcon className="s-h-4 s-w-4 s-text-element-600" />
<div className="s-text-lg s-font-semibold">847</div>
</div>
</ComposableCard.Content>
<ComposableCard.Footer>
<div className="s-flex -s-space-x-2">
<Avatar
size="sm"
name="John Doe"
visual="https://dust.tt/static/droidavatar/Droid_Lime_3.jpg"
/>
<Avatar
size="sm"
name="Jane Smith"
visual="https://dust.tt/static/droidavatar/Droid_Yellow_3.jpg"
/>
<Avatar
size="sm"
name="Bob Johnson"
visual="https://dust.tt/static/droidavatar/Droid_Red_3.jpg"
/>
</div>
</ComposableCard.Footer>
</ComposableCard.Root>
),
};