diff --git a/ui/storybook/stories/navigation-layout.stories.tsx b/ui/storybook/stories/navigation-layout.stories.tsx index 3b9ac4c098..c78bdfb879 100644 --- a/ui/storybook/stories/navigation-layout.stories.tsx +++ b/ui/storybook/stories/navigation-layout.stories.tsx @@ -335,6 +335,17 @@ function NavigationLayoutStories() { const meta = { title: "Product/Navigation & Layout", component: NavigationLayoutStories, + // Sidebar mounts PluginLauncherOutlet unconditionally once a company is + // selected; without this provider the story raced company selection and + // intermittently threw "usePluginLauncherRuntime must be used within + // PluginLauncherProvider". + decorators: [ + (Story) => ( + + + + ), + ], parameters: { docs: { description: { diff --git a/ui/storybook/stories/primitives-coverage.stories.tsx b/ui/storybook/stories/primitives-coverage.stories.tsx new file mode 100644 index 0000000000..f703bc6f80 --- /dev/null +++ b/ui/storybook/stories/primitives-coverage.stories.tsx @@ -0,0 +1,324 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FileText, FolderOpen, Settings, Users } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { + Avatar, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, +} from "@/components/ui/avatar"; +import { + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from "@/components/ui/command"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { RadioCard, RadioCardGroup } from "@/components/ui/radio-card"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; + +/** + * Minimal coverage stories for the shared primitives in ui/src/components/ui/ + * that no existing story renders directly (or that existing stories only + * reach behind a user interaction). Each story renders the primitive in a + * static, deterministic state so the visual snapshot suite baselines it. + */ +const meta = { + title: "Foundations/Primitive Coverage", + parameters: { + docs: { + description: { + component: + "Static snapshot coverage for shared ui/ primitives not exercised by existing stories: alert-dialog, avatar, breadcrumb, collapsible, command, dropdown-menu, radio-card, scroll-area, sheet, skeleton.", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +function StoryFrame({ children }: { children: React.ReactNode }) { + return ( +
+
+
{children}
+
+
+ ); +} + +export const AvatarStates: Story = { + render: () => ( + +
+ + BO + + + + + + + + + PL + + + QA + + +3 + +
+
+ ), +}; + +export const BreadcrumbTrail: Story = { + render: () => ( + + + + + Paperclip + + + + + + + + Projects + + + + Control plane + + + + + ), +}; + +export const CollapsibleOpenClosed: Story = { + render: () => ( + +
+ + + Open section + + + Visible collapsible content used for snapshot coverage. + + + + + Closed section + + + Hidden content. + + +
+
+ ), +}; + +export const CommandPaletteInline: Story = { + render: () => ( + + + + + No results found. + + + + Open project + ⌘O + + + + View tasks + ⌘T + + + + + + + Instance settings + + + + + + ), +}; + +export const RadioCards: Story = { + render: () => ( + +
+ {}} + options={[ + { + value: "draft", + title: "Draft for review", + description: "The agent proposes; you approve before it ships.", + }, + { + value: "auto", + title: "Deliver automatically", + description: "Finished work is delivered without a review gate.", + }, + ]} + /> + +
+
+ ), +}; + +export const ScrollAreaList: Story = { + render: () => ( + + +
+ {Array.from({ length: 12 }, (_, i) => ( +
+ Heartbeat run #{1200 + i} +
+ ))} +
+
+
+ ), +}; + +export const SkeletonLoading: Story = { + render: () => ( + +
+ + + + +
+
+ ), +}; + +export const AlertDialogOpen: Story = { + render: () => ( + + + + + Cancel this run? + + The agent will stop immediately and the task returns to the queue. + + + + Keep running + Cancel run + + + + + ), +}; + +export const DropdownMenuOpen: Story = { + render: () => ( + +
+ + + + + + Actions + Assign agent + Move to project + Watch updates + + Delete task + + +
+
+ ), +}; + +export const SheetOpen: Story = { + render: () => ( + + + + + Run details + Inspect the run without leaving the board. + +
+ Sheet body content for snapshot coverage. +
+ + + +
+
+
+ ), +}; diff --git a/ui/storybook/stories/user-secrets.stories.tsx b/ui/storybook/stories/user-secrets.stories.tsx index 72d95eccb2..2244ba4a20 100644 --- a/ui/storybook/stories/user-secrets.stories.tsx +++ b/ui/storybook/stories/user-secrets.stories.tsx @@ -124,7 +124,10 @@ function SeedFixtures({ children }: { children: ReactNode }) { }, [selectedCompanyId, setSelectedCompanyId]); if (selectedCompanyId !== COMPANY_ID) return null; - return {children}; + // The preview decorator already provides a MemoryRouter; nesting a second + // react-router Router here raced it and intermittently threw + // "You cannot render a inside another ". + return <>{children}; } function Section({ title, children }: { title: string; children: ReactNode }) {