feat: add release baseline and workspace scene
This commit is contained in:
@@ -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
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user