feat: add token system and ui contracts
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
authoringChecklist,
|
||||
commonSlotNames,
|
||||
commonStateNames,
|
||||
cvaConventions,
|
||||
getMotionRecipeClassNames,
|
||||
motionRecipes
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const componentRecipeExample = `const buttonVariants = cva(
|
||||
[
|
||||
"inline-flex items-center justify-center gap-2",
|
||||
"rounded-[var(--radius-sm)] font-medium",
|
||||
"motion-pressable motion-ring"
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-[var(--color-primary)] text-[var(--color-primary-foreground)]",
|
||||
secondary: "bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)]"
|
||||
},
|
||||
size: {
|
||||
sm: "h-9 px-3 text-sm",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-5 text-base"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "md"
|
||||
}
|
||||
}
|
||||
);`;
|
||||
|
||||
const stateRecipeExample = `const rootProps = withRootProps(
|
||||
{ className: cn(buttonVariants({ variant, size }), className) },
|
||||
{
|
||||
slot: "root",
|
||||
states: {
|
||||
disabled,
|
||||
loading,
|
||||
state: open ? "open" : "closed"
|
||||
}
|
||||
}
|
||||
);`;
|
||||
|
||||
function ContractsOverview() {
|
||||
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="max-w-3xl space-y-3">
|
||||
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
AI UI / Phase 2
|
||||
</p>
|
||||
<h1
|
||||
className="font-semibold tracking-[var(--tracking-tight)]"
|
||||
style={{
|
||||
fontFamily: "var(--font-display)",
|
||||
fontSize: "var(--text-4xl)",
|
||||
lineHeight: "var(--leading-tight)"
|
||||
}}
|
||||
>
|
||||
Component authoring now follows one repeatable contract.
|
||||
</h1>
|
||||
<p className="text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
|
||||
Phase 2 does not ship real components yet. It defines the shared state,
|
||||
slot, variant, and motion conventions that every future component will use.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
|
||||
<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">Authoring Checklist</h2>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{authoringChecklist.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
>
|
||||
<p className="text-sm leading-6 text-[var(--color-foreground)]">{item}</p>
|
||||
</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">CVA Conventions</h2>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{cvaConventions.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
>
|
||||
<p className="text-sm leading-6 text-[var(--color-foreground)]">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</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">State Naming</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
Public styling state should flow through stable `data-*` attributes.
|
||||
</p>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{commonStateNames.map((item) => (
|
||||
<div
|
||||
key={item.state}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<code className="text-sm font-medium">{`data-${item.state}`}</code>
|
||||
<span className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
convention
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
{item.guidance}
|
||||
</p>
|
||||
</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">Slot Naming</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
Slots create stable styling hooks for component internals and docs.
|
||||
</p>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{commonSlotNames.map((item) => (
|
||||
<div
|
||||
key={item.slot}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<code className="text-sm font-medium">{`data-slot="${item.slot}"`}</code>
|
||||
<span className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
slot
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
{item.guidance}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
|
||||
<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">Variant Base Example</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
Variants should start from a strong base string, then branch only where
|
||||
appearance semantics actually change.
|
||||
</p>
|
||||
<pre className="mt-5 overflow-x-auto rounded-[var(--radius-md)] bg-[var(--color-surface-contrast)] p-4 text-sm leading-6 text-[var(--color-background)]">
|
||||
<code>{componentRecipeExample}</code>
|
||||
</pre>
|
||||
</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">State Helper Example</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
Shared helpers standardize slot names and `data-*` state attributes.
|
||||
</p>
|
||||
<pre className="mt-5 overflow-x-auto rounded-[var(--radius-md)] bg-[var(--color-surface-contrast)] p-4 text-sm leading-6 text-[var(--color-background)]">
|
||||
<code>{stateRecipeExample}</code>
|
||||
</pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Motion Recipe Helpers</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
These class names map directly to the recipe layer defined in
|
||||
`motion.css`.
|
||||
</p>
|
||||
</div>
|
||||
<code className="rounded-[var(--radius-full)] bg-[var(--color-surface)] px-3 py-2 text-sm">
|
||||
{getMotionRecipeClassNames("transition", "ring", "pressable")}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(motionRecipes).map(([name, className]) => (
|
||||
<div
|
||||
key={name}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-medium">{name}</p>
|
||||
<code className="text-xs text-[var(--color-muted-foreground)]">{className}</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Foundation/Contracts",
|
||||
component: ContractsOverview,
|
||||
parameters: {
|
||||
layout: "fullscreen"
|
||||
}
|
||||
} satisfies Meta<typeof ContractsOverview>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Overview: Story = {};
|
||||
@@ -5,3 +5,13 @@
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#storybook-root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
import {
|
||||
colorTokens,
|
||||
motionTokens,
|
||||
radiusTokens,
|
||||
shadowTokens,
|
||||
themeDetails,
|
||||
themeNames,
|
||||
typographyTokens,
|
||||
type MotionModeName,
|
||||
type ThemeName
|
||||
} from "@ai-ui/tokens";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
type TokensOverviewProps = {
|
||||
motionMode: MotionModeName;
|
||||
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({ motionMode, 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 Mode
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
|
||||
{motionMode === "system" ? "System preference" : "Reduced motion"}
|
||||
</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 lg:grid-cols-3">
|
||||
{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 can force reduced motion for preview validation.
|
||||
</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: {
|
||||
motionMode: "system",
|
||||
theme: "light"
|
||||
},
|
||||
render: (_args, context) => (
|
||||
<TokensOverview
|
||||
motionMode={context.globals.motion as MotionModeName}
|
||||
theme={context.globals.theme as ThemeName}
|
||||
/>
|
||||
)
|
||||
};
|
||||
Reference in New Issue
Block a user