---
name: wibo-ui
description: Build Wibo screens and artifacts with the Wibo UI design system (@wibo/ui). Lists every token utility, component, shell piece and pattern with its purpose and import path. Use it whenever you generate UI for the Workspace, the Admin Panel, the website or an artifact.
---

# Wibo UI

Wibo UI is the only allowed source of UI for Wibo apps and artifacts. Screens built from it look native; anything else fails review and lint. The app surface follows the Mastra Studio console with the Luminars console's shell: achromatic greys, a sidebar on the darkest surface with the signed-in person at its foot, the page in a raised frame with no top bar, each page a centred column that opens with a breadcrumb and a title, rounded-rectangle controls (8 px corner, 34 px tall) with 13 px labels, dense 40 px table rows, short motion on everything clickable, Wibo indigo as the one accent.

## Rules

1. **Registry components only.** Import from `@wibo/ui` (monorepo) or install from the Wibo registry (`@wibo/<name>`, artifacts). No other component library (`wibo/registry-only-ui`).
2. **Colours only from tokens.** Use the semantic utilities below. Never hex, `rgb()`, `oklch()` or palette classes such as `bg-blue-500`: they do not exist in the Wibo theme and fail lint (`wibo/no-raw-colors`). Tints use opacity modifiers on tokens: `bg-success/10`, `border-danger/25`.
3. **Icons are Lucide** (`lucide-react`), 16 px (`size-4`) in controls and nav rows, 14 px (`size-3.5`) in small controls and chips. Icons in neutral controls are muted and take the row colour.
4. **Numbers are data.** Ids, costs, tokens and durations use `font-mono tabular-nums`; numeric table cells use the `numeric` prop.
5. **One primary action per view.** `Button` default (black on the app surface). Everything else is `outline`, `secondary` or `ghost`.
6. **Accessible by default.** Every control has a label (`Label`, `Field` or `aria-label`); icon-only buttons have `aria-label`; dialogs and sheets have a title.
7. **Copy comes from props.** Components ship English defaults; an app passes every word in its own language through the label props (`closeLabel`, `cancelLabel`, `doneLabel`, `labels`, `label`, `aria-label`), never relying on a default, and passes `locale` and `timeZone` to a component that formats a date (`StaffBanner`). No provider: the props work in server components too. Code stays English.
8. **No router imports.** Pass the app's link component once: `<AppShell linkComponent={Link}>`, and to `PageHeader` for the breadcrumb (`linkComponent`).
9. **Every page has the same frame.** `PageContainer` first, `PageHeader` first inside it (breadcrumb, title, actions), then the page's blocks. The one exception is a canvas that is its own app (an artifact): `PageContainer width="bleed"` fills the content frame edge to edge with no header, named by an `sr-only` `h1`. Every route segment has a `loading.tsx` built from the page skeletons in the page's shape.
10. **Motion is short and optional.** 120 to 220 ms; anything clickable presses (`press`, `press-item`, `press-icon`); nothing moves under `prefers-reduced-motion`.
11. **A surface shows data or takes it, never both.** Lists, boards and detail pages only show data and offer verbs: a primary button in the page header's `actions` ("Add staff member", "Register version") or a section's `actions`, and a `RowMenu` on each row ("Edit", "Disable", "Remove"). The fields open over the page: a `FormSheet` by default (several fields), a `ConfirmDialog` for confirmations and for one or two fields (a note), a page of its own only when the flow is the page's whole reason (sign-in, a signed link, a chat). After a successful submit the sheet closes, the list refreshes and a toast confirms; errors stay inside the sheet, next to the fields (`FieldError`) or under them (`FormSheetError`). Every destructive act confirms in a `ConfirmDialog`. A switch that applies at once (entitlements, settings levers) lives in the sheet or settings surface, not on the list.

## Setup

- CSS entry of the app or artifact: `@import "@wibo/ui/styles.css";` (registry: `@wibo/styles`).
- Root element: `<html data-surface="app" data-theme="light">` (`marketing` for the website; omit `data-theme` to follow the OS). Artifacts receive the theme through the bridge and call `applyTheme(document.documentElement, { surface, theme })`.
- Fonts: the app loads Geist Sans and Geist Mono and exposes them as `--font-geist-sans` and `--font-geist-mono`.

## Token utilities

| Group | Utilities |
|---|---|
| Surfaces | `bg-sidebar` (lowest) < `bg-background` (canvas) < `bg-card` (raised); `bg-popover`, `bg-muted`, `bg-secondary`, `bg-overlay` |
| Text | `text-foreground`, `text-muted-foreground`, `text-card-foreground`, `text-primary-foreground` |
| Lines | `border-border` (default, a 9 % alpha of the ink), `border-input` (14 %, field rims and the dashed rim of a framed `EmptyState`), `ring-ring` |
| Actions | `bg-primary` + `hover:bg-primary-hover` (black in app, indigo on marketing), `bg-accent` / `text-accent` / `hover:bg-accent-hover` (Wibo indigo: selection, focus, links) |
| State fills | Hover and selection on transparent controls are alphas of the ink: `hover:bg-foreground/4`, pressed or selected `bg-foreground/6`, active pill `bg-foreground/9` |
| Status | `success`, `warning`, `danger`, `info`, each with `-foreground`: `text-danger`, `bg-success/10` |
| Agent avatars | `avatar-red`, `avatar-orange`, `avatar-amber`, `avatar-green`, `avatar-teal`, `avatar-blue`, `avatar-indigo`, `avatar-pink` and `avatar-face`: the discs and the white of `AgentAvatar`, nothing else (`text-avatar-teal`, `fill-avatar-face`) |
| Charts | `chart-1` to `chart-6`, in order (blue, orange, aqua, yellow, magenta, green; validated for colour-vision deficiency): `bg-chart-1`, `text-chart-2`, `fill-chart-3`, `stroke-chart-4`. Sequential data uses one hue; status colours are never series colours |
| Elevation | `shadow-card` (almost none in app), `shadow-overlay` (popovers, dialogs) |
| Radius | `rounded-control` (8 px: buttons, inputs, selects, comboboxes, date ranges, textareas, filter chips, the pill tab track), `rounded-md` (6 px: badges, avatars, inner pills), `rounded-lg` (10 px: nav and menu rows), `rounded-xl` (14 px: cards, menus, dialogs, the app frame), `rounded-full` (switches, radios, status dots only) |
| Heights | `h-7` (28 px, small control), `h-8.5` (34 px, default control: button, input, select), `h-9` (36 px, large), `h-7.5` (30 px, nav and menu rows), table rows `h-10` |
| Type | `text-xs` to `text-3xl` (UI), `text-label` (13 px: controls, nav, table cells), `text-display`, `text-display-lg` (marketing headlines), `font-sans`, `font-mono` |
| Motion | `press` (settles to 97 % while pressed: buttons, chips, tabs), `press-item` (99 %: rows, cards), `press-icon` (92 %: icon buttons, checkboxes); `ease-glide` for movement; `animate-pop-in` / `pop-out` (menus, popovers, selects, tooltips, from their `data-side`), `animate-dialog-in` / `dialog-out`, `animate-slide-in-*` / `slide-out-*` (sheets), `animate-toast-in` / `toast-out`, `animate-content-in` (a page on arrival), `animate-skeleton-in` (loading states, after 50 ms), `animate-check-in` (check marks), `animate-fade-in` / `fade-out` |

Tokens in code (charts, canvas, theme handoff): `import { tokens, applyTheme } from "@wibo/ui/tokens"` (registry `@wibo/tokens`). Class merging: `import { cn } from "@wibo/ui/lib/utils"` (registry `@wibo/utils`). Square crops of a picked picture and their PNG encoding in the browser: `import { squarePng, encodeSquarePng } from "@wibo/ui/lib/square-image"` (registry `@wibo/square-image`). A sliding highlight for a list of your own (the sidebar and tabs use it): `import { useSlidingIndicator } from "@wibo/ui/lib/use-sliding-indicator"` (registry `@wibo/use-sliding-indicator`). The org chart's tree logic, pure: `import { buildOrgTree, canMoveInOrg, orgMoveTargets, MAX_ORG_DEPTH } from "@wibo/ui/lib/org-tree"` (registry `@wibo/org-tree`), for a manager picker that offers only the moves the chart accepts.

## Components

| Component | Purpose | Import |
|---|---|---|
| Button | Action trigger, 8 px corner, presses on click. Variants `default` (black primary), `secondary`, `outline` (white with a rim), `ghost` (muted, ink on hover), `destructive`, `link`; sizes `sm` 28 px, `md` 34 px, `lg` 36 px, `icon` 34 px, `icon-sm` 28 px; `loading` (spinner over the label, width kept, not dimmed), `asChild` | `@wibo/ui/components/button` |
| SubmitButton | Submit button of a server-action form: `loading` while the form submits (`useFormStatus`); render it through the app's client boundary | `@wibo/ui/components/submit-button` |
| Input | Single-line text input (34 px, 8 px corner) | `@wibo/ui/components/input` |
| Textarea | Multi-line text input that grows with content | `@wibo/ui/components/textarea` |
| Label | Label for a control (`htmlFor`) | `@wibo/ui/components/label` |
| Select | Single choice from a short list (`Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`) | `@wibo/ui/components/select` |
| Combobox | Select with search for long lists (`options`, `value`, `onValueChange`) | `@wibo/ui/components/combobox` |
| Checkbox | Boolean choice, supports `indeterminate` | `@wibo/ui/components/checkbox` |
| Switch | Toggle for settings that apply immediately | `@wibo/ui/components/switch` |
| RadioGroup | One choice among a few visible options (`RadioGroup`, `RadioGroupItem`) | `@wibo/ui/components/radio-group` |
| SwatchGroup | One choice among visual options (colours, shapes, avatars): a grid of square tiles with radio-group semantics (one tab stop, arrow keys, `name` submits with a form), an accent rim and a check on the chosen tile; name every `SwatchGroupItem` with `aria-label` | `@wibo/ui/components/swatch-group` |
| Badge | Status or count label: `default`, `outline`, `solid`, `accent`, `success`, `warning`, `danger`, `info`; `mono` | `@wibo/ui/components/badge` |
| Card | Bordered container (`Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`) | `@wibo/ui/components/card` |
| Separator | One-pixel divider | `@wibo/ui/components/separator` |
| Dialog | Modal for focused tasks and confirmations | `@wibo/ui/components/dialog` |
| Sheet | Side panel for details, filters, mobile navigation (`side`) | `@wibo/ui/components/sheet` |
| DropdownMenu | Action menu from a button (items, checkbox and radio items, sub menus) | `@wibo/ui/components/dropdown-menu` |
| Popover | Floating panel for small forms and pickers | `@wibo/ui/components/popover` |
| Tooltip | Hint on hover and focus; never the only place for information | `@wibo/ui/components/tooltip` |
| Tabs | Views of one object; `TabsList variant="pill"` (track with a darker pill) or `"line"` (underline); the indicator slides to the active tab | `@wibo/ui/components/tabs` |
| Table | Plain table primitives; `numeric` on `TableHead` and `TableCell` | `@wibo/ui/components/table` |
| DataTable | TanStack Table v9: sorting, `toolbar` slot, `globalFilter`, column visibility (`DataTableColumnToggle`), `pageSize`, `loading`, `emptyState`, `onRowClick`; columns via `createDataTableColumnHelper` | `@wibo/ui/components/data-table` |
| Pagination | Link pagination, and `PaginationControls` (controlled previous/next, page size) | `@wibo/ui/components/pagination` |
| Skeleton | Loading placeholder; compose page loading states from the page skeletons below | `@wibo/ui/components/skeleton` |
| Alert | Inline message: `default`, `info`, `success`, `warning`, `danger` (`Alert`, `AlertTitle`, `AlertDescription`) | `@wibo/ui/components/alert` |
| Toast | `ToastProvider` once at the root, then `useToast().toast({ title, variant })` | `@wibo/ui/components/toast` |
| EmptyState | Icon or illustration, title, a balanced one-sentence `description`, then the `action` under it: the verb that fills the list (default `Button`, opening its sheet) or a way out ("Clear filters", `outline`), none when the viewer can do nothing. The verb shows once: while the empty state offers it, the page header or the section does not draw the same button, which comes back with the first row; a header button that does something else stays. `variant` `framed` (default: full-width panel with a dashed rim, standing on the page or in a section) or `plain` (inside a card, table, sheet, dialog or chat thread) | `@wibo/ui/components/empty-state` |
| StatCard | KPI: label, large mono number, optional `delta` chip, one-line caption (`StatDelta` alone too) | `@wibo/ui/components/stat-card` |
| Chart | Primitives on Recharts: `ChartContainer` (series `config` to `--color-<key>` variables, fixed hue order), `ChartTooltipContent`, `ChartLegendContent`, `ChartDataTable` (hidden table of the values), `seriesColor(n)`; `Recharts` re-exported for custom plots | `@wibo/ui/components/chart` |
| Charts | Ready-made: `LineChart` and `BarChart` (`data`, `xKey`, `series`, `formatValue`, `valueAxisWidth` for long tick labels such as costs; bar `stacked` or `horizontal`), `DistributionChart` (one-hue histogram with an optional reference line), `Sparkline` (plain SVG trend for a stat card). Thin marks, legend for two or more series, labels and a table always, one axis, never a cycled colour | `@wibo/ui/components/charts` |
| KeyValueList | Labelled values (`items` or `KeyValuePair` children), `inline` or `stacked` | `@wibo/ui/components/key-value-list` |
| Timeline | Ordered steps with status: `pending`, `running`, `success`, `warning`, `error`, `skipped` | `@wibo/ui/components/timeline` |
| OrgChart | A tree by parent (a person under their manager) drawn as nested lists joined by hairlines: the roots side by side in name order, a root's reports in a row, every deeper level stacked under its manager; scrolls sideways inside its box, opening on its centre. `nodes` (`{ id, parentId }` plus your fields), `renderCard(node, { depth, reports, dragging })` draws the card's body, `renderActions` its menu (a `RowMenu`), `label` names the chart, `cardLabel(node)` each card (every card is a Tab stop), `sortKey` and `locale` order the names. `editable` with `onMove(id, parentId)` and `labels` (`dropHint`, `dropTop`) turns on drag and drop onto a card or onto the drop zone above (`parentId` null); moves onto oneself, into one's own subtree or deeper than twelve levels are never offered. Dragging needs a pointer: the card menu must offer every move too ("Change manager", "Report to nobody"), with `orgMoveTargets` for the picker. Holds state: render it from client components | `@wibo/ui/components/org-chart` |
| CodeBlock | Monospace code with copy; `json={value}` for a collapsible JSON viewer (`expandDepth`, `countLabels` for the words of a folded node, `copyLabel` and `copiedLabel`) | `@wibo/ui/components/code-block` |
| Collapsible | A block shown and hidden under the row that names it (`Collapsible`, `CollapsibleTrigger` with a turning chevron, `CollapsibleContent`): run steps, trace spans, long JSON | `@wibo/ui/components/collapsible` |
| Command | Command list and `CommandDialog` palette; `useCommandShortcut(toggle)` binds Cmd/Ctrl+K | `@wibo/ui/components/command` |
| Breadcrumb | Location trail parts: muted links, chevrons, the current page in ink (`BreadcrumbLink asChild` for router links). Pages get it from `PageHeader` | `@wibo/ui/components/breadcrumb` |
| AgentAvatar | An agent's face: a coloured disc, a white geometric head with two eyes, translucent shoulders. `agentId` (seeds the default look and the motion's timing), `shape` and `color` ids (`AGENT_AVATAR_SHAPES`: circle, square, wide, tall, triangle, pentagon, hexagon, diamond; `AGENT_AVATAR_COLORS`: red, orange, amber, green, teal, blue, indigo, pink; unknown or missing takes `agentAvatarDefaults(agentId)`), `size` in px (16 to 96+, larger eyes below 28), `motion` `static` (lists, tables), `idle` (blink and breath, the agent's own page), `thinking` (while it works), `greeting` (a hop on mount); `label` (the agent's name) makes it an image, without it the avatar is decorative next to the name. Never cycled or picked by index: always by the agent's id | `@wibo/ui/components/agent-avatar` |
| Avatar | Picture with initials fallback (`initials(name)`): a rounded square (`shape="circle"` for people in lists), `tone="accent"` for the mark of a tenant, workspace or agent; sizes `sm` 24, `md` 32, `lg` 40 px | `@wibo/ui/components/avatar` |
| Chip | Filter chip as a segmented control (label, value and remove button split by hairlines): `label`, `value`, `onRemove` | `@wibo/ui/components/chip` |
| DateRange | From and to as two native date inputs | `@wibo/ui/components/date-range` |
| Form | `Field`, `FieldLabel`, `FieldControl`, `FieldDescription`, `FieldError`, `Fieldset`, `FieldLegend`, `SettingsSection` | `@wibo/ui/components/form` |

## Shell (`@wibo/ui/shell`)

| Component | Purpose | Registry |
|---|---|---|
| AppShell | Layout: `sidebar`, `banner` (slim strip at the top of the content frame), content; no top bar (a mobile bar with the menu button and `mobileTitle` below `md`; `topbar` only for a surface that wants one); collapsible rail; mobile sheet; skip link; `linkComponent`; `useLinkStatus` (Next's, for instant feedback on clicked nav rows) | `@wibo/app-shell` |
| Sidebar | `Sidebar`, `SidebarHeader` (shows the Wibo mark; `logo` replaces it, `logo={null}` hides it), `SidebarContent`, `SidebarSection` (label, `collapsible`), `SidebarItem` (`icon`, `label`, `href`, `active`, `badge`; a spinner while its navigation is pending), `SidebarGroup` (nested `SidebarItem`s that fold: with `href` the row stays a link to the section's page and a chevron button beside it, `expandLabel` and `collapseLabel`, folds them; `active` for the section, `childActive` when a nested row is the page, which then takes the highlight while open; controlled with `open` and `onOpenChange`; no nested rows on the icon rail), `SidebarList`, `SidebarFooter` (rows pinned at the foot of the navigation, then the account row), `SidebarToggle`. One hover highlight slides from row to row and one active highlight glides to the current row | `@wibo/sidebar` |
| SidebarAccount | The signed-in person at the foot of the sidebar, under a hairline that sets them apart from the navigation and the footer's rows: avatar, name, email, up-down chevron; opens the account menu upwards: `details`, `onOpenSettings`, `theme` with `onToggleTheme`, extra items as children, `onSignOut` | `@wibo/sidebar-account` |
| Topbar | Optional top bar: `breadcrumb`, `contextSwitcher` and `actions` slots; `ContextSwitcherTrigger` for a context switcher. The Workspace and the Admin Panel use none | `@wibo/topbar` |
| StaffBanner | Slim strip at the top of the content while Wibo staff are in a Workspace: `purpose`, `expiresAt` (with `locale` and `timeZone`, so server and browser print the same time), `exitHref` or `onExit` | `@wibo/staff-banner` |
| PoweredByWibo | Footer for artifacts | `@wibo/powered-by-wibo` |
| WiboMark, WiboWordmark | The Wibo logo as an `<img>` with alt text: the square indigo "w" (24 px by default) and the "wibo" wordmark (20 px tall). Pass `alt=""` next to a visible "Wibo" label. Image data in `@wibo/wibo-logo-data` | `@wibo/wibo-logo` |

## Patterns (`@wibo/ui/patterns`)

| Pattern | Purpose | Registry |
|---|---|---|
| PageContainer | The page's centred column: `width` `narrow` 760, `medium` 860, `wide` 1080 (default), `full` (no cap, inside the gutters); 16 px gutters on phones, 32 px from `md`; fades in on arrival. `bleed`: one canvas across the whole content frame, no gutters, full height, the page itself never scrolls (its child takes `flex-1` or `size-full` and scrolls inside) | `@wibo/page-container` |
| PageHeader | Breadcrumb trail (`breadcrumbs`: `{ label, href? }[]`, the page last; drawn from two crumbs up; folds the middle on phones), the title row with an optional `avatar` or `icon`, `description`, `actions`; `linkComponent` for the crumbs | `@wibo/page-header` |
| Page skeletons | `PageSkeleton` (the column, busy, with a status label), `PageHeaderSkeleton` (`breadcrumbs`, `avatar`, `actions`), `TabsSkeleton`, `TableSkeleton`, `CardGridSkeleton`, `StatGridSkeleton`, `FormSkeleton`, `KeyValueSkeleton`, `ListSkeleton`, `BlockSkeleton` | `@wibo/page-skeletons` |
| TableToolbar | Search, filter chips, `filterSlot`, `columnToggle`, `timeRange`, `actions` | `@wibo/table-toolbar` |
| SectionGroup | Titled section; `columns={1..4}` for a responsive grid of cards | `@wibo/section-group` |
| InboxList | Items waiting for a person: `kind`, `title`, `meta`, `status`, `time`, `unread`; `mark` puts who it comes from (an `AgentAvatar`, 28 px) in place of the kind's icon | `@wibo/inbox-list` |
| ApprovalCard | Proposed agent action: `preview`, `details`, `state` (`pending`, `approved`, `rejected`, `expired`), `onApprove`, `onEdit`, `onReject`, `busy`; `mark` puts the proposing agent's `AgentAvatar` (32 px) before the title in place of the shield | `@wibo/approval-card` |

Client patterns hold state or React context: import them by path (`@wibo/ui/patterns/form-sheet`) from client components; they are not in the `@wibo/ui/patterns` barrel, which server components import.

| Pattern | Purpose | Registry |
|---|---|---|
| FormSheet | Data entry over the page (rule 11): `FormSheet` (`trigger` or `open`/`onOpenChange`, `title`, `description`, `size` `default` 448 px or `wide` 672 px, full width on phones), then `FormSheetForm` (the `<form>`; a function `action` runs in a transition, so what was typed stays when the action answers with an error), `FormSheetBody` (the fields, scrolling), `FormSheetError` (the form's own error, under the fields), `FormSheetFooter` (Cancel, `cancelLabel`, and the submit button as children, pinned to the bottom edge). `useFormSheet().close()` after a successful submit. Opened by state, it gives focus back on close to what opened it, also when a field takes the focus as it opens (`autoFocus`) | `@wibo/form-sheet` |
| ConfirmDialog | Confirmation before an act (`tone` `destructive` by default, `default` for a decision): `title`, `description` (what goes, what stays), `confirmLabel`, `typed` (the name typed before the verb unlocks), children for one or two fields; `onConfirm` resolves to close, rejects to show its message in the dialog, or resolves to an element shown once with `doneLabel` (a new secret) | `@wibo/confirm-dialog` |
| RowMenu | A row's verbs behind three dots (`label` names the row): `entries` of `item` (`icon`, `label`, `onSelect`, `destructive`, `disabled`), `separator` and `note` (why an item is disabled); destructive items last and red; `placement="header"` for the same menu among a page header's actions. `onSelect` runs once the menu has closed, so the sheet or dialog it opens returns focus to the row | `@wibo/row-menu` |
| SettingsDialog, SettingsBlock | Settings in a large dialog (shadcn sidebar-13): `groups` of sections on the left, breadcrumb and scrolling content on the right, `section` and `onSectionChange` (keep it in the URL, `?settings=profile`), full screen with a stacked list of sections on phones, Escape closes. `SettingsBlock` is one card of a section: `title`, `description`, `footer` and `hint`; a list block shows data with `actions` and opens its forms in a `FormSheet` or `ConfirmDialog`. `SettingsForm` is a block that edits one preference in place (rule 11's settings surface): `dirty` holds Save until something changed, `onSave(formData)` resolves to an error message or null, the footer says unsaved, saved or why not | `@wibo/settings-dialog` |
| ImageUpload | Picture field (avatar, logo): preview with `fallback` initials, upload or replace, remove; a picked file opens a crop dialog (drag, zoom slider, arrow keys) and `onUpload` receives a square PNG (256 px) | `@wibo/image-upload` |

## Recipes

Shell of the Workspace (once, in the layout of a client component):

```tsx
<AppShell
  linkComponent={Link}
  useLinkStatus={useLinkStatus}
  mobileTitle="Demo S.p.A."
  banner={staff ? <StaffBanner purpose={staff.purpose} expiresAt={staff.expiresAt} onExit={signOut} /> : null}
  sidebar={
    <Sidebar>
      <SidebarHeader>
        <span className="flex-1 truncate">Demo S.p.A.</span>
        <SidebarToggle />
      </SidebarHeader>
      <SidebarContent>
        <SidebarSection>
          <SidebarItem icon={House} label="Home" href="/" active />
          <SidebarItem icon={Inbox} label="Inbox" href="/inbox" badge={3} />
        </SidebarSection>
      </SidebarContent>
      <SidebarFooter>
        <SidebarList>
          <SidebarItem icon={Settings} label="Settings" href="/settings" />
        </SidebarList>
        <SidebarAccount
          name="Ada Lovelace"
          email="ada@example.com"
          onOpenSettings={openSettings}
          theme={theme}
          onToggleTheme={toggleTheme}
          onSignOut={signOut}
        />
      </SidebarFooter>
    </Sidebar>
  }
>
  {children}
</AppShell>
```

A page (server component), and its `loading.tsx`:

```tsx
<PageContainer>
  <PageHeader
    linkComponent={Link}
    breadcrumbs={[{ label: "Agents", href: "/agents" }, { label: "PMP Copilot" }]}
    avatar={<Avatar tone="accent"><AvatarFallback>P</AvatarFallback></Avatar>}
    title="PMP Copilot"
    description="Plans and checks project work."
    actions={<Button>Publish</Button>}
  />
  <SectionGroup title="This month" columns={4}>
    <StatCard label="Agent runs" value="1,284" caption="in this range" />
  </SectionGroup>
</PageContainer>

// loading.tsx
<PageSkeleton label="Loading">
  <PageHeaderSkeleton breadcrumbs={2} avatar actions={1} />
  <StatGridSkeleton count={4} columns={4} />
</PageSkeleton>
```

Table with toolbar:

```tsx
const helper = createDataTableColumnHelper<Run>();
const columns = helper.columns([
  helper.accessor("agent", { header: "Agent" }),
  helper.accessor("cost", { header: "Cost", meta: { numeric: true } }),
]);

<DataTable
  columns={columns}
  data={runs}
  globalFilter={search}
  pageSize={25}
  toolbar={(table) => (
    <TableToolbar
      searchValue={search}
      onSearchChange={setSearch}
      columnToggle={<DataTableColumnToggle table={table} />}
    />
  )}
  emptyState={<EmptyState variant="plain" icon={<Play />} title="No runs yet" description="Runs appear here." />}
/>
```

A list that takes data in a sheet and acts on rows through a menu (client components for the sheet's form and the row menu; the page stays a server component):

```tsx
// The page: data only, the verb in the header.
<PageHeader
  title="Staff"
  breadcrumbs={[{ label: "Staff" }]}
  actions={<AddStaffSheet />}
/>
<StaffTable rows={staff} />

// The sheet (client component).
function AddStaffSheet() {
  const [state, action, pending] = useActionState(addStaff, null);
  return (
    <FormSheet trigger={<Button><UserPlus />Add staff member</Button>} title="Add a staff member" description="Creates the identity and emails a sign-in link.">
      <FormSheetForm action={action}>
        <FormSheetBody>
          <Field required>
            <FieldLabel>Email</FieldLabel>
            <FieldControl><Input name="email" type="email" /></FieldControl>
            <FieldError>{state?.fieldErrors?.email}</FieldError>
          </Field>
          <FormSheetError>{state?.ok === false ? state.message : null}</FormSheetError>
        </FormSheetBody>
        <FormSheetFooter cancelLabel="Cancel">
          <Button type="submit" loading={pending}>Add staff member</Button>
        </FormSheetFooter>
      </FormSheetForm>
    </FormSheet>
  );
}
// On success: useFormSheet().close() and a toast; the server action revalidates the list.

// A row's verbs (client component), the destructive one confirmed.
const [removing, setRemoving] = useState(false);
<RowMenu
  label={`Actions for ${row.name}`}
  entries={[
    { kind: "item", key: "role", label: "Change role", icon: <UserCog />, onSelect: () => setEditing(true) },
    { kind: "separator", key: "danger" },
    { kind: "item", key: "remove", label: "Remove from staff", icon: <Trash2 />, destructive: true, onSelect: () => setRemoving(true) },
  ]}
/>
<ConfirmDialog
  open={removing}
  onOpenChange={setRemoving}
  title={`Remove ${row.name} from staff?`}
  description="They can no longer sign in to the Admin Panel. Their audit trail stays."
  confirmLabel="Remove from staff"
  onConfirm={async () => {
    const result = await removeStaff(row.id);
    if (!result.ok) throw new Error(result.message);
    toast({ title: "Removed from staff", variant: "success" });
  }}
/>
```
