265 lines
9.0 KiB
TypeScript
265 lines
9.0 KiB
TypeScript
import {
|
||
motionPackDetails,
|
||
motionPackNames,
|
||
type MotionPackName
|
||
} from "@ai-ui/tokens";
|
||
import {
|
||
Button,
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
Input,
|
||
Skeleton,
|
||
Switch,
|
||
skinDetails,
|
||
skinNames,
|
||
type SkinName
|
||
} from "@ai-ui/ui";
|
||
import type { Meta, StoryObj } from "@storybook/react";
|
||
|
||
const motionAccessibilityModes = [
|
||
{
|
||
label: "System accessibility",
|
||
value: "system"
|
||
},
|
||
{
|
||
label: "Reduced accessibility",
|
||
value: "reduced"
|
||
}
|
||
] as const;
|
||
|
||
type MotionAccessibilityMode = (typeof motionAccessibilityModes)[number]["value"];
|
||
|
||
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 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))"
|
||
}}
|
||
>
|
||
<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>
|
||
</div>
|
||
<Button aria-hidden="true" size="icon" tabIndex={-1} variant="ghost">
|
||
×
|
||
</Button>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Skeleton shape="line" />
|
||
<Skeleton shape="block" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ComparisonCell({
|
||
motionAccessibility,
|
||
motionPack,
|
||
skin
|
||
}: {
|
||
motionAccessibility: MotionAccessibilityMode;
|
||
motionPack: MotionPackName;
|
||
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={motionAccessibility === "reduced" ? "reduced" : undefined}
|
||
data-motion-pack={motionPack}
|
||
data-skin={skin}
|
||
>
|
||
<div className="flex flex-wrap gap-2">
|
||
<RuntimePill>{motionPackDetails[motionPack].label}</RuntimePill>
|
||
<RuntimePill>{skinDetails[skin].label}</RuntimePill>
|
||
<RuntimePill>{motionAccessibility}</RuntimePill>
|
||
</div>
|
||
|
||
<Card interactive tone="default">
|
||
<CardHeader>
|
||
<CardTitle>Release routing</CardTitle>
|
||
<CardDescription>
|
||
The same component tree should now pick up distinct skin treatments.
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="grid gap-3">
|
||
<Input 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 checked />
|
||
</div>
|
||
<div className="flex flex-wrap gap-3">
|
||
<Button>Primary</Button>
|
||
<Button variant="secondary">Secondary</Button>
|
||
<Button variant="subtle">Subtle</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<PanelPreview />
|
||
</section>
|
||
);
|
||
}
|
||
|
||
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() {
|
||
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
|
||
</p>
|
||
<h1
|
||
className="font-semibold tracking-[var(--tracking-tight)]"
|
||
style={{
|
||
fontFamily: "var(--font-display)",
|
||
fontSize: "var(--text-4xl)",
|
||
lineHeight: "var(--leading-tight)"
|
||
}}
|
||
>
|
||
Style matrix compares the same product surface across skin and motion scopes.
|
||
</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`, `data-motion-pack`, and
|
||
`data-motion="reduced"` scopes so the same building blocks can be
|
||
reviewed side by side.
|
||
</p>
|
||
</header>
|
||
|
||
<section className="grid gap-4">
|
||
{motionPackNames.map((motionPack) => (
|
||
<div key={motionPack} className="grid gap-4">
|
||
<div>
|
||
<h2 className="text-2xl font-semibold">{motionPackDetails[motionPack].label}</h2>
|
||
<p className="mt-1 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||
{motionPackDetails[motionPack].note}
|
||
</p>
|
||
</div>
|
||
{motionAccessibilityModes.map((motionAccessibilityMode) => (
|
||
<div key={`${motionPack}-${motionAccessibilityMode.value}`} className="grid gap-3">
|
||
<div className="flex items-center justify-between gap-4">
|
||
<div>
|
||
<h3 className="text-xl font-semibold">
|
||
{motionAccessibilityMode.label}
|
||
</h3>
|
||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||
{motionAccessibilityMode.value === "reduced"
|
||
? '`data-motion="reduced"`'
|
||
: "System preference"}
|
||
{" "}with{" "}
|
||
<code className="text-[var(--color-foreground)]">
|
||
{`data-motion-pack="${motionPack}"`}
|
||
</code>
|
||
.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-4 xl:grid-cols-3">
|
||
{skinNames.map((skin) => (
|
||
<ComparisonCell
|
||
key={`${motionPack}-${motionAccessibilityMode.value}-${skin}`}
|
||
motionAccessibility={motionAccessibilityMode.value}
|
||
motionPack={motionPack}
|
||
skin={skin}
|
||
/>
|
||
))}
|
||
</div>
|
||
</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 packs and reduced-motion overlay. The control below
|
||
covers the live overlay behavior.
|
||
</p>
|
||
</div>
|
||
<div className="flex justify-start lg:justify-end">
|
||
<MatrixDialogSandbox />
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const meta = {
|
||
title: "Foundation/Style Matrix",
|
||
component: StyleMatrixShowcase,
|
||
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."
|
||
}
|
||
}
|
||
}
|
||
} satisfies Meta<typeof StyleMatrixShowcase>;
|
||
|
||
export default meta;
|
||
|
||
type Story = StoryObj<typeof meta>;
|
||
|
||
export const Overview: Story = {};
|