Files
cadence-ui/apps/docs/src/tokens.stories.tsx
T

410 lines
17 KiB
TypeScript

import {
colorTokens,
defaultTheme,
defaultMotionAccessibility,
defaultMotionPack,
motionTokens,
motionAccessibilityDetails,
motionPackDetails,
radiusTokens,
shadowTokens,
themeDetails,
themeNames,
typographyTokens,
type MotionAccessibilityName,
type MotionPackName,
type ThemeName
} from "@ai-ui/tokens";
import type { Meta, StoryObj } from "@storybook/react";
type TokensOverviewProps = {
motionAccessibility: MotionAccessibilityName;
motionPack: MotionPackName;
theme: ThemeName;
};
function ResolvedTokenValue({ cssVar }: { cssVar: string }) {
const value =
typeof document === "undefined"
? ""
: getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
return <span>{value || cssVar}</span>;
}
function TokenSwatch({
cssVar,
name,
role
}: {
cssVar: string;
name: string;
role: string;
}) {
return (
<article className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-xs)]">
<div
aria-hidden="true"
className="h-20 rounded-[var(--radius-sm)] border border-[var(--color-border)]"
style={{ background: `var(${cssVar})` }}
/>
<div className="mt-3 space-y-1">
<div className="flex items-center justify-between gap-3">
<code className="text-sm font-medium text-[var(--color-foreground)]">{cssVar}</code>
<span className="text-xs text-[var(--color-muted-foreground)]">
<ResolvedTokenValue cssVar={cssVar} />
</span>
</div>
<p className="text-sm font-medium text-[var(--color-foreground)]">{name}</p>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">{role}</p>
</div>
</article>
);
}
function ThemeCard({ themeName }: { themeName: ThemeName }) {
const theme = themeDetails[themeName];
return (
<article
data-theme={themeName}
className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-background)] p-5 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]"
>
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
{theme.label}
</p>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
{theme.note}
</p>
</div>
<span className="rounded-full bg-[var(--color-secondary)] px-3 py-1 text-xs font-medium text-[var(--color-secondary-foreground)]">
{themeName}
</span>
</div>
<div className="mt-5 grid gap-3 sm:grid-cols-3">
<div className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
<div
aria-hidden="true"
className="h-16 rounded-[calc(var(--radius-sm)-4px)] border border-[var(--color-border)] bg-[var(--color-surface)]"
/>
<div className="mt-3 space-y-1">
<p className="text-sm font-medium">Surface</p>
<p className="text-sm text-[var(--color-muted-foreground)]">
Secondary containers and ambient panels.
</p>
</div>
</div>
<div className="rounded-[var(--radius-sm)] border border-[var(--color-border-strong)] bg-[var(--color-background)] p-4">
<div
aria-hidden="true"
className="h-16 rounded-[calc(var(--radius-sm)-4px)] border border-[var(--color-border-strong)] bg-[var(--color-card)]"
/>
<div className="mt-3 space-y-1">
<p className="text-sm font-medium">Card</p>
<p className="text-sm text-[var(--color-muted-foreground)]">
Elevated containers for composition.
</p>
</div>
</div>
<div className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] p-4 text-[var(--color-foreground)]">
<div
aria-hidden="true"
className="h-16 rounded-[calc(var(--radius-sm)-4px)] bg-[var(--color-primary)] ring-1 ring-white/20"
/>
<div className="mt-3 space-y-1">
<p className="text-sm font-medium">Primary</p>
<p className="text-sm text-[var(--color-muted-foreground)]">
Shared action color and emphasis layer.
</p>
</div>
</div>
</div>
</article>
);
}
function TokensOverview({
motionAccessibility,
motionPack,
theme
}: TokensOverviewProps) {
return (
<div className="min-h-screen bg-[var(--color-background)] px-6 py-10 text-[var(--color-foreground)] sm:px-10">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8">
<header className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl space-y-3">
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
AI UI / Phase 1
</p>
<h1
className="max-w-4xl font-semibold tracking-[var(--tracking-tight)]"
style={{
fontFamily: "var(--font-display)",
fontSize: "var(--text-4xl)",
lineHeight: "var(--leading-tight)"
}}
>
The first stable token layer defines color, type, surface depth, and
motion rhythm.
</h1>
<p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
Theme switching now happens at the token layer, not inside component
implementations. Motion is also represented as named tokens and starter
recipes rather than ad hoc transition values.
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 shadow-[var(--shadow-xs)]">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Active Theme
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{themeDetails[theme].label}
</p>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 shadow-[var(--shadow-xs)]">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Motion Pack
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{motionPackDetails[motionPack].label}
</p>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 shadow-[var(--shadow-xs)]">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Accessibility Override
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{motionAccessibilityDetails[motionAccessibility].label}
</p>
</div>
</div>
</header>
<section className="space-y-4">
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold">Theme scaffolds</h2>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
These cards render their own nested theme roots, so tokens can be
validated side by side without touching component code.
</p>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{themeNames.map((themeName) => (
<ThemeCard key={themeName} themeName={themeName} />
))}
</div>
</section>
<section className="space-y-4">
<div>
<h2 className="text-2xl font-semibold">Color roles</h2>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
Semantic color tokens replace hardcoded brand values. Components consume
roles such as primary, muted, destructive, or surface.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{colorTokens.map((token) => (
<TokenSwatch
key={token.cssVar}
cssVar={token.cssVar}
name={token.name}
role={token.role}
/>
))}
</div>
</section>
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<div>
<h2 className="text-2xl font-semibold">Typography scale</h2>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
The system now separates display voice, body readability, and metadata
density into named text roles.
</p>
</div>
<div className="mt-6 space-y-6">
{typographyTokens.map((token) => (
<div key={token.name} className="space-y-2">
<div className="flex flex-wrap items-center gap-3 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
<span>{token.name}</span>
<code>{token.fontVar}</code>
<code>{token.familyVar}</code>
</div>
<p
style={{
fontFamily: `var(${token.familyVar})`,
fontSize: `var(${token.fontVar})`,
lineHeight: `var(${token.lineHeightVar})`
}}
>
{token.sample}
</p>
</div>
))}
</div>
</article>
<div className="grid gap-4">
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h2 className="text-2xl font-semibold">Radius scale</h2>
<div className="mt-5 grid gap-3">
{radiusTokens.map((token) => (
<div key={token.cssVar} className="flex items-center gap-4">
<div
className="h-12 w-24 border border-[var(--color-border)] bg-[var(--color-surface)]"
style={{ borderRadius: `var(${token.cssVar})` }}
/>
<div className="space-y-1">
<p className="text-sm font-medium">{token.name}</p>
<code className="text-xs text-[var(--color-muted-foreground)]">
{token.cssVar}
</code>
</div>
</div>
))}
</div>
</article>
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h2 className="text-2xl font-semibold">Shadow scale</h2>
<div className="mt-5 grid gap-4">
{shadowTokens.map((token) => (
<div key={token.cssVar} className="flex items-center gap-4">
<div
className="h-14 w-24 rounded-[var(--radius-sm)] bg-[var(--color-background)]"
style={{ boxShadow: `var(${token.cssVar})` }}
/>
<div className="space-y-1">
<p className="text-sm font-medium">{token.name}</p>
<code className="text-xs text-[var(--color-muted-foreground)]">
{token.cssVar}
</code>
</div>
</div>
))}
</div>
</article>
</div>
</section>
<section className="grid gap-4 lg:grid-cols-2">
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h2 className="text-2xl font-semibold">Motion tokens</h2>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
Timing and motion scale now live in variables that components can consume
directly. The toolbar now separates the active motion pack from the
accessibility override.
</p>
<div className="mt-6 grid gap-4 md:grid-cols-2">
<div className="space-y-3">
<h3 className="text-sm font-semibold uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Durations
</h3>
{motionTokens.durations.map((token) => (
<div key={token.cssVar} className="rounded-[var(--radius-sm)] bg-[var(--color-surface)] px-4 py-3">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium">{token.name}</span>
<code className="text-xs text-[var(--color-muted-foreground)]">
<ResolvedTokenValue cssVar={token.cssVar} />
</code>
</div>
</div>
))}
</div>
<div className="space-y-3">
<h3 className="text-sm font-semibold uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Distances
</h3>
{motionTokens.distances.map((token) => (
<div key={token.cssVar} className="rounded-[var(--radius-sm)] bg-[var(--color-surface)] px-4 py-3">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium">{token.name}</span>
<code className="text-xs text-[var(--color-muted-foreground)]">
<ResolvedTokenValue cssVar={token.cssVar} />
</code>
</div>
</div>
))}
</div>
</div>
</article>
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h2 className="text-2xl font-semibold">Starter recipes</h2>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
`motion.css` now includes a small recipe layer for transitions, press
feedback, enters, and overlays.
</p>
<div className="mt-6 grid gap-3">
{[
"motion-transition",
"motion-pressable",
"motion-enter-fade",
"motion-enter-rise",
"motion-overlay-enter",
"motion-overlay-exit"
].map((recipe) => (
<div
key={recipe}
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3"
>
<div className="flex items-center justify-between gap-3">
<code className="text-sm font-medium text-[var(--color-foreground)]">
{recipe}
</code>
<span className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
recipe
</span>
</div>
</div>
))}
</div>
</article>
</section>
</div>
</div>
);
}
const meta = {
title: "Foundation/Tokens",
component: TokensOverview,
parameters: {
layout: "fullscreen"
}
} satisfies Meta<typeof TokensOverview>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Overview: Story = {
args: {
motionAccessibility: defaultMotionAccessibility,
motionPack: defaultMotionPack,
theme: defaultTheme
},
render: (_args, context) => (
<TokensOverview
motionAccessibility={
(context.globals.motionAccessibility as MotionAccessibilityName | undefined) ??
defaultMotionAccessibility
}
motionPack={
(context.globals.motionPack as MotionPackName | undefined) ?? defaultMotionPack
}
theme={context.globals.theme as ThemeName}
/>
)
};