feat(ui): polish core component surfaces
This commit is contained in:
@@ -31,6 +31,12 @@ const meta = {
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Alert is the inline callout surface for status, guidance, and blocking information that should stay inside the current page flow. It now enters with a restrained rise/fade and uses a softly plated icon treatment so state changes feel present without turning the component into a hero panel."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
|
||||
@@ -30,6 +30,12 @@ const meta = {
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Avatar is the compact identity surface for people, teams, and owners. The treatment stays quiet, but the image now resolves over the fallback with a soft crossfade so identity changes feel polished instead of abrupt."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
|
||||
@@ -34,6 +34,12 @@ const meta = {
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Badge is the smallest status and chip surface in the system. It still reads as a compact token, but subtle and outline badges now carry a lighter tactile response so filter-like and live-status usage stops feeling mechanically flat."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
@@ -70,3 +76,40 @@ export const Matrix: Story = {
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const LiveAndFilters: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use `Badge` as a small status token by default, and as a tactile chip when it is rendered through `asChild` on an actual interactive element. The motion should stay restrained and chip-like, not button-heavy."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge tone="success">Live</Badge>
|
||||
<Badge tone="warning">Reviewing</Badge>
|
||||
<Badge tone="primary">Forecast</Badge>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge asChild tone="primary" variant="outline">
|
||||
<button aria-pressed="true" type="button">
|
||||
Design
|
||||
</button>
|
||||
</Badge>
|
||||
<Badge asChild tone="neutral" variant="subtle">
|
||||
<button aria-pressed="false" type="button">
|
||||
Editorial
|
||||
</button>
|
||||
</Badge>
|
||||
<Badge asChild tone="success" variant="outline">
|
||||
<button aria-pressed="false" type="button">
|
||||
Quiet queue
|
||||
</button>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@ai-ui/ui";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta = {
|
||||
@@ -6,7 +14,7 @@ const meta = {
|
||||
component: Card,
|
||||
args: {
|
||||
tone: "default",
|
||||
interactive: false
|
||||
interactive: true
|
||||
},
|
||||
argTypes: {
|
||||
className: {
|
||||
@@ -21,6 +29,12 @@ const meta = {
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Card is the base slab surface for panels, compact workflows, and supporting information blocks. It now defaults to a gentle interactive lift so common business cards feel alive by default, while `interactive={false}` remains available for deliberately static surfaces."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
@@ -34,22 +48,61 @@ export const Playground: Story = {
|
||||
render: (args) => (
|
||||
<Card {...args} className="w-[420px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Release card</CardTitle>
|
||||
<CardDescription>Summarize state, ownership, and next action.</CardDescription>
|
||||
<CardTitle>Routing overview</CardTitle>
|
||||
<CardDescription>Keep status, ownership, and the next action in one calm surface.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
This surface is tuned for editorial dashboards and settings views.
|
||||
This base card now carries a light hover lift by default so common workflow slabs do not
|
||||
feel inert.
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button size="sm">Open</Button>
|
||||
<Button size="sm">Open queue</Button>
|
||||
<Button size="sm" variant="ghost">
|
||||
Dismiss
|
||||
Snooze
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
};
|
||||
|
||||
export const States: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use the default interactive treatment for navigable or actionable slabs, and opt out only when the card is meant to behave like a static document section."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[920px] gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Default interactive</CardTitle>
|
||||
<CardDescription>Gentle lift and internal light cue for most business cards.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Good for dashboards, queues, and next-step summaries.</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card interactive={false} tone="subtle">
|
||||
<CardHeader>
|
||||
<CardTitle>Static document slab</CardTitle>
|
||||
<CardDescription>Opt out when the surface should read as anchored reference content.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Useful for long-form notes, legal copy, and passive explanations.</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card tone="accent">
|
||||
<CardHeader>
|
||||
<CardTitle>Accent emphasis</CardTitle>
|
||||
<CardDescription>Carry the same motion language while keeping the tonal emphasis richer.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Use when the card itself is part of the call to action.</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Grid: Story = {
|
||||
render: () => (
|
||||
<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)]">
|
||||
@@ -80,11 +133,11 @@ export const Grid: Story = {
|
||||
<Card className="motion-float">
|
||||
<CardHeader>
|
||||
<CardTitle>Default tone</CardTitle>
|
||||
<CardDescription>Standard elevated panel for data and form sections.</CardDescription>
|
||||
<CardDescription>Standard elevated panel with the new default hover cue.</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">
|
||||
<Card className="motion-float-delayed justify-self-end md:w-[88%]" tone="accent">
|
||||
<CardHeader>
|
||||
<CardTitle>Interactive accent</CardTitle>
|
||||
<CardDescription>Hover-capable treatment for navigable cards.</CardDescription>
|
||||
|
||||
@@ -222,6 +222,12 @@ const meta = {
|
||||
title: "Components/Combobox",
|
||||
component: ControlledDemo,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Combobox is the searchable single-select surface for longer option sets, recent picks, and async lookup flows. The list should feel gently staged as results appear, with active rows gliding into place instead of snapping."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
@@ -240,6 +246,14 @@ export const RecentAndSuggested: Story = {
|
||||
};
|
||||
|
||||
export const AsyncResults: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Async comboboxes should crossfade between loading, empty, and result states. Keep the motion short and directional so the user understands the state change without losing the current context."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => <AsyncResultsDemo />
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,18 @@ function InlineCommandShowcase() {
|
||||
<CommandGroup heading={group.heading}>
|
||||
{group.items.map((item) => (
|
||||
<CommandItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">{item.label}</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
{item.value === "open-docs"
|
||||
? "Jump into authoring guidance, slots, and motion references."
|
||||
: item.value === "go-releases"
|
||||
? "Open the current release desk and recent launch notes."
|
||||
: item.value === "publish-update"
|
||||
? "Queue the current workspace update for approval."
|
||||
: "Assign the next reviewer without leaving the palette."}
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>{item.shortcut}</CommandShortcut>
|
||||
</CommandItem>
|
||||
))}
|
||||
@@ -77,22 +88,42 @@ function OperationsWorkbenchShowcase() {
|
||||
<CommandEmpty>No matching actions.</CommandEmpty>
|
||||
<CommandGroup heading="Recent">
|
||||
<CommandItem keywords={["launch", "release"]} value="recent-launch-review">
|
||||
Launch review
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Launch review</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Reopen the last routed launch brief and approvals queue.
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>R</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem keywords={["blocked", "queue"]} value="recent-blocked-queue">
|
||||
Blocked queue
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Blocked queue</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Inspect stalled items, owners, and next unblock steps.
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>B</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Suggestions">
|
||||
<CommandItem keywords={["docs", "stories"]} value="docs-storybook">
|
||||
Open Storybook docs
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Open Storybook docs</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Review the live component contract and interaction stories.
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>G D</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem disabled value="compliance-review">
|
||||
Compliance review locked
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Compliance review locked</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
This action unlocks after policy artifacts finish syncing.
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>Locked</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
@@ -127,18 +158,49 @@ function DialogCommandShowcase() {
|
||||
<CommandList>
|
||||
<CommandEmpty>No commands available.</CommandEmpty>
|
||||
<CommandGroup heading="Suggestions">
|
||||
<CommandItem value="launch-checklist">Launch checklist</CommandItem>
|
||||
<CommandItem value="rollout-audit">Rollout audit</CommandItem>
|
||||
<CommandItem value="brand-theme">Brand theme tokens</CommandItem>
|
||||
<CommandItem value="launch-checklist">
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Launch checklist</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Open the current milestone handoff and owner checklist.
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
<CommandItem value="rollout-audit">
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Rollout audit</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Review timing, communications, and incident readiness.
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
<CommandItem value="brand-theme">
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Brand theme tokens</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Jump to the token surface that controls tonal palette shifts.
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="People">
|
||||
<CommandItem value="jordan-lee">
|
||||
Jordan Lee
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Jordan Lee</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Product lead for launch sequencing and review coordination.
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>@</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem value="avery-carter">
|
||||
Avery Carter
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Avery Carter</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Owns release notes, docs framing, and approval summaries.
|
||||
</span>
|
||||
</span>
|
||||
<CommandShortcut>@</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
@@ -152,6 +214,16 @@ function LoadingResultsShowcase() {
|
||||
return (
|
||||
<Command
|
||||
className="w-[520px]"
|
||||
footer={
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="m-0 text-xs text-[var(--color-muted-foreground)]">
|
||||
Cached suggestions stay visible while the workspace search refreshes.
|
||||
</p>
|
||||
<Button size="sm" variant="ghost">
|
||||
View queue
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
label="Remote workspace search"
|
||||
loading
|
||||
loadingMessage="Searching remote workspace actions…"
|
||||
@@ -159,6 +231,24 @@ function LoadingResultsShowcase() {
|
||||
<CommandInput placeholder="Search actions from all workspaces" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No commands available.</CommandEmpty>
|
||||
<CommandGroup heading="Cached suggestions">
|
||||
<CommandItem value="cached-launch-checklist">
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Launch checklist</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Reopen the previous launch checklist while fresh results stream in.
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
<CommandItem value="cached-review-queue">
|
||||
<span className="grid min-w-0 flex-1 gap-0.5">
|
||||
<span className="truncate font-medium">Review queue</span>
|
||||
<span className="text-xs leading-5 text-[var(--color-muted-foreground)]">
|
||||
Keep the last known reviewer list visible during the remote refresh.
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
);
|
||||
@@ -168,6 +258,12 @@ const meta = {
|
||||
title: "Components/Command",
|
||||
component: InlineCommandShowcase,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Command is the system surface for in-app palettes and quick action search. The highlighted row should feel adhesive and spatial, with enough motion to clarify focus and loading state without turning the palette into a busy animation surface."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
@@ -190,5 +286,13 @@ export const DialogPalette: Story = {
|
||||
};
|
||||
|
||||
export const LoadingResults: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Loading should not feel like a hard swap. Keep a soft status band at the top, let cached results dim underneath, and preserve the same highlighted-row motion language."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => <LoadingResultsShowcase />
|
||||
};
|
||||
|
||||
@@ -189,15 +189,15 @@ const routingColumns: DataTableColumn<RoutingLaneRow>[] = [
|
||||
|
||||
function SignalGlyph() {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-[0.8rem_2.8rem_1.5rem] gap-2">
|
||||
<span className="h-3 rounded-[var(--radius-full)] bg-[var(--color-primary)]" />
|
||||
<span className="h-3 rounded-[var(--radius-full)] bg-[var(--color-surface-strong)]" />
|
||||
<span className="h-3 rounded-[var(--radius-full)] bg-[var(--color-accent)]" />
|
||||
<div className="grid gap-3">
|
||||
<div className="grid grid-cols-[0.9rem_3.1rem_1.75rem] gap-2">
|
||||
<span className="h-3.5 rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-primary)_88%,white_12%)]" />
|
||||
<span className="h-3.5 rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-surface-strong)_82%,white_18%)]" />
|
||||
<span className="h-3.5 rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-accent)_82%,white_18%)]" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border-strong)]" />
|
||||
<span className="h-2 w-16 rounded-[var(--radius-full)] bg-[var(--color-border)]" />
|
||||
<span className="h-2.5 rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-border-strong)_78%,white_22%)]" />
|
||||
<span className="h-2.5 w-16 rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-border)_88%,white_12%)]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -244,7 +244,7 @@ function RoutingEmptyState({ onReset }: { onReset: () => void }) {
|
||||
function RoutingLaneDetail({ row }: { row: RoutingLaneRow }) {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-xs)]">
|
||||
<div className="grid gap-3 rounded-[var(--radius-xl)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface-container-low)_82%,white_18%),color-mix(in_oklch,var(--color-surface)_90%,white_10%))] p-4 shadow-[var(--shadow-sm)]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge size="sm" tone={getStateTone(row.state)} variant="outline">
|
||||
{row.state}
|
||||
@@ -260,14 +260,14 @@ function RoutingLaneDetail({ row }: { row: RoutingLaneRow }) {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
|
||||
<div className="rounded-[var(--radius-lg)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-background)_86%,white_14%),color-mix(in_oklch,var(--color-surface-container-low)_82%,white_18%))] p-4 shadow-[var(--shadow-xs)]">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Owner
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">{row.owner}</p>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">{row.ownerEmail}</p>
|
||||
</div>
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
|
||||
<div className="rounded-[var(--radius-lg)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-background)_86%,white_14%),color-mix(in_oklch,var(--color-surface-container-low)_82%,white_18%))] p-4 shadow-[var(--shadow-xs)]">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Next gate
|
||||
</p>
|
||||
@@ -307,8 +307,8 @@ function DataTablePlayground() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 text-[var(--color-foreground)]">
|
||||
<div className="grid gap-3 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)] lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<div className="grid gap-5 text-[var(--color-foreground)]">
|
||||
<div className="grid gap-3 rounded-[calc(var(--radius-xl)+0.2rem)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_82%,white_18%),color-mix(in_oklch,var(--color-surface-container-low)_78%,white_22%))] p-5 shadow-[var(--shadow-sm)] lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Standalone workflow
|
||||
@@ -321,7 +321,7 @@ function DataTablePlayground() {
|
||||
exercising built-in search, sorting, pagination, selection, and toolbar actions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3">
|
||||
<div className="rounded-[var(--radius-xl)] border border-[color-mix(in_oklch,var(--color-primary)_14%,var(--color-border))] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-primary-container)_52%,white_48%),color-mix(in_oklch,var(--color-background)_84%,white_16%))] px-4 py-3 shadow-[var(--shadow-xs)]">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Last action
|
||||
</p>
|
||||
@@ -329,7 +329,7 @@ function DataTablePlayground() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-[1120px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)]">
|
||||
<div className="w-full max-w-[1120px] rounded-[calc(var(--radius-xl)+0.2rem)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_76%,white_24%),color-mix(in_oklch,var(--color-surface-container-low)_88%,white_12%))] p-4 shadow-[var(--shadow-sm)]">
|
||||
<DataTable
|
||||
columns={routingColumns}
|
||||
defaultDensity="comfortable"
|
||||
@@ -430,7 +430,7 @@ function DataTablePlayground() {
|
||||
|
||||
function DataTableLoadingState() {
|
||||
return (
|
||||
<div className="w-full max-w-[1120px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)]">
|
||||
<div className="w-full max-w-[1120px] rounded-[calc(var(--radius-xl)+0.2rem)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_76%,white_24%),color-mix(in_oklch,var(--color-surface-container-low)_88%,white_12%))] p-4 shadow-[var(--shadow-sm)]">
|
||||
<DataTable
|
||||
columns={routingColumns}
|
||||
getRowId={(row) => row.id}
|
||||
@@ -448,7 +448,7 @@ function DataTableLoadingState() {
|
||||
|
||||
function DataTableEmptyStateDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-[1120px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)]">
|
||||
<div className="w-full max-w-[1120px] rounded-[calc(var(--radius-xl)+0.2rem)] border border-[color-mix(in_oklch,var(--color-border)_74%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_76%,white_24%),color-mix(in_oklch,var(--color-surface-container-low)_88%,white_12%))] p-4 shadow-[var(--shadow-sm)]">
|
||||
<DataTable
|
||||
columns={routingColumns}
|
||||
empty={<RoutingEmptyState onReset={() => undefined} />}
|
||||
@@ -475,7 +475,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A production-style table primitive with built-in search, sorting, pagination, row selection, and external toolbar actions. This standalone page isolates the component from the larger workspace scene while keeping a realistic routing-desk narrative."
|
||||
"A production-style table primitive with built-in search, sorting, pagination, row selection, and external toolbar actions. This standalone page isolates the component from the larger workspace scene while keeping a realistic routing-desk narrative. Motion should stay calm: rows deepen slightly on hover, sort feedback stays crisp, selected surfaces feel staged instead of loud, and the surrounding control chrome stays mostly anchored instead of all lifting at once."
|
||||
}
|
||||
},
|
||||
layout: "padded"
|
||||
|
||||
@@ -9,13 +9,24 @@ import {
|
||||
EmptyStateTitle
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
function EmptyStateGlyph() {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-[1.3rem_3.4rem] gap-2">
|
||||
<span className="h-5 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-primary)_18%,var(--color-card))]" />
|
||||
<span className="h-5 rounded-[var(--radius-sm)] bg-[var(--color-surface-strong)]" />
|
||||
<div className="grid gap-3">
|
||||
<div className="grid grid-cols-[1.1rem_3.6rem] items-center gap-2">
|
||||
<span className="size-4 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_22%,var(--color-card))] shadow-[0_0_0_4px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]" />
|
||||
<span className="h-4 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-surface-strong)_86%,white_14%)]" />
|
||||
</div>
|
||||
<div className="grid gap-2 rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_70%,transparent)] bg-[color-mix(in_oklch,var(--color-surface-container-low)_72%,white_28%)] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]">
|
||||
<div className="grid grid-cols-[1.2rem_3.4rem] gap-2">
|
||||
<span className="h-4 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-tertiary-container)_60%,white_40%)]" />
|
||||
<span className="h-4 rounded-[var(--radius-sm)] bg-[color-mix(in_oklch,var(--color-surface-strong)_80%,white_20%)]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-[4.8rem] gap-2">
|
||||
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border-strong)]" />
|
||||
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[4.8rem] gap-2">
|
||||
<span className="h-2 rounded-[var(--radius-full)] bg-[var(--color-border-strong)]" />
|
||||
@@ -25,11 +36,43 @@ function EmptyStateGlyph() {
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceSetupGlyph() {
|
||||
return (
|
||||
<div className="grid w-full gap-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_70%,transparent)] bg-[color-mix(in_oklch,var(--color-surface-container-low)_72%,white_28%)] px-3 py-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]">
|
||||
<div className="grid gap-1.5">
|
||||
<span className="h-2.5 w-20 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_20%,var(--color-card))]" />
|
||||
<span className="h-2.5 w-12 rounded-full bg-[var(--color-border)]" />
|
||||
</div>
|
||||
<span className="grid size-10 place-items-center rounded-[1rem] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-primary-container)_76%,white_24%),color-mix(in_oklch,var(--color-tertiary-container)_72%,white_28%))] shadow-[0_10px_22px_color-mix(in_oklch,var(--color-primary)_14%,transparent)]">
|
||||
<span className="size-4 rounded-full border-2 border-[color-mix(in_oklch,var(--color-primary)_58%,white_42%)]" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-2 rounded-[1.25rem] border border-[color-mix(in_oklch,var(--color-border)_68%,transparent)] bg-[color-mix(in_oklch,var(--color-surface-bright)_76%,white_24%)] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.52)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-[color-mix(in_oklch,var(--color-success)_72%,white_28%)]" />
|
||||
<span className="h-2.5 w-18 rounded-full bg-[var(--color-border-strong)]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_56%,white_44%)]" />
|
||||
<span className="h-2.5 w-24 rounded-full bg-[var(--color-border)]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 rounded-full bg-[color-mix(in_oklch,var(--color-tertiary)_58%,white_42%)]" />
|
||||
<span className="h-2.5 w-16 rounded-full bg-[var(--color-border)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReleaseEmptyState({
|
||||
actionsLayout = "inline",
|
||||
align = "center",
|
||||
className,
|
||||
description = "Adjust the current filters or create a new release to start routing work.",
|
||||
eyebrow = "No results",
|
||||
glyph,
|
||||
layout = "default",
|
||||
mediaSize = "default",
|
||||
tone = "default",
|
||||
@@ -37,8 +80,10 @@ function ReleaseEmptyState({
|
||||
}: {
|
||||
actionsLayout?: "inline" | "stack";
|
||||
align?: "center" | "start";
|
||||
className?: string;
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
glyph?: ReactNode;
|
||||
layout?: "compact" | "default" | "split";
|
||||
mediaSize?: "compact" | "default" | "hero";
|
||||
title?: string;
|
||||
@@ -47,12 +92,14 @@ function ReleaseEmptyState({
|
||||
return (
|
||||
<EmptyState
|
||||
align={align}
|
||||
className="w-[min(100%,42rem)]"
|
||||
className={[layout === "split" ? "w-full max-w-none" : "w-[min(100%,42rem)]", className]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
layout={layout}
|
||||
tone={tone}
|
||||
>
|
||||
<EmptyStateMedia size={mediaSize}>
|
||||
<EmptyStateGlyph />
|
||||
{glyph ?? <EmptyStateGlyph />}
|
||||
</EmptyStateMedia>
|
||||
<EmptyStateHeader align={align}>
|
||||
<EmptyStateEyebrow>{eyebrow}</EmptyStateEyebrow>
|
||||
@@ -140,8 +187,10 @@ export const SplitWorkspace: Story = {
|
||||
<div className="w-[940px] rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-background)] p-6">
|
||||
<ReleaseEmptyState
|
||||
align="start"
|
||||
className="shadow-[0_26px_60px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]"
|
||||
description="Invite operators, connect the first runtime, and define the initial routing lane to turn this empty workspace into an active control surface."
|
||||
eyebrow="Workspace setup"
|
||||
glyph={<WorkspaceSetupGlyph />}
|
||||
layout="split"
|
||||
mediaSize="hero"
|
||||
title="This command center needs its first operator"
|
||||
@@ -151,6 +200,42 @@ export const SplitWorkspace: Story = {
|
||||
)
|
||||
};
|
||||
|
||||
export const Motion: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"EmptyState keeps motion local to the decorative media and the call-to-action group. The slab itself stays anchored while the media drifts gently and the actions enter in a short sequence. In static motion mode those effects collapse back to near-instant feedback."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[960px] gap-4 lg:grid-cols-[minmax(0,0.92fr)_minmax(0,1.08fr)]">
|
||||
<ReleaseEmptyState
|
||||
actionsLayout="stack"
|
||||
align="start"
|
||||
description="Keep the empty queue readable without turning it into a hero banner. The media should feel alive, not restless."
|
||||
eyebrow="Queue calm"
|
||||
layout="compact"
|
||||
mediaSize="default"
|
||||
title="No routed work is waiting right now"
|
||||
tone="subtle"
|
||||
/>
|
||||
<ReleaseEmptyState
|
||||
align="start"
|
||||
className="shadow-[0_26px_60px_color-mix(in_oklch,var(--color-primary)_10%,transparent)]"
|
||||
description="The split workspace uses the larger hero media, but the slab stays planted. Motion belongs in the supporting ornament and the CTA entrance, not in the whole panel."
|
||||
eyebrow="Workspace setup"
|
||||
glyph={<WorkspaceSetupGlyph />}
|
||||
layout="split"
|
||||
mediaSize="hero"
|
||||
title="Invite the first operator to activate this command center"
|
||||
tone="accent"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Anatomy: Story = {
|
||||
render: () => (
|
||||
<div className="w-[760px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
|
||||
|
||||
@@ -1,10 +1,60 @@
|
||||
import { Progress } from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
function ProgressCard({
|
||||
accent,
|
||||
label,
|
||||
note,
|
||||
pattern = "linear",
|
||||
segmentCount,
|
||||
tone = "default",
|
||||
value,
|
||||
variant = "default"
|
||||
}: {
|
||||
accent: string;
|
||||
label: string;
|
||||
note: string;
|
||||
pattern?: "linear" | "segmented";
|
||||
segmentCount?: number;
|
||||
tone?: "default" | "subtle";
|
||||
value: number | null;
|
||||
variant?: "default" | "success" | "warning" | "destructive";
|
||||
}) {
|
||||
const displayValue = value == null ? "Syncing" : `${value}%`;
|
||||
|
||||
return (
|
||||
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-[var(--shadow-sm)]">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<p
|
||||
className="text-xs uppercase tracking-[var(--tracking-caps)]"
|
||||
style={{ color: accent }}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">{note}</p>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[var(--color-foreground)]">{displayValue}</span>
|
||||
</div>
|
||||
<Progress
|
||||
aria-label={label}
|
||||
className="mt-4"
|
||||
pattern={pattern}
|
||||
segmentCount={segmentCount}
|
||||
tone={tone}
|
||||
value={value}
|
||||
variant={variant}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Components/Progress",
|
||||
component: Progress,
|
||||
args: {
|
||||
pattern: "linear",
|
||||
segmentCount: 24,
|
||||
size: "md",
|
||||
value: 64,
|
||||
variant: "default"
|
||||
@@ -17,6 +67,18 @@ const meta = {
|
||||
control: "radio",
|
||||
options: ["sm", "md", "lg"]
|
||||
},
|
||||
pattern: {
|
||||
control: "radio",
|
||||
options: ["linear", "segmented"]
|
||||
},
|
||||
segmentCount: {
|
||||
control: {
|
||||
type: "range",
|
||||
min: 6,
|
||||
max: 36,
|
||||
step: 1
|
||||
}
|
||||
},
|
||||
tone: {
|
||||
control: "radio",
|
||||
options: ["default", "subtle"]
|
||||
@@ -35,6 +97,12 @@ const meta = {
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Progress is the system's inline status channel for work that is actively advancing inside the current view. Use the default linear pattern for classic completion bars, or switch to the segmented pattern when a dashboard metric needs a more discrete meter like rollout targets, cost reduction goals, and operational scorecards."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
@@ -45,35 +113,194 @@ export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {
|
||||
render: (args) => <Progress aria-label="Upload progress" className="w-[320px]" {...args} />
|
||||
render: (args) => (
|
||||
<div className="w-[380px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
|
||||
Asset Upload
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)]">
|
||||
Publishing release visuals
|
||||
</h3>
|
||||
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
Use the same component for classic uploads and segmented dashboard meters by swapping
|
||||
only the pattern instead of reaching for a separate one-off implementation.
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-[var(--radius-full)] bg-[color-mix(in_oklch,var(--color-primary-container)_70%,white_30%)] px-3 py-1 text-sm font-medium text-[var(--color-on-primary-container)]">
|
||||
{args.value}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress aria-label="Upload progress" className="mt-5" {...args} />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Variants: Story = {
|
||||
export const Patterns: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[360px] gap-4">
|
||||
<Progress aria-label="Primary progress" value={42} variant="default" />
|
||||
<Progress aria-label="Success progress" value={74} variant="success" />
|
||||
<Progress aria-label="Warning progress" value={58} variant="warning" />
|
||||
<Progress aria-label="Destructive progress" value={86} variant="destructive" />
|
||||
<div className="grid w-[720px] gap-4 md:grid-cols-2">
|
||||
<ProgressCard
|
||||
accent="var(--color-primary)"
|
||||
label="Release Assets"
|
||||
note="The marketing bundle is halfway through the upload queue."
|
||||
value={46}
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="var(--color-foreground)"
|
||||
label="Operational Cost Reduction"
|
||||
note="Segmented meters work better for dashboard targets where discrete progress reads faster than a single slab."
|
||||
pattern="segmented"
|
||||
segmentCount={24}
|
||||
tone="subtle"
|
||||
value={42}
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="color-mix(in oklch, var(--color-success) 78%, var(--color-foreground))"
|
||||
label="Audience Sync"
|
||||
note="Subscriber segments are reconciling against the latest CRM export."
|
||||
tone="subtle"
|
||||
value={74}
|
||||
variant="success"
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="color-mix(in oklch, var(--color-warning) 72%, var(--color-foreground))"
|
||||
label="Compliance Review"
|
||||
note="A few rule checks are still pending before the rollout can advance."
|
||||
value={58}
|
||||
variant="warning"
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="var(--color-destructive)"
|
||||
label="Recovery Job"
|
||||
note="The pipeline is retrying a failed package publish after signature drift."
|
||||
tone="subtle"
|
||||
value={91}
|
||||
variant="destructive"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const States: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-[360px] gap-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Determinate</p>
|
||||
<Progress aria-label="Determinate progress" value={68} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Indeterminate</p>
|
||||
<Progress aria-label="Indeterminate progress" value={null} variant="success" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Complete</p>
|
||||
<Progress aria-label="Complete progress" value={100} variant="success" />
|
||||
<div className="grid w-[720px] gap-4 md:grid-cols-3">
|
||||
<ProgressCard
|
||||
accent="var(--color-primary)"
|
||||
label="Determinate"
|
||||
note="Linear progress remains the best fit for straightforward task completion."
|
||||
value={68}
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="var(--color-foreground)"
|
||||
label="Segmented Goal"
|
||||
note="The segmented pattern makes discrete target progress easier to scan in dense dashboards."
|
||||
pattern="segmented"
|
||||
segmentCount={20}
|
||||
tone="subtle"
|
||||
value={42}
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="color-mix(in oklch, var(--color-success) 78%, var(--color-foreground))"
|
||||
label="Complete"
|
||||
note="Completion stays quiet and stable instead of introducing a second celebratory treatment."
|
||||
value={100}
|
||||
variant="success"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Anatomy: Story = {
|
||||
render: () => (
|
||||
<div className="w-[760px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Progress anatomy
|
||||
</p>
|
||||
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
The public styling contract stays intentionally small: a root track and one indicator
|
||||
layer, with the data attributes carrying the meaningful state.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-[var(--radius-md)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-5">
|
||||
<div className="grid gap-4">
|
||||
<Progress aria-label="Progress anatomy example" size="lg" value={68} />
|
||||
<Progress
|
||||
aria-label="Progress anatomy segmented example"
|
||||
pattern="segmented"
|
||||
segmentCount={20}
|
||||
size="lg"
|
||||
tone="subtle"
|
||||
value={42}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
<p>
|
||||
<code className="text-[var(--color-foreground)]">data-slot="root"</code> is the tonal
|
||||
track. It exposes <code className="text-[var(--color-foreground)]">data-size</code>,{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-pattern</code>,{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-tone</code>,{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-variant</code>, and{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-state</code> so consumers can
|
||||
respond to contract-level state instead of incidental class names.
|
||||
</p>
|
||||
<p>
|
||||
<code className="text-[var(--color-foreground)]">data-slot="indicator"</code> is the
|
||||
filled value layer for the linear pattern. It mirrors pattern, size, variant, and
|
||||
state so tests and downstream themes can target the active bar without reaching into
|
||||
private markup.
|
||||
</p>
|
||||
<p>
|
||||
In segmented mode, <code className="text-[var(--color-foreground)]">data-slot="segments"</code>{" "}
|
||||
wraps the repeated meter cells and each{" "}
|
||||
<code className="text-[var(--color-foreground)]">data-slot="segment"</code> exposes
|
||||
active state for dashboard-style styling and assertions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Motion: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Determinate values should feel calm and weighted, with the filled slab scaling into place instead of animating layout width. Indeterminate progress keeps a restrained sweep in interactive mode, then falls back to a static reading when reduced or static motion is active."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[720px] gap-4 md:grid-cols-3">
|
||||
<ProgressCard
|
||||
accent="var(--color-primary)"
|
||||
label="Determinate"
|
||||
note="A known value keeps the indicator anchored and scales the filled slab instead of animating layout width."
|
||||
value={68}
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="var(--color-foreground)"
|
||||
label="Segmented Meter"
|
||||
note="Discrete bars stay crisp at a glance and match dashboard KPI treatments."
|
||||
pattern="segmented"
|
||||
segmentCount={24}
|
||||
tone="subtle"
|
||||
value={42}
|
||||
/>
|
||||
<ProgressCard
|
||||
accent="color-mix(in oklch, var(--color-success) 78%, var(--color-foreground))"
|
||||
label="Segmented Indeterminate"
|
||||
note="Unknown duration shifts the segmented pattern into a staggered pulse instead of pretending there is a precise width."
|
||||
pattern="segmented"
|
||||
segmentCount={20}
|
||||
tone="subtle"
|
||||
value={null}
|
||||
variant="success"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { Field, FieldControl, FieldDescription, FieldError, Label, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "@ai-ui/ui";
|
||||
import {
|
||||
Field,
|
||||
FieldControl,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
async function waitForCondition(
|
||||
@@ -23,6 +37,12 @@ const meta = {
|
||||
title: "Components/Select",
|
||||
component: Select,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Select is the compact choice surface for a fixed option set. It should feel lighter than Combobox, but it should still share the same motion language: a restrained rise on open, adhesive highlighted rows, and a selected state that reads as a settled tonal slab instead of a hard admin-menu snap."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
@@ -34,7 +54,7 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {
|
||||
render: () => (
|
||||
<div className="w-[320px]">
|
||||
<div className="w-[360px]">
|
||||
<Select defaultValue="editorial">
|
||||
<SelectTrigger aria-label="Review lane">
|
||||
<SelectValue placeholder="Choose a review lane" />
|
||||
@@ -44,7 +64,10 @@ export const Playground: Story = {
|
||||
<SelectLabel>Review lane</SelectLabel>
|
||||
<SelectItem value="editorial">Editorial review</SelectItem>
|
||||
<SelectItem value="design">Design review</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectLabel>Specialist lane</SelectLabel>
|
||||
<SelectItem value="legal">Legal review</SelectItem>
|
||||
<SelectItem value="ops">Operations review</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -111,3 +134,49 @@ export const WithField: Story = {
|
||||
</Field>
|
||||
)
|
||||
};
|
||||
|
||||
export const Motion: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Open the list and move through the options with the pointer or keyboard. The menu should fade and rise as one surface, highlighted rows should glide lightly instead of snapping, and the checked row should feel a touch more settled than the transient highlight."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid w-[420px] gap-4 rounded-[var(--radius-xl)] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-surface)_78%,white_22%),color-mix(in_oklch,var(--color-surface-container-low)_86%,white_14%))] p-6 shadow-[var(--shadow-sm)]">
|
||||
<div className="grid gap-2">
|
||||
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-primary)]">
|
||||
Motion review
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
|
||||
Keep Select lighter than Combobox, but not mechanically flat.
|
||||
</h3>
|
||||
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
This surface is still a simple fixed-choice select. The motion should clarify focus and
|
||||
settled selection without turning the menu into an animated showcase.
|
||||
</p>
|
||||
</div>
|
||||
<Select defaultValue="design">
|
||||
<SelectTrigger aria-label="Motion review lane">
|
||||
<SelectValue placeholder="Choose a review lane" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Core lanes</SelectLabel>
|
||||
<SelectItem value="editorial">Editorial review</SelectItem>
|
||||
<SelectItem value="design">Design review</SelectItem>
|
||||
<SelectItem value="engineering">Engineering review</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Specialist lanes</SelectLabel>
|
||||
<SelectItem value="legal">Legal review</SelectItem>
|
||||
<SelectItem value="ops">Operations review</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Tabs is the system's in-place content switcher for peer views that belong to the same context. It exposes stable slots for the root, list, triggers, and panels, supports orientation changes, and keeps active-state styling on the trigger rather than moving users into a new page."
|
||||
"Tabs is the system's in-place content switcher for peer views that belong to the same context. It exposes stable slots for the root, list, triggers, and panels, supports orientation changes, and now uses a shared active pill so state changes feel like one continuous surface sliding between peer views instead of isolated button toggles."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
@@ -104,6 +104,18 @@ export const Anatomy: Story = {
|
||||
)
|
||||
};
|
||||
|
||||
export const Motion: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"The active state now uses a shared pill that glides between triggers with restrained timing. It should feel like one calm slab moving across the control, not like each tab is popping independently."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => <ReleaseTabs includeDisabled />
|
||||
};
|
||||
|
||||
export const Accessibility: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
@@ -57,7 +57,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Tooltip is the system's smallest assistive overlay. Use it for terse clarification, shortcut hints, or icon-only controls that benefit from a compact label on hover and focus. If the content needs interaction or more than a sentence, switch to popover instead."
|
||||
"Tooltip is the system's smallest assistive overlay. Use it for terse clarification, shortcut hints, or icon-only controls that benefit from a compact label on hover and focus. It uses a short rise-and-fade so the label still feels spatial without carrying the heavier panel motion reserved for popovers and dialogs. If the content needs interaction or more than a sentence, switch to popover instead."
|
||||
}
|
||||
},
|
||||
layout: "centered"
|
||||
|
||||
Reference in New Issue
Block a user