feat: add release baseline and workspace scene

This commit is contained in:
2026-03-19 19:32:25 +08:00
parent 132bb6961d
commit d5e4d5ece3
9 changed files with 2137 additions and 90 deletions
+51
View File
@@ -0,0 +1,51 @@
# Changesets In This Repo
This directory is the release-intent ledger for Cadence UI.
The repo is still an internal/private monorepo, so Changesets is used conservatively:
- track version intent for releasable workspace packages
- keep release notes attached to code changes
- avoid auto-committing release files
- avoid tagging private packages during early internal iteration
## What should get a changeset
Create a changeset when a change affects:
- `@ai-ui/ui`
- `@ai-ui/tokens`
Do not create a changeset for docs-only work in `@ai-ui/docs` unless that work ships alongside
a package change that already needs one. The docs app is ignored in the config on purpose.
## What kind of summary to write
Keep the summary short and consumer-facing:
- describe what changed
- mention the component, token, or contract surface
- avoid implementation detail unless it changes behavior
Good:
```md
Add the Sheet component for contextual side panels and bottom trays.
```
Less useful:
```md
Refactor dialog wrapper internals and update story coverage.
```
## Current release posture
The root repo is private and package publishing is not fully wired yet. Until the main release
flow is enabled, treat Changesets as the source of truth for:
- which package versions should move
- which changes deserve release notes
- which internal dependency bumps should be coordinated
See `docs/releasing.md` for the expected workflow around creating and consuming changesets.
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": false,
"commit": false,
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"fixed": [],
"linked": [],
"ignore": [
"@ai-ui/docs"
],
"privatePackages": {
"version": true,
"tag": false
}
}
+1
View File
@@ -9,4 +9,5 @@ coverage
.tmp-home
playwright-report
test-results
output
.DS_Store
+739
View File
@@ -0,0 +1,739 @@
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
EmptyState,
EmptyStateActions,
EmptyStateDescription,
EmptyStateEyebrow,
EmptyStateHeader,
EmptyStateMedia,
EmptyStateTitle,
Form,
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
Input,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
Switch,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Textarea,
Toast,
ToastAction,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
import { Controller, useForm } from "react-hook-form";
import { useState } from "react";
type RoutingValues = {
lane: string;
notifications: boolean;
ownerEmail: string;
summary: string;
};
type ReleaseWorkspaceProps = {
quietMode?: boolean;
};
const reviewerColumns = [
{
lane: "Editorial",
note: "Copy is locked. Waiting on final legal phrasing for the migration footnote.",
state: "Ready"
},
{
lane: "Engineering",
note: "Canary checks are green. Queue the 10% wave after route owners sign off.",
state: "Watching"
},
{
lane: "Support",
note: "No escalations yet. Macro pack and customer note are staged for handoff.",
state: "Quiet"
}
] as const;
const timelineStops = [
{
title: "Narrative lock",
window: "18:40",
note: "Release notes, migration callouts, and support macros align on one framing."
},
{
title: "10% canary",
window: "19:15",
note: "Routing lane owners watch live traffic, rollback metrics, and conversion deltas."
},
{
title: "Customer notice",
window: "20:05",
note: "Send the public note only after the queue remains stable for one quiet cycle."
}
] as const;
function SignalGlyph() {
return (
<div className="grid gap-2">
<div className="grid grid-cols-[0.8rem_2.8rem_1.4rem] gap-2">
<span className="h-4 rounded-[var(--radius-full)] bg-[var(--color-primary)]" />
<span className="h-4 rounded-[var(--radius-full)] bg-[var(--color-surface-strong)]" />
<span className="h-4 rounded-[var(--radius-full)] bg-[var(--color-accent)]" />
</div>
<div className="grid grid-cols-[5.2rem] 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>
);
}
function MetricPanel({
eyebrow,
tone = "default",
value,
children
}: {
children: React.ReactNode;
eyebrow: string;
tone?: "default" | "accent";
value: string;
}) {
return (
<div
className={[
"rounded-[var(--radius-lg)] border px-4 py-4 shadow-[var(--shadow-xs)]",
tone === "accent"
? "border-[color-mix(in_oklch,var(--color-primary)_30%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-primary)_10%,var(--color-card))]"
: "border-[var(--color-border)] bg-[color-mix(in_oklch,var(--color-card)_88%,white_12%)]"
].join(" ")}
>
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
{eyebrow}
</p>
<p className="mt-3 font-semibold tracking-[var(--tracking-tight)] text-[clamp(1.7rem,2vw,2.4rem)] text-[var(--color-foreground)]">
{value}
</p>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">{children}</p>
</div>
);
}
function ReleaseWorkspaceScene({ quietMode = false }: ReleaseWorkspaceProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const [toastOpen, setToastOpen] = useState(false);
const form = useForm<RoutingValues>({
defaultValues: {
lane: "engineering",
notifications: true,
ownerEmail: "routing@cadence.dev",
summary: "Hold the customer note until the support lane stays quiet for one full cycle."
}
});
return (
<ToastProvider swipeDirection="right">
<TooltipProvider delayDuration={120}>
<div className="min-h-screen overflow-hidden bg-[radial-gradient(circle_at_top_left,color-mix(in_oklch,var(--color-accent)_14%,transparent),transparent_34%),radial-gradient(circle_at_85%_18%,color-mix(in_oklch,var(--color-primary)_12%,transparent),transparent_28%),var(--color-background)] text-[var(--color-foreground)]">
<div className="mx-auto flex w-full max-w-[92rem] flex-col gap-8 px-6 py-8 sm:px-10 lg:gap-10 lg:px-12">
<section className="relative overflow-hidden rounded-[calc(var(--radius-xl)+0.35rem)] border border-[color-mix(in_oklch,var(--color-border)_78%,transparent)] bg-[linear-gradient(145deg,color-mix(in_oklch,var(--color-card)_84%,white_16%),color-mix(in_oklch,var(--color-surface)_88%,white_12%))] p-6 shadow-[var(--shadow-md)] sm:p-8">
<div className="absolute -right-16 top-0 h-40 w-40 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_14%,transparent)] blur-3xl" />
<div className="absolute bottom-0 left-0 h-px w-full bg-[linear-gradient(90deg,transparent,color-mix(in_oklch,var(--color-border-strong)_58%,transparent),transparent)]" />
<div className="relative grid gap-8 xl:grid-cols-[minmax(0,1.25fr)_20rem]">
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-3">
<Badge>March 24 / Controlled rollout</Badge>
<Badge variant="outline">Workspace / Ops editorial</Badge>
<Tooltip>
<TooltipTrigger asChild>
<button
className="inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-[color-mix(in_oklch,var(--color-primary)_30%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-primary)_12%,var(--color-card))] px-3 py-1 text-xs font-medium tracking-[var(--tracking-caps)] text-[var(--color-foreground)]"
type="button"
>
Risk 14
</button>
</TooltipTrigger>
<TooltipContent size="sm">
Risk remains contained because support is quiet and rollback guardrails are staged.
</TooltipContent>
</Tooltip>
</div>
<div className="max-w-4xl space-y-4">
<p className="text-sm uppercase tracking-[0.26em] text-[var(--color-muted-foreground)]">
Cadence / Release operations board
</p>
<h1
className="max-w-4xl font-semibold tracking-[var(--tracking-tight)] text-[clamp(2.7rem,5vw,5.4rem)]"
style={{
fontFamily: "var(--font-display)",
lineHeight: "0.94"
}}
>
The launch is calm enough to move fast, but only if routing stays disciplined.
</h1>
<p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
This workspace treats release ops like an editorial desk: one narrative,
one routing owner, one quiet signal loop. The goal is not to ship more
controls. The goal is to keep a high-stakes rollout readable.
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
onClick={() => {
setDialogOpen(true);
}}
>
Queue rollout wave
</Button>
<Button
variant="secondary"
onClick={() => {
setSheetOpen(true);
}}
>
Edit routing lane
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost">Signal blend</Button>
</PopoverTrigger>
<PopoverContent className="grid gap-3" side="bottom" size="sm">
<p className="text-sm font-medium text-[var(--color-foreground)]">
Current signal blend
</p>
<div className="grid gap-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>Canary checks: stable for 28 minutes.</p>
<p>Support queue: zero new escalations in the current pass.</p>
<p>Legal note: one copy footnote remains under review.</p>
</div>
<PopoverArrow />
</PopoverContent>
</Popover>
</div>
</div>
<div className="grid gap-3">
<MetricPanel eyebrow="Next gate" tone="accent" value="19:15">
Queue the first wave after engineering and support both stay green.
</MetricPanel>
<MetricPanel eyebrow="Reviewers" value="6 / 7">
Legal is the only lane still holding a sentence-level note.
</MetricPanel>
<MetricPanel eyebrow="Quiet window" value={quietMode ? "46m" : "18m"}>
{quietMode
? "The board is almost empty, which is when routing discipline matters most."
: "Support is quiet, but the customer note should stay staged until the next check."}
</MetricPanel>
</div>
</div>
</section>
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_21rem]">
<div className="grid gap-8">
<Tabs className="grid gap-6" defaultValue="overview">
<div className="flex flex-wrap items-center justify-between gap-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="routing">Routing</TabsTrigger>
<TabsTrigger value="audience">Audience</TabsTrigger>
</TabsList>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
One workspace for the story, the gate, and the next decision.
</p>
</div>
<TabsContent value="overview" className="grid gap-6">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.08fr)_minmax(18rem,0.92fr)]">
<article className="overflow-hidden rounded-[calc(var(--radius-lg)+0.15rem)] border border-[var(--color-border)] bg-[linear-gradient(180deg,color-mix(in_oklch,var(--color-card)_88%,white_12%),var(--color-card))] shadow-[var(--shadow-sm)]">
<div className="grid gap-6 p-6 sm:p-7">
<div className="flex flex-wrap 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)]">
Release narrative
</p>
<h2 className="text-2xl font-semibold tracking-[var(--tracking-tight)]">
The migration is technically ready, but the customer story should still feel measured.
</h2>
</div>
<Badge variant="solid">10% wave pending</Badge>
</div>
<p className="max-w-3xl text-sm leading-7 text-[var(--color-muted-foreground)]">
Engineering has reduced the technical risk. What remains is message
discipline: route the launch through one owner, keep the customer
note staged, and avoid turning a calm rollout into a noisy one.
</p>
<div className="grid gap-4 md:grid-cols-3">
{timelineStops.map((item) => (
<div
key={item.title}
className="grid gap-3 rounded-[var(--radius-md)] border border-[color-mix(in_oklch,var(--color-border)_84%,transparent)] bg-[color-mix(in_oklch,var(--color-background)_74%,white_26%)] p-4"
>
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-medium text-[var(--color-foreground)]">
{item.title}
</p>
<span className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
{item.window}
</span>
</div>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
{item.note}
</p>
</div>
))}
</div>
</div>
</article>
{quietMode ? (
<EmptyState className="h-full justify-center" tone="subtle">
<EmptyStateMedia>
<SignalGlyph />
</EmptyStateMedia>
<EmptyStateHeader>
<EmptyStateEyebrow>Quiet shift</EmptyStateEyebrow>
<EmptyStateTitle>No escalations are waiting on this desk.</EmptyStateTitle>
<EmptyStateDescription>
Keep the board sparse, hold the customer note, and use the next
quiet cycle to confirm routing before the wave is queued.
</EmptyStateDescription>
</EmptyStateHeader>
<EmptyStateActions>
<Button
onClick={() => {
setSheetOpen(true);
}}
>
Review routing
</Button>
<Button variant="ghost">Open checklist</Button>
</EmptyStateActions>
</EmptyState>
) : (
<Card>
<CardHeader>
<CardTitle>Support pulse</CardTitle>
<CardDescription>
The queue is calm, but not calm enough to go unsupervised.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-3">
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-medium text-[var(--color-foreground)]">
Customer note
</p>
<Badge>Staged</Badge>
</div>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Keep the note unpublished until the canary stays quiet for one
more cycle.
</p>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-medium text-[var(--color-foreground)]">
Macro pack
</p>
<Badge variant="outline">Ready</Badge>
</div>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Support copy aligns with the release narrative and rollback path.
</p>
</div>
</div>
<EmptyState tone="subtle">
<EmptyStateHeader>
<EmptyStateEyebrow>Escalations</EmptyStateEyebrow>
<EmptyStateTitle>No active escalations</EmptyStateTitle>
<EmptyStateDescription>
Stay disciplined anyway. Quiet boards are where rushed launches
usually create avoidable noise.
</EmptyStateDescription>
</EmptyStateHeader>
</EmptyState>
</CardContent>
</Card>
)}
</div>
</TabsContent>
<TabsContent value="routing" className="grid gap-6">
<div className="grid gap-4 md:grid-cols-3">
{reviewerColumns.map((column, index) => (
<Card
key={column.lane}
className={[
"relative overflow-hidden",
index === 1
? "border-[color-mix(in_oklch,var(--color-primary)_24%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-primary)_6%,var(--color-card))]"
: undefined
]
.filter(Boolean)
.join(" ")}
>
<CardHeader>
<div className="flex items-center justify-between gap-3">
<CardTitle>{column.lane}</CardTitle>
<Badge variant={index === 1 ? "solid" : "outline"}>
{column.state}
</Badge>
</div>
<CardDescription>{column.note}</CardDescription>
</CardHeader>
</Card>
))}
</div>
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="space-y-2">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Routing principle
</p>
<h2 className="text-2xl font-semibold tracking-[var(--tracking-tight)]">
Keep one owner visible, one fallback explicit, and one customer note staged.
</h2>
</div>
<Button
variant="secondary"
onClick={() => {
setSheetOpen(true);
}}
>
Adjust lane settings
</Button>
</div>
</article>
</TabsContent>
<TabsContent value="audience" className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_19rem]">
<Card>
<CardHeader>
<CardTitle>Audience framing</CardTitle>
<CardDescription>
The internal narrative should be more detailed than the external one.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
<p className="text-sm font-medium text-[var(--color-foreground)]">
Internal viewers
</p>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Mention rollback thresholds, reviewer routing, and support macro timing.
</p>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
<p className="text-sm font-medium text-[var(--color-foreground)]">
Customers
</p>
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
Keep the message short, calm, and reversible. Explain the value, not the pipeline.
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[color-mix(in_oklch,var(--color-surface)_90%,white_10%)]">
<CardHeader>
<CardTitle>Presenter note</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
If the launch feels quiet in this tab, that is success. The board should
only get louder when a decision actually changes.
</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<aside className="grid gap-4 self-start xl:sticky xl:top-6">
<Card className="bg-[color-mix(in_oklch,var(--color-card)_88%,white_12%)]">
<CardHeader>
<CardTitle>Wave checklist</CardTitle>
<CardDescription>
A calm rollout still needs a visible last-mile checklist.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-3">
{[
"Legal footnote approved",
"Support queue stays quiet for one more pass",
"Customer note remains staged",
"Fallback owner is visible in routing"
].map((item, index) => (
<div
key={item}
className="flex items-start gap-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] px-3 py-3"
>
<span
className={[
"mt-1 inline-flex size-4 shrink-0 rounded-full border",
index < 2
? "border-[var(--color-primary)] bg-[var(--color-primary)]"
: "border-[var(--color-border-strong)] bg-transparent"
].join(" ")}
/>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
{item}
</p>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Desk note</CardTitle>
<CardDescription>Operational restraint is the product here.</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>
A healthy release workspace should help someone decide what to do next in one
glance.
</p>
<p>
If the board starts repeating the same information in three places, the page
is becoming nervous.
</p>
</CardContent>
</Card>
</aside>
</div>
</div>
<Dialog onOpenChange={setDialogOpen} open={dialogOpen}>
<DialogContent size="sm">
<DialogHeader>
<DialogTitle>Queue the 10% wave?</DialogTitle>
<DialogDescription>
This sends the first controlled wave, not the full customer note. Use it only if
support stays quiet and the routing owner is live.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => {
setDialogOpen(false);
}}
>
Keep staged
</Button>
<Button
type="button"
onClick={() => {
setDialogOpen(false);
setToastOpen(true);
}}
>
Queue wave
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Sheet onOpenChange={setSheetOpen} open={sheetOpen}>
<SheetContent side="right" size="lg">
<SheetHeader>
<SheetTitle>Routing lane settings</SheetTitle>
<SheetDescription>
Keep the lane owner explicit and the customer note disciplined before the wave is
queued.
</SheetDescription>
</SheetHeader>
<Form {...form}>
<form
className="grid gap-5"
onSubmit={form.handleSubmit(() => {
setSheetOpen(false);
setToastOpen(true);
})}
>
<FormItem name="ownerEmail" required>
<FormLabel>Lane owner email</FormLabel>
<FormControl>
<Input
placeholder="routing@cadence.dev"
{...form.register("ownerEmail", {
required: "Lane owner email is required."
})}
/>
</FormControl>
<FormDescription>This person owns the final routing decision.</FormDescription>
<FormMessage />
</FormItem>
<Controller
control={form.control}
name="lane"
render={({ field }) => (
<FormItem name="lane">
<FormLabel>Primary lane</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger aria-label="Primary lane">
<SelectValue placeholder="Choose a lane" />
</SelectTrigger>
<SelectContent>
<SelectItem value="engineering">Engineering</SelectItem>
<SelectItem value="editorial">Editorial</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
The highlighted lane stays visible on the release desk.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormItem name="summary">
<FormLabel>Routing note</FormLabel>
<FormControl>
<Textarea
placeholder="Capture the condition that must remain true before queueing."
{...form.register("summary")}
/>
</FormControl>
<FormDescription>
Use this for the one sentence the next reviewer needs to trust.
</FormDescription>
</FormItem>
<Controller
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem name="notifications">
<div className="flex items-start justify-between gap-4 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
<div className="space-y-1">
<FormLabel>Escalation digest</FormLabel>
<FormDescription>
Send a quiet-cycle summary to the routing owner before queueing.
</FormDescription>
</div>
<FormControl className="gap-0">
<Switch
aria-label="Escalation digest"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</FormItem>
)}
/>
<SheetFooter>
<Button
type="button"
variant="ghost"
onClick={() => {
setSheetOpen(false);
}}
>
Cancel
</Button>
<Button type="submit">Save routing</Button>
</SheetFooter>
</form>
</Form>
</SheetContent>
</Sheet>
<Toast onOpenChange={setToastOpen} open={toastOpen} variant="success">
<ToastTitle>Routing updated</ToastTitle>
<ToastDescription>
The lane owner and release note are staged for the next quiet cycle.
</ToastDescription>
<ToastAction altText="Open desk">Open desk</ToastAction>
<ToastClose />
</Toast>
<ToastViewport />
</div>
</TooltipProvider>
</ToastProvider>
);
}
const meta = {
title: "Scenes/Release Workspace",
component: ReleaseWorkspaceScene,
parameters: {
docs: {
description: {
component:
"Release Workspace is a realistic composition story that shows how Cadence UI behaves when the design system stops being a component shelf and becomes an actual operations surface. It combines layered decisions, contextual overlays, empty states, routing controls, and transient feedback in one believable release desk."
}
},
layout: "fullscreen"
},
tags: ["autodocs"]
} satisfies Meta<typeof ReleaseWorkspaceScene>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const QuietShift: Story = {
args: {
quietMode: true
}
};
+144
View File
@@ -0,0 +1,144 @@
# Releasing
This repo is not fully on a public-package release pipeline yet, but it is ready to use
Changesets as the canonical record of release intent.
The current goal is modest:
- version `@ai-ui/ui` and `@ai-ui/tokens` deliberately
- keep release notes attached to the changes that caused them
- avoid inventing ad hoc version bumps when the component system evolves
## Current assumptions
- The repository root is private.
- Workspace packages currently use explicit package versions even when they are not yet published.
- `@ai-ui/docs` is a consumer app, not a releasable package, so it is ignored by Changesets.
- Publishing mechanics, registry credentials, and CI release automation are still to be added.
Because of that, this baseline is intentionally conservative.
## Packages in scope
Changesets should currently be used for:
- `@ai-ui/ui`
- `@ai-ui/tokens`
Changes to the docs app alone usually do not need a changeset.
## When to create a changeset
Create a changeset when a merged change affects any consumer-facing surface of a releasable package:
- new components or slots
- changed props or variants
- token additions or token behavior changes
- accessibility changes that alter behavior
- bug fixes that consumers will notice
- breaking contract changes
You can usually skip a changeset for:
- docs-only edits
- test-only edits
- internal refactors with no consumer-visible behavior change
## Versioning guidance
Use semver pragmatically:
- `patch`: bug fixes, QA-only behavior fixes, docs fixes bundled with a small behavior correction
- `minor`: new components, new props, new variants, new tokens, additive API work
- `major`: breaking prop changes, renamed slots or states, removed variants, contract changes that require consumer updates
When in doubt, bias toward `minor` over underselling a visible new surface.
## Recommended workflow
### 1. Make the code change
Complete the implementation, docs, and tests first.
At minimum, run:
```bash
pnpm lint
pnpm typecheck
pnpm test
```
Use the docs and smoke checks when the change touches behavior-heavy UI:
```bash
pnpm build:docs
pnpm test:e2e:smoke
```
### 2. Create a changeset
After the change is ready, create a changeset entry for the affected package or packages.
Once `@changesets/cli` is installed in the repo, the intended command is:
```bash
pnpm changeset
```
The generated markdown file should:
- select the impacted package(s)
- choose the correct version bump type
- include a short consumer-facing summary
### 3. Review internal dependency impact
This repo is configured to update internal dependencies with a patch bump.
That means if `@ai-ui/tokens` changes and `@ai-ui/ui` depends on it, the versioning step should
keep the dependency graph coherent without requiring manual package edits.
### 4. Version the packages
When it is time to cut a release, run the Changesets version step:
```bash
pnpm changeset version
```
That step is expected to:
- update package versions
- update internal dependency ranges where needed
- consume the pending changeset files
Review the resulting package diffs carefully before merging.
### 5. Publish or tag
Publishing is not fully wired in this repo yet, so treat this step as pending infrastructure.
The intended future flow is:
```bash
pnpm changeset publish
```
But until registry, auth, and CI behavior are explicit, do not assume publish is automated.
## Notes for maintainers
- Keep `packages/ui/src/index.ts` and package exports aligned with any release-worthy surface.
- If a component lands without docs or tests, it should not move toward release yet.
- Prefer one clear changeset per consumer-visible change rather than bundling unrelated work.
- If a PR contains both infra and component work, separate the release notes so consumers can
understand what actually changed.
## Main-thread follow-up still needed
This baseline adds config and process docs only. To make it operational, the repo still needs:
- `@changesets/cli` added to root `devDependencies`
- root scripts such as `changeset`, `version-packages`, or `release`
- a decision on whether private packages should be published, mirrored internally, or versioned only
- CI wiring for version PRs and/or publish jobs
+439
View File
@@ -0,0 +1,439 @@
# RFC: DataTable
## Status
Proposed
## Summary
`DataTable` is the next high-value advanced pattern for `@ai-ui/ui`, but it should not ship as a giant "kitchen sink" component.
The recommended path is:
1. adopt a headless row-model dependency for the hard table state problems
2. keep the public API source-owned and design-system specific
3. ship a narrow core table first
4. layer richer behaviors only after the base contract stabilizes
The recommended dependency choice is `@tanstack/react-table`, but only as an internal implementation detail. Consumers should build against `@ai-ui/ui`, not against TanStack APIs directly.
## Why now
The repo has already completed most of the preconditions that the roadmap called out before advanced patterns:
- token system is in place
- core component layer is in place
- Storybook coverage exists across the current primitives
- interaction and smoke coverage are present
- `Sheet` and `EmptyState` are now available as adjacent workflow patterns
Relevant current building blocks already exist in `packages/ui`:
- input and form controls: `Input`, `Select`, `Checkbox`, `RadioGroup`, `Switch`, `Form`
- structure and feedback: `Card`, `Badge`, `Alert`, `Separator`, `Skeleton`, `Progress`
- overflow and contextual actions: `DropdownMenu`, `Popover`, `Tooltip`, `Sheet`, `Dialog`, `Toast`
- empty and first-run states: `EmptyState`
That means the main missing piece for list-heavy product surfaces is not another primitive. It is a stable data presentation pattern.
## Problem statement
Today the design system can express forms, overlays, command/search, and empty states, but it cannot yet express a reusable operational list surface such as:
- release queues
- approval backlogs
- rollout logs
- audit result lists
- environment health tables
- reviewer assignments
Without a `DataTable` pattern, application code will drift into one-off table wrappers with inconsistent:
- toolbar layout
- filter/search interactions
- row selection behavior
- loading and empty states
- keyboard behavior
- sticky headers and scrolling decisions
- bulk action affordances
- pagination treatment
This is exactly the kind of "parallel wrapper" problem the roadmap warns against.
## Non-goals
The first implementation should **not** attempt to solve every table problem.
Explicit non-goals for the first slice:
- virtualization
- column resizing
- column reordering
- nested tree rows
- grouped/aggregated rows
- drag and drop
- inline cell editing
- Excel-style spreadsheet behavior
- server transport concerns
- generic charting or pivot-table behavior
Those can come later if real product surfaces prove they are needed.
## Current repo constraints
This RFC is anchored in the current repo, not an idealized future state.
### Architectural constraints
- Components are source-owned and exported from `packages/ui/src/index.ts`
- Styling is token-first and stateful styling flows through stable `data-*` attributes
- Slot naming is already standardized in `packages/ui/src/lib/contracts.ts`
- Motion should be purposeful and reduced-motion safe
- Stories are expected to explain anatomy and behavior, not just render a demo
- QA already uses `Vitest`, Storybook interaction coverage, and Playwright smoke
### Dependency constraints
Current `packages/ui/package.json` has no table model dependency, no date library, and no grid engine.
That means the first `DataTable` implementation must either:
- hand-roll row modeling, sorting, selection, and visibility management, or
- adopt a focused headless dependency
Hand-rolling is not recommended for this repo stage.
## Recommendation
Use `@tanstack/react-table` internally for the first `DataTable` implementation.
### Why TanStack Table
It solves the complex headless data problems we do not want to rediscover:
- row modeling
- sorting state
- filtering state
- pagination state
- selection state
- column visibility
- stable cell/header modeling
This matches the repo's principle of using a strong interaction/state base under a source-owned UI layer, the same way Radix underpins overlays and controls.
### Why not build it from scratch
Building even a "simple" table pattern from scratch will immediately force the repo to own:
- sorting semantics
- accessor APIs
- row identity rules
- selection bookkeeping
- controlled vs uncontrolled state design
- column metadata normalization
That is too much API design surface for a first table release.
### Why not expose TanStack directly
Exposing TanStack types and concepts as the public API would weaken the repo's current direction:
- it would leak a third-party mental model into consumer code
- it would make docs look like a wrapper around someone else's library
- it would make future refactors harder
The public contract should stay `@ai-ui/ui` first. TanStack should remain an implementation detail wherever possible, with any unavoidable type exposure deliberately minimized.
## Proposed scope: Stage 1 core
The first shipping slice should cover the operational table use case, not the spreadsheet use case.
### Must-have behaviors
- typed columns
- sortable columns
- optional row selection
- optional client-side text filter
- empty state rendering
- loading state rendering
- pagination controls
- column-level cell formatting
- row-level actions slot
### Must-have supporting surfaces
- toolbar for search, filters, and view actions
- sticky or visually distinct header treatment
- bulk selection affordance when selection is enabled
- responsive overflow strategy
## Proposed public API shape
The API should be compositional and stable, following the repo's current component contract.
### Root component
```tsx
<DataTable
columns={columns}
rows={rows}
getRowId={(row) => row.id}
loading={loading}
empty={<DataTableEmpty ... />}
searchValue={search}
onSearchValueChange={setSearch}
selection={selection}
onSelectionChange={setSelection}
sorting={sorting}
onSortingChange={setSorting}
/>
```
### Suggested slot family
- `DataTable`
- `DataTableToolbar`
- `DataTableSearch`
- `DataTableFilters`
- `DataTableContent`
- `DataTableTable`
- `DataTableHeader`
- `DataTableHeaderCell`
- `DataTableBody`
- `DataTableRow`
- `DataTableCell`
- `DataTableEmpty`
- `DataTableLoading`
- `DataTablePagination`
- `DataTableSelectionBar`
This is intentionally a pattern family, not a single monolith.
### Suggested stable slots
The following `data-slot` names should exist in the first implementation:
- `root`
- `toolbar`
- `input`
- `content`
- `table`
- `header`
- `row`
- `cell`
- `empty`
- `pagination`
- `actions`
Additional slot names should only be added when they create a durable styling or testing hook.
### Suggested stable state surface
The following public state hooks are likely worth exposing:
- `data-loading`
- `data-empty`
- `data-selected`
- `data-sort`
- `data-density`
`data-state` should still be used for finite row/header states where that is the clearest representation.
## Proposed column model
The public API should accept a narrow column definition owned by this repo, even if it is internally adapted to TanStack.
Example direction:
```ts
type DataTableColumn<TData> = {
id: string;
header: ReactNode | ((column: DataTableColumnContext<TData>) => ReactNode);
cell: (row: TData) => ReactNode;
accessor?: keyof TData | ((row: TData) => unknown);
sortable?: boolean;
align?: "start" | "center" | "end";
width?: number | string;
priority?: "primary" | "supporting" | "low";
};
```
Key point:
- keep the first public column shape small
- do not surface every TanStack option
- do not make users learn a second full configuration language on day one
## Composition model
The table should integrate naturally with the patterns the repo already ships.
### DataTable should compose with
- `Input` for search
- `Select` and `DropdownMenu` for filters and view options
- `Checkbox` for row selection
- `Button` for primary and secondary actions
- `Badge` for row metadata
- `Tooltip` for truncated or status-heavy cells
- `Popover` for compact detail reveal
- `Sheet` for row inspection or bulk edit side panels
- `EmptyState` for no-results and first-run moments
- `Skeleton` for loading rows
### DataTable should not try to replace
- `Sheet` detail workflows
- `Dialog` destructive confirmations
- `EmptyState` standalone onboarding/empty moments
- `Form` validation and edit flows
The table is the list surface. Other patterns handle adjacent work.
## Accessibility requirements
The first release should treat accessibility as a primary contract, not a follow-up.
### Baseline requirements
- semantic table structure for the standard grid use case
- keyboard reachable sort controls
- keyboard reachable row selection controls
- visible focus styling on row actions and interactive header controls
- accessible labeling for search, filters, and pagination controls
- empty and loading states that remain understandable to screen readers
- no motion dependency for critical state changes
### First-release accessibility decisions
- Prefer semantic HTML table markup for the default implementation
- Do not start with `role="grid"` unless we truly need spreadsheet-like keyboard control
- Treat sorting controls as buttons inside header cells
- Keep row selection explicit with `Checkbox`, not hidden click-to-select rows
This fits the repo's current accessibility posture better than jumping straight to a complex grid interaction model.
## QA expectations
The table should meet the repo's current QA bar from the first release.
### Unit and interaction coverage
Minimum expected coverage:
- renders header, rows, and cells with stable slots
- sortable columns update controlled and uncontrolled sort state
- selection toggles update row state and bulk action surface
- loading state renders skeleton or loading rows
- empty state renders when no rows remain after filtering
- search/filter plumbing updates the rendered row set
- pagination changes page and respects bounds
### Storybook coverage
Minimum story recipe:
- `Playground`
- `States`
- `Anatomy`
- `Accessibility`
Likely additional stories:
- `Selection`
- `Empty and Loading`
### Playwright smoke
At least one smoke scenario should cover:
- table renders
- search narrows rows
- sorting changes row order
- selecting rows reveals a bulk-action affordance
- opening row detail in `Sheet` still works
## Release plan
### Stage 0: RFC and composition rehearsal
- finalize API direction in this RFC
- build one realistic docs composition page that uses a table-like operational surface
- identify missing supporting primitives before table code starts
### Stage 1: Headless core
Ship a narrow `DataTable` that supports:
- typed columns
- sorting
- selection
- search
- pagination
- loading and empty states
No virtualization, resizing, pinning, or editing.
### Stage 2: Workflow integration
Add only after Stage 1 stabilizes:
- row action menu patterns
- bulk action bar
- column visibility menu
- row details opening in `Sheet`
### Stage 3: Scale features
Only if justified by real product usage:
- server-driven pagination hooks
- column pinning
- virtualization
- advanced filters
## Likely file layout
When implementation begins, the initial table slice should probably live in:
```txt
packages/ui/src/components/data-table.tsx
packages/ui/src/components/data-table.variants.ts
packages/ui/src/components/data-table.test.tsx
apps/docs/src/components/data-table.stories.tsx
```
If the implementation grows into a larger family, the repo may eventually want a dedicated component folder, but the first slice should stay consistent with the current flat component layout.
## Dependency recommendation
### Recommended
- add `@tanstack/react-table` to `packages/ui`
### Not recommended for the first slice
- virtualization package
- drag and drop package
- date package for table filters
- full export/import package support
The first implementation should minimize dependency expansion and keep the complexity budget focused on a single new capability.
## Open questions
1. Should the public column type stay fully source-owned, or should the first release expose a thin alias around TanStack column defs?
2. Should the first release include client-side search/filter state inside `DataTable`, or should filtering remain externally controlled?
3. Do we want a density variant in the first release, or should row height stay fixed until real product usage appears?
4. Should pagination UI be part of the root component family, or remain a separate companion component?
5. Should row selection be opt-in only, or should the root always reserve structure for it?
## Decision
The next `DataTable` implementation should:
- be built as an advanced pattern, not a primitive
- be source-owned at the public API layer
- use TanStack Table internally from the first implementation
- ship as a narrow, headless/core-first operational table
- defer scale features until real usage proves they are necessary
+3
View File
@@ -9,6 +9,8 @@
"scripts": {
"build": "pnpm --filter @ai-ui/tokens build && pnpm --filter @ai-ui/ui build",
"build:docs": "pnpm --filter @ai-ui/docs build-storybook",
"changeset": "changeset",
"changeset:status": "changeset status --verbose",
"dev:docs": "pnpm --filter @ai-ui/docs storybook",
"lint": "eslint .",
"test": "pnpm --filter @ai-ui/ui test",
@@ -18,6 +20,7 @@
"typecheck": "pnpm -r typecheck"
},
"devDependencies": {
"@changesets/cli": "^2.30.0",
"@eslint/js": "^9.39.4",
"@playwright/test": "^1.55.0",
"@storybook/addon-a11y": "^8.6.14",
+4 -3
View File
@@ -7,6 +7,7 @@ export const sheetContentVariants = cva(
[
"fixed z-50 grid gap-5 overflow-y-auto",
"border bg-[var(--color-card)] text-[var(--color-card-foreground)] shadow-[var(--shadow-md)] outline-none",
"px-5 py-6 sm:px-6",
"transition-[transform,opacity,box-shadow] duration-[var(--dur-slow)] ease-[var(--ease-emphasized)]",
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0"
],
@@ -52,17 +53,17 @@ export const sheetContentVariants = cva(
{
side: "bottom",
size: "sm",
class: "pb-5 px-5 pt-6 sm:px-6"
class: "pb-5"
},
{
side: "bottom",
size: "md",
class: "pb-5 px-5 pt-6 sm:px-6"
class: "pb-5"
},
{
side: "bottom",
size: "lg",
class: "pb-6 px-5 pt-6 sm:px-6"
class: "pb-6"
}
],
defaultVariants: {
+739 -87
View File
File diff suppressed because it is too large Load Diff