Add harness workflow and Material showcase design system
This commit is contained in:
@@ -146,11 +146,42 @@ export const Motion: Story = {
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-3 sm:grid-cols-2">
|
||||
<Button>Premium primary</Button>
|
||||
<Button variant="subtle">Subtle surface</Button>
|
||||
<Button variant="secondary">Secondary action</Button>
|
||||
<Button loading>Saving changes</Button>
|
||||
<div className="relative grid w-[840px] gap-5 overflow-hidden rounded-[2.2rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_84%,white_16%),color-mix(in_oklch,var(--color-background)_90%,white_10%))] p-6 shadow-[0_24px_72px_color-mix(in_oklch,var(--color-primary)_10%,transparent)] sm:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<div className="motion-drift absolute left-[-2rem] top-[-2rem] h-28 w-28 rounded-full bg-[color-mix(in_oklch,var(--color-primary-container)_62%,transparent)] blur-3xl" />
|
||||
<div className="motion-breathe absolute right-0 top-10 h-24 w-24 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary-container)_58%,transparent)] blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Material motion deck
|
||||
</p>
|
||||
<h3 className="max-w-md text-3xl font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
|
||||
Buttons should feel like touchable capsules floating over tinted light.
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Button>Premium primary</Button>
|
||||
<Button variant="subtle">Subtle surface</Button>
|
||||
<Button variant="secondary">Secondary action</Button>
|
||||
<Button loading>Saving changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center justify-center">
|
||||
<div className="motion-float absolute left-5 top-8 rounded-full border border-white/45 bg-[color-mix(in_oklch,var(--color-surface-container-low)_78%,white_22%)] px-4 py-2 text-xs font-medium tracking-[0.14em] text-[var(--color-muted-foreground)] shadow-[0_12px_30px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]">
|
||||
SOFT LIFT
|
||||
</div>
|
||||
<div className="motion-float-delayed absolute bottom-6 right-6 rounded-full bg-[var(--color-primary-container)] px-4 py-2 text-sm font-medium text-[var(--color-on-primary-container)] shadow-[0_14px_28px_color-mix(in_oklch,var(--color-primary)_12%,transparent)]">
|
||||
PRESSED
|
||||
</div>
|
||||
<div className="grid w-full max-w-[16rem] gap-3 rounded-[2rem] border border-white/40 bg-[color-mix(in_oklch,var(--color-surface-container-low)_82%,white_18%)] p-4 shadow-[0_24px_60px_color-mix(in_oklch,var(--color-primary)_12%,transparent)]">
|
||||
<div className="h-28 rounded-[1.5rem] bg-[linear-gradient(165deg,color-mix(in_oklch,var(--color-primary-container)_88%,white_12%),color-mix(in_oklch,var(--color-tertiary-container)_82%,white_18%))]" />
|
||||
<Button>Shop set</Button>
|
||||
<Button variant="ghost">Maybe later</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -52,21 +52,46 @@ export const Playground: Story = {
|
||||
|
||||
export const Grid: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[760px] gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Default tone</CardTitle>
|
||||
<CardDescription>Standard elevated panel for data and form sections.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Reliable baseline for most admin surfaces.</CardContent>
|
||||
</Card>
|
||||
<Card interactive tone="accent">
|
||||
<CardHeader>
|
||||
<CardTitle>Interactive accent</CardTitle>
|
||||
<CardDescription>Hover-capable treatment for navigable cards.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Use sparingly for overview screens with clear primary actions.</CardContent>
|
||||
</Card>
|
||||
<div className="relative grid w-[940px] gap-6 overflow-hidden rounded-[2.3rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_82%,white_18%),color-mix(in_oklch,var(--color-background)_88%,white_12%))] p-6 shadow-[0_24px_72px_color-mix(in_oklch,var(--color-primary)_10%,transparent)] md:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<div className="motion-drift absolute left-[-1.5rem] top-6 h-24 w-24 rounded-full bg-[color-mix(in_oklch,var(--color-primary-container)_58%,transparent)] blur-3xl" />
|
||||
<div className="motion-breathe absolute right-10 top-0 h-20 w-20 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary-container)_52%,transparent)] blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative grid gap-4 self-start">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Showcase slabs
|
||||
</p>
|
||||
<h3 className="max-w-sm text-3xl font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
|
||||
Cards should feel like lit objects on a display plinth, not admin rectangles.
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid gap-3 rounded-[1.6rem] bg-[color-mix(in_oklch,var(--color-surface-container)_82%,white_18%)] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.4)]">
|
||||
<div className="h-40 rounded-[1.4rem] bg-[linear-gradient(160deg,color-mix(in_oklch,var(--color-primary-container)_78%,white_22%),color-mix(in_oklch,var(--color-tertiary-container)_74%,white_26%))]" />
|
||||
<div className="grid gap-2">
|
||||
<span className="h-3 w-24 rounded-full bg-[var(--color-foreground)]/12" />
|
||||
<span className="h-3 w-40 rounded-full bg-[var(--color-foreground)]/9" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative grid gap-4">
|
||||
<Card className="motion-float">
|
||||
<CardHeader>
|
||||
<CardTitle>Default tone</CardTitle>
|
||||
<CardDescription>Standard elevated panel for data and form sections.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Reliable baseline for most admin surfaces.</CardContent>
|
||||
</Card>
|
||||
<Card className="motion-float-delayed justify-self-end md:w-[88%]" interactive tone="accent">
|
||||
<CardHeader>
|
||||
<CardTitle>Interactive accent</CardTitle>
|
||||
<CardDescription>Hover-capable treatment for navigable cards.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Use sparingly for overview screens with clear primary actions.</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ContextMenu,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuContent,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
ContextMenuRadioGroup,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
|
||||
@@ -414,6 +414,7 @@ function DataTablePlayground() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
className="border-[var(--color-border-strong)] bg-[var(--color-background)] text-[var(--color-foreground)] hover:bg-[var(--color-surface)]"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={resetView}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
|
||||
@@ -173,7 +173,12 @@ function LaunchSettingsForm() {
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit">Save settings</Button>
|
||||
<Button
|
||||
className="bg-[var(--color-foreground)] text-[var(--color-background)] hover:bg-[color-mix(in_oklch,var(--color-foreground)_88%,white_12%)]"
|
||||
type="submit"
|
||||
>
|
||||
Save settings
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
|
||||
|
||||
@@ -8,14 +8,15 @@ function FoundationShowcase() {
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
|
||||
<div className="max-w-3xl space-y-3">
|
||||
<p className="text-sm uppercase tracking-[0.28em] text-[var(--color-muted-foreground)]">
|
||||
AI UI / Phase 0
|
||||
AI UI / Foundation
|
||||
</p>
|
||||
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Monorepo scaffolding for a source-owned component system.
|
||||
Source-owned infrastructure for a Material-first component system.
|
||||
</h1>
|
||||
<p className="max-w-2xl text-base leading-7 text-[var(--color-muted-foreground)] sm:text-lg">
|
||||
The repo now has workspace packages for tokens, UI utilities, and docs.
|
||||
The next phase can focus on component contracts instead of repo setup.
|
||||
The workspace foundation now supports dynamic seed color, a shared UI package,
|
||||
and a Storybook review surface. The next work can stay focused on component
|
||||
quality instead of repo setup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +47,7 @@ function FoundationShowcase() {
|
||||
|
||||
<aside className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] p-6 shadow-[var(--shadow-xs)]">
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">Theme baseline</h2>
|
||||
<h2 className="text-xl font-semibold">Seed presets</h2>
|
||||
<ul className="space-y-2 text-sm text-[var(--color-muted-foreground)]">
|
||||
{themeNames.map((themeName) => (
|
||||
<li key={themeName} className="flex items-center justify-between gap-4">
|
||||
|
||||
@@ -1,159 +1,196 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Skeleton,
|
||||
Switch,
|
||||
defaultSkin,
|
||||
skinDetails,
|
||||
skinNames,
|
||||
type SkinName
|
||||
defaultSkin
|
||||
} from "@ai-ui/ui";
|
||||
import {
|
||||
createDynamicColorVariables,
|
||||
defaultMotionMode,
|
||||
defaultTheme,
|
||||
motionModeDetails,
|
||||
themeDetails,
|
||||
themeNames,
|
||||
type MotionModeName,
|
||||
type ThemeName
|
||||
} from "@ai-ui/tokens";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
type StyleContractShowcaseProps = {
|
||||
type MaterialRuntimeShowcaseProps = {
|
||||
motionMode: MotionModeName;
|
||||
skin: SkinName;
|
||||
theme: ThemeName;
|
||||
};
|
||||
|
||||
function RuntimeBadge({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-[var(--radius-full)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
<div className="rounded-[var(--radius-full)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container)] px-3 py-2 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
<span className="mr-2 text-[var(--color-foreground)]">{label}</span>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkinPanel({
|
||||
description,
|
||||
name
|
||||
function FloatingNote({
|
||||
className,
|
||||
lines,
|
||||
title
|
||||
}: {
|
||||
description: string;
|
||||
name: SkinName;
|
||||
className?: string;
|
||||
lines: string[];
|
||||
title: string;
|
||||
}) {
|
||||
const [enabled, setEnabled] = useState(name !== "minimal");
|
||||
|
||||
return (
|
||||
<article
|
||||
data-skin={name}
|
||||
className="grid gap-4 border p-5"
|
||||
style={{
|
||||
background: "var(--ui-surface-bg)",
|
||||
borderColor: "var(--ui-surface-border)",
|
||||
borderRadius: "var(--ui-surface-radius)",
|
||||
boxShadow: "var(--ui-surface-shadow)",
|
||||
backdropFilter: "blur(var(--ui-surface-backdrop-blur))"
|
||||
}}
|
||||
className={`grid gap-3 rounded-[1.4rem] border border-white/45 bg-[color-mix(in_oklch,var(--color-surface-container-low)_78%,white_22%)] p-4 shadow-[0_18px_40px_color-mix(in_oklch,var(--color-primary)_12%,transparent)] backdrop-blur-sm ${className ?? ""}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
data-skin="{name}"
|
||||
</p>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-[var(--color-foreground)]">
|
||||
{skinDetails[name].label}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="rounded-full border px-3 py-1 text-xs font-medium text-[var(--color-foreground)]"
|
||||
style={{
|
||||
background: "var(--ui-control-bg)",
|
||||
borderColor: "var(--ui-control-border)",
|
||||
borderRadius: "var(--ui-control-radius)",
|
||||
boxShadow: "var(--ui-control-shadow)"
|
||||
}}
|
||||
>
|
||||
phase 1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div
|
||||
className="relative overflow-hidden border p-4"
|
||||
style={{
|
||||
background: "var(--ui-control-bg)",
|
||||
borderColor: "var(--ui-control-border)",
|
||||
borderRadius: "var(--ui-control-radius)",
|
||||
boxShadow: "var(--ui-control-shadow)"
|
||||
}}
|
||||
>
|
||||
<p className="text-[0.72rem] font-medium uppercase tracking-[0.16em] text-[var(--color-muted-foreground)]">
|
||||
{title}
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{lines.map((line, index) => (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-4 top-0 h-12"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, color-mix(in oklch, var(--color-primary) 24%, transparent), transparent)",
|
||||
mixBlendMode:
|
||||
name === "glass" ? "screen" : name === "pixel" ? "multiply" : "normal",
|
||||
opacity: "var(--ui-ornament-opacity)"
|
||||
}}
|
||||
key={line}
|
||||
className="h-2.5 rounded-full bg-[var(--color-foreground)]/10"
|
||||
style={{ width: `${100 - index * 16}%` }}
|
||||
/>
|
||||
<p className="text-sm font-medium text-[var(--color-foreground)]">Surface hooks</p>
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
This panel reads the new Phase 1 skin variables directly. It is the proof that
|
||||
root or nested `data-skin` scopes are now a stable runtime contract.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grid gap-3 border p-4"
|
||||
style={{
|
||||
background: "var(--ui-control-bg)",
|
||||
borderColor: "var(--ui-control-border)",
|
||||
borderRadius: "var(--ui-control-radius)",
|
||||
boxShadow: "var(--ui-control-shadow)"
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-medium text-[var(--color-foreground)]">
|
||||
Preview controls
|
||||
</span>
|
||||
<Switch
|
||||
aria-label={`${skinDetails[name].label} preview controls`}
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
aria-label={`${name} skin search`}
|
||||
defaultValue={skinDetails[name].note}
|
||||
readOnly
|
||||
/>
|
||||
<Button variant={name === "glass" ? "secondary" : "primary"}>
|
||||
Same API, future skin target
|
||||
</Button>
|
||||
<Skeleton shape="line" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function StyleContractShowcase({
|
||||
motionMode,
|
||||
skin,
|
||||
theme
|
||||
}: StyleContractShowcaseProps) {
|
||||
function ShowcasePhone({
|
||||
accentClassName,
|
||||
className,
|
||||
eyebrow,
|
||||
title
|
||||
}: {
|
||||
accentClassName: string;
|
||||
className?: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<article
|
||||
className={`relative grid aspect-[0.53] w-[13.5rem] overflow-hidden rounded-[2.4rem] border border-white/50 bg-[color-mix(in_oklch,var(--color-surface-container-low)_82%,white_18%)] p-3 shadow-[0_28px_80px_color-mix(in_oklch,var(--color-primary)_14%,transparent)] ${className ?? ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-1 text-[0.6rem] font-medium text-[var(--color-muted-foreground)]">
|
||||
<span>9:30</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-foreground)]/70" />
|
||||
<span className="h-1.5 w-4 rounded-full bg-[var(--color-foreground)]/50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-3">
|
||||
<div
|
||||
className={`rounded-[1.8rem] p-5 text-[var(--color-foreground)] shadow-[inset_0_1px_0_rgba(255,255,255,0.45)] ${accentClassName}`}
|
||||
>
|
||||
<p className="text-[0.7rem] uppercase tracking-[0.16em] text-[var(--color-foreground)]/62">
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h3 className="mt-3 text-[2rem] font-semibold leading-[0.95] tracking-[-0.04em]">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 rounded-[1.4rem] bg-[color-mix(in_oklch,var(--color-surface-container)_88%,white_12%)] p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-8 rounded-[1rem] bg-[var(--color-tertiary-container)] motion-breathe" />
|
||||
<div className="grid gap-1">
|
||||
<span className="h-2.5 w-24 rounded-full bg-[var(--color-foreground)]/16" />
|
||||
<span className="h-2.5 w-16 rounded-full bg-[var(--color-foreground)]/10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="h-9 flex-1 rounded-[1.1rem] bg-[var(--color-primary-container)]/78" />
|
||||
<span className="h-9 w-14 rounded-[1.1rem] bg-[var(--color-surface-container-highest)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center justify-between rounded-[1.2rem] bg-[color-mix(in_oklch,var(--color-surface-container)_72%,white_28%)] px-4 py-3 text-[0.72rem] text-[var(--color-muted-foreground)]">
|
||||
<span>Home</span>
|
||||
<span className="rounded-full bg-[var(--color-primary-container)] px-2 py-1 text-[var(--color-on-primary-container)]">
|
||||
Flow
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function SeedPanel({ name }: { name: ThemeName }) {
|
||||
const [enabled, setEnabled] = useState(name !== "sunset");
|
||||
const theme = themeDetails[name];
|
||||
|
||||
return (
|
||||
<article
|
||||
style={createDynamicColorVariables(theme.seed) as CSSProperties}
|
||||
className="grid gap-4 rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container-low)] p-5 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
{name}
|
||||
</p>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">{theme.label}</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
{theme.note}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-[var(--radius-full)] bg-[var(--color-secondary-container)] px-3 py-1 text-xs font-medium text-[var(--color-on-secondary-container)]">
|
||||
{theme.seed}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Card tone="accent">
|
||||
<CardHeader>
|
||||
<CardTitle>One style, many palettes</CardTitle>
|
||||
<CardDescription>
|
||||
The palette changes, but the component language stays recognizably Material.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Input aria-label={`${name} preset input`} defaultValue="team@cadence.dev" />
|
||||
<div className="flex items-center justify-between rounded-[var(--radius-md)] bg-[var(--color-surface-container)] px-4 py-3">
|
||||
<span className="text-sm font-medium text-[var(--color-foreground)]">
|
||||
Tonal preference
|
||||
</span>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Filled action</Button>
|
||||
<Button variant="secondary">Tonal action</Button>
|
||||
<Button variant="subtle">Surface action</Button>
|
||||
<Button variant="ghost">Text action</Button>
|
||||
</div>
|
||||
<Skeleton shape="line" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function MaterialRuntimeShowcase({ motionMode, theme }: MaterialRuntimeShowcaseProps) {
|
||||
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-4xl space-y-4">
|
||||
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
AI UI / Phase 1
|
||||
AI UI / Material Runtime
|
||||
</p>
|
||||
<h1
|
||||
className="font-semibold tracking-[var(--tracking-tight)]"
|
||||
@@ -163,35 +200,107 @@ function StyleContractShowcase({
|
||||
lineHeight: "var(--leading-tight)"
|
||||
}}
|
||||
>
|
||||
Runtime skin switching is now a first-class docs contract, even before
|
||||
component recipes are extracted.
|
||||
Cadence UI now treats Material as the system language, with dynamic seed color
|
||||
and one consistent motion baseline.
|
||||
</h1>
|
||||
<p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
|
||||
Phase 1 introduces `data-skin`, root helpers, Storybook toolbar wiring, and a
|
||||
dedicated skin CSS entrypoint. Phase 2 will move component recipes onto this
|
||||
contract.
|
||||
The old multi-skin showcase has been collapsed into a single rounded, tonal
|
||||
component system. Personalization now comes from seed color rather than
|
||||
competing style packs.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="relative overflow-hidden rounded-[2rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_82%,white_18%),color-mix(in_oklch,var(--color-background)_86%,white_14%))] px-6 py-8 shadow-[0_28px_80px_color-mix(in_oklch,var(--color-primary)_10%,transparent)] sm:px-8 lg:px-10">
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<div className="motion-drift absolute left-[-4rem] top-[-3rem] h-40 w-40 rounded-full bg-[color-mix(in_oklch,var(--color-primary-container)_66%,transparent)] blur-3xl" />
|
||||
<div className="motion-breathe absolute right-[-3rem] top-10 h-36 w-36 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary-container)_72%,transparent)] blur-3xl" />
|
||||
<div className="motion-drift absolute bottom-[-4rem] left-1/3 h-44 w-44 rounded-full bg-[color-mix(in_oklch,var(--color-secondary-container)_72%,transparent)] blur-3xl" />
|
||||
<div className="absolute inset-x-10 bottom-0 h-24 rounded-[2rem_2rem_0_0] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface-container)_40%,transparent),color-mix(in_oklch,var(--color-surface-container-high)_88%,white_12%))] blur-xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative grid gap-8 lg:grid-cols-[minmax(0,1.15fr)_minmax(16rem,0.85fr)] lg:items-center">
|
||||
<div className="space-y-5">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-[color-mix(in_oklch,var(--color-surface-container)_82%,white_18%)] px-4 py-2 text-sm font-medium text-[var(--color-muted-foreground)] shadow-[var(--shadow-xs)]">
|
||||
<span className="motion-breathe size-2.5 rounded-full bg-[var(--color-primary)]" />
|
||||
Material showcase mode
|
||||
</div>
|
||||
<div className="max-w-2xl space-y-4">
|
||||
<h2 className="text-[clamp(2.2rem,5vw,4.6rem)] font-semibold leading-[0.94] tracking-[-0.05em]">
|
||||
Softer slabs, tinted light, and motion that feels staged instead of flat.
|
||||
</h2>
|
||||
<p className="max-w-xl text-base leading-7 text-[var(--color-muted-foreground)] sm:text-lg">
|
||||
This is the target mood for the system: pastel-tonal, editorial enough to
|
||||
feel premium, but still obviously usable as a product surface.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Generate tonal palette</Button>
|
||||
<Button variant="secondary">Preview motion</Button>
|
||||
<Button variant="ghost">Inspect tokens</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto flex h-[32rem] w-full max-w-[32rem] items-center justify-center">
|
||||
<div className="absolute inset-x-3 bottom-2 h-12 rounded-[999px] bg-[color-mix(in_oklch,var(--color-primary)_16%,transparent)] blur-2xl" />
|
||||
<div className="absolute inset-x-6 bottom-0 h-10 rounded-[1.6rem_1.6rem_0.9rem_0.9rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface-container)_70%,white_30%),color-mix(in_oklch,var(--color-surface-container-high)_82%,white_18%))] shadow-[inset_0_1px_0_rgba(255,255,255,0.6)]" />
|
||||
<div className="absolute left-3 top-12 h-[20rem] w-[10.5rem] rounded-[2.6rem] border border-white/40 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface-container)_72%,white_28%),color-mix(in_oklch,var(--color-surface-bright)_88%,white_12%))] shadow-[0_24px_64px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]" />
|
||||
<div className="absolute right-4 top-8 h-[23rem] w-[11rem] rounded-[2.8rem] border border-white/40 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface-container-high)_76%,white_24%),color-mix(in_oklch,var(--color-surface-bright)_90%,white_10%))] shadow-[0_26px_70px_color-mix(in_oklch,var(--color-tertiary)_10%,transparent)]" />
|
||||
|
||||
<ShowcasePhone
|
||||
accentClassName="bg-[linear-gradient(160deg,color-mix(in_oklch,var(--color-primary-container)_88%,white_12%),color-mix(in_oklch,var(--color-secondary-container)_82%,white_18%))]"
|
||||
className="motion-float absolute left-0 top-10 -rotate-[10deg]"
|
||||
eyebrow="Expressive type"
|
||||
title="Move with tonal depth"
|
||||
/>
|
||||
<ShowcasePhone
|
||||
accentClassName="bg-[linear-gradient(165deg,color-mix(in_oklch,var(--color-tertiary-container)_84%,white_16%),color-mix(in_oklch,var(--color-primary-container)_54%,white_46%))]"
|
||||
className="motion-float-delayed relative z-10"
|
||||
eyebrow="Dynamic color"
|
||||
title="Palette from one seed"
|
||||
/>
|
||||
<ShowcasePhone
|
||||
accentClassName="bg-[linear-gradient(160deg,color-mix(in_oklch,var(--color-secondary-container)_86%,white_14%),color-mix(in_oklch,var(--color-surface-container-highest)_64%,white_36%))]"
|
||||
className="motion-float absolute right-0 top-14 rotate-[11deg]"
|
||||
eyebrow="Calm feedback"
|
||||
title="Motion with restraint"
|
||||
/>
|
||||
|
||||
<FloatingNote
|
||||
className="motion-float-delayed absolute left-2 top-3 z-20 w-36"
|
||||
lines={["", "", ""]}
|
||||
title="Launch deck"
|
||||
/>
|
||||
<FloatingNote
|
||||
className="motion-float absolute bottom-16 right-4 z-20 w-40"
|
||||
lines={["", "", "", ""]}
|
||||
title="Material pulse"
|
||||
/>
|
||||
<div className="motion-breathe absolute left-[42%] top-8 z-20 rounded-full border border-white/50 bg-[color-mix(in_oklch,var(--color-primary-container)_72%,white_28%)] px-3 py-2 text-xs font-medium text-[var(--color-on-primary-container)] shadow-[0_12px_26px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]">
|
||||
Dynamic color
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-wrap gap-3">
|
||||
<RuntimeBadge label="theme" value={theme} />
|
||||
<RuntimeBadge label="skin" value={skin} />
|
||||
<RuntimeBadge label="motion" value={motionMode} />
|
||||
<RuntimeBadge label="theme" value={themeDetails[theme].label} />
|
||||
<RuntimeBadge label="motion" value={motionModeDetails[motionMode].label} />
|
||||
<RuntimeBadge label="skin" value={defaultSkin} />
|
||||
</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)]">
|
||||
<h2 className="text-2xl font-semibold">What Phase 1 includes</h2>
|
||||
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||
<article className="rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container-low)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<h2 className="text-2xl font-semibold">Runtime contract</h2>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{[
|
||||
"A new runtime attribute: `data-skin`",
|
||||
"Public helpers from `@ai-ui/ui` for skin names, defaults, and root updates",
|
||||
"A dedicated `@ai-ui/ui/skins.css` entrypoint imported by the docs app",
|
||||
"Storybook globals that apply theme, skin, and interactive/static motion mode together"
|
||||
"`setTheme(preset)` applies a named seed preset for docs and common app defaults.",
|
||||
"`setDynamicColor(seed)` generates a full tonal palette from one color.",
|
||||
"`setSkin(\"material\")` remains as the stable UI runtime marker, but no longer branches into multiple aesthetics.",
|
||||
"`setMotionMode(mode)` keeps one default motion language plus a static accessibility override."
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container)] px-4 py-3"
|
||||
>
|
||||
<p className="text-sm leading-6 text-[var(--color-foreground)]">{item}</p>
|
||||
</div>
|
||||
@@ -199,18 +308,18 @@ function StyleContractShowcase({
|
||||
</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">What still waits for Phase 2</h2>
|
||||
<article className="rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container-low)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<h2 className="text-2xl font-semibold">Material priorities</h2>
|
||||
<div className="mt-5 grid gap-3">
|
||||
{[
|
||||
"Button, card, input, dialog, switch, and skeleton recipe extraction",
|
||||
"Skin-specific component semantic variables such as `--button-*` and `--panel-*`",
|
||||
"A docs comparison page where existing components fully restyle under each skin",
|
||||
"Consumer-facing polish after the runtime contract and docs surface are stable"
|
||||
"Dynamic color replaces fixed stylistic theme packs.",
|
||||
"Tonal surfaces replace decorative gradients, blur, and ornamental skins.",
|
||||
"Large radii and softer outlines create warmth without losing system discipline.",
|
||||
"Motion stays predictable: expressive enough to communicate, restrained enough to stay calm."
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||
className="rounded-[var(--radius-sm)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container)] px-4 py-3"
|
||||
>
|
||||
<p className="text-sm leading-6 text-[var(--color-foreground)]">{item}</p>
|
||||
</div>
|
||||
@@ -219,13 +328,9 @@ function StyleContractShowcase({
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4">
|
||||
{skinNames.map((name) => (
|
||||
<SkinPanel
|
||||
key={name}
|
||||
description={skinDetails[name].note}
|
||||
name={name}
|
||||
/>
|
||||
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)_minmax(0,1fr)]">
|
||||
{themeNames.map((name) => (
|
||||
<SeedPanel key={name} name={name} />
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
@@ -234,34 +339,34 @@ function StyleContractShowcase({
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Foundation/Style Contract",
|
||||
component: StyleContractShowcase,
|
||||
args: {
|
||||
motionMode: defaultMotionMode,
|
||||
skin: defaultSkin,
|
||||
theme: defaultTheme
|
||||
},
|
||||
title: "Foundation/Material Runtime",
|
||||
component: MaterialRuntimeShowcase,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Phase 1 adds the runtime style contract. Use the Storybook toolbar to switch the active `theme`, `skin`, and `motion` mode globally, or inspect the side-by-side nested `data-skin` panels below."
|
||||
"Use this page to review the new Material-centric runtime contract: one visual language, one motion baseline, and dynamic palette generation from a seed color."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: (_args, context) => (
|
||||
<StyleContractShowcase
|
||||
motionMode={
|
||||
(context.globals.motionMode as MotionModeName | undefined) ?? defaultMotionMode
|
||||
}
|
||||
skin={(context.globals.skin as SkinName | undefined) ?? defaultSkin}
|
||||
theme={(context.globals.theme as ThemeName | undefined) ?? defaultTheme}
|
||||
/>
|
||||
)
|
||||
} satisfies Meta<typeof StyleContractShowcase>;
|
||||
},
|
||||
layout: "fullscreen"
|
||||
}
|
||||
} satisfies Meta<typeof MaterialRuntimeShowcase>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Overview: Story = {};
|
||||
export const Overview: Story = {
|
||||
args: {
|
||||
motionMode: defaultMotionMode,
|
||||
theme: defaultTheme
|
||||
},
|
||||
render: (_args, context) => (
|
||||
<MaterialRuntimeShowcase
|
||||
motionMode={
|
||||
(context.globals.motionMode as MotionModeName | undefined) ?? defaultMotionMode
|
||||
}
|
||||
theme={(context.globals.theme as ThemeName | undefined) ?? defaultTheme}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
motionModeDetails,
|
||||
motionModeNames,
|
||||
type MotionModeName
|
||||
} from "@ai-ui/tokens";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -10,162 +7,152 @@ import {
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
Skeleton,
|
||||
Switch,
|
||||
skinDetails,
|
||||
skinNames,
|
||||
type SkinName
|
||||
EmptyState,
|
||||
EmptyStateActions,
|
||||
EmptyStateDescription,
|
||||
EmptyStateHeader,
|
||||
EmptyStateTitle,
|
||||
Input
|
||||
} from "@ai-ui/ui";
|
||||
import {
|
||||
createDynamicColorVariables,
|
||||
motionModeDetails,
|
||||
motionModeNames,
|
||||
themeDetails,
|
||||
themeNames
|
||||
} from "@ai-ui/tokens";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
function ClosePreviewIcon() {
|
||||
function MiniPhone({
|
||||
className,
|
||||
title
|
||||
}: {
|
||||
className?: string;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="m4.5 4.5 7 7m0-7-7 7"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.75"
|
||||
/>
|
||||
</svg>
|
||||
<article
|
||||
className={`grid aspect-[0.56] w-[10.5rem] overflow-hidden rounded-[2rem] border border-white/40 bg-[color-mix(in_oklch,var(--color-surface-container-low)_82%,white_18%)] p-3 shadow-[0_22px_58px_color-mix(in_oklch,var(--color-primary)_10%,transparent)] ${className ?? ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between text-[0.58rem] font-medium text-[var(--color-muted-foreground)]">
|
||||
<span>9:30</span>
|
||||
<span className="h-1.5 w-5 rounded-full bg-[var(--color-foreground)]/45" />
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3">
|
||||
<div className="rounded-[1.4rem] bg-[linear-gradient(160deg,color-mix(in_oklch,var(--color-primary-container)_74%,white_26%),color-mix(in_oklch,var(--color-tertiary-container)_82%,white_18%))] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.42)]">
|
||||
<p className="text-[0.66rem] uppercase tracking-[0.14em] text-[var(--color-foreground)]/60">
|
||||
M3 panel
|
||||
</p>
|
||||
<h3 className="mt-3 text-[1.3rem] font-semibold leading-[0.96] tracking-[-0.04em] text-[var(--color-foreground)]">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid gap-2 rounded-[1.2rem] bg-[color-mix(in_oklch,var(--color-surface-container)_86%,white_14%)] p-3">
|
||||
<span className="h-8 rounded-[1rem] bg-[var(--color-surface-container-highest)]" />
|
||||
<span className="h-8 rounded-[1rem] bg-[var(--color-secondary-container)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex items-center justify-between rounded-[1rem] bg-[color-mix(in_oklch,var(--color-surface-container)_70%,white_30%)] px-3 py-2 text-[0.64rem] text-[var(--color-muted-foreground)]">
|
||||
<span>Home</span>
|
||||
<span>Feed</span>
|
||||
<span>Save</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimePill({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<span className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
function SurfaceCluster({
|
||||
motionMode,
|
||||
themeName
|
||||
}: {
|
||||
motionMode: (typeof motionModeNames)[number];
|
||||
themeName: (typeof themeNames)[number];
|
||||
}) {
|
||||
const theme = themeDetails[themeName];
|
||||
|
||||
function PanelPreview() {
|
||||
return (
|
||||
<div
|
||||
className="grid gap-3 border p-4"
|
||||
style={{
|
||||
background: "var(--ui-panel-bg)",
|
||||
borderColor: "var(--ui-panel-border)",
|
||||
borderRadius: "var(--ui-panel-radius)",
|
||||
borderWidth: "var(--ui-panel-border-width)",
|
||||
boxShadow: "var(--ui-panel-shadow)",
|
||||
backdropFilter: "blur(var(--ui-panel-backdrop-blur))"
|
||||
}}
|
||||
<article
|
||||
data-motion={motionMode}
|
||||
style={createDynamicColorVariables(theme.seed) as CSSProperties}
|
||||
className={`grid gap-4 rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)] bg-[var(--color-surface-container-low)] p-5 shadow-[var(--shadow-sm)] ${motionMode === "interactive" ? "motion-float" : ""}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--color-foreground)]">
|
||||
Dialog panel contract
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
Panel vars preview the dialog surface without opening an overlay in every
|
||||
matrix cell.
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
{motionModeDetails[motionMode].label}
|
||||
</p>
|
||||
<h3 className="mt-1 text-xl font-semibold text-[var(--color-foreground)]">
|
||||
{theme.label}
|
||||
</h3>
|
||||
</div>
|
||||
<Button aria-hidden="true" size="icon" tabIndex={-1} variant="ghost">
|
||||
<ClosePreviewIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Skeleton shape="line" />
|
||||
<Skeleton shape="block" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComparisonCell({
|
||||
motionMode,
|
||||
skin
|
||||
}: {
|
||||
motionMode: MotionModeName;
|
||||
skin: SkinName;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className="grid gap-4 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)]"
|
||||
data-motion={motionMode}
|
||||
data-skin={skin}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<RuntimePill>{motionModeDetails[motionMode].label}</RuntimePill>
|
||||
<RuntimePill>{skinDetails[skin].label}</RuntimePill>
|
||||
<span className="rounded-[var(--radius-full)] bg-[var(--color-secondary-container)] px-3 py-1 text-xs font-medium text-[var(--color-on-secondary-container)]">
|
||||
{theme.seed}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Card interactive tone="default">
|
||||
<div className="relative flex items-center justify-center gap-3 rounded-[1.6rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_78%,white_22%),color-mix(in_oklch,var(--color-surface-container)_82%,white_18%))] px-3 py-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.4)]">
|
||||
<div className="absolute left-6 top-4 h-12 w-12 rounded-full bg-[color-mix(in_oklch,var(--color-primary-container)_72%,transparent)] blur-2xl" />
|
||||
<div className="absolute right-8 bottom-5 h-12 w-12 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary-container)_66%,transparent)] blur-2xl" />
|
||||
<MiniPhone className={motionMode === "interactive" ? "motion-float -rotate-[8deg]" : "-rotate-[8deg]"} title="Soft motion" />
|
||||
<MiniPhone className={motionMode === "interactive" ? "motion-float-delayed rotate-[7deg]" : "rotate-[7deg]"} title="Tonal lift" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{[
|
||||
["Surface", "var(--color-surface)"],
|
||||
["Container", "var(--color-surface-container)"],
|
||||
["Highest", "var(--color-surface-container-highest)"]
|
||||
].map(([label, value]) => (
|
||||
<div
|
||||
key={label}
|
||||
className={`rounded-[var(--radius-md)] border border-[var(--color-outline-variant)] p-4 ${motionMode === "interactive" ? "motion-breathe" : ""}`}
|
||||
style={{ background: value }}
|
||||
>
|
||||
<p className="text-sm font-medium text-[var(--color-foreground)]">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card tone="default">
|
||||
<CardHeader>
|
||||
<CardTitle>Release routing</CardTitle>
|
||||
<CardTitle>Unified Material surface</CardTitle>
|
||||
<CardDescription>
|
||||
The same component tree should now pick up distinct skin treatments.
|
||||
The palette changes, but density, radius, and tonal layering remain stable.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
<Input
|
||||
aria-label={`${motionMode} ${skin} release note status`}
|
||||
defaultValue="Launch notes approved"
|
||||
readOnly
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-[var(--color-muted-foreground)]">
|
||||
Quiet notifications
|
||||
</span>
|
||||
<Switch
|
||||
aria-label={`${motionMode} ${skin} quiet notifications`}
|
||||
checked
|
||||
/>
|
||||
</div>
|
||||
<CardContent className="grid gap-4">
|
||||
<Input aria-label={`${themeName} input`} defaultValue="Release cadence" />
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="subtle">Subtle</Button>
|
||||
<Button variant="secondary">Tonal</Button>
|
||||
<Button variant="subtle">Surface</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<PanelPreview />
|
||||
</section>
|
||||
<EmptyState tone="subtle">
|
||||
<EmptyStateHeader>
|
||||
<EmptyStateTitle>No alternate skin to choose</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
This matrix now validates one Material language across palettes and motion
|
||||
modes instead of branching into separate aesthetics.
|
||||
</EmptyStateDescription>
|
||||
</EmptyStateHeader>
|
||||
<EmptyStateActions>
|
||||
<Button variant="ghost">Review tonal roles</Button>
|
||||
</EmptyStateActions>
|
||||
</EmptyState>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function MatrixDialogSandbox() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">Open live dialog preview</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dialog validation sandbox</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use the Storybook toolbar to validate the real overlay under the active theme,
|
||||
skin, and motion settings.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost">Back</Button>
|
||||
<Button>Approve</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function StyleMatrixShowcase() {
|
||||
function MaterialToneMatrix() {
|
||||
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-7xl flex-col gap-8">
|
||||
<header className="max-w-4xl space-y-4">
|
||||
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
AI UI / Phase 3
|
||||
AI UI / Material Tone Matrix
|
||||
</p>
|
||||
<h1
|
||||
className="font-semibold tracking-[var(--tracking-tight)]"
|
||||
@@ -175,69 +162,75 @@ function StyleMatrixShowcase() {
|
||||
lineHeight: "var(--leading-tight)"
|
||||
}}
|
||||
>
|
||||
Style matrix compares the same product surface across skin and motion scopes.
|
||||
Review the same Material component language across seed presets and the
|
||||
standard versus static motion baselines.
|
||||
</h1>
|
||||
<p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
|
||||
This page is the screenshot-friendly regression target for the pilot skin work.
|
||||
The grid uses nested `data-skin` and `data-motion` scopes so the same
|
||||
building blocks can be
|
||||
reviewed side by side.
|
||||
This page is now the regression surface for tonal hierarchy. If a preset feels
|
||||
off, the fix belongs in the token generator, not in a separate skin branch.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-4">
|
||||
{motionModeNames.map((motionMode) => (
|
||||
<div key={motionMode} className="grid gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">{motionModeDetails[motionMode].label}</h2>
|
||||
<p className="mt-1 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
{motionModeDetails[motionMode].note}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{skinNames.map((skin) => (
|
||||
<ComparisonCell
|
||||
key={`${motionMode}-${skin}`}
|
||||
motionMode={motionMode}
|
||||
skin={skin}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<section className="relative overflow-hidden rounded-[2rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_84%,white_16%),color-mix(in_oklch,var(--color-background)_88%,white_12%))] px-6 py-7 shadow-[0_24px_72px_color-mix(in_oklch,var(--color-primary)_10%,transparent)] sm:px-8">
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<div className="motion-drift absolute left-10 top-0 h-28 w-28 rounded-full bg-[color-mix(in_oklch,var(--color-primary-container)_60%,transparent)] blur-3xl" />
|
||||
<div className="motion-drift absolute right-12 top-8 h-24 w-24 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary-container)_56%,transparent)] blur-3xl" />
|
||||
</div>
|
||||
<div className="relative flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Tonal regression rig
|
||||
</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-[var(--tracking-tight)]">
|
||||
Interactive mode should feel alive. Static mode should still feel expensive.
|
||||
</h2>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button>Interactive baseline</Button>
|
||||
<Button variant="secondary">Static fallback</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)] lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Live overlay validation</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
Dialog still portals to the document root, so compare its real overlay and
|
||||
panel treatment with the Storybook toolbar. The matrix above covers scoped
|
||||
inline regression across interactive and static motion modes. The control below
|
||||
covers the live overlay behavior.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-start lg:justify-end">
|
||||
<MatrixDialogSandbox />
|
||||
</div>
|
||||
</section>
|
||||
{motionModeNames.map((motionMode) => (
|
||||
<section key={motionMode} className="grid gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">
|
||||
{motionModeDetails[motionMode].label}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
{motionModeDetails[motionMode].note}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
{themeNames.map((themeName) => (
|
||||
<SurfaceCluster
|
||||
key={`${motionMode}-${themeName}`}
|
||||
motionMode={motionMode}
|
||||
themeName={themeName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Foundation/Style Matrix",
|
||||
component: StyleMatrixShowcase,
|
||||
title: "Foundation/Material Tone Matrix",
|
||||
component: MaterialToneMatrix,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Phase 3 adds the regression-oriented comparison surface. Use this page for screenshots and visual review, then use the live dialog sandbox below to validate portal-driven overlays under the active toolbar settings."
|
||||
"A regression surface for checking that seed presets and reduced/static motion still read as one coherent Material system."
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: "fullscreen"
|
||||
}
|
||||
} satisfies Meta<typeof StyleMatrixShowcase>;
|
||||
} satisfies Meta<typeof MaterialToneMatrix>;
|
||||
|
||||
export default meta;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
colorTokens,
|
||||
createDynamicColorVariables,
|
||||
defaultTheme,
|
||||
defaultMotionMode,
|
||||
motionTokens,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
type ThemeName
|
||||
} from "@ai-ui/tokens";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
type TokensOverviewProps = {
|
||||
motionMode: MotionModeName;
|
||||
@@ -63,8 +65,8 @@ function ThemeCard({ themeName }: { themeName: 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)]"
|
||||
style={createDynamicColorVariables(theme.seed) as CSSProperties}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
@@ -143,19 +145,19 @@ function TokensOverview({
|
||||
}}
|
||||
>
|
||||
The first stable token layer defines color, type, surface depth, and
|
||||
motion rhythm.
|
||||
motion rhythm around a Material You style system.
|
||||
</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.
|
||||
Seed color now drives the palette. Components inherit tonal surfaces and
|
||||
emphasis roles from the token layer instead of shipping disconnected visual
|
||||
skins.
|
||||
</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
|
||||
Active Seed Preset
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
|
||||
{themeDetails[theme].label}
|
||||
@@ -175,10 +177,10 @@ function TokensOverview({
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Theme scaffolds</h2>
|
||||
<h2 className="text-2xl font-semibold">Seed presets</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.
|
||||
These cards render their own seed-derived palettes, so the tonal system
|
||||
can be reviewed side by side without changing component code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user