feat(ui): expand workflow-ready components

This commit is contained in:
2026-03-20 18:11:48 +08:00
parent 36822f05e0
commit a8c1d3f256
27 changed files with 1562 additions and 85 deletions
@@ -62,6 +62,83 @@ function ControlledDemo() {
);
}
function RecentAndSuggestedDemo() {
const [value, setValue] = useState("");
const items = [
{
value: "recent-legal",
label: "Legal review",
group: "Recent",
description: "Last used in yesterdays policy update."
},
{
value: "recent-design",
label: "Design review",
group: "Recent",
description: "Common pick for UI launches."
},
...teamItems
];
return (
<div className="grid w-[420px] gap-4">
<Combobox
aria-label="Suggested routing team"
emptyMessage={(query) => `No team named “${query}”. Create a custom routing lane instead.`}
footer={
<div className="flex items-center justify-between gap-3">
<p className="text-xs text-[var(--color-muted-foreground)]">
Need a specialist lane?
</p>
<Button size="sm" variant="ghost">
Create lane
</Button>
</div>
}
items={items}
onValueChange={setValue}
searchPlaceholder="Search recent and suggested teams"
value={value}
/>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 text-sm text-[var(--color-muted-foreground)] shadow-[var(--shadow-xs)]">
Current routing lane:{" "}
<span className="font-medium text-[var(--color-foreground)]">
{value || "No lane selected"}
</span>
</div>
</div>
);
}
function AsyncResultsDemo() {
const [searchValue, setSearchValue] = useState("");
const trimmedSearch = searchValue.trim().toLowerCase();
const isSearching = trimmedSearch.length > 0 && trimmedSearch.length < 3;
return (
<div className="grid w-[420px] gap-4">
<Combobox
aria-label="Async routing search"
emptyMessage={(query) =>
`No routing lane matched “${query}”. Try a broader keyword or create a new lane.`
}
items={[...teamItems]}
loading={isSearching}
loadingMessage="Searching routing lanes…"
onSearchValueChange={setSearchValue}
searchPlaceholder="Type at least 3 characters"
searchValue={searchValue}
/>
<p className="m-0 text-sm leading-6 text-[var(--color-muted-foreground)]">
This pattern is useful when the results come from an API and you need a clear
transition between loading, empty, and selectable states.
</p>
</div>
);
}
function LaunchRoutingForm() {
const [submitted, setSubmitted] = useState<Record<string, string> | null>(null);
const form = useForm<{ team: string }>({
@@ -158,6 +235,14 @@ export const Controlled: Story = {
render: () => <ControlledDemo />
};
export const RecentAndSuggested: Story = {
render: () => <RecentAndSuggestedDemo />
};
export const AsyncResults: Story = {
render: () => <AsyncResultsDemo />
};
export const WithForm: Story = {
render: () => <LaunchRoutingForm />
};
+86 -1
View File
@@ -55,13 +55,74 @@ function InlineCommandShowcase() {
);
}
function OperationsWorkbenchShowcase() {
return (
<Command
className="w-[560px]"
footer={
<div className="flex items-center justify-between gap-3">
<p className="m-0 text-xs text-[var(--color-muted-foreground)]">
Tip: press <kbd className="rounded border px-1.5 py-0.5">Enter</kbd> to run the highlighted action.
</p>
<Button size="sm" variant="ghost">
Manage actions
</Button>
</div>
}
label="Operations workbench"
loop
>
<CommandInput placeholder="Search releases, docs, and recent work" />
<CommandList>
<CommandEmpty>No matching actions.</CommandEmpty>
<CommandGroup heading="Recent">
<CommandItem keywords={["launch", "release"]} value="recent-launch-review">
Launch review
<CommandShortcut>R</CommandShortcut>
</CommandItem>
<CommandItem keywords={["blocked", "queue"]} value="recent-blocked-queue">
Blocked queue
<CommandShortcut>B</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Suggestions">
<CommandItem keywords={["docs", "stories"]} value="docs-storybook">
Open Storybook docs
<CommandShortcut>G D</CommandShortcut>
</CommandItem>
<CommandItem disabled value="compliance-review">
Compliance review locked
<CommandShortcut>Locked</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
);
}
function DialogCommandShowcase() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button onClick={() => setOpen(true)}>Open command palette</Button>
<CommandDialog onOpenChange={setOpen} open={open}>
<CommandDialog
description="Jump to docs, recent launches, and operational shortcuts without leaving the current view."
footer={
<div className="flex items-center justify-between gap-3">
<p className="m-0 text-xs text-[var(--color-muted-foreground)]">
Need a new action? Add it to the workspace command registry.
</p>
<Button size="sm" variant="ghost">
Manage shortcuts
</Button>
</div>
}
onOpenChange={setOpen}
open={open}
title="Workspace command palette"
>
<CommandInput placeholder="Type a command or search" />
<CommandList>
<CommandEmpty>No commands available.</CommandEmpty>
@@ -87,6 +148,22 @@ function DialogCommandShowcase() {
);
}
function LoadingResultsShowcase() {
return (
<Command
className="w-[520px]"
label="Remote workspace search"
loading
loadingMessage="Searching remote workspace actions…"
>
<CommandInput placeholder="Search actions from all workspaces" />
<CommandList>
<CommandEmpty>No commands available.</CommandEmpty>
</CommandList>
</Command>
);
}
const meta = {
title: "Components/Command",
component: InlineCommandShowcase,
@@ -104,6 +181,14 @@ export const Playground: Story = {
render: () => <InlineCommandShowcase />
};
export const OperationsWorkbench: Story = {
render: () => <OperationsWorkbenchShowcase />
};
export const DialogPalette: Story = {
render: () => <DialogCommandShowcase />
};
export const LoadingResults: Story = {
render: () => <LoadingResultsShowcase />
};
@@ -128,6 +128,7 @@ const routingColumns: DataTableColumn<RoutingLaneRow>[] = [
</div>
),
header: "Lane",
hideable: false,
id: "lane",
sortable: true,
width: "18rem"
@@ -141,6 +142,7 @@ const routingColumns: DataTableColumn<RoutingLaneRow>[] = [
</div>
),
header: "Owner",
hideable: true,
id: "owner",
sortable: true,
width: "15rem"
@@ -166,6 +168,7 @@ const routingColumns: DataTableColumn<RoutingLaneRow>[] = [
</div>
),
header: "Signal",
hideable: true,
id: "state",
sortable: true,
width: "12rem"
@@ -178,6 +181,7 @@ const routingColumns: DataTableColumn<RoutingLaneRow>[] = [
</p>
),
header: "Routing note",
hideable: true,
id: "note",
width: "34rem"
}
@@ -237,6 +241,46 @@ 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="flex flex-wrap items-center gap-2">
<Badge size="sm" tone={getStateTone(row.state)} variant="outline">
{row.state}
</Badge>
<Badge size="sm" variant="outline">
{row.lane}
</Badge>
<Badge size="sm" variant="outline">
{row.audience}
</Badge>
</div>
<p className="text-sm leading-6 text-[var(--color-foreground)]">{row.note}</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-background)] p-4">
<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">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Next gate
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">{row.nextGate}</p>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
Signal score {row.signalScore}
</p>
</div>
</div>
</div>
);
}
function DataTablePlayground() {
const [filter, setFilter] = useState<RoutingFilter>("all");
const [searchValue, setSearchValue] = useState("");
@@ -288,6 +332,7 @@ function DataTablePlayground() {
<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)]">
<DataTable
columns={routingColumns}
defaultDensity="comfortable"
empty={<RoutingEmptyState onReset={resetView} />}
enableSelection
getRowId={(row) => row.id}
@@ -296,6 +341,7 @@ function DataTablePlayground() {
onSortingChange={setSorting}
pageSize={3}
pageSizeOptions={[3, 5]}
renderRowDetails={(row) => <RoutingLaneDetail row={row} />}
renderRowActions={(row) => (
<Button
size="sm"
@@ -308,6 +354,10 @@ function DataTablePlayground() {
</Button>
)}
rows={visibleRows}
rowDetailsDescription={(row) =>
`${row.lane} routing handoff for ${row.audience.toLowerCase()}`
}
rowDetailsTitle={(row) => `${row.lane} lane detail`}
searchLabel="Search routing lanes"
searchPlaceholder="Search lanes, owners, and notes"
searchValue={searchValue}
@@ -16,6 +16,34 @@ import {
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
function StatusIcon() {
return (
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
<path
d="M3 8.5 6.25 11.75 13 5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
);
}
function LayersIcon() {
return (
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
<path
d="m8 2 6 3.5L8 9 2 5.5 8 2Zm0 5 6 3.5L8 14l-6-3.5L8 7Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.3"
/>
</svg>
);
}
async function waitForCondition(
predicate: () => boolean,
message: string,
@@ -46,31 +74,72 @@ function ReleaseMenu({ triggerLabel = "Open menu" }: ReleaseMenuProps) {
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Launch actions</DropdownMenuLabel>
<DropdownMenuItem>
<DropdownMenuItem
description="Open the latest rollout summary and reviewer comments."
leading={<StatusIcon />}
shortcut="R"
>
Review summary
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem
description="Copy the current branch preview and release notes."
leading={<LayersIcon />}
shortcut="S"
>
Share preview
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<DropdownMenuItem
description="Disabled while the current verification step is still running."
disabled
shortcut="⌘R"
>
Retry checks
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Notify stakeholders</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked
description="Post a summary to launch-updates after approval."
shortcut="N"
>
Notify stakeholders
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value="staged">
<DropdownMenuRadioItem value="staged">Staged rollout</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="global">Global rollout</DropdownMenuRadioItem>
<DropdownMenuRadioItem
description="Ship to 10% of traffic first."
shortcut="1"
value="staged"
>
Staged rollout
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
description="Ship globally as soon as the release is approved."
shortcut="2"
value="global"
>
Global rollout
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger inset>More actions</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Duplicate release</DropdownMenuItem>
<DropdownMenuItem variant="destructive">Archive release</DropdownMenuItem>
<DropdownMenuSubTrigger
description="Secondary actions that should stay grouped away from the primary flow."
inset
>
More actions
</DropdownMenuSubTrigger>
<DropdownMenuSubContent size="lg">
<DropdownMenuItem
description="Create a sibling release with the same routing and metadata."
>
Duplicate release
</DropdownMenuItem>
<DropdownMenuItem
description="Move the release out of the active workspace without deleting it."
variant="destructive"
>
Archive release
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
@@ -137,7 +206,7 @@ export const States: Story = {
docs: {
description: {
story:
"Open the menu to inspect the checked checkbox item, the selected radio item, a disabled action, the inset submenu trigger, and the destructive nested action."
"Open the menu to inspect richer action rows with descriptions, leading icons, inline shortcuts, checked and selected states, a disabled row, an inset submenu trigger, and a destructive nested action."
}
}
},
@@ -149,6 +218,74 @@ export const States: Story = {
)
};
export const ContextPanels: Story = {
parameters: {
docs: {
description: {
story:
"These examples show common product-style menu compositions: a release action menu and a denser context menu for row-level operations."
}
}
},
render: () => (
<div className="grid w-[920px] gap-4 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
Release actions
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Use richer labels when the command needs more context than a single verb.
</p>
<div className="mt-6">
<ReleaseMenu triggerLabel="Open release actions" />
</div>
</div>
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
Row context menu
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Denser menus can still remain readable when every item communicates hierarchy,
risk, and keyboard hints clearly.
</p>
<div className="mt-6">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary">Open row menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent size="xl">
<DropdownMenuLabel inset>Task 17A · routing</DropdownMenuLabel>
<DropdownMenuItem
description="Move the task into the launch lane without changing ownership."
leading={<StatusIcon />}
shortcut="M"
>
Move to launch lane
</DropdownMenuItem>
<DropdownMenuItem
description="Open the full thread timeline for the assigned reviewer."
leading={<LayersIcon />}
shortcut="T"
>
Open thread timeline
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
description="Escalate this task into the blocked queue and notify the operator."
variant="destructive"
shortcut="E"
>
Escalate blocker
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
)
};
export const Anatomy: Story = {
render: () => (
<div className="w-[720px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
@@ -172,7 +309,10 @@ export const Anatomy: Story = {
<code className="text-[var(--color-foreground)]">data-slot="label"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot="separator"</code>, and{" "}
<code className="text-[var(--color-foreground)]">data-slot="icon"</code> support
grouping, dividers, and selection markers.
grouping, dividers, and selection markers, while{" "}
<code className="text-[var(--color-foreground)]">data-slot="description"</code> and{" "}
<code className="text-[var(--color-foreground)]">data-slot="leading"</code> support
denser contextual rows.
</p>
</div>
</div>
@@ -26,27 +26,40 @@ function EmptyStateGlyph() {
}
function ReleaseEmptyState({
actionsLayout = "inline",
align = "center",
description = "Adjust the current filters or create a new release to start routing work.",
eyebrow = "No results",
layout = "default",
mediaSize = "default",
tone = "default",
title = "No matching releases"
}: {
actionsLayout?: "inline" | "stack";
align?: "center" | "start";
description?: string;
eyebrow?: string;
layout?: "compact" | "default" | "split";
mediaSize?: "compact" | "default" | "hero";
title?: string;
tone?: "default" | "subtle" | "accent";
}) {
return (
<EmptyState className="w-[min(100%,42rem)]" tone={tone}>
<EmptyStateMedia>
<EmptyState
align={align}
className="w-[min(100%,42rem)]"
layout={layout}
tone={tone}
>
<EmptyStateMedia size={mediaSize}>
<EmptyStateGlyph />
</EmptyStateMedia>
<EmptyStateHeader>
<EmptyStateHeader align={align}>
<EmptyStateEyebrow>{eyebrow}</EmptyStateEyebrow>
<EmptyStateTitle>{title}</EmptyStateTitle>
<EmptyStateDescription>{description}</EmptyStateDescription>
</EmptyStateHeader>
<EmptyStateActions>
<EmptyStateActions layout={actionsLayout}>
<Button>Create release</Button>
<Button variant="ghost">Reset filters</Button>
</EmptyStateActions>
@@ -89,6 +102,55 @@ export const Scenarios: Story = {
)
};
export const CompactQueue: Story = {
parameters: {
docs: {
description: {
story:
"Use the compact layout when an empty state lives inside a dense operational card, list, or queue panel and should preserve surrounding information density."
}
}
},
render: () => (
<div className="w-[720px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<ReleaseEmptyState
actionsLayout="stack"
align="start"
description="The global queue is clear. Create a task template now or wait for the next dispatch cycle."
eyebrow="Queue clear"
layout="compact"
mediaSize="compact"
title="No blocked work right now"
tone="subtle"
/>
</div>
)
};
export const SplitWorkspace: Story = {
parameters: {
docs: {
description: {
story:
"Use the split layout when the empty state needs more narrative weight, richer actions, or a larger visual anchor inside a dashboard or setup surface."
}
}
},
render: () => (
<div className="w-[940px] rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-background)] p-6">
<ReleaseEmptyState
align="start"
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"
layout="split"
mediaSize="hero"
title="This command center needs its first operator"
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)]">
@@ -117,6 +179,13 @@ export const Anatomy: Story = {
<code className="text-[var(--color-foreground)]">data-tone</code> exposes whether the
surface stays neutral, subtle, or accent-led.
</p>
<p>
<code className="text-[var(--color-foreground)]">layout</code>,{" "}
<code className="text-[var(--color-foreground)]">align</code>,{" "}
<code className="text-[var(--color-foreground)]">size</code>, and{" "}
<code className="text-[var(--color-foreground)]">actions layout</code> let the same
composition flex between dense cards and broader setup moments.
</p>
</div>
</div>
</div>
+168 -2
View File
@@ -1,5 +1,6 @@
import {
Button,
Input,
Popover,
PopoverArrow,
PopoverClose,
@@ -61,6 +62,111 @@ function SummaryPopover({
);
}
function InspectorPopover() {
return (
<Popover>
<PopoverTrigger asChild>
<Button>Open release inspector</Button>
</PopoverTrigger>
<PopoverContent padding="none" size="xl" side="right" className="overflow-hidden">
<div className="border-b border-[var(--color-border)] px-5 py-4">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Release inspector
</p>
<h3 className="mt-2 text-lg font-semibold text-[var(--color-foreground)]">
Rollout checklist
</h3>
<p className="mt-1 text-sm leading-6 text-[var(--color-muted-foreground)]">
Keep the surface anchored to the trigger while still supporting denser,
panel-like review content.
</p>
</div>
<div className="grid gap-4 px-5 py-4">
<div className="grid gap-1">
<span className="text-sm font-medium text-[var(--color-foreground)]">
Rollout owner
</span>
<Input defaultValue="ops@cadence.dev" readOnly />
</div>
<div className="grid gap-3 sm:grid-cols-2">
{[
["Checks", "12 passed"],
["Approvals", "2 waiting"],
["Scope", "10% traffic"],
["Alerts", "Muted"]
].map(([label, value]) => (
<div
key={label}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3"
>
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
{label}
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{value}
</p>
</div>
))}
</div>
</div>
<div className="flex justify-between gap-3 border-t border-[var(--color-border)] bg-[color-mix(in_oklch,var(--color-surface)_72%,white_28%)] px-5 py-4">
<PopoverClose asChild>
<Button size="sm" variant="ghost">
Close
</Button>
</PopoverClose>
<Button size="sm">Approve rollout</Button>
</div>
<PopoverArrow />
</PopoverContent>
</Popover>
);
}
function InlineComposerPopover() {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Assign reviewer</Button>
</PopoverTrigger>
<PopoverContent side="bottom" size="md" className="grid gap-4">
<div className="space-y-1">
<h3 className="text-base font-semibold text-[var(--color-foreground)]">
Reviewer routing
</h3>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
Lightweight form-like content can stay in a popover when the interaction is
single-purpose and fast to dismiss.
</p>
</div>
<div className="grid gap-3">
<div className="grid gap-1.5">
<label className="text-sm font-medium text-[var(--color-foreground)]">
Reviewer
</label>
<Input placeholder="Search reviewer" />
</div>
<div className="grid gap-1.5">
<label className="text-sm font-medium text-[var(--color-foreground)]">
Note
</label>
<Input defaultValue="Focus on launch risk and fallback copy." />
</div>
</div>
<div className="flex justify-end gap-2">
<PopoverClose asChild>
<Button size="sm" variant="ghost">
Cancel
</Button>
</PopoverClose>
<Button size="sm">Send request</Button>
</div>
<PopoverArrow />
</PopoverContent>
</Popover>
);
}
const meta = {
title: "Components/Popover",
component: Popover,
@@ -125,6 +231,27 @@ export const Sizes: Story = {
)
};
export const ComposedPanels: Story = {
parameters: {
docs: {
description: {
story:
"Use `padding=\"none\"` to compose richer inspector-style panels with custom header, content, and footer sections while keeping popover positioning and dismissal behavior."
}
}
},
render: () => (
<div className="grid w-[920px] gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<div className="rounded-[var(--radius-lg)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-8">
<InspectorPopover />
</div>
<div className="rounded-[var(--radius-lg)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-8">
<InlineComposerPopover />
</div>
</div>
)
};
export const Anatomy: Story = {
render: () => (
<div className="w-[700px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
@@ -132,11 +259,12 @@ export const Anatomy: Story = {
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Popover anatomy
</p>
<SummaryPopover triggerLabel="Preview popover structure" />
<InspectorPopover />
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>
<code className="text-[var(--color-foreground)]">data-slot="content"</code> wraps
the floating surface and exposes <code className="text-[var(--color-foreground)]">data-size</code>.
the floating surface and exposes <code className="text-[var(--color-foreground)]">data-size</code> and{" "}
<code className="text-[var(--color-foreground)]">data-padding</code>.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="arrow"</code> visually
@@ -170,3 +298,41 @@ export const Motion: Story = {
</div>
)
};
export const ContextualWorkflows: Story = {
parameters: {
docs: {
description: {
story:
"Popover works best for anchored context, lightweight review panels, and compact forms that should stay attached to a trigger instead of escalating to dialog."
}
}
},
render: () => (
<div className="grid w-[920px] gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold text-[var(--color-foreground)]">
Anchored release context
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Use a richer popover when the user needs more detail than a tooltip, but not a
full modal interruption.
</p>
<div className="mt-6">
<InspectorPopover />
</div>
</div>
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold text-[var(--color-foreground)]">
Quick assignment flow
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Single-purpose forms can stay lightweight and local to the trigger.
</p>
<div className="mt-6">
<InlineComposerPopover />
</div>
</div>
</div>
)
};