feat: add release baseline and workspace scene
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,5 @@ coverage
|
|||||||
.tmp-home
|
.tmp-home
|
||||||
playwright-report
|
playwright-report
|
||||||
test-results
|
test-results
|
||||||
|
output
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm --filter @ai-ui/tokens build && pnpm --filter @ai-ui/ui build",
|
"build": "pnpm --filter @ai-ui/tokens build && pnpm --filter @ai-ui/ui build",
|
||||||
"build:docs": "pnpm --filter @ai-ui/docs build-storybook",
|
"build:docs": "pnpm --filter @ai-ui/docs build-storybook",
|
||||||
|
"changeset": "changeset",
|
||||||
|
"changeset:status": "changeset status --verbose",
|
||||||
"dev:docs": "pnpm --filter @ai-ui/docs storybook",
|
"dev:docs": "pnpm --filter @ai-ui/docs storybook",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "pnpm --filter @ai-ui/ui test",
|
"test": "pnpm --filter @ai-ui/ui test",
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
"typecheck": "pnpm -r typecheck"
|
"typecheck": "pnpm -r typecheck"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@changesets/cli": "^2.30.0",
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
"@storybook/addon-a11y": "^8.6.14",
|
"@storybook/addon-a11y": "^8.6.14",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const sheetContentVariants = cva(
|
|||||||
[
|
[
|
||||||
"fixed z-50 grid gap-5 overflow-y-auto",
|
"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",
|
"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)]",
|
"transition-[transform,opacity,box-shadow] duration-[var(--dur-slow)] ease-[var(--ease-emphasized)]",
|
||||||
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0"
|
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0"
|
||||||
],
|
],
|
||||||
@@ -52,17 +53,17 @@ export const sheetContentVariants = cva(
|
|||||||
{
|
{
|
||||||
side: "bottom",
|
side: "bottom",
|
||||||
size: "sm",
|
size: "sm",
|
||||||
class: "pb-5 px-5 pt-6 sm:px-6"
|
class: "pb-5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
side: "bottom",
|
side: "bottom",
|
||||||
size: "md",
|
size: "md",
|
||||||
class: "pb-5 px-5 pt-6 sm:px-6"
|
class: "pb-5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
side: "bottom",
|
side: "bottom",
|
||||||
size: "lg",
|
size: "lg",
|
||||||
class: "pb-6 px-5 pt-6 sm:px-6"
|
class: "pb-6"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
Generated
+739
-87
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user