Files
cadence-ui/apps/docs/src/components/stat-card.stories.tsx
T

207 lines
8.3 KiB
TypeScript

import {
StatCard,
StatCardDelta,
StatCardDescription,
StatCardEyebrow,
StatCardHeader,
StatCardLabel,
StatCardMetric,
StatCardValue
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
function RevenueStatCard({
interactive = true,
tone = "default"
}: {
interactive?: boolean;
tone?: "default" | "subtle" | "accent";
}) {
return (
<StatCard className="w-[360px]" interactive={interactive} tone={tone}>
<StatCardHeader>
<StatCardEyebrow>Revenue pulse</StatCardEyebrow>
<StatCardLabel>Monthly recurring revenue</StatCardLabel>
</StatCardHeader>
<StatCardMetric>
<StatCardValue>$101,820</StatCardValue>
<StatCardDelta tone="success">+8.4%</StatCardDelta>
</StatCardMetric>
<StatCardDescription>
Assisted follow-up and steadier route timing are lifting close quality this month.
</StatCardDescription>
</StatCard>
);
}
const meta = {
title: "Components/StatCard",
component: RevenueStatCard,
parameters: {
docs: {
description: {
component:
"StatCard is the compact KPI surface for one headline metric, one supporting delta, and one short line of context. Use it when a dashboard needs a readable value slab without the extra regions that belong to a richer analytics panel. The default treatment stays lightly hover-ready so high-value summaries feel like lit objects instead of flat admin tiles."
}
},
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof RevenueStatCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Motion: Story = {
parameters: {
docs: {
description: {
story:
"Hover the interactive cards directly in the canvas. StatCard motion should stay compact: the slab lifts as one object, the value sharpens slightly, and the delta chip follows with a softer secondary response instead of turning the panel into a busy dashboard tile."
}
}
},
render: () => (
<div className="grid w-[840px] gap-4 rounded-[var(--radius-xl)] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_78%,white_22%),color-mix(in_oklch,var(--color-surface-container-low)_86%,white_14%))] p-6 shadow-[var(--shadow-sm)]">
<div className="grid gap-2">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
Motion review
</p>
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
Keep the KPI slab buoyant, not theatrical.
</h3>
<p className="max-w-[40rem] text-sm leading-6 text-[var(--color-muted-foreground)]">
The card should read like one lifted object. The headline value and delta chip can echo
the lift, but the motion should still feel quieter than the richer choreography used by
MetricCard.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<RevenueStatCard />
<StatCard className="w-full" interactive tone="accent">
<StatCardHeader>
<StatCardEyebrow>Pipeline pulse</StatCardEyebrow>
<StatCardLabel>Qualified expansion forecast</StatCardLabel>
</StatCardHeader>
<StatCardMetric>
<StatCardValue>31%</StatCardValue>
<StatCardDelta tone="primary">+9.2%</StatCardDelta>
</StatCardMetric>
<StatCardDescription>
The uplift signal is strong enough to justify a wider follow-up wave this week.
</StatCardDescription>
</StatCard>
</div>
</div>
)
};
export const States: Story = {
render: () => (
<div className="grid w-[820px] gap-4 md:grid-cols-2 xl:grid-cols-3">
<RevenueStatCard />
<StatCard className="w-full" interactive={false} tone="subtle">
<StatCardHeader>
<StatCardEyebrow>Risk watch</StatCardEyebrow>
<StatCardLabel>Qualified pipeline</StatCardLabel>
</StatCardHeader>
<StatCardMetric>
<StatCardValue>$82,450</StatCardValue>
<StatCardDelta tone="warning">-3.1%</StatCardDelta>
</StatCardMetric>
<StatCardDescription>
Mid-market deal spread is still wider than the board would like.
</StatCardDescription>
</StatCard>
<StatCard className="w-full" interactive tone="accent">
<StatCardHeader>
<StatCardEyebrow>AI influence</StatCardEyebrow>
<StatCardLabel>Forecast confidence</StatCardLabel>
</StatCardHeader>
<StatCardMetric>
<StatCardValue>31%</StatCardValue>
<StatCardDelta tone="primary">+9.2%</StatCardDelta>
</StatCardMetric>
<StatCardDescription>
Commercial planning is stable enough to expand the next follow-up wave.
</StatCardDescription>
</StatCard>
</div>
)
};
export const Anatomy: Story = {
render: () => (
<div className="w-[760px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<div className="grid gap-5">
<div className="grid gap-2">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Stat card anatomy
</p>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
The contract stays intentionally small: framing copy, one metric group, and one line of
context. If the panel needs media, actions, or footer detail, move up to
<code className="ml-1 text-[var(--color-foreground)]">MetricCard</code>.
</p>
</div>
<RevenueStatCard />
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>
<code className="text-[var(--color-foreground)]">data-slot="header"</code> groups
the eyebrow and label.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="metric"</code> holds
the headline value and its delta badge.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="value"</code>,
<code className="ml-1 text-[var(--color-foreground)]">data-slot="delta"</code>, and
<code className="ml-1 text-[var(--color-foreground)]">data-slot="description"</code>
keep KPI styling hooks stable for docs and consumers.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-interactive</code> keeps the
hover-ready treatment explicit when the stat should feel more lifted than surrounding
utility panels.
</p>
</div>
</div>
</div>
)
};
export const Accessibility: Story = {
parameters: {
docs: {
description: {
story:
"Keep stat-card labels concrete, keep delta color from being the only signal, and reserve the description for the reason the number matters right now rather than repeating the value."
}
}
},
render: () => (
<div className="grid w-[840px] gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
Review guidance
</h3>
<div className="mt-4 grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>Use a label that names the metric, not just a vague business area.</p>
<p>Delta should still be readable as text such as <code>+8.4%</code> or <code>-3.1%</code>.</p>
<p>Use the chip tone and shape as reinforcement, not as the only signal that the number moved.</p>
<p>The description should explain the current signal, not restate the number.</p>
</div>
</article>
<div className="flex items-center justify-center rounded-[var(--radius-lg)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-6">
<RevenueStatCard />
</div>
</div>
)
};