feat: add core UI components and baseline tests

This commit is contained in:
2026-03-19 16:56:27 +08:00
parent 12642e0a92
commit 063179933c
73 changed files with 5756 additions and 2 deletions
@@ -0,0 +1,57 @@
import { Checkbox, Label } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Checkbox",
component: Checkbox,
args: {
defaultChecked: true
},
argTypes: {
className: {
control: false
},
defaultChecked: {
control: "boolean"
},
disabled: {
control: "boolean"
},
invalid: {
control: "boolean"
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Checkbox>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const States: Story = {
render: () => (
<div className="grid gap-4">
<div className="flex items-center gap-3">
<Checkbox defaultChecked id="checkbox-default" />
<Label htmlFor="checkbox-default">Default checked</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox id="checkbox-unchecked" />
<Label htmlFor="checkbox-unchecked">Unchecked</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox disabled id="checkbox-disabled" />
<Label htmlFor="checkbox-disabled">Disabled</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox id="checkbox-invalid" invalid />
<Label htmlFor="checkbox-invalid">Invalid state</Label>
</div>
</div>
)
};
@@ -0,0 +1,46 @@
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Dialog",
component: Dialog,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<Dialog>
<DialogTrigger asChild>
<Button>Open approval dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Launch this release?</DialogTitle>
<DialogDescription>
This will notify the routing team and publish the release note to the activity feed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost">Cancel</Button>
<Button>Confirm launch</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
};
@@ -0,0 +1,66 @@
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/DropdownMenu",
component: DropdownMenu,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof DropdownMenu>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary">Open menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Launch actions</DropdownMenuLabel>
<DropdownMenuItem>
Review summary
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Share preview
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Notify stakeholders</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value="staged">
<DropdownMenuRadioItem value="staged">Staged rollout</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="global">Global rollout</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger inset>More actions</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Duplicate release</DropdownMenuItem>
<DropdownMenuItem variant="destructive">Archive release</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
)
};
@@ -0,0 +1,50 @@
import { Field, FieldControl, FieldDescription, FieldError, Input, Label, Textarea } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
function FieldExamples() {
return (
<div className="grid w-[720px] gap-6">
<Field required>
<Label requiredIndicator>Project name</Label>
<FieldControl>
<Input placeholder="Cadence launch" />
<FieldDescription>
This appears in your internal release log and changelog.
</FieldDescription>
</FieldControl>
</Field>
<Field invalid>
<Label requiredIndicator>Slug</Label>
<FieldControl>
<Input defaultValue="launch party!" />
<FieldDescription>Use lowercase letters, numbers, and hyphens only.</FieldDescription>
<FieldError>Slug cannot contain spaces or punctuation.</FieldError>
</FieldControl>
</Field>
<Field orientation="horizontal" readOnly>
<Label>Internal note</Label>
<FieldControl>
<Textarea defaultValue="Read-only example for system generated notes." />
<FieldDescription>Horizontal layout is useful for denser settings forms.</FieldDescription>
</FieldControl>
</Field>
</div>
);
}
const meta = {
title: "Components/Field",
component: FieldExamples,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof FieldExamples>;
export default meta;
type Story = StoryObj<typeof meta>;
export const States: Story = {};
@@ -0,0 +1,71 @@
import { Field, FieldControl, FieldDescription, FieldError, Input, Label } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Input",
component: Input,
args: {
placeholder: "name@company.com",
size: "md"
},
argTypes: {
className: {
control: false
},
disabled: {
control: "boolean"
},
invalid: {
control: "boolean"
},
placeholder: {
control: "text"
},
readOnly: {
control: "boolean"
},
required: {
control: "boolean"
},
size: {
control: "select",
options: ["sm", "md", "lg"]
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: (args) => <Input {...args} className="w-[320px]" />
};
export const States: Story = {
render: () => (
<div className="grid w-[720px] gap-4 sm:grid-cols-2">
<Input defaultValue="studio@cadence.dev" />
<Input disabled defaultValue="Disabled value" />
<Input invalid defaultValue="launch!" />
<Input readOnly defaultValue="Read only value" />
</div>
)
};
export const WithField: Story = {
render: () => (
<Field invalid className="w-[420px]">
<Label requiredIndicator>Email address</Label>
<FieldControl>
<Input defaultValue="studio" required />
<FieldDescription>Use your company email so teammates can identify you.</FieldDescription>
<FieldError>Please enter a valid email address.</FieldError>
</FieldControl>
</Field>
)
};
@@ -0,0 +1,43 @@
import { Label } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Label",
component: Label,
args: {
children: "Email address"
},
argTypes: {
children: {
control: "text"
},
className: {
control: false
},
requiredIndicator: {
control: "boolean"
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Label>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Required: Story = {
args: {
requiredIndicator: true
}
};
export const Invalid: Story = {
args: {
"aria-invalid": true
}
};
@@ -0,0 +1,32 @@
import { Button, Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Popover",
component: Popover,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Popover>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Inspect summary</Button>
</PopoverTrigger>
<PopoverContent className="grid gap-3">
<p className="text-sm font-medium">Release health</p>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">
12 checks passed, 2 reviewers pending, and rollout is limited to 10% of traffic.
</p>
<PopoverArrow />
</PopoverContent>
</Popover>
)
};
@@ -0,0 +1,53 @@
import { Label, RadioGroup, RadioGroupItem } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
function RadioGroupExamples() {
return (
<div className="grid w-[720px] gap-6">
<RadioGroup defaultValue="startup">
<div className="flex items-center gap-3">
<RadioGroupItem id="mode-startup" value="startup" />
<Label htmlFor="mode-startup">Startup review</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem id="mode-scale" value="scale" />
<Label htmlFor="mode-scale">Scale review</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem id="mode-enterprise" value="enterprise" />
<Label htmlFor="mode-enterprise">Enterprise review</Label>
</div>
</RadioGroup>
<RadioGroup defaultValue="email" orientation="horizontal">
<div className="flex items-center gap-3 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4">
<RadioGroupItem id="channel-email" value="email" />
<Label htmlFor="channel-email">Email</Label>
</div>
<div className="flex items-center gap-3 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4">
<RadioGroupItem id="channel-slack" value="slack" />
<Label htmlFor="channel-slack">Slack</Label>
</div>
<div className="flex items-center gap-3 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4">
<RadioGroupItem id="channel-push" value="push" />
<Label htmlFor="channel-push">Push</Label>
</div>
</RadioGroup>
</div>
);
}
const meta = {
title: "Components/RadioGroup",
component: RadioGroupExamples,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof RadioGroupExamples>;
export default meta;
type Story = StoryObj<typeof meta>;
export const States: Story = {};
@@ -0,0 +1,66 @@
import { Field, FieldControl, FieldDescription, FieldError, Label, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Select",
component: Select,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Select>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<div className="w-[320px]">
<Select defaultValue="editorial">
<SelectTrigger>
<SelectValue placeholder="Choose a review lane" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Review lane</SelectLabel>
<SelectItem value="editorial">Editorial review</SelectItem>
<SelectItem value="design">Design review</SelectItem>
<SelectItem value="legal">Legal review</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
)
};
export const WithField: Story = {
render: () => (
<Field invalid className="w-[420px]">
<Label requiredIndicator>Routing team</Label>
<FieldControl>
<Select>
<SelectTrigger invalid>
<SelectValue placeholder="Choose a team" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Primary owners</SelectLabel>
<SelectItem value="product">Product</SelectItem>
<SelectItem value="design">Design</SelectItem>
<SelectItem value="engineering">Engineering</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Support teams</SelectLabel>
<SelectItem value="ops">Operations</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FieldDescription>The routing team receives launch and rollout notifications.</FieldDescription>
<FieldError>Select a primary owning team before publishing.</FieldError>
</FieldControl>
</Field>
)
};
@@ -0,0 +1,81 @@
import { Separator } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Separator",
component: Separator,
args: {
orientation: "horizontal",
tone: "subtle"
},
argTypes: {
className: {
control: false
},
decorative: {
control: "boolean"
},
orientation: {
control: "radio",
options: ["horizontal", "vertical"]
},
tone: {
control: "radio",
options: ["subtle", "strong"]
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Separator>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: (args) => (
<div className="w-80">
<Separator {...args} />
</div>
)
};
export const InCard: Story = {
render: () => (
<div className="w-[420px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-[var(--shadow-sm)]">
<div className="space-y-3">
<div>
<h3 className="m-0 text-base font-semibold">Workspace</h3>
<p className="m-0 text-sm text-[var(--color-muted-foreground)]">
Project status and recent activity
</p>
</div>
<Separator />
<div className="grid gap-2 text-sm text-[var(--color-muted-foreground)]">
<div className="flex items-center justify-between">
<span>Tokens</span>
<span className="text-[var(--color-foreground)]">Stable</span>
</div>
<div className="flex items-center justify-between">
<span>Components</span>
<span className="text-[var(--color-foreground)]">In progress</span>
</div>
</div>
</div>
</div>
)
};
export const Vertical: Story = {
render: () => (
<div className="flex h-16 items-center gap-4 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] px-5 shadow-[var(--shadow-xs)]">
<span className="text-sm text-[var(--color-muted-foreground)]">Overview</span>
<Separator className="h-8" orientation="vertical" />
<span className="text-sm text-[var(--color-muted-foreground)]">Metrics</span>
<Separator className="h-8" orientation="vertical" tone="strong" />
<span className="text-sm text-[var(--color-muted-foreground)]">Activity</span>
</div>
)
};
@@ -0,0 +1,50 @@
import { Skeleton } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Skeleton",
component: Skeleton,
args: {
shape: "line",
tone: "default"
},
argTypes: {
className: {
control: false
},
shape: {
control: "select",
options: ["line", "block", "pill", "avatar"]
},
tone: {
control: "radio",
options: ["default", "muted"]
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Skeleton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const ContentPlaceholder: Story = {
render: () => (
<div className="w-[420px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-[var(--shadow-sm)]">
<div className="flex items-start gap-4">
<Skeleton shape="avatar" />
<div className="min-w-0 flex-1 space-y-3">
<Skeleton className="w-1/3" />
<Skeleton className="w-full" />
<Skeleton className="w-5/6" tone="muted" />
<Skeleton shape="pill" />
</div>
</div>
</div>
)
};
@@ -0,0 +1,49 @@
import { Spinner } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Spinner",
component: Spinner,
args: {
size: "md",
tone: "primary"
},
argTypes: {
className: {
control: false
},
size: {
control: "radio",
options: ["sm", "md", "lg"]
},
tone: {
control: "radio",
options: ["default", "current", "primary"]
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Spinner>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const InButtons: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-4">
<div className="inline-flex items-center gap-2 rounded-[var(--radius-full)] bg-[var(--color-primary)] px-4 py-2 text-sm font-medium text-[var(--color-primary-foreground)]">
<Spinner size="sm" />
Saving
</div>
<div className="inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-[var(--color-border-strong)] px-4 py-2 text-sm font-medium">
<Spinner tone="default" />
Syncing
</div>
</div>
)
};
@@ -0,0 +1,57 @@
import { Label, Switch } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Switch",
component: Switch,
args: {
defaultChecked: true
},
argTypes: {
className: {
control: false
},
defaultChecked: {
control: "boolean"
},
disabled: {
control: "boolean"
},
invalid: {
control: "boolean"
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Switch>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const States: Story = {
render: () => (
<div className="grid gap-4">
<div className="flex items-center gap-3">
<Switch defaultChecked id="switch-live" />
<Label htmlFor="switch-live">Live publishing</Label>
</div>
<div className="flex items-center gap-3">
<Switch id="switch-draft" />
<Label htmlFor="switch-draft">Draft only</Label>
</div>
<div className="flex items-center gap-3">
<Switch disabled id="switch-disabled" />
<Label htmlFor="switch-disabled">Disabled</Label>
</div>
<div className="flex items-center gap-3">
<Switch id="switch-invalid" invalid />
<Label htmlFor="switch-invalid">Invalid state</Label>
</div>
</div>
)
};
+36
View File
@@ -0,0 +1,36 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Tabs",
component: Tabs,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Tabs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<Tabs className="w-[720px]" defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="timeline">Timeline</TabsTrigger>
<TabsTrigger value="audience">Audience</TabsTrigger>
</TabsList>
<TabsContent value="overview">
High-level release summary, current risk score, and owners.
</TabsContent>
<TabsContent value="timeline">
Rollout checkpoints, notification windows, and approval milestones.
</TabsContent>
<TabsContent value="audience">
Impacted customer groups, internal reviewers, and communication channels.
</TabsContent>
</Tabs>
)
};
@@ -0,0 +1,71 @@
import { Field, FieldControl, FieldDescription, FieldError, Label, Textarea } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Textarea",
component: Textarea,
args: {
placeholder: "Add release notes or supporting context",
size: "md"
},
argTypes: {
className: {
control: false
},
disabled: {
control: "boolean"
},
invalid: {
control: "boolean"
},
placeholder: {
control: "text"
},
readOnly: {
control: "boolean"
},
required: {
control: "boolean"
},
size: {
control: "select",
options: ["sm", "md", "lg"]
}
},
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Textarea>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: (args) => <Textarea {...args} className="w-[420px]" />
};
export const States: Story = {
render: () => (
<div className="grid w-[760px] gap-4 sm:grid-cols-2">
<Textarea defaultValue="Default note with enough content to show the surface." />
<Textarea disabled defaultValue="Disabled note content." />
<Textarea invalid defaultValue="This content has a validation error." />
<Textarea readOnly defaultValue="Read-only generated notes stay muted." />
</div>
)
};
export const WithField: Story = {
render: () => (
<Field invalid className="w-[480px]">
<Label requiredIndicator>Launch summary</Label>
<FieldControl>
<Textarea required />
<FieldDescription>Keep it short enough for the changelog card.</FieldDescription>
<FieldError>Summary needs at least 24 characters.</FieldError>
</FieldControl>
</Field>
)
};
@@ -0,0 +1,46 @@
import {
Button,
Toast,
ToastAction,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
function ToastDemo() {
const [open, setOpen] = useState(false);
return (
<ToastProvider swipeDirection="right">
<Button onClick={() => setOpen(true)}>Show toast</Button>
<Toast onOpenChange={setOpen} open={open} variant="success">
<ToastTitle>Release queued</ToastTitle>
<ToastDescription>
The rollout has been scheduled and reviewers were notified.
</ToastDescription>
<ToastAction altText="Open rollout">Open rollout</ToastAction>
<ToastClose />
</Toast>
<ToastViewport />
</ToastProvider>
);
}
const meta = {
title: "Components/Toast",
component: ToastDemo,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof ToastDemo>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
@@ -0,0 +1,31 @@
import { Button, Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipTrigger } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
title: "Components/Tooltip",
component: Tooltip,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Tooltip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost">Hover for note</Button>
</TooltipTrigger>
<TooltipContent>
Inline notes stay terse and avoid blocking the main flow.
<TooltipArrow />
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
};