207 lines
8.3 KiB
TypeScript
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>
|
|
)
|
|
};
|