# Plavon consistency library

Every piece a Plavon build is made of, in one way only. Use these pieces and nothing else; a screen that needs a new one adds it to the library first.

House rules: buttons, menu items and tabs in Title Case; labels and sentences in sentence case; no em-dashes in text a person reads; colours through the tokens (consistency/tokens.css); anything that scrolls fades at its edges (consistency/scroll.css); a person without a photo is their gradient, never initials. A client's logo is drawn as itself, full size: their mark (or their whole lockup) at its own shape, then their name beside it, on the ground it sits on; never inside a tile, a coloured or black square, a rounded box or a circle, and never their initials in a box. Motion is one curve, the glide (`ease-glide`, no overshoot, nothing bounces), with the durations in consistency/tokens.css; a box whose content changes (a list filtered, a row added, a message shown) gets `useGlide` (hooks/use-glide.ts) so it follows its content with its top edge fixed, rows sliding, leaving blurred and arriving sharpening; anything floating is anchored by its top, never its bottom.

## Pieces

### Foundations
The tokens everything reads: colour, type, corners, shadows and motion.

- Colours (`colours`): The palette: one near-black ink in five softer steps for text, warm off-white grounds, fills of the ink at a few percent, and four state colours.
- Type (`type`): The type scale: Inter in seven steps, from a 44px display to an 11.5px caption, with tabular figures for numbers.
- Corners and shadows (`surfaces`): The corners and shadows: 10px menu rows, 11px keys and small chips (rounded-md), 14px fields and menus, 20px cards, 28px stages and dialogs, pills for what is pressed, and three shadows (rest, lift, float).
- Motion (`motion`): One curve, the glide (--ease-glide, no overshoot), and a few durations, applied to every piece by its data-slot in consistency/motion.css; a box whose content changes follows it with useGlide.

### Buttons
Everything pressed: buttons, toggles and the bars they sit in.

- Button (`button`): A pill-shaped button for an action, in seven variants (default, secondary, outline, ghost, destructive, destructive-outline, link) and five sizes, with a loading state.
- Icon button (`icon-button`): A Button holding only a glyph, round, in five sizes (icon-xs to icon-xl); the glyph grows a little on hover and sinks on press.
- Button group (`button-group`): Buttons side by side as one control: attached into one pill sharing a hairline, or split a hair apart for an action and its options.
- Toggle (`toggle`): A button that stays down: a quiet pill that fills while on, for one on or off choice made in place.
- Toggle group (`toggle-group`): Toggles in a row on one track, letting several be on at once (multiple) or one at a time.
- Segmented control (`segmented-control`): A few labelled options on a pill track, with a white thumb that glides to the one chosen.

### Inputs
Everything typed into, with its label, hint and error.

- Input (`input`): A single-line text field with a soft edge that darkens and takes a grey halo on focus, in three heights (sm, default, lg).
- Search field (`search`): A pill-shaped search box with the glass, the words and a ⌘ K hint while empty, and a clear button once something is typed; Escape clears it.
- Text area (`textarea`): A multi-line text field that grows with what is typed, from a 96px minimum, with the same edge and focus as the input.
- Input group (`input-group`): An input (or text area) with addons inside its edge: an icon, a fixed text such as https:// or .ca, a unit, or a small button.
- Number field (`number-field`): A number with minus and plus buttons on either side, centred tabular figures, and min, max and step limits.
- One-time code (`otp-field`): A row of cells, one per digit, for a one-time code sent by email or text, with paste filling every cell and an optional separator.
- Field, label and form (`field`): The frame around one form control: Field ties a FieldLabel, a FieldDescription and a FieldError to it, and Fieldset groups fields under a legend.

### Checkboxes, switches and sliders
Every on and off, alone and in groups, and a value along a track.

- Checkbox (`checkbox`): A small box that is ticked or not, alone or beside its words, with a mixed state for a box standing for several.
- Checkbox group (`checkbox-group`): Several checkboxes answering one question together, holding the ticked ones as one list of values.
- Radio group (`radio-group`): A short list of options where exactly one is picked, each a round button beside its words.
- Switch (`switch`): An on and off control that takes effect the moment it is flipped.
- Slider (`slider`): A value, or a range between two values, picked by dragging a thumb along a track.

### Dropdowns and selects
Everything that opens a list: selects, comboboxes, menus, the team selector, the command bar.

- Select (`select`): A field that opens a list of fixed options and holds the one picked.
- Combobox (`combobox`): A field you type into to narrow a list and pick one of its items (or several), never free text.
- Autocomplete (`autocomplete`): A text field that suggests values as you type and keeps whatever is typed, suggestion or not.
- Team selector (`team-selector`): A pill showing the current team or company that opens a menu to switch to another, create one or open its settings.
- Dropdown menu (`menu`): A button that opens a list of actions, with shortcuts, submenus, checks and a destructive end.
- Context menu (`context-menu`): A menu of actions that opens at the pointer on a right click, or a long press on a touch screen, over an area.
- Command bar (`command`): The ⌘K bar: a dialog with one search field that finds pages, records and actions in groups and runs the one picked.

### Dates
A month to pick from, alone or in a field, and a period for a report.

- Calendar (`calendar`): A month of days to pick one day, or a range of days, from, drawn in the page.
- Date picker (`date-picker`): A field-like button that opens a calendar in a popover and shows the picked day in words.
- Date range picker (`date-range-picker`): A pill that says a period in words and opens named periods, "the last N", two typed ends and two months to pick any range from.

### Dialogs, sheets and popovers
What opens over the page, how it opens and how it closes.

- Dialog (`dialog`): A card that opens over a dimmed page and holds one short task: a header, a panel that scrolls, and a footer of actions.
- Alert dialog and confirm (`alert-dialog`): A dialog that asks one question before something happens; `ConfirmDialog` is its ready form for a destructive action, where the thing's name must be typed first.
- Sheet (`sheet`): A panel that slides in from an edge of the window over a dimmed page, with a header, a scrolling panel and a footer.
- Drawer (`drawer`): A panel that rises from the bottom (or comes in from any edge), follows the finger and closes with a swipe, the way a phone's sheets do.
- Popover (`popover`): A small floating card anchored to a button, with no backdrop, that closes on a click outside or on Escape.
- Tooltip (`tooltip`): A small dark label that appears beside a control on hover or keyboard focus, after a short wait.
- Term (`term`): A word with a dotted underline whose meaning shows in a tooltip on hover or focus.
- Preview card (`preview-card`): A card that opens when the pointer rests on a link, previewing what the link leads to.

### Navigation
How a person moves through a tool.

- Sidebar (`sidebar`): The tool's main navigation down the left side: the team selector at its head, each place a `NavItem` row, the current one white and lifted off the warm ground.
- Tabs (`tabs`): A row of tabs that switches between panels of one page, drawn as pills with a sliding white thumb or as an underline.
- Breadcrumb (`breadcrumb`): The path from a list down to the record being shown, each level a link and the last one the current page.
- Pagination (`pagination`): Previous and Next with page numbers between them, for moving through a long list one page at a time.
- Toolbar (`toolbar`): A floating pill that groups related controls, such as formatting buttons, into one stop for Tab with the arrow keys moving between them.
- Accordion (`accordion`): A stack of headings that each open their panel below, one at a time unless `multiple` is set.
- Collapsible (`collapsible`): One trigger that shows or hides one panel below it, its height animated.

### Badges, avatars and cards
What content sits on and what labels it.

- Badge (`badge`): A small pill that labels something with a word or a number: a status, a count or a tag.
- Avatar (`avatar`): A person's round picture, or their own colour gradient when there is no photo.
- Card (`card`): A white surface with rounded corners and a soft shadow that holds one subject.
- Frame (`frame`): A grey tray that holds one or more white panels under a quiet heading.
- Keyboard key (`kbd`): A keycap that shows a key or a keyboard shortcut.
- Separator (`separator`): A one-pixel line that divides content, across or upright.
- Scroll area (`scroll-area`): A box that scrolls its content behind a thin scrollbar and fades the edge that has more to show.

### Tables and figures
Rows of data and how far along something is.

- Table (`table`): Rows and columns of records under a header row, scrolling sideways with a fade when wider than its box.
- Progress (`progress`): A bar that fills as a task moves toward done.
- Meter (`meter`): A bar that shows where a measure sits within its range.

### Charts
Numbers drawn: over time, in parts, ranked, toward a goal, by hour. Every one answers the pointer and glides to new data.

- Stat (`stat`): One figure that matters: its label, its value, how it moved against a named period, and an optional sparkline under it.
- Sparkline (`sparkline`): A trend drawn as a small line beside or under a figure, with no axis: the past in grey, the latest stretch and its point in blue.
- Area chart (`area-chart`): A value over time: one 2px line per series, the first washed underneath, the period before as a quiet dashed line, a crosshair that finds the nearest date.
- Bar chart (`bar-chart`): Amounts as columns from one baseline: one series, several side by side, or stacked as the parts of each period.
- Bar list (`bar-list`): A ranking: one row per name, longest first, a thin bar of its share of the first, the value in text, its share of the whole on hover.
- Donut chart (`donut-chart`): The parts of one whole as a ring, five at most (more fold into Other), the total in the middle and a legend with each value and share.
- Progress ring (`progress-ring`): How far toward a goal: a ring whose track is a lighter blue, filled in blue from the top, the share in the middle.
- Heatmap (`heatmap`): When things happen: a grid of cells, one blue from light to dark for how many, the empty cell grey, the scale under it.

### Feedback and empty states
What tells a person something happened, is loading, or that nothing is here yet.

- Toast (`toast`): A short message that slides in at the corner to say something just happened, then goes away on its own.
- Alert (`alert`): A block in the page, tinted by its meaning, that states a condition and what to do about it.
- Empty state (`empty`): A centred message that says a place has nothing in it yet and offers the way to fill it.
- Skeleton (`skeleton`): A grey shape with a soft shimmer that stands where content will appear while it loads.
- Spinner (`spinner`): A small spinning circle that says something is working.

## Colours (`colours`)

Section: Foundations. File: `consistency/tokens.css`. Page: /consistency/colours

The palette: one near-black ink in five softer steps for text, warm off-white grounds, fills of the ink at a few percent, and four state colours.

**Use case.** Read this before colouring anything a layout draws itself. Text takes the ink steps by importance (a client's name in the ink, body text in ink 2, a column's label in ink 4), grounds take the page white or the warm surfaces (a side column, a well), and a quiet fill or hairline is the ink at a few percent rather than a grey of its own. Success, warning, destructive and info are kept for states such as paid, due soon, overdue and a note to read. What is on (a tick, a filled radio, a switch turned on, how far a progress bar has come) is the blue `selected`, never the ink, which stays the one button that matters.

**Use it when:**
- Colouring text by importance: the ink, then ink 2 to ink 5
- Choosing the ground of a page, a well or a side column
- A quiet fill or hairline that has to read on white
- Showing a state such as paid, late or failed

**Use something else when:**
- A status word on a row (Paid, Overdue): use Badge (`badge`)
- A tinted message about the page or a form: use Alert (`alert`)
- The corner and shadow of a block: use Corners and shadows (`surfaces`)

**Rules:**
- Colours come from the tokens only (consistency/tokens.css), never a hex in a piece or a layout, so a client's brand is that one file.
- The ink itself is text-foreground or bg-primary; its steps are text-ink-2 to text-ink-5 (there is no text-ink utility).
- A fill or an edge is the ink at a few percent (bg-secondary, border-border, bg-[rgb(var(--ink-rgb)/0.09)]), never a grey of its own.
- On a white ground, text in a state colour uses its -foreground token (text-destructive-foreground); the plain token is for a fill (bg-destructive).
- The chrome-* tokens belong to the showcase's frame and are never used in a client's tool.

**Import:**
```tsx
/* the tokens load once, in app/globals.css */
@import "../consistency/tokens.css";
```

**Usage:**
```tsx
<section className="grid gap-1 rounded-[20px] bg-background p-5 shadow-(--shadow-rest)">
  <p className="text-[12px] text-ink-4">Balance due</p>
  <p className="text-[22px] font-medium tabular-nums text-foreground">$1,204.60</p>
  <p className="text-[15px] text-ink-2">Invoice 2041 for Atelier Nord.</p>
  <p className="text-[12px] text-destructive-foreground">Overdue by 4 days</p>
</section>

<aside className="bg-surface p-4">
  <div className="rounded-[14px] bg-secondary p-3 text-[13px] text-ink-3">
    Three orders are waiting for pickup.
  </div>
  <div className="my-3 h-px bg-border" />
  <p className="text-[12px] text-success-foreground">All invoices sent</p>
</aside>
```

## Type (`type`)

Section: Foundations. File: `consistency/tokens.css`. Page: /consistency/type

The type scale: Inter in seven steps, from a 44px display to an 11.5px caption, with tabular figures for numbers.

**Use case.** Use it to size any text a layout writes around the pieces: a page title at 28px, a card's heading at 17px, a paragraph at 15px in ink 2, a row or a button's words at 13px, a timestamp or caption at 11.5px in ink 4. Every amount, count and date that lines up in a column (an invoice total, a quantity, an order number) takes tabular figures. The pieces already set their own type; this is for what the layout adds.

**Use it when:**
- A page title, a card heading or a section heading
- Body text, a hint or a caption a layout writes itself
- A figure that has to line up: an amount, a count, a date

**Use something else when:**
- The label, hint and error of a form control: use Field, label and form (`field`)
- A short status word on a row: use Badge (`badge`)
- A keyboard shortcut written as keys: use Keyboard key (`kbd`)

**Rules:**
- Weights are 400 and 500 (font-medium): every step of the scale is one or the other, never bold.
- Numbers that are compared or stacked carry tabular-nums.
- Labels, headings and sentences in sentence case; Title Case only on buttons, menu items and tabs.
- No em-dashes in text a person reads: use a colon, a comma or parentheses.
- Inter (font-sans) throughout; font-mono is for code only.

**Import:**
```tsx
/* Inter is font-sans, set on html in app/globals.css */
```

**Usage:**
```tsx
<h1 className="text-[28px] font-medium tracking-[-0.02em]">Invoices</h1>
<h2 className="text-[17px] font-medium tracking-[-0.01em]">Waiting to be paid</h2>
<p className="text-[15px] text-ink-2">Three invoices are past their due date.</p>
<span className="text-[13px] font-medium">Atelier Nord</span>
<span className="text-[22px] font-medium tabular-nums tracking-[-0.01em]">$1,204.60</span>
<span className="text-[11.5px] text-ink-4">Sent October 2</span>
```

## Corners and shadows (`surfaces`)

Section: Foundations. File: `consistency/tokens.css`. Page: /consistency/surfaces

The corners and shadows: 10px menu rows, 11px keys and small chips (rounded-md), 14px fields and menus, 20px cards, 28px stages and dialogs, pills for what is pressed, and three shadows (rest, lift, float).

**Use case.** Use it whenever a layout draws a shape of its own: a panel of order details is a 20px block resting on --shadow-rest, a tile that rises under the pointer takes --shadow-lift, and anything floating over the page takes --shadow-float. Most of the time a piece (card, dialog, popover) already wears the right shape, and the layout should use the piece rather than redraw it.

**Use it when:**
- A layout block that needs a corner or a shadow of its own
- Checking which radius belongs to which kind of thing
- A tile that lifts when the pointer is over it

**Use something else when:**
- A block of content with a title and a body: use Card (`card`)
- Several panels on one warm ground: use Frame (`frame`)
- Something that floats over the page from a trigger: use Popover (`popover`)

**Rules:**
- Corners come from the ladder: rounded-[10px] for menu rows, rounded-[14px] for fields and menus (rounded-lg is 14px), rounded-[20px] for cards, rounded-[28px] for stages and dialogs, rounded-full for what is pressed.
- Shadows come from the three tokens only: shadow-(--shadow-rest) on the page, shadow-(--shadow-lift) as it rises, shadow-(--shadow-float) for what floats over it.
- A focusable element of a layout's own shows focus with focus-visible:shadow-(--focus-ring), as the button does.
- A field being typed into shows --field-focus-border and --field-focus-halo, never a black outline.

**Import:**
```tsx
/* the radius and the shadows are tokens in consistency/tokens.css */
```

**Usage:**
```tsx
<div className="rounded-[20px] bg-background p-5 shadow-(--shadow-rest)">
  <h3 className="text-[17px] font-medium">Order 1048</h3>
  <p className="text-[13px] text-ink-4">Ready for pickup</p>
</div>

<a
  href="/clients/atelier-nord"
  className="block rounded-[20px] bg-background p-5 shadow-(--shadow-rest) outline-none transition-[box-shadow] duration-[240ms] ease-glide hover:shadow-(--shadow-lift) focus-visible:shadow-(--focus-ring)"
>
  Atelier Nord
</a>

<div className="rounded-[28px] bg-surface p-8">A stage around the content</div>
```

## Motion (`motion`)

Section: Foundations. File: `consistency/motion.css`. Page: /consistency/motion

One curve, the glide (--ease-glide, no overshoot), and a few durations, applied to every piece by its data-slot in consistency/motion.css; a box whose content changes follows it with useGlide.

**Use case.** The pieces already move: a button sinks to 0.96 on press, a check pops in, a switch knob and a tab's pill slide, menus grow in from their trigger, the command bar and the comboboxes glide to their new height as you type. Reach for this when a layout changes content of its own: a list filtered by a view, a row added to an order, a condition added to a filter, a message that appears in a card. Put useGlide on the box that holds the content and it follows every change, its top edge fixed, the rows sliding, leaving blurred and arriving sharpening. Take any other curve or duration from the tokens; nothing in the library bounces.

**Use it when:**
- A box whose content changes size: useGlide on the box, data-glide-item on its rows
- Something that moves to a new place, such as a pill or a knob (ease-glide, --dur-slide or --dur-tick)
- Something that appears in place (ease-glide, --dur-enter, from opacity 0 and a few pixels)
- A press on something the layout draws itself (ease-press down in --dur-press, ease-glide back)

**Use something else when:**
- Something that opens over the page: its motion comes with the piece: use Dialog (`dialog`)
- A panel that opens and shuts: its height is the piece's own: use Collapsible (`collapsible`)
- Content that is still loading: use Skeleton (`skeleton`)
- A short wait on an action: use Spinner (`spinner`)

**Rules:**
- One curve: ease-glide (cubic-bezier(0.32, 0.72, 0, 1)) for everything that moves, grows, shrinks or slides; ease-press only for the finger going down. Never a spring that overshoots, never ease-in, never linear except a progress that is time itself.
- Durations are tokens: --dur-press 130ms, --dur-color 160ms, --dur-popup 160ms, --dur-tick 200ms, --dur-enter 240ms, --dur-slide 300ms, --dur-glide 360ms. A size or a position following content may take the full 360ms because nothing waits on it; anything a person waits on stays under 300ms.
- A box that changes height keeps its top edge fixed and grows downward: anchor anything that floats by its top, never by its bottom, so a card that gains a row does not climb.
- A box whose content changes gets useGlide (hooks/use-glide.ts, lib/glide.ts outside React) and overflow-hidden; its rows carry data-glide-item (library rows are found by their data-slot). Never animate height by hand with a fixed number.
- A pill under a picked tab or segment slides to it (TabsList does it); never swap a fill from one tab to the other.
- A piece gets its motion from its data-slot in consistency/motion.css: never add transition classes to a piece.
- Name the exact properties (transition-[opacity,translate]), never transition-all; animate transform, opacity and filter, and height only through the glide.
- Respect reduced motion: the glide applies the change at once; elsewhere motion-reduce:transition-none, or keep only the fade.
- Hover effects only behind (hover: hover) and (pointer: fine); press feedback on :active.

**Import:**
```tsx
import { useGlide } from "@/hooks/use-glide"
/* the curves are tokens.css, the pieces' motion is motion.css; both load once in app/globals.css */
```

**Usage:**
```tsx
{/* a list filtered by a view: the card follows it, top edge fixed */}
const glide = useGlide<HTMLDivElement>()
<div ref={glide} className="overflow-hidden rounded-[18px] bg-background p-2">
  <SegmentedControl value={view} onValueChange={setView} options={VIEWS} />
  <ul>
    {rows.map((r) => (
      <li key={r.id} data-glide-item>{r.name}</li>
    ))}
  </ul>
</div>

{/* something appearing in place, on the glide */}
<li className="transition-[opacity,translate] duration-(--dur-enter) ease-glide starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none">
  Order 1049, Atelier Nord
</li>
```

## Button (`button`)

Section: Buttons. File: `components/ui/button.tsx`. Page: /consistency/button

A pill-shaped button for an action, in seven variants (default, secondary, outline, ghost, destructive, destructive-outline, link) and five sizes, with a loading state.

**Use case.** Use it for anything a person presses to make something happen: Save Changes at the foot of a client form, New Order on a page's title row, Send Invoice, Cancel in a dialog. The default (ink) variant is the one thing to do on the view; secondary, outline and ghost carry the rest; destructive marks an action that deletes or voids something. With render it can draw a link that looks like a button, such as View Invoice going to another page.

**Use it when:**
- The main action of a form or a page (the default variant)
- Supporting actions beside it: Cancel, Export, Duplicate
- An action that deletes or voids something (destructive, destructive-outline)
- A link that should look like a button (render a Link)

**Use something else when:**
- The button shows only an icon: use Icon button (`icon-button`)
- It stays pressed and shows an on or off state: use Toggle (`toggle`)
- Several related buttons that read as one control: use Button group (`button-group`)
- The action destroys data and needs a confirmation first: use Alert dialog and confirm (`alert-dialog`)

**Rules:**
- One default (ink) Button per view, the one thing to do; the rest are secondary, outline or ghost.
- Labels in Title Case (Save Changes, Send Invoice).
- A Button is type="button" by default: the submit button of a form must say type="submit".
- Never set a size class on an icon inside a Button: each size sets its icons.
- While its action runs, pass loading: the label hides, a spinner shows and the button is disabled at the same width.

**Import:**
```tsx
import Link from "next/link"
import { PlusIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
```

**Usage:**
```tsx
<div className="flex justify-end gap-2">
  <Button variant="ghost">Cancel</Button>
  <Button type="submit" loading={saving}>
    Save Changes
  </Button>
</div>

<Button variant="secondary">
  <PlusIcon />
  New Order
</Button>

<Button variant="destructive-outline">Void Invoice</Button>

<Button variant="link" render={<Link href="/invoices/2041" />}>
  View Invoice
</Button>
```

## Icon button (`icon-button`)

Section: Buttons. File: `components/ui/button.tsx`. Page: /consistency/icon-button

A Button holding only a glyph, round, in five sizes (icon-xs to icon-xl); the glyph grows a little on hover and sinks on press.

**Use case.** Use it for a compact action whose glyph is unmistakable: the trash on an order line, copy beside a portal link, a plus to add a line to a quote, the eye that shows a password inside a field. It saves room in tables, toolbars and title rows where a worded button would crowd. Anything a person could misread needs words, so it becomes a Button with a label.

**Use it when:**
- A row action in a table (delete, copy, edit)
- A small action inside a field (show the password, clear)
- Paging arrows beside a date or a record
- A plus that adds a line or an item

**Use something else when:**
- The action is not obvious from the glyph alone: use Button (`button`)
- The icon stays on or off (bold, favourite): use Toggle (`toggle`)
- Several actions on a row, behind one button: use Dropdown menu (`menu`)
- A name shown on hover for the glyph: use Tooltip (`tooltip`)

**Rules:**
- Always pass aria-label naming the action (Delete Line, Copy Link).
- size is icon-xs, icon-sm, icon, icon-lg or icon-xl; sm and lg are for buttons with words.
- Never set a size class on the glyph: each icon size sets it.
- Inside a field (an input group) use the ghost variant at icon-xs or icon-sm.
- On a touch screen the hit area grows to 44px by itself; do not pad it.

**Import:**
```tsx
import { CopyIcon, PlusIcon, TrashIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
```

**Usage:**
```tsx
<Button size="icon" variant="ghost" aria-label="Delete Line">
  <TrashIcon />
</Button>

<Button size="icon-sm" variant="secondary" aria-label="Copy Link">
  <CopyIcon />
</Button>

<Button size="icon" aria-label="Add a Line">
  <PlusIcon />
</Button>
```

## Button group (`button-group`)

Section: Buttons. File: `components/ui/group.tsx`. Page: /consistency/button-group

Buttons side by side as one control: attached into one pill sharing a hairline, or split a hair apart for an action and its options.

**Use case.** Use it when buttons belong together and should read as one thing: Previous, Today and Next above a delivery schedule, or a split button where Send Invoice is the action and a chevron beside it opens the other ways to send. Attached (the default) joins them into one pill; attached={false} keeps each its own shape so a chevron reads centred.

**Use it when:**
- Paging through dates or records: previous, the current one, next
- A split button: the main action and a chevron for its options
- A short row of related actions that read as one control

**Use something else when:**
- One of several options stays selected (Day, Week, Month): use Segmented control (`segmented-control`)
- Formatting switches that stay pressed: use Toggle group (`toggle-group`)
- A whole bar of tools with separators: use Toolbar (`toolbar`)

**Rules:**
- The buttons of one group share a variant and a size.
- An icon-only button in the group carries aria-label.
- The Buttons are direct children of Group: its corners and shared hairline reach only direct children with data-slot="button".
- attached={false} makes a split button; orientation="vertical" stacks the buttons.

**Import:**
```tsx
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Group } from "@/components/ui/group"
```

**Usage:**
```tsx
<Group>
  <Button variant="secondary" size="icon" aria-label="Previous Week">
    <ChevronLeftIcon />
  </Button>
  <Button variant="secondary">Today</Button>
  <Button variant="secondary" size="icon" aria-label="Next Week">
    <ChevronRightIcon />
  </Button>
</Group>

<Group attached={false}>
  <Button>Send Invoice</Button>
  <Button size="icon" aria-label="More Ways to Send">
    <ChevronDownIcon />
  </Button>
</Group>
```

## Toggle (`toggle`)

Section: Buttons. File: `components/ui/toggle.tsx`. Page: /consistency/toggle

A button that stays down: a quiet pill that fills while on, for one on or off choice made in place.

**Use case.** Use it for one switch that changes what is shown and is flipped often, right where the content is: Unread Only above a list of messages, Assigned to Me over the orders table, bold in a note's toolbar, a heart to mark a client as a favourite. It is a view or tool state, not a saved setting.

**Use it when:**
- A filter on the list on screen (Unread Only)
- A single formatting switch (bold)
- Marking something as a favourite in place

**Use something else when:**
- Several related switches side by side (bold, italic, underline): use Toggle group (`toggle-group`)
- Exactly one of several options must be chosen: use Segmented control (`segmented-control`)
- A saved setting that is on or off (email reminders): use Switch (`switch`)
- An action that happens once when pressed: use Button (`button`)

**Rules:**
- An icon-only Toggle carries aria-label.
- The label stays the same whether on or off; the fill says it is on.
- Use pressed with onPressedChange when the state lives in your code, defaultPressed when it does not.
- variant is default (a fill while on) or outline; size is sm, default or lg.
- Labels in Title Case.

**Import:**
```tsx
import { BoldIcon } from "@/components/icons"
import { Toggle } from "@/components/ui/toggle"
```

**Usage:**
```tsx
const [unreadOnly, setUnreadOnly] = useState(false)

<Toggle variant="outline" pressed={unreadOnly} onPressedChange={setUnreadOnly}>
  Unread Only
</Toggle>

<Toggle aria-label="Bold">
  <BoldIcon />
</Toggle>
```

## Toggle group (`toggle-group`)

Section: Buttons. File: `components/ui/toggle-group.tsx`. Page: /consistency/toggle-group

Toggles in a row on one track, letting several be on at once (multiple) or one at a time.

**Use case.** Use it for related tool switches that sit together: bold, italic and underline above a note, or grid and list icons that change how a product catalogue is laid out. With multiple any number can be on; without it, pressing one turns the others off, and pressing the one that is on leaves none.

**Use it when:**
- Formatting switches that can combine (multiple)
- Icon switches for how a list is laid out (grid or list)
- Several filters that can be on together

**Use something else when:**
- A labelled choice where exactly one is always picked: use Segmented control (`segmented-control`)
- A single on or off switch: use Toggle (`toggle`)
- Several options in a form that are saved: use Checkbox group (`checkbox-group`)

**Rules:**
- value and defaultValue are arrays of item values, even one at a time (defaultValue={["grid"]}).
- Set variant and size on ToggleGroup: the items take them from the group.
- Each icon-only ToggleGroupItem carries aria-label.
- Without multiple, one at a time can end with none pressed; when that is not allowed, use a segmented control.

**Import:**
```tsx
import { BoldIcon, ItalicIcon, UnderlineIcon } from "@/components/icons"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
```

**Usage:**
```tsx
<ToggleGroup multiple defaultValue={["bold"]}>
  <ToggleGroupItem value="bold" aria-label="Bold">
    <BoldIcon />
  </ToggleGroupItem>
  <ToggleGroupItem value="italic" aria-label="Italic">
    <ItalicIcon />
  </ToggleGroupItem>
  <ToggleGroupItem value="underline" aria-label="Underline">
    <UnderlineIcon />
  </ToggleGroupItem>
</ToggleGroup>
```

## Segmented control (`segmented-control`)

Section: Buttons. File: `components/ui/segmented-control.tsx`. Page: /consistency/segmented-control

A few labelled options on a pill track, with a white thumb that glides to the one chosen.

**Use case.** Use it when exactly one of two to five short options is always chosen and the choice changes what is shown: Day, Week and Month over a schedule, 7D, 30D and 90D over a sales figure, Open, Paid and All above invoices. It is controlled: the value lives in state and the view reads it.

**Use it when:**
- Switching between views of the same content (Day, Week, Month)
- A date range over a figure or a chart
- A setting with two to five short options, one always chosen

**Use something else when:**
- Sections of a page that each have their own panel: use Tabs (`tabs`)
- More options than fit on one line, or long labels: use Select (`select`)
- Icon switches that may combine or may all be off: use Toggle group (`toggle-group`)
- A choice in a form with a sentence for each option: use Radio group (`radio-group`)

**Rules:**
- It is controlled: pass value and onValueChange, and keep the value in state.
- Always pass aria-label naming what is chosen (View, Range).
- Option labels are short and in Title Case.
- Each option is { value, label, disabled? }; size is sm, default or lg.
- The thumb slides to the one picked on the glide in --dur-slide; never swap a fill from one option to the other.
- It is exactly as tall as a Button of the same size (sm 30px, default 36px, lg 40px): in a row beside buttons, give both the same size, never a segmented control one size up.

**Import:**
```tsx
import { SegmentedControl } from "@/components/ui/segmented-control"
```

**Usage:**
```tsx
const [view, setView] = useState<"day" | "week" | "month">("week")

<SegmentedControl
  aria-label="View"
  value={view}
  onValueChange={setView}
  options={[
    { value: "day", label: "Day" },
    { value: "week", label: "Week" },
    { value: "month", label: "Month" },
  ]}
/>
```

## Input (`input`)

Section: Inputs. File: `components/ui/input.tsx`. Page: /consistency/input

A single-line text field with a soft edge that darkens and takes a grey halo on focus, in three heights (sm, default, lg).

**Use case.** Use it for one line of typed text in a form: a client's company name, an email, a phone number, an invoice number, a postal code. Put it in a Field with a FieldLabel so it is named and can show a hint or an error. The type attribute (email, tel, url, password) brings the right keyboard on a phone.

**Use it when:**
- A name, an email, a phone number or an address line
- An identifier typed once: an invoice number, a postal code
- Any one-line answer in a form

**Use something else when:**
- Several lines of text (notes, a message): use Text area (`textarea`)
- Searching or filtering a list: use Search field (`search`)
- An icon, a prefix, a unit or a button inside the field: use Input group (`input-group`)
- A quantity stepped up and down: use Number field (`number-field`)

**Rules:**
- Every Input is named: a FieldLabel in the same Field, or aria-label.
- Mark an error with aria-invalid (or invalid on its Field): the edge turns red.
- Do not add a border, ring or outline: focus is the darker edge and grey halo from the tokens.
- size is sm (34px), default (40px) or lg (48px).

**Import:**
```tsx
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
```

**Usage:**
```tsx
<Field>
  <FieldLabel>Company name</FieldLabel>
  <Input placeholder="Atelier Nord" />
</Field>

<Input type="email" aria-label="Email" placeholder="bonjour@ateliernord.ca" />

<Input aria-invalid defaultValue="marc@" aria-label="Email" />
```

## Search field (`search`)

Section: Inputs. File: `components/ui/search-field.tsx`. Page: /consistency/search

A pill-shaped search box with the glass, the words and a ⌘ K hint while empty, and a clear button once something is typed; Escape clears it.

**Use case.** Use it above a list or a table to narrow what is on screen: clients by name, orders by number, invoices by company. Large, it is a page's main search; small, it fits a toolbar above a table, level with the small buttons there. It is controlled: the value lives in state and filters the rows as it changes.

**Use it when:**
- Filtering a table or a list on the page (clients, orders)
- A page's main search (size lg)
- A search in a toolbar above a table (size sm)

**Use something else when:**
- Jumping to any record or action from anywhere, by keyboard: use Command bar (`command`)
- Typing a value in a form with suggestions as you type: use Autocomplete (`autocomplete`)
- Picking one item from a long list in a form: use Combobox (`combobox`)
- A plain typed value in a form: use Input (`input`)

**Rules:**
- It is controlled: value and onValueChange are required.
- Always pass aria-label (Search clients); the placeholder defaults to Search.
- The ⌘ K hint is only drawn: wire the shortcut yourself, or pass shortcut={false}.
- Escape and the clear button empty it; do not add a clear button of your own.
- size is xs, sm, default, lg or xl, the buttons' own heights (26, 30, 36, 40, 48px): in a bar, give the search the size of the buttons beside it, never a height of its own.

**Import:**
```tsx
import { SearchField } from "@/components/ui/search-field"
```

**Usage:**
```tsx
const [query, setQuery] = useState("")

<SearchField
  value={query}
  onValueChange={setQuery}
  placeholder="Search clients"
  aria-label="Search clients"
  shortcut={false}
/>
```

## Text area (`textarea`)

Section: Inputs. File: `components/ui/textarea.tsx`. Page: /consistency/textarea

A multi-line text field that grows with what is typed, from a 96px minimum, with the same edge and focus as the input.

**Use case.** Use it for free text longer than a line: notes from a call with a client, a message to send, delivery instructions on an order, the description of the work on a quote. Put it in a Field with a FieldLabel, and a FieldDescription when there is a limit or a hint.

**Use it when:**
- Notes, a message, instructions or a description
- Anything that may run past one line

**Use something else when:**
- One line of text: use Input (`input`)
- A text box with a button or a row of actions attached (a composer): use Input group (`input-group`)

**Rules:**
- Every Textarea is named: a FieldLabel in the same Field, or aria-label.
- It grows with its content (field-sizing-content): do not give it a fixed height.
- Mark an error with aria-invalid (or invalid on its Field).
- Do not add a border or ring: focus is the darker edge and grey halo.

**Import:**
```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"
```

**Usage:**
```tsx
<Field>
  <FieldLabel>Notes</FieldLabel>
  <Textarea placeholder="What did they say on the call?" />
  <FieldDescription>Only your team sees these notes.</FieldDescription>
</Field>
```

## Input group (`input-group`)

Section: Inputs. File: `components/ui/input-group.tsx`. Page: /consistency/input-group

An input (or text area) with addons inside its edge: an icon, a fixed text such as https:// or .ca, a unit, or a small button.

**Use case.** Use it when something has to sit inside the field: a mail icon before an email, https:// before a website, $ or % around an amount, a show and hide button on a password, a Send button under a message. The whole group wears one edge and one focus, and pressing an addon puts the cursor in the field.

**Use it when:**
- An icon before the text (email, website, phone)
- A fixed prefix or suffix (https://, .ca, $, kg)
- A button inside the field (show the password, copy)
- A text box with a row of actions under it (align block-end)

**Use something else when:**
- Nothing needs to sit inside the field: use Input (`input`)
- Searching a list: use Search field (`search`)
- A number with minus and plus buttons: use Number field (`number-field`)

**Rules:**
- The field inside is InputGroupInput or InputGroupTextarea, never a plain Input: the group draws the one edge.
- Each addon is an InputGroupAddon; align is inline-start (the default), inline-end, block-start or block-end, and words in it go in InputGroupText.
- A button in an addon is a ghost icon button (icon-xs or icon-sm) with aria-label.
- Mark an error with aria-invalid on InputGroupInput.

**Import:**
```tsx
import { EyeIcon, EyeOffIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"
```

**Usage:**
```tsx
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="ateliernord" aria-label="Website" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>.ca</InputGroupText>
  </InputGroupAddon>
</InputGroup>

<InputGroup>
  <InputGroupInput type={shown ? "text" : "password"} aria-label="Password" />
  <InputGroupAddon align="inline-end">
    <Button size="icon-xs" variant="ghost" aria-label={shown ? "Hide Password" : "Show Password"} onClick={() => setShown((s) => !s)}>
      {shown ? <EyeOffIcon /> : <EyeIcon />}
    </Button>
  </InputGroupAddon>
</InputGroup>
```

## Number field (`number-field`)

Section: Inputs. File: `components/ui/number-field.tsx`. Page: /consistency/number-field

A number with minus and plus buttons on either side, centred tabular figures, and min, max and step limits.

**Use case.** Use it for a count a person nudges up or down: the quantity on an order line, the seats on a plan, the days of a rental, the hours on a time entry. min and max stop the buttons at the limits, and format with a locale shows the value as money or a percentage while the value stays a number.

**Use it when:**
- A quantity on an order or an invoice line
- A count with limits (seats, days, people)
- A number stepped by a fixed amount (step={0.25} for hours)

**Use something else when:**
- Digits that are not a quantity (a phone, an invoice number, a postal code): use Input (`input`)
- A value picked along a range by dragging: use Slider (`slider`)
- A free amount with a currency sign and no stepping: use Input group (`input-group`)

**Rules:**
- Compose it as NumberField holding a NumberFieldGroup with NumberFieldDecrement, NumberFieldInput and NumberFieldIncrement.
- Name it: a FieldLabel in a Field, or aria-label on NumberField.
- Set min (and max when there is one) so the buttons stop at the limit.
- For money or a percentage pass format and locale (format={{ style: "currency", currency: "CAD" }}, locale="fr-CA"); onValueChange gets a number or null.
- size (sm, default, lg) goes on NumberField.

**Import:**
```tsx
import { Field, FieldLabel } from "@/components/ui/field"
import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
} from "@/components/ui/number-field"
```

**Usage:**
```tsx
<Field>
  <FieldLabel>Quantity</FieldLabel>
  <NumberField defaultValue={1} min={1} max={99}>
    <NumberFieldGroup>
      <NumberFieldDecrement />
      <NumberFieldInput />
      <NumberFieldIncrement />
    </NumberFieldGroup>
  </NumberField>
</Field>
```

## One-time code (`otp-field`)

Section: Inputs. File: `components/ui/otp-field.tsx`. Page: /consistency/otp-field

A row of cells, one per digit, for a one-time code sent by email or text, with paste filling every cell and an optional separator.

**Use case.** Use it on the step where a person types a code they were sent: signing in with an email code, confirming a phone number, a two-step check. Pasting the whole code fills every cell, and onValueComplete fires once the last cell is filled, so the code can be checked without a button.

**Use it when:**
- Signing in with a code sent by email or text
- Confirming an email address or a phone number
- A two-step verification code

**Use something else when:**
- A password: use Input group (`input-group`)
- A long number typed once (an account or an invoice number): use Input (`input`)

**Rules:**
- length equals the number of OTPFieldInput cells and the length of the code sent.
- Name it with aria-label (or a FieldLabel in a Field).
- Check the code in onValueComplete, disable the field while it is checked, and mark a refused code with aria-invalid on each OTPFieldInput.
- An OTPFieldSeparator in the middle splits a six-digit code into two threes.
- size is default or lg.

**Import:**
```tsx
import { OTPField, OTPFieldInput, OTPFieldSeparator } from "@/components/ui/otp-field"
```

**Usage:**
```tsx
<OTPField
  length={6}
  aria-label="One-time code"
  value={code}
  onValueChange={setCode}
  onValueComplete={(value) => verify(value)}
  disabled={checking}
>
  <OTPFieldInput />
  <OTPFieldInput />
  <OTPFieldInput />
  <OTPFieldSeparator />
  <OTPFieldInput />
  <OTPFieldInput />
  <OTPFieldInput />
</OTPField>
```

## Field, label and form (`field`)

Section: Inputs. File: `components/ui/field.tsx`. Page: /consistency/field

The frame around one form control: Field ties a FieldLabel, a FieldDescription and a FieldError to it, and Fieldset groups fields under a legend.

**Use case.** Use it for every control in a form, so each has its label, its hint and its error in the same place: the company name and email on a new client form, the due date and terms on an invoice, a Delivery section on an order form. Field marks its control invalid and shows the error; Fieldset with a FieldsetLegend titles a section. When the server answers with errors by field name, Base UI's Form (components/ui/form) takes them through its errors prop, keyed by each Field's name.

**Use it when:**
- Any input, text area, number field or select in a form
- A hint under a control (As it appears on invoices.)
- An error under a control after it is checked
- A titled section of a form (Fieldset)

**Use something else when:**
- An error about the whole form, not one control (Could not save): use Alert (`alert`)
- A short form that opens over the page: use Dialog (`dialog`)
- A single search box above a list: use Search field (`search`)

**Rules:**
- One control per Field; the Field ties the FieldLabel to it, so no htmlFor is needed.
- Show an error by setting invalid on the Field and rendering FieldError match only while there is an error (match always shows it).
- Labels, legends and descriptions in sentence case; the buttons in Title Case, the submit one with type="submit".
- FieldsetLegend is semibold by default: give it className="text-[15px] font-medium", as the showcase does, since weights stop at 500.
- A label for something outside a Field is Label (components/ui/label) with htmlFor.

**Import:**
```tsx
import { Button } from "@/components/ui/button"
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"
```

**Usage:**
```tsx
<form className="grid gap-6" onSubmit={save}>
  <Fieldset className="grid gap-5">
    <FieldsetLegend className="text-[15px] font-medium">Your company</FieldsetLegend>
    <Field>
      <FieldLabel>Company name</FieldLabel>
      <Input name="company" placeholder="Atelier Nord" />
      <FieldDescription>As it appears on invoices.</FieldDescription>
    </Field>
    <Field invalid={Boolean(emailError)}>
      <FieldLabel>Email</FieldLabel>
      <Input name="email" type="email" placeholder="bonjour@ateliernord.ca" />
      {emailError && <FieldError match>{emailError}</FieldError>}
    </Field>
  </Fieldset>
  <div className="flex justify-end gap-2">
    <Button variant="ghost">Cancel</Button>
    <Button type="submit">Save Changes</Button>
  </div>
</form>
```

## Checkbox (`checkbox`)

Section: Checkboxes, switches and sliders. File: `components/ui/checkbox.tsx`. Page: /consistency/checkbox

A small box that is ticked or not, alone or beside its words, with a mixed state for a box standing for several.

**Use case.** Use it for one yes or no inside a form that is saved later: "Client is tax exempt", "Send the invoice by email", "I have read the contract". It is also the tick at the start of a table row for acting on several orders or invoices at once, with the header's box `indeterminate` when only some rows are ticked. It changes nothing until the form is saved or the bulk action is run.

**Use it when:**
- One yes or no inside a form saved with a button
- Ticking rows in a table to act on several at once
- A checklist item that is done or not (a step on a job)
- A box standing for several others, mixed when some are ticked

**Use something else when:**
- The change applies the moment it is flipped (a setting, Do not disturb): use Switch (`switch`)
- One question with several answers (notify by email, text, push): use Checkbox group (`checkbox-group`)
- Exactly one of a few options: use Radio group (`radio-group`)
- On and off for a view option inside a menu (Compact Rows): use Dropdown menu (`menu`)

**Rules:**
- Give it a name: wrap it with its words in a `Label`, or pass `aria-label` when it stands alone (a table row's tick).
- Its words are a sentence in sentence case ("Send the monthly report"), never Title Case.
- `indeterminate` is only for a box standing for several when some are ticked; it is not a third answer.
- Inside a form, pass `name` (and `value` when the server expects more than "on") so it is submitted.

**Import:**
```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
```

**Usage:**
```tsx
<Label className="flex cursor-pointer items-start gap-2.5 font-normal text-[14px] text-foreground">
  <Checkbox name="tax-exempt" defaultChecked />
  Client is tax exempt
</Label>

{/* alone, at the start of a table row */}
<Checkbox
  checked={selected.includes(invoice.id)}
  onCheckedChange={(checked) => toggle(invoice.id, checked)}
  aria-label={"Select " + invoice.number}
/>
```

## Checkbox group (`checkbox-group`)

Section: Checkboxes, switches and sliders. File: `components/ui/checkbox-group.tsx`. Page: /consistency/checkbox-group

Several checkboxes answering one question together, holding the ticked ones as one list of values.

**Use case.** Use it when one question takes several answers: the channels a client is notified by, the services included in a quote, the days a crew works. The group holds the ticked values as one string array (`value` and `onValueChange`), so the form saves a list rather than a row of separate booleans. Each option can carry a line of detail under its name.

**Use it when:**
- One question, several answers allowed
- The options are few enough to read at a glance
- Each option needs a line of detail under its name
- A Select All box over the options (a `parent` checkbox with `allValues`)

**Use something else when:**
- Only one answer may be picked: use Radio group (`radio-group`)
- Many options, or options that come from data (several clients, tags): use Combobox (`combobox`)
- A single yes or no: use Checkbox (`checkbox`)
- Formatting options in a toolbar (bold, italic): use Toggle group (`toggle-group`)

**Rules:**
- Every `Checkbox` inside carries a `value`; the group's `value` is the array of the ticked ones.
- Name the group with `aria-label`, or `aria-labelledby` pointing at a visible heading.
- Each option's name is sentence case, its detail under it in `text-ink-4`, both inside one `Label` with the box.
- A `parent` checkbox needs a controlled group (`value`, `onValueChange`) and `allValues` listing every option's value.

**Import:**
```tsx
import { useState } from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { CheckboxGroup } from "@/components/ui/checkbox-group"
import { Label } from "@/components/ui/label"
```

**Usage:**
```tsx
const CHANNELS = [
  { value: "email", name: "Email", hint: "A letter the moment it happens" },
  { value: "text", name: "Text", hint: "On your phone, for what is urgent" },
]

export function NotifyBy() {
  const [value, setValue] = useState(["email"])
  return (
    <CheckboxGroup value={value} onValueChange={setValue} aria-label="Notify by" className="grid gap-3">
      {CHANNELS.map((c) => (
        <Label key={c.value} className="flex cursor-pointer items-start gap-3 font-normal">
          <Checkbox value={c.value} className="mt-0.5" />
          <span className="grid gap-0.5">
            <span className="text-[14px] font-medium text-foreground">{c.name}</span>
            <span className="text-[12.5px] text-ink-4">{c.hint}</span>
          </span>
        </Label>
      ))}
    </CheckboxGroup>
  )
}
```

## Radio group (`radio-group`)

Section: Checkboxes, switches and sliders. File: `components/ui/radio-group.tsx`. Page: /consistency/radio-group

A short list of options where exactly one is picked, each a round button beside its words.

**Use case.** Use it for one answer among a few that a person should compare before choosing: billing monthly or yearly, delivery or pickup on an order, how a quote is paid (a deposit, in full, in instalments). Every option stays visible with its detail (a price, a delay) under its name, which a select would hide behind a click.

**Use it when:**
- Exactly one answer among a few options
- Each option needs its detail visible to compare
- A choice inside a form saved later

**Use something else when:**
- Many options, or a tight form row (a province, a status): use Select (`select`)
- Switching a view or a period at once, above a chart or a list: use Segmented control (`segmented-control`)
- Several answers allowed: use Checkbox group (`checkbox-group`)
- A single yes or no: use Checkbox (`checkbox`)

**Rules:**
- Every `Radio` has a `value`; give the group a `defaultValue` (or `value` with `onValueChange`) so the starting answer is clear.
- Name the group with `aria-label` (or `aria-labelledby`), and wrap each radio with its words in a `Label`.
- Option names are sentence case; a detail sits under the name in `text-ink-4`.
- Inside a form, pass `name` on the group so the picked value is submitted.

**Import:**
```tsx
import { Label } from "@/components/ui/label"
import { Radio, RadioGroup } from "@/components/ui/radio-group"
```

**Usage:**
```tsx
const PLANS = [
  { value: "monthly", name: "Monthly", hint: "$49 a month" },
  { value: "yearly", name: "Yearly", hint: "$490 a year, two months free" },
]

<RadioGroup name="billing" defaultValue="monthly" aria-label="Billing" className="grid gap-3">
  {PLANS.map((p) => (
    <Label key={p.value} className="flex cursor-pointer items-start gap-3 font-normal">
      <Radio value={p.value} className="mt-0.5" />
      <span className="grid gap-0.5">
        <span className="text-[14px] font-medium text-foreground">{p.name}</span>
        <span className="text-[12.5px] text-ink-4">{p.hint}</span>
      </span>
    </Label>
  ))}
</RadioGroup>
```

## Switch (`switch`)

Section: Checkboxes, switches and sliders. File: `components/ui/switch.tsx`. Page: /consistency/switch

An on and off control that takes effect the moment it is flipped.

**Use case.** Use it for settings that apply at once, with no Save button: Do not disturb, email the client when an invoice is paid, show archived orders, a feature turned on for a team. It sits at the end of a settings row, the setting's name on the left, and the change is saved in `onCheckedChange`.

**Use it when:**
- The change applies immediately, with no Save button
- A row in a settings list
- Turning a notification or a feature on or off

**Use something else when:**
- Part of a form that is submitted later: use Checkbox (`checkbox`)
- Picking one of several modes, not on or off: use Segmented control (`segmented-control`)
- On and off inside a menu (Compact Rows): use Dropdown menu (`menu`)

**Rules:**
- A switch applies at once: save in `onCheckedChange`. Inside a form submitted later, use a checkbox.
- Its words name the setting in sentence case ("Show previews"), never "On" or "Off".
- Wrap the whole row in a `Label`: the row flips it, and the thumb stretches while the row is pressed.
- Give it a name: its row's `Label`, or `aria-label` when it stands alone.

**Import:**
```tsx
import { useState } from "react"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
```

**Usage:**
```tsx
export function PaidEmailSetting({ initial, save }: { initial: boolean; save: (on: boolean) => void }) {
  const [on, setOn] = useState(initial)
  return (
    <Label className="flex h-11 cursor-pointer items-center justify-between rounded-[12px] px-3 font-normal text-[14px] text-foreground hover:bg-surface">
      Email the client when an invoice is paid
      <Switch
        checked={on}
        onCheckedChange={(next) => {
          setOn(next)
          save(next)
        }}
      />
    </Label>
  )
}
```

## Slider (`slider`)

Section: Checkboxes, switches and sliders. File: `components/ui/slider.tsx`. Page: /consistency/slider

A value, or a range between two values, picked by dragging a thumb along a track.

**Use case.** Use it where the rough position matters more than the exact figure: a price range filter over a product list, a discount between 0 and 30 percent, a search radius around a client's address. Pass one number for one thumb, an array of two for a range, and show the figure with `SliderValue` when the person needs to read it.

**Use it when:**
- Filtering by a range (a price, a quantity)
- A rough value where dragging is quicker than typing
- The value has a known minimum and maximum
- A range between two values: `defaultValue={[20, 70]}`

**Use something else when:**
- An exact figure (a price or a quantity on an invoice line): use Number field (`number-field`)
- A few named steps (low, medium, high): use Segmented control (`segmented-control`)
- Showing how far along something is, not setting it: use Progress (`progress`)

**Rules:**
- Set `min`, `max` and `step` to the real bounds; without them it runs from 0 to 100.
- Name it with `aria-label`, and show the figure with `SliderValue` (tabular numbers) when it matters; `format` takes `Intl.NumberFormatOptions` for money or percent.
- A range reads in words, "$200 to $1,500": `SliderValue` joins the two ends with its `separator` (" to " by default); a French tool passes `separator=" à "`.
- When the change reaches the server, save in `onValueCommitted` (the thumb let go), not on every `onValueChange`.

**Import:**
```tsx
import { Slider, SliderValue } from "@/components/ui/slider"
```

**Usage:**
```tsx
<Slider
  defaultValue={[200, 1500]}
  min={0}
  max={5000}
  step={50}
  format={{ style: "currency", currency: "CAD", maximumFractionDigits: 0 }}
  onValueCommitted={(value) => applyPriceFilter(value)}
  aria-label="Price range"
>
  <div className="flex w-full justify-between text-[12.5px] text-ink-4">
    <span>Price</span>
    <SliderValue className="tabular-nums text-foreground" />
  </div>
</Slider>
```

## Select (`select`)

Section: Dropdowns and selects. File: `components/ui/select.tsx`. Page: /consistency/select

A field that opens a list of fixed options and holds the one picked.

**Use case.** Use it to pick one value from a short, known list inside a form or a filter bar: an order's status, a client's province, a payment method, an invoice's currency. The trigger looks like a field and shows the picked label; the list opens over it with a tick on the current option, and options can be grouped under quiet labels.

**Use it when:**
- One value from a fixed list that fits on screen
- A status or category on a record
- A filter above a table (status, payment method)
- Options grouped under labels (`SelectGroup`, `SelectGroupLabel`)

**Use something else when:**
- A long list, or one that comes from data, where typing finds it faster (a client among hundreds): use Combobox (`combobox`)
- The person may type a value that is not in the list (a city): use Autocomplete (`autocomplete`)
- The list is actions to run, not a value to hold: use Dropdown menu (`menu`)
- Two or three options that should all stay visible with their details: use Radio group (`radio-group`)

**Rules:**
- Pass `items` (`{ value, label }` pairs) to `Select` so the trigger prints the label, not the raw value.
- Name it: `SelectLabel` inside `Select` for a visible label, or `aria-label` on `SelectTrigger`.
- Options are values, written in sentence case ("In review"), unlike menu items.
- Inside a form, pass `name` so the picked value is submitted; `SelectValue` takes a `placeholder` for no value yet.

**Import:**
```tsx
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "@/components/ui/select"
```

**Usage:**
```tsx
const STATUSES = [
  { value: "draft", label: "Draft" },
  { value: "sent", label: "Sent" },
  { value: "paid", label: "Paid" },
  { value: "overdue", label: "Overdue" },
]

<Select name="status" defaultValue="sent" items={STATUSES}>
  <SelectTrigger aria-label="Status">
    <SelectValue />
  </SelectTrigger>
  <SelectPopup>
    {STATUSES.map((s) => (
      <SelectItem key={s.value} value={s.value}>
        {s.label}
      </SelectItem>
    ))}
  </SelectPopup>
</Select>
```

## Combobox (`combobox`)

Section: Dropdowns and selects. File: `components/ui/combobox.tsx`. Page: /consistency/combobox

A field you type into to narrow a list and pick one of its items (or several), never free text.

**Use case.** Use it to pick a record from a list too long to scroll: the client on a new invoice, the product on an order line, the employee assigned to a job. Typing narrows the list, the arrows move and Enter picks; the field only ever holds an item from the list. With `multiple` and `ComboboxChips` it holds several (tags on a client).

**Use it when:**
- Picking one record from a long or data-driven list (clients, products)
- Typing part of a name is quicker than scrolling
- The value must be one of the list's items, never free text
- Several values from a long list: `multiple` with `ComboboxChips`

**Use something else when:**
- A short fixed list where no typing is needed (a status, a province): use Select (`select`)
- Free text is allowed and the list only suggests (a city, a street): use Autocomplete (`autocomplete`)
- Searching the whole tool to jump to a page or run an action: use Command bar (`command`)
- Filtering the table on the page by text: use Search field (`search`)

**Rules:**
- Pass `items` as `{ value, label }` objects: the input shows the label and a form submits the value.
- Always include `ComboboxEmpty` with a sentence saying nothing matched ("No client by that name.").
- Name the input: a `FieldLabel` in a form, or `aria-label`.
- Keep `ComboboxList`: it scrolls inside a `ScrollArea` that fades at its edges, never a bare `overflow-y-auto`.
- The list glides to its new height as you type, its top fixed (useGlide on the popup, built in).

**Import:**
```tsx
import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup } from "@/components/ui/combobox"
```

**Usage:**
```tsx
const CLIENTS = [
  { value: "auberge-du-lac-brule", label: "Auberge du Lac Brûlé" },
  { value: "brasserie-la-voute", label: "Brasserie La Voûte" },
  { value: "mairie-de-saint-aurele", label: "Mairie de Saint-Aurèle" },
]

<Combobox items={CLIENTS}>
  <ComboboxInput placeholder="Pick a client" aria-label="Client" showClear />
  <ComboboxPopup>
    <ComboboxEmpty>No client by that name.</ComboboxEmpty>
    <ComboboxList>
      {(item: (typeof CLIENTS)[number]) => (
        <ComboboxItem key={item.value} value={item}>
          {item.label}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxPopup>
</Combobox>
```

## Autocomplete (`autocomplete`)

Section: Dropdowns and selects. File: `components/ui/autocomplete.tsx`. Page: /consistency/autocomplete

A text field that suggests values as you type and keeps whatever is typed, suggestion or not.

**Use case.** Use it for a text value where suggestions help but are not the only answer: the city on a client's address, a job title, a supplier that may be new. The value is the input's text (a string), so a new entry is kept as typed; picking a suggestion only fills the text in.

**Use it when:**
- Free text with suggestions (a city, a street, a tag that may be new)
- The value saved is the text, not a record's id
- Speeding up the typing of common values

**Use something else when:**
- The value must be an existing record (a client, a product): use Combobox (`combobox`)
- Searching the whole tool for pages, records and actions: use Command bar (`command`)
- Filtering the list on the page as you type: use Search field (`search`)
- A short fixed list: use Select (`select`)

**Rules:**
- The value is a string (`value`, `onValueChange`), never limited to the suggestions.
- `AutocompleteEmpty` says the typed text is still fine ("Keep typing, any city works."), not that it is wrong.
- Name the input: a `FieldLabel` in a form, or `aria-label`; pass `name` so a form submits the text.
- Keep `AutocompleteList`: it scrolls inside a `ScrollArea` that fades at its edges.
- The list glides to its new height as you type, its top fixed (useGlide on the popup, built in).

**Import:**
```tsx
import { Autocomplete, AutocompleteEmpty, AutocompleteInput, AutocompleteItem, AutocompleteList, AutocompletePopup } from "@/components/ui/autocomplete"
```

**Usage:**
```tsx
const CITIES = ["Montréal", "Laval", "Longueuil", "Québec", "Gatineau", "Sherbrooke"].map((c) => ({ value: c, label: c }))

<Autocomplete items={CITIES}>
  <AutocompleteInput name="city" placeholder="City" aria-label="City" />
  <AutocompletePopup>
    <AutocompleteEmpty>Keep typing, any city works.</AutocompleteEmpty>
    <AutocompleteList>
      {(item: (typeof CITIES)[number]) => (
        <AutocompleteItem key={item.value} value={item}>
          {item.label}
        </AutocompleteItem>
      )}
    </AutocompleteList>
  </AutocompletePopup>
</Autocomplete>
```

## Team selector (`team-selector`)

Section: Dropdowns and selects. File: `components/ui/team-selector.tsx`. Page: /consistency/team-selector

A pill showing the current team or company that opens a menu to switch to another, create one or open its settings.

**Use case.** Use it when a person works for more than one company or workspace in the same tool: an accountant moving between client companies, an owner moving between locations, a studio moving between client workspaces. It sits at the head of the sidebar or in the top bar, and switching it changes what every page shows.

**Use it when:**
- The person belongs to more than one company, location or workspace
- Every page's data depends on which one is current
- At the head of a sidebar or in a top bar

**Use something else when:**
- The tool belongs to one company only: show its brand (`Logo` from components/brand/logo.tsx), not a selector
- Picking a company as a value inside a form: use Combobox (`combobox`)
- Searching for a company to jump to its page: use Command bar (`command`)

**Rules:**
- It holds no state: pass `teams` (`{ id, name, plan?, logo? }`), the current `value` (an id) and `onValueChange`.
- Pass each team's `logo` when it has one; without one the team wears its name's gradient, never letters.
- Wire `onCreate` and `onSettings`: Create Team and Team Settings are always drawn and do nothing without them.
- A plan is one short word shown as a small badge ("Pro", "Team"); only "Pro" takes the primary badge.

**Import:**
```tsx
import { useState } from "react"
import { useRouter } from "next/navigation"
import { TeamSelector, type Team } from "@/components/ui/team-selector"
```

**Usage:**
```tsx
const TEAMS: Team[] = [
  { id: "nord", name: "Atelier Nord", plan: "Pro" },
  { id: "auberge", name: "Auberge du Lac Brûlé", plan: "Team" },
  { id: "brasserie", name: "Brasserie La Voûte", plan: "Team" },
]

export function TeamSwitch() {
  const router = useRouter()
  const [team, setTeam] = useState("nord")
  return (
    <TeamSelector
      teams={TEAMS}
      value={team}
      onValueChange={setTeam}
      onCreate={() => router.push("/teams/new")}
      onSettings={() => router.push("/settings/team")}
    />
  )
}
```

## Dropdown menu (`menu`)

Section: Dropdowns and selects. File: `components/ui/menu.tsx`. Page: /consistency/menu

A button that opens a list of actions, with shortcuts, submenus, checks and a destructive end.

**Use case.** Use it for the actions on a record or a page that do not each deserve a button: the ⋯ at the end of an invoice row (Edit, Duplicate, Send Reminder, Delete), the Actions button on an order's page, the account menu behind the avatar (Your Profile, Settings, Sign Out). Each item runs something; the menu holds no value of its own.

**Use it when:**
- Secondary actions on a record: a row's ⋯ or an Actions button
- The account menu behind an avatar
- View options that toggle (Compact Rows) with `MenuCheckboxItem`
- Related actions under a submenu (Share › Copy the Link, Email It)

**Use something else when:**
- Picking a value a form or a filter holds (a status, a province): use Select (`select`)
- The same actions on a right click over a row or a file: use Context menu (`context-menu`)
- Finding any page, record or action by typing: use Command bar (`command`)
- Switching between teams or companies: use Team selector (`team-selector`)

**Rules:**
- Items are Title Case ("Send Reminder", "Copy the Link"); a `MenuGroupLabel` is sentence case or an address.
- A destructive item uses `variant="destructive"` and sits last, after a `MenuSeparator`.
- The trigger is a real button: `MenuTrigger render={<Button variant="secondary" />}`, or `size="icon"` with an `aria-label` for an icon alone (⋯, the avatar).
- An icon goes before the words and a shortcut in `MenuShortcut` at the end.

**Import:**
```tsx
import { CopyIcon, MoreHorizontalIcon, PencilIcon, TrashIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Menu, MenuItem, MenuPopup, MenuSeparator, MenuShortcut, MenuTrigger } from "@/components/ui/menu"
```

**Usage:**
```tsx
<Menu>
  <MenuTrigger render={<Button size="icon" variant="ghost" aria-label="Invoice actions" />}>
    <MoreHorizontalIcon />
  </MenuTrigger>
  <MenuPopup align="end" className="w-52">
    <MenuItem onClick={onEdit}>
      <PencilIcon />
      Edit
      <MenuShortcut>⌘E</MenuShortcut>
    </MenuItem>
    <MenuItem onClick={onDuplicate}>
      <CopyIcon />
      Duplicate
    </MenuItem>
    <MenuSeparator />
    <MenuItem variant="destructive" onClick={onDelete}>
      <TrashIcon />
      Delete
    </MenuItem>
  </MenuPopup>
</Menu>
```

## Context menu (`context-menu`)

Section: Dropdowns and selects. File: `components/ui/context-menu.tsx`. Page: /consistency/context-menu

A menu of actions that opens at the pointer on a right click, or a long press on a touch screen, over an area.

**Use case.** Use it on items where people expect a right click: a file in a client's documents, a card on a job board, a row in a dense table of orders. It repeats actions that are also reachable another way (the row's ⋯, a button), as a shortcut at the pointer.

**Use it when:**
- Files, cards or rows where a right click is expected
- Repeating a row's ⋯ actions at the pointer
- A board or a canvas with no room for buttons on each item

**Use something else when:**
- The actions must be found without knowing to right click: a visible ⋯ or Actions button: use Dropdown menu (`menu`)
- Running any action by name from anywhere: use Command bar (`command`)

**Rules:**
- Items are Title Case; a destructive item uses `variant="destructive"` and sits last, after a `ContextMenuSeparator`.
- `ContextMenuTrigger` is the item itself (the row, the card), the area that answers the right click.
- A shortcut goes in `ContextMenuShortcut` at the item's end.

**Import:**
```tsx
import {
  ContextMenu,
  ContextMenuItem,
  ContextMenuPopup,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
```

**Usage:**
```tsx
<ContextMenu>
  <ContextMenuTrigger className="flex h-11 items-center rounded-[12px] px-3 text-[14px]">
    Contract, Auberge du Lac Brûlé.pdf
  </ContextMenuTrigger>
  <ContextMenuPopup className="w-52">
    <ContextMenuItem onClick={onOpen}>
      Open
      <ContextMenuShortcut>↵</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuItem onClick={onRename}>
      Rename
      <ContextMenuShortcut>F2</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="destructive" onClick={onDelete}>
      Delete
    </ContextMenuItem>
  </ContextMenuPopup>
</ContextMenu>
```

## Command bar (`command`)

Section: Dropdowns and selects. File: `components/ui/command.tsx`. Page: /consistency/command

The ⌘K bar: a dialog with one search field that finds pages, records and actions in groups and runs the one picked.

**Use case.** Use it to move fast across a whole internal tool: jumping to a client by name, opening the Invoices page, running New Invoice or New Client from any page. It opens with ⌘K (Ctrl K) and a button in the top bar, and is keyboard first: type, the arrows, Enter.

**Use it when:**
- One place to jump to any page or record by name
- Running common actions from anywhere (New Invoice, New Client)
- A tool with more pages and records than the sidebar can show

**Use something else when:**
- Filtering the list on the current page: use Search field (`search`)
- Picking a record as a value in a form: use Combobox (`combobox`)
- The actions on one record: use Dropdown menu (`menu`)

**Rules:**
- The page wires ⌘K itself: a `keydown` listener for K with `metaKey` or `ctrlKey` that toggles `open`; the component binds no key.
- Each item's `onClick` runs it and closes the dialog (`setOpen(false)`).
- Groups are `items` of `{ value, items }`: `CommandList` renders each group, `CommandCollection` its rows.
- Actions are Title Case ("New Invoice"), with their icon in `CommandItemIcon` and their keys in `CommandShortcut`; `CommandEmpty` says what to try.
- The bar glides to its new height as results change, its top fixed, rows leaving blurred and arriving sharpening (useGlide on the popup, built in): never size the bar by hand.

**Import:**
```tsx
import { useEffect, useState } from "react"
import { FileTextIcon, SearchIcon, UsersIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandCollection,
  CommandDialog,
  CommandDialogPopup,
  CommandDialogTrigger,
  CommandEmpty,
  CommandGroup,
  CommandGroupLabel,
  CommandInput,
  CommandItem,
  CommandItemIcon,
  CommandList,
  CommandPanel,
  CommandShortcut,
} from "@/components/ui/command"
```

**Usage:**
```tsx
type Entry = { value: string; label: string; icon: React.ComponentType; keys: string[] }
const GROUPS: Array<{ value: string; items: Entry[] }> = [
  { value: "Pages", items: [{ value: "clients", label: "Clients", icon: UsersIcon, keys: ["G", "C"] }] },
  { value: "Actions", items: [{ value: "new-invoice", label: "New Invoice", icon: FileTextIcon, keys: ["⌘", "I"] }] },
]

export function CommandBar() {
  const [open, setOpen] = useState(false)
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen((o) => !o)
      }
    }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [])
  return (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandDialogTrigger render={<Button variant="secondary" />}>
        <SearchIcon />
        Search or Jump To
      </CommandDialogTrigger>
      <CommandDialogPopup>
        <Command items={GROUPS}>
          <CommandInput placeholder="Search clients, pages and actions…" />
          <CommandPanel>
            <CommandEmpty>Nothing matches that. Try a client&apos;s name.</CommandEmpty>
            <CommandList>
              {(group: (typeof GROUPS)[number]) => (
                <CommandGroup key={group.value} items={group.items}>
                  <CommandGroupLabel>{group.value}</CommandGroupLabel>
                  <CommandCollection>
                    {(item: Entry) => (
                      <CommandItem key={item.value} value={item} onClick={() => setOpen(false)}>
                        <CommandItemIcon>
                          <item.icon />
                        </CommandItemIcon>
                        <span className="truncate">{item.label}</span>
                        <CommandShortcut keys={item.keys} />
                      </CommandItem>
                    )}
                  </CommandCollection>
                </CommandGroup>
              )}
            </CommandList>
          </CommandPanel>
        </Command>
      </CommandDialogPopup>
    </CommandDialog>
  )
}
```

## Calendar (`calendar`)

Section: Dates. File: `components/ui/calendar.tsx`. Page: /consistency/calendar

A month of days to pick one day, or a range of days, from, drawn in the page.

**Use case.** Use it when the month itself belongs on the page: the side panel of a schedule, the day step of a booking, the vacation range of an employee. Days are round, today carries a dot and the picked day is in the primary colour. Standing alone it sits on a card; inside a form field it is the date picker.

**Use it when:**
- A month shown in the page (a schedule's side panel, a booking step)
- A range of days: `mode="range"` (a vacation, a reporting period)
- Days that cannot be picked: `disabled` (past days, weekends)
- A date years away: `captionLayout="dropdown"` with `startMonth` and `endMonth`

**Use something else when:**
- A date in a form row or a filter, opened from a field: use Date picker (`date-picker`)
- Showing jobs or events on each day of a week (an agenda): this picks a date, it does not list events

**Rules:**
- Keep `selected` and `onSelect` in state; with `mode="single"` (the default) `onSelect` hands a `Date` or `undefined`.
- Standing alone it sits on a card (`rounded-[20px] bg-background p-3 shadow-(--shadow-rest)`), as the showcase draws it.
- Dates follow the interface's language: for a French tool pass `locale={frCA}` from `@daypicker/react/locale`.

**Import:**
```tsx
import { useState } from "react"
import { Calendar } from "@/components/ui/calendar"
```

**Usage:**
```tsx
export function DeliveryDay() {
  const [date, setDate] = useState<Date | undefined>(new Date())
  return (
    <div className="rounded-[20px] bg-background p-3 shadow-(--shadow-rest)">
      <Calendar mode="single" selected={date} onSelect={setDate} disabled={{ before: new Date() }} />
    </div>
  )
}
```

## Date picker (`date-picker`)

Section: Dates. File: `components/ui/calendar.tsx`. Page: /consistency/date-picker

A field-like button that opens a calendar in a popover and shows the picked day in words.

**Use case.** Use it for a date inside a form or a filter where a whole month should not take room until needed: an invoice's due date, a job's start day, an order's delivery date. The button reads "Pick a date" until a day is chosen, then the day in words, and the popover closes as the day is picked.

**Use it when:**
- A date field in a form (a due date, a start date)
- A date filter above a table
- Any date picked where the page has no room for a whole month

**Use something else when:**
- The month itself belongs on the page (a schedule's side panel): use Calendar (`calendar`)
- Choosing among a few named periods (Today, This Week, This Month): use Segmented control (`segmented-control`)

**Rules:**
- Control the popover (`open`, `onOpenChange`) and close it in `onSelect`, so it closes as the day is picked.
- The trigger is `Button variant="outline"` with a `CalendarIcon` first; with no date it says "Pick a date" in `text-ink-4`.
- Print the day in words with `Intl.DateTimeFormat` ("Sat, Oct 3, 2026" in `en-CA`), never a raw ISO string.
- `PopoverPopup` takes `align="start"` and `className="w-auto p-0"`; it rounds itself to the calendar's corner.

**Import:**
```tsx
import { useState } from "react"
import { CalendarIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover"
```

**Usage:**
```tsx
const dayWords = new Intl.DateTimeFormat("en-CA", { weekday: "short", month: "short", day: "numeric", year: "numeric" })

export function DueDate() {
  const [date, setDate] = useState<Date | undefined>()
  const [open, setOpen] = useState(false)
  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger render={<Button variant="outline" className="w-64 justify-start font-normal" />}>
        <CalendarIcon className="text-ink-4" />
        {date ? dayWords.format(date) : <span className="text-ink-4">Pick a date</span>}
      </PopoverTrigger>
      <PopoverPopup align="start" className="w-auto p-0">
        <Calendar
          mode="single"
          selected={date}
          onSelect={(d) => {
            setDate(d)
            setOpen(false)
          }}
        />
      </PopoverPopup>
    </Popover>
  )
}
```

## Date range picker (`date-range-picker`)

Section: Dates. File: `components/ui/date-range-picker.tsx`. Page: /consistency/date-range-picker

A pill that says a period in words and opens named periods, "the last N", two typed ends and two months to pick any range from.

**Use case.** Use it wherever a page is read for a period: a sales report, an activity log, a dashboard's figures, an accountant's export, the numbers of a campaign. The pill reads "Last 30 days" or "This month"; it opens a rail of named periods (each showing its dates), "Last [N] [days, weeks, months]", the start and end typed ("oct 3", "3 octobre") or clicked on two months, and Cancel / Apply. A report that can show the period before passes `onCompareChange` and gets "Compare to: nothing, previous period, last year".

**Use it when:**
- A report or a log read for a period, kept in the address (`?period=last-30-days`)
- Any custom range of days, typed or clicked
- A figure compared to the period before or to last year
- Choosing among the finished periods an export covers (pass `presets`, `max`, `maxDays`)

**Use something else when:**
- One day in a form (a due date, a start day): use Date picker (`date-picker`)
- Two or three fixed views a page switches between at once, with no custom range: use Segmented control (`segmented-control`)
- The month itself belongs on the page (a schedule's side panel): use Calendar (`calendar`)

**Rules:**
- The value is a key of `lib/date-range.ts` ("this-month", "last-30-days", "2026-09-01..2026-09-30", "all"), never frozen dates: keep the key in the address and resolve it on the server with `resolveRange(key, today)`, so "This month" stays this month.
- Pass `today` as the tool's own day (`YYYY-MM-DD` in its time zone); the browser's day is only the fallback.
- Nothing changes until Apply: the rail, the typed ends and the calendar only move the draft.
- Leave "all" out of `presets` where a page cannot read everything; cap a heavy read with `maxDays` and future days with `max`.
- Show `onCompareChange` only on a page that really draws the comparison; it is a segmented control, never option cards.
- French by default (`lang="fr"`), English with `lang="en"`; the words are the piece's own.

**Import:**
```tsx
import { useState } from "react"
import { DateRangePicker } from "@/components/ui/date-range-picker"
import { resolveRange } from "@/lib/date-range"
```

**Usage:**
```tsx
export function SalesPeriod({ today }: { today: string }) {
  const [period, setPeriod] = useState("last-30-days")
  const range = resolveRange(period, today) // { from, to }, or null for all time
  return <DateRangePicker value={period} onValueChange={setPeriod} today={today} max={today} />
}
```

## Dialog (`dialog`)

Section: Dialogs, sheets and popovers. File: `components/ui/dialog.tsx`. Page: /consistency/dialog

A card that opens over a dimmed page and holds one short task: a header, a panel that scrolls, and a footer of actions.

**Use case.** Use it when a person must finish a small, focused task without leaving the page they are on: editing a client's billing details from the clients list, adding a line to an invoice, inviting a member with a role. The page waits behind the backdrop and comes back exactly as it was when the dialog closes. On a phone it sits at the foot of the screen (`bottomStickOnMobile` is on by default).

**Use it when:**
- A form of a few fields that edits or creates one record
- A step the page must wait for, such as choosing a plan
- Showing something once that must be read, such as a new API key
- The task is short enough to finish in one sitting

**Use something else when:**
- The only question is whether to go ahead with something that cannot be undone: use Alert dialog and confirm (`alert-dialog`)
- Long content, filters for a list, or a record read beside the page: use Sheet (`sheet`)
- A few words or one small control attached to a button, with no backdrop: use Popover (`popover`)
- A panel on a phone that the person swipes away: use Drawer (`drawer`)

**Rules:**
- A `<form>` placed directly inside `DialogPopup` wraps the header, panel and footer; it is laid out as contents, so the footer stays in the card and the panel scrolls between the bands.
- The footer holds the actions: Cancel first (`variant="ghost"`, with `type="button"` inside a form), the primary last, both in Title Case.
- The title says the task in sentence case and the description says what it changes in one line.
- Content that can grow goes in `DialogPanel`, which scrolls and fades at its edges; never a bare `overflow-y-auto` inside the dialog.
- Triggers and closes take their button through `render={<Button />}`, never a button nested in a button.

**Import:**
```tsx
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
```

**Usage:**
```tsx
export function EditClient() {
  const [open, setOpen] = useState(false)
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger render={<Button />}>Edit the Client</DialogTrigger>
      <DialogPopup className="sm:max-w-md">
        <form onSubmit={(e) => (e.preventDefault(), setOpen(false))}>
          <DialogHeader>
            <DialogTitle>Edit the client</DialogTitle>
            <DialogDescription>What goes on their invoices and their portal.</DialogDescription>
          </DialogHeader>
          <DialogPanel className="grid gap-4">
            <Field>
              <FieldLabel>Company name</FieldLabel>
              <Input name="company" defaultValue="Bistro Le Marchand" />
            </Field>
          </DialogPanel>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" type="button" />}>Cancel</DialogClose>
            <Button type="submit">Save Changes</Button>
          </DialogFooter>
        </form>
      </DialogPopup>
    </Dialog>
  )
}
```

## Alert dialog and confirm (`alert-dialog`)

Section: Dialogs, sheets and popovers. File: `components/ui/confirm-dialog.tsx`. Page: /consistency/alert-dialog

A dialog that asks one question before something happens; `ConfirmDialog` is its ready form for a destructive action, where the thing's name must be typed first.

**Use case.** Reach for it when an action cannot be undone or throws work away: deleting an invoice, removing a client and everything filed under it, leaving a page with unsaved changes. For anything destructive use `ConfirmDialog`: it says what is lost, asks for the record's own name to be typed exactly, and wakes the red button only on a match. A question that destroys nothing (leave the page, keep a draft) is a plain `AlertDialog` with no typing.

**Use it when:**
- Deleting a record that other records depend on
- An action with no undo, such as voiding an invoice
- Leaving a page that holds unsaved work
- A choice the person must answer before anything else happens

**Use something else when:**
- Entering or editing values rather than answering a question: use Dialog (`dialog`)
- A reversible action: do it at once and offer Undo: use Toast (`toast`)
- A warning that stays on the page while the person keeps working: use Alert (`alert`)

**Rules:**
- A destructive action goes through `ConfirmDialog`: the description names what is lost, and the red button wakes only once `confirmText` is typed exactly.
- The title is the question about the thing by its name ("Delete Inventory Dashboard?"), and the action button repeats the verb ("Delete the Project"), never a bare OK.
- An alert dialog has no close button and ignores a click outside, so its footer always holds the way back, first: Cancel or Stay.
- `onConfirm` may return a promise: the button shows its loading state and the dialog closes only when it resolves, staying open if it throws.
- On a French page pass `typeLead`, `typeTail` and `cancelLabel` so every word around the name is French.

**Import:**
```tsx
import { Button } from "@/components/ui/button"
import { ConfirmDialog } from "@/components/ui/confirm-dialog"
import {
  AlertDialog,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogPopup,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
```

**Usage:**
```tsx
<>
  <ConfirmDialog
    trigger={<Button variant="destructive-outline" />}
    triggerLabel="Delete the Invoice"
    title="Delete INV-2026-014?"
    description="Its lines, payments and history go with it. This cannot be undone."
    confirmText="INV-2026-014"
    actionLabel="Delete the Invoice"
    onConfirm={() => deleteInvoice(invoice.id)}
  />
  <AlertDialog>
    <AlertDialogTrigger render={<Button variant="secondary" />}>Leave the Page</AlertDialogTrigger>
    <AlertDialogPopup className="sm:max-w-sm">
      <AlertDialogHeader>
        <AlertDialogTitle>Leave without saving?</AlertDialogTitle>
        <AlertDialogDescription>Your changes to the invoice are kept as a draft.</AlertDialogDescription>
      </AlertDialogHeader>
      <AlertDialogFooter>
        <AlertDialogClose render={<Button variant="ghost" />}>Stay</AlertDialogClose>
        <AlertDialogClose render={<Button />}>Leave</AlertDialogClose>
      </AlertDialogFooter>
    </AlertDialogPopup>
  </AlertDialog>
</>
```

## Sheet (`sheet`)

Section: Dialogs, sheets and popovers. File: `components/ui/sheet.tsx`. Page: /consistency/sheet

A panel that slides in from an edge of the window over a dimmed page, with a header, a scrolling panel and a footer.

**Use case.** Use it for work that needs more height than a dialog while keeping the page in sight: the filters of an orders list, a client's full details opened from a table row, a new invoice with many lines. It comes from the right by default (`side` also takes left, top and bottom), and `variant="inset"` floats it a little off the window's edges with rounded corners.

**Use it when:**
- Filters or view settings for the list behind it
- Reading or editing one record opened from a table row
- A form longer than a dialog comfortably holds
- The tool's places on a narrow window (`side="left"`)

**Use something else when:**
- A short task of a few fields: use Dialog (`dialog`)
- A panel on a phone that follows the finger and closes with a swipe: use Drawer (`drawer`)
- The tool's main navigation on a wide window: use Sidebar (`sidebar`)

**Rules:**
- Long content goes in `SheetPanel`, which scrolls between the header and footer and fades at its edges.
- A `<form>` placed directly inside `SheetPopup` wraps the header, panel and footer and is laid out as contents, so the footer stays at the sheet's foot.
- The footer's primary is last and says what it does, with a count when it has one ("Show 24 Results"); the way back (Reset, Cancel) comes before it.
- A left or right sheet is capped at `max-w-md` and leaves part of the page showing; narrow it with `sm:max-w-sm`, never widen it into a page.

**Import:**
```tsx
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPanel,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
```

**Usage:**
```tsx
<Sheet>
  <SheetTrigger render={<Button variant="secondary" />}>Filters</SheetTrigger>
  <SheetPopup side="right" variant="inset" className="sm:max-w-sm">
    <SheetHeader>
      <SheetTitle>Filters</SheetTitle>
      <SheetDescription>Narrow the list to what you need now.</SheetDescription>
    </SheetHeader>
    <SheetPanel className="grid gap-1">
      {["Unpaid only", "Assigned to me", "Due this week"].map((f) => (
        <Label key={f} className="flex h-11 items-center justify-between rounded-[12px] px-3 font-normal hover:bg-surface">
          {f}
          <Switch />
        </Label>
      ))}
    </SheetPanel>
    <SheetFooter>
      <SheetClose render={<Button variant="ghost" />}>Reset</SheetClose>
      <SheetClose render={<Button />}>Show 24 Results</SheetClose>
    </SheetFooter>
  </SheetPopup>
</Sheet>
```

## Drawer (`drawer`)

Section: Dialogs, sheets and popovers. File: `components/ui/drawer.tsx`. Page: /consistency/drawer

A panel that rises from the bottom (or comes in from any edge), follows the finger and closes with a swipe, the way a phone's sheets do.

**Use case.** Use it where the tool is used on a phone or a tablet: picking a time slot to book a meeting, setting an order's status on the shop floor, a short list of actions on a row. It tracks the swipe, stacks when a second drawer opens over it, and `showBar` draws the handle that says it can be pulled down. For a phone-sized action list it has its own rows (`DrawerMenu`, `DrawerMenuItem`, and checkbox and radio rows).

**Use it when:**
- The screen is a phone or a tablet and the thumb is the pointer
- A short choice from the foot of the screen: a slot, a status, an action
- A list of actions that would be cramped as a dropdown on a phone
- One drawer opening over another, stacked

**Use something else when:**
- On a wide window, a side panel for filters or a record: use Sheet (`sheet`)
- A form or a short task on a wide window: use Dialog (`dialog`)
- A list of actions from a button on a wide window: use Dropdown menu (`menu`)

**Rules:**
- There is no close button by default (`showCloseButton` is false): a swipe, the backdrop or a `DrawerClose` closes it, so the footer always offers one.
- A bottom drawer shows its handle with `showBar`, so the swipe can be found.
- Set the edge with `position` on `Drawer`: the popup reads it, and the swipe direction follows it.
- Content that can grow goes in `DrawerPanel`, which scrolls and fades at its edges.
- A `<form>` placed directly inside `DrawerPopup` is laid out as contents, so the footer stays at the bottom.

**Import:**
```tsx
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
```

**Usage:**
```tsx
<Drawer>
  <DrawerTrigger render={<Button variant="secondary" />}>Book a Meeting</DrawerTrigger>
  <DrawerPopup showBar>
    <DrawerHeader>
      <DrawerTitle>Book a meeting</DrawerTitle>
      <DrawerDescription>Pick a time; swipe down to close.</DrawerDescription>
    </DrawerHeader>
    <DrawerPanel className="grid gap-3">
      {["Tomorrow, 9:00", "Tomorrow, 14:30", "Thursday, 10:00"].map((t) => (
        <Button key={t} variant="outline" className="justify-start">
          {t}
        </Button>
      ))}
    </DrawerPanel>
    <DrawerFooter>
      <DrawerClose render={<Button className="w-full" size="lg" />}>Confirm</DrawerClose>
    </DrawerFooter>
  </DrawerPopup>
</Drawer>
```

## Popover (`popover`)

Section: Dialogs, sheets and popovers. File: `components/ui/popover.tsx`. Page: /consistency/popover

A small floating card anchored to a button, with no backdrop, that closes on a click outside or on Escape.

**Use case.** Use it for a little more than a tooltip and much less than a dialog: explaining what an "Outstanding balance" figure counts, a calendar under a date field, one setting changed in place beside the control it belongs to. The page stays live behind it, and its content scrolls and fades if it outgrows the screen. The date picker is a popover holding a calendar.

**Use it when:**
- A short explanation a person asks for by pressing a button
- A calendar or another small control that belongs to a field
- One or two settings changed in place
- Content with a link or a button, which a tooltip cannot hold

**Use something else when:**
- A list of actions to choose from: use Dropdown menu (`menu`)
- A name for an icon button, shown on hover: use Tooltip (`tooltip`)
- Picking a date for a field: use Date picker (`date-picker`)
- A form with a save, or a task that must be finished: use Dialog (`dialog`)

**Rules:**
- The trigger is a real button through `render={<Button />}`, so it opens on a press and from the keyboard, never on hover alone.
- Give it a width on `PopoverPopup` (`w-72`); a calendar inside takes `w-auto p-0`.
- Size `PopoverTitle` as the showcase does (`text-[14px] font-medium`): its default is larger than the library's popups.
- When choosing something should close it (a date), control `open` and close it in the handler.

**Import:**
```tsx
import { InfoIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Popover, PopoverDescription, PopoverPopup, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"
```

**Usage:**
```tsx
<Popover>
  <PopoverTrigger render={<Button variant="secondary" />}>
    <InfoIcon />
    What Is This?
  </PopoverTrigger>
  <PopoverPopup className="w-72">
    <PopoverTitle className="text-[14px] font-medium">Outstanding balance</PopoverTitle>
    <PopoverDescription className="mt-1 text-[13px] leading-relaxed text-ink-4">
      Everything invoiced and not yet paid, taxes included, across every live invoice.
    </PopoverDescription>
  </PopoverPopup>
</Popover>
```

## Tooltip (`tooltip`)

Section: Dialogs, sheets and popovers. File: `components/ui/tooltip.tsx`. Page: /consistency/tooltip

A small dark label that appears beside a control on hover or keyboard focus, after a short wait.

**Use case.** Use it to name an icon-only button (Archive, Copy the Link, Bold) or to teach its keyboard shortcut, in toolbars, table row actions and dense headers. Mount `TooltipProvider` once near the root with a `delay` (the library's pages use 400): the first tooltip waits, and the next ones along the same row open at once.

**Use it when:**
- An icon-only button needs its name on hover
- A shortcut is worth teaching beside its action
- A row of tool buttons where written names would crowd the bar

**Use something else when:**
- The words are needed to do the task, or hold a link or a button: use Popover (`popover`)
- A preview of a person or a record behind a link: use Preview card (`preview-card`)
- Something every person must read: use Alert (`alert`)

**Rules:**
- A tooltip never holds the only way to read something: touch screens have no hover, so an icon button still carries its `aria-label`.
- A few words in sentence case; a shortcut may follow in a dimmer span (`opacity-60`).
- The trigger is the control itself, through `render={<Button />}`, never a wrapper around it.
- `TooltipProvider` is mounted once, so neighbouring tooltips share one delay.

**Import:**
```tsx
import { ArchiveIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
```

**Usage:**
```tsx
<TooltipProvider delay={400}>
  <Tooltip>
    <TooltipTrigger render={<Button variant="ghost" size="icon" aria-label="Archive" />}>
      <ArchiveIcon />
    </TooltipTrigger>
    <TooltipPopup>
      Archive <span className="opacity-60">⌘E</span>
    </TooltipPopup>
  </Tooltip>
</TooltipProvider>
```

## Term (`term`)

Section: Dialogs, sheets and popovers. File: `components/ui/term.tsx`. Page: /consistency/term

A word with a dotted underline whose meaning shows in a tooltip on hover or focus.

**Use case.** Use it for a word a person may not know where it is used (Deposit at signing, Available, Cover, ABC class): in a summary, a column's header or a property's label. The screen keeps the word; the explanation is one hover away instead of a line of text under it (the house rule: a term explains itself on hover).

**Use it when:**
- A label or a header uses a word of the trade or of the tool
- A number needs its definition, not its value

**Use something else when:**
- An icon-only button needs its name: use Tooltip (`tooltip`)
- The words must be read to do the task: use Alert (`alert`)

**Rules:**
- The children are the word itself; tip is one or two short sentences.
- Never put a Term on every cell of a column: the header carries it once.
- Nothing a person must know lives only in the tip: touch screens show it on a tap.

**Import:**
```tsx
import { Term } from "@/components/ui/term"
```

**Usage:**
```tsx
<Term tip="Paid at signing, deducted from the total.">Deposit at signing</Term>
```

## Preview card (`preview-card`)

Section: Dialogs, sheets and popovers. File: `components/ui/preview-card.tsx`. Page: /consistency/preview-card

A card that opens when the pointer rests on a link, previewing what the link leads to.

**Use case.** Use it on names and references inside text and tables: resting on the person a project is assigned to shows their role and workload, on a client's name in an invoice list their contact and balance, on an order number its total and status. The link still opens the full record when pressed, so the card saves a trip and is never the only way in.

**Use it when:**
- A person, a client or a record number appears inline in text or a table
- A glance at a few facts would save opening the record
- The trigger is a link to the record's own page

**Use something else when:**
- The content is needed to act, or must work on a touch screen: use Popover (`popover`)
- A one-line name for a control: use Tooltip (`tooltip`)
- The full record, read or edited beside the page: use Sheet (`sheet`)

**Rules:**
- The trigger is a link with a real `href`: touch screens never hover, so pressing it must reach the record.
- A few facts only (a face, a name, a role, one line of status), about `w-72`; no form and no buttons in it.
- A person without a photo is their gradient (`AvatarFallback seed=`), never two letters in a grey circle.
- The trigger reads as a link (underlined) so people know there is something behind it.

**Import:**
```tsx
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { PreviewCard, PreviewCardPopup, PreviewCardTrigger } from "@/components/ui/preview-card"
```

**Usage:**
```tsx
<p className="text-[14px] text-ink-3">
  Assigned to{" "}
  <PreviewCard>
    <PreviewCardTrigger href="/team/nadia" className="font-medium text-foreground underline underline-offset-4">
      Nadia Okonkwo
    </PreviewCardTrigger>
    <PreviewCardPopup className="w-72">
      <div className="flex gap-3">
        <Avatar className="size-10">
          <AvatarFallback seed="nadia@ateliernord.ca" />
        </Avatar>
        <div className="grid gap-0.5">
          <span className="text-[14px] font-medium text-foreground">Nadia Okonkwo</span>
          <span className="text-[12.5px] text-ink-4">Design lead, Montréal</span>
          <span className="mt-1.5 text-[12.5px] text-ink-3">3 projects in review, back on Monday.</span>
        </div>
      </div>
    </PreviewCardPopup>
  </PreviewCard>{" "}
  until the handoff.
</p>
```

## Sidebar (`sidebar`)

Section: Navigation. File: `components/ui/nav-item.tsx`. Page: /consistency/sidebar

The tool's main navigation down the left side: the team selector at its head, each place a `NavItem` row, the current one white and lifted off the warm ground.

**Use case.** Every internal tool with more than a few sections gets one: Orders, Clients, Invoices, Schedule, and Settings at the foot. It runs the window's full height and is sticky, its list fills what is left under the head and fades at both ends, and it never scrolls itself to follow the page. Each `NavItem` renders as a Next `Link`, with `current` on the page being shown and a count of what waits (unread, unpaid) as a small badge at its end.

**Use it when:**
- The tool has four or more top-level sections
- People move between sections all day and need to see where they are
- The tool serves several companies or workspaces (the team selector at its head)

**Use something else when:**
- Sections of one record or one page: use Tabs (`tabs`)
- The way back up from a nested record: use Breadcrumb (`breadcrumb`)
- The places on a narrow window, opened from a button: use Sheet (`sheet`)
- Only switching between companies or workspaces: use Team selector (`team-selector`)

**Rules:**
- It runs the window's full height and is sticky (`sticky top-0 h-svh`); its list takes the rest with `min-h-0 flex-1 overflow-y-auto scroll-fade`, so rows fade at both ends instead of being cut.
- It never scrolls itself: the current row is highlighted, but nothing calls `scrollIntoView` to move the list as the page changes.
- One row has `current` (it sets `aria-current="page"`); rows are links through `render={<Link href="/clients" />}`.
- An icon leads each row and the row sizes it; a count is a `<Badge size="sm">` at the row's end.
- Settings sit at the foot, under the list.

**Import:**
```tsx
import { useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { InboxIcon, SettingsIcon, UsersIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { NavItem } from "@/components/ui/nav-item"
import { TeamSelector, type Team } from "@/components/ui/team-selector"
```

**Usage:**
```tsx
const NAV = [
  { href: "/orders", label: "Orders", icon: InboxIcon, count: 4 },
  { href: "/clients", label: "Clients", icon: UsersIcon, count: 0 },
]

export function AppSidebar({ teams }: { teams: Team[] }) {
  const pathname = usePathname()
  const [team, setTeam] = useState(teams[0].id)
  return (
    <aside className="sticky top-0 flex h-svh w-60 shrink-0 flex-col gap-4 bg-surface p-3">
      <TeamSelector teams={teams} value={team} onValueChange={setTeam} className="max-w-full" />
      <nav aria-label="Main" className="scroll-fade grid min-h-0 flex-1 content-start gap-0.5 overflow-y-auto">
        {NAV.map((n) => (
          <NavItem key={n.href} current={pathname === n.href} render={<Link href={n.href} />}>
            <n.icon />
            <span className="flex-1">{n.label}</span>
            {n.count > 0 && <Badge size="sm">{n.count}</Badge>}
          </NavItem>
        ))}
      </nav>
      <NavItem current={pathname === "/settings"} render={<Link href="/settings" />}>
        <SettingsIcon />
        Settings
      </NavItem>
    </aside>
  )
}
```

## Tabs (`tabs`)

Section: Navigation. File: `components/ui/tabs.tsx`. Page: /consistency/tabs

A row of tabs that switches between panels of one page, drawn as pills with a sliding white thumb or as an underline.

**Use case.** Use it to split one record or page into sections a person moves between without leaving it: a client's Invoices, Contracts and Files; an order's Details, Items and History. The pill list suits a short switch inside a card; `variant="underline"` heads a page's sections. Each `TabsTab` has a `TabsPanel` with the same `value`; a choice that only changes a value or a view, with no panels, is the segmented control.

**Use it when:**
- One record has several sections of content
- Two to six sections, each with a short name
- The sections belong to the page, not to separate places in the tool

**Use something else when:**
- Choosing a value or a view (Day, Week, Month) with no panels of its own: use Segmented control (`segmented-control`)
- Moving between the tool's main sections: use Sidebar (`sidebar`)
- Several sections a person may open at once on one scrolling page: use Accordion (`accordion`)
- Turning several options on and off: use Toggle group (`toggle-group`)

**Rules:**
- Tab names are short and in Title Case (Overview, Activity, Settings).
- Every `TabsTab` has a `TabsPanel` with the same `value`; `Tabs` takes `defaultValue`, or `value` with `onValueChange`.
- Underline tabs heading a page take `grow-0` on each tab and `w-full justify-start` on the list, so they sit at the left on one rule.
- The thumb or the underline is drawn by `TabsList` and slides on its own; never colour the active tab yourself.
- The pill slides to the picked tab on the glide in --dur-slide; content below that changes size goes in a box with useGlide.

**Import:**
```tsx
import { Tabs, TabsList, TabsPanel, TabsTab } from "@/components/ui/tabs"
```

**Usage:**
```tsx
<Tabs defaultValue="invoices" className="w-full">
  <TabsList variant="underline" className="w-full justify-start">
    <TabsTab value="invoices" className="grow-0">Invoices</TabsTab>
    <TabsTab value="contracts" className="grow-0">Contracts</TabsTab>
    <TabsTab value="files" className="grow-0">Files</TabsTab>
  </TabsList>
  <TabsPanel value="invoices" className="pt-4">Three invoices, one overdue.</TabsPanel>
  <TabsPanel value="contracts" className="pt-4">One signed contract.</TabsPanel>
  <TabsPanel value="files" className="pt-4">Twelve files.</TabsPanel>
</Tabs>
```

## Breadcrumb (`breadcrumb`)

Section: Navigation. File: `components/ui/breadcrumb.tsx`. Page: /consistency/breadcrumb

The path from a list down to the record being shown, each level a link and the last one the current page.

**Use case.** Use it at the top of a nested record so a person sees where they are and can go back up a level: Clients, then Bistro Le Marchand, then Inventory Dashboard; or Orders, then order 1042, then its return. It belongs to records two or more levels deep; a top-level page reached from the sidebar needs none.

**Use it when:**
- A record sits two or more levels below a list
- People arrive on a record from a link and need the way up
- The page's title alone does not say which client or project it belongs to

**Use something else when:**
- Switching between sections at the same level: use Tabs (`tabs`)
- The tool's main places: use Sidebar (`sidebar`)
- Moving through the pages of a long list: use Pagination (`pagination`)

**Rules:**
- The last item is a `BreadcrumbPage` (it carries `aria-current="page"`), never a link to itself.
- Every other level is a `BreadcrumbLink`; in Next it renders the router's link with `render={<Link href="/clients" />}`.
- A `BreadcrumbSeparator` goes between items and nowhere else; it draws the chevron and is hidden from screen readers.

**Import:**
```tsx
import Link from "next/link"
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
```

**Usage:**
```tsx
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink render={<Link href="/clients" />}>Clients</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink render={<Link href="/clients/bistro-le-marchand" />}>Bistro Le Marchand</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>Inventory Dashboard</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

## Pagination (`pagination`)

Section: Navigation. File: `components/ui/pagination.tsx`. Page: /consistency/pagination

Previous and Next with page numbers between them, for moving through a long list one page at a time.

**Use case.** Use it under a long list read page by page: every invoice of the year, a client's order history, a log of activity. The current page is outlined and marked for screen readers; Previous and Next keep their arrows and drop their words on a phone. A list short enough to show whole needs none.

**Use it when:**
- A table or list longer than a screen, read in pages
- The page number belongs in the address, so a link or a reload lands on the same page
- People jump to a page, not only to the next one

**Use something else when:**
- Going up from a record to its list: use Breadcrumb (`breadcrumb`)
- Sections of one record: use Tabs (`tabs`)

**Rules:**
- The current page's `PaginationLink` takes `isActive`: it sets `aria-current="page"` and the outlined style.
- Each link carries an `href` to its page; when the page is held in state instead, call `preventDefault` in `onClick` as the showcase does.
- Passing `render` to `PaginationLink` drops its button styles, so use `href` and `onClick` rather than a custom element.
- Keep the page inside its bounds in the handler (`Math.max(1, p - 1)`), and mark a gap in the numbers with `PaginationEllipsis` rather than listing every page.

**Import:**
```tsx
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
```

**Usage:**
```tsx
export function InvoicePages({ page }: { page: number }) {
  return (
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationPrevious href={`?page=${Math.max(1, page - 1)}`} />
        </PaginationItem>
        {[1, 2, 3].map((n) => (
          <PaginationItem key={n}>
            <PaginationLink href={`?page=${n}`} isActive={page === n}>
              {n}
            </PaginationLink>
          </PaginationItem>
        ))}
        <PaginationItem>
          <PaginationEllipsis />
        </PaginationItem>
        <PaginationItem>
          <PaginationNext href={`?page=${page + 1}`} />
        </PaginationItem>
      </PaginationContent>
    </Pagination>
  )
}
```

## Toolbar (`toolbar`)

Section: Navigation. File: `components/ui/toolbar.tsx`. Page: /consistency/toolbar

A floating pill that groups related controls, such as formatting buttons, into one stop for Tab with the arrow keys moving between them.

**Use case.** Use it for a set of tools that act on one thing: the formatting controls of a notes editor (Bold, Italic, Link, Heading), the controls over a document preview, the actions on rows a person has ticked in a table. Each `ToolbarButton` renders a `Toggle` for something that switches on and off, or a `Button` for a one-time action.

**Use it when:**
- Several controls act on the same selection or document
- Formatting that switches on and off
- A floating bar of actions over content

**Use something else when:**
- Two or three attached buttons with no shared keyboard stop: use Button group (`button-group`)
- Choosing one value among a few: use Segmented control (`segmented-control`)
- Several on and off options read as one value: use Toggle group (`toggle-group`)
- Actions tucked behind one button: use Dropdown menu (`menu`)

**Rules:**
- Each `ToolbarButton` renders its control through `render`: `<Toggle size="sm" />` for a state, `<Button />` for an action.
- An icon-only control carries an `aria-label` (Bold, Italic, Link).
- Related tools sit in a `ToolbarGroup`, with a `ToolbarSeparator` between groups and never at an end.
- Words on toolbar buttons are in Title Case (Heading, Quote).

**Import:**
```tsx
import { BoldIcon, ItalicIcon, LinkIcon } from "@/components/icons"
import { Toggle } from "@/components/ui/toggle"
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator } from "@/components/ui/toolbar"
```

**Usage:**
```tsx
<Toolbar>
  <ToolbarGroup>
    <ToolbarButton render={<Toggle size="sm" aria-label="Bold" />}>
      <BoldIcon />
    </ToolbarButton>
    <ToolbarButton render={<Toggle size="sm" aria-label="Italic" />}>
      <ItalicIcon />
    </ToolbarButton>
    <ToolbarButton render={<Toggle size="sm" aria-label="Link" />}>
      <LinkIcon />
    </ToolbarButton>
  </ToolbarGroup>
  <ToolbarSeparator />
  <ToolbarGroup>
    <ToolbarButton render={<Toggle size="sm" />}>Heading</ToolbarButton>
    <ToolbarButton render={<Toggle size="sm" />}>Quote</ToolbarButton>
  </ToolbarGroup>
</Toolbar>
```

## Accordion (`accordion`)

Section: Navigation. File: `components/ui/accordion.tsx`. Page: /consistency/accordion

A stack of headings that each open their panel below, one at a time unless `multiple` is set.

**Use case.** Use it for content a person scans by heading and opens only where needed: the answers on a client portal's help page, the terms of a maintenance plan, the optional sections of a long settings page. Opening one item closes the others by default; `multiple` lets several stay open. `defaultValue` takes an array of the values open at first.

**Use it when:**
- A list of questions and answers
- Several sections of reading where most people need one
- Long optional detail that would bury the page shown whole

**Use something else when:**
- One block of extra content behind one trigger: use Collapsible (`collapsible`)
- Sections a person switches between in the same place: use Tabs (`tabs`)
- Content every person must see

**Rules:**
- Each `AccordionItem` has a unique `value`, and `defaultValue` is an array of those values.
- The trigger is the heading's words (a question or a section's name) and draws its own chevron; never add another.
- Items are divided by the hairline `AccordionItem` draws, none under the last.
- The panel's height animates on its own; put the content straight in `AccordionPanel`.
- The panel opens and shuts on the glide in --dur-glide, from its top: never a fixed height.

**Import:**
```tsx
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@/components/ui/accordion"
```

**Usage:**
```tsx
<div className="w-full max-w-md rounded-[20px] bg-background px-5 shadow-(--shadow-rest)">
  <Accordion defaultValue={["included"]}>
    <AccordionItem value="included">
      <AccordionTrigger>What is included in maintenance?</AccordionTrigger>
      <AccordionPanel>Daily backups, monthly updates and up to two hours of small changes a month.</AccordionPanel>
    </AccordionItem>
    <AccordionItem value="faults">
      <AccordionTrigger>When is a critical fault answered?</AccordionTrigger>
      <AccordionPanel>Within two business hours, and worked on the same day.</AccordionPanel>
    </AccordionItem>
  </Accordion>
</div>
```

## Collapsible (`collapsible`)

Section: Navigation. File: `components/ui/collapsible.tsx`. Page: /consistency/collapsible

One trigger that shows or hides one panel below it, its height animated.

**Use case.** Use it to tuck away one block that most people skip: the archived projects under a client's list, the advanced options under a short form, the full address under a summary. The trigger has no style of its own, so it is drawn as the showcase draws it: a full-width row with a chevron that turns when the panel is open.

**Use it when:**
- A single block of secondary content
- An archived or "more" group at the foot of a list
- Advanced options under a short form

**Use something else when:**
- Several sections in a stack, such as questions and answers: use Accordion (`accordion`)
- Content floating over the page rather than pushing it down: use Popover (`popover`)
- Switching between views in the same place: use Tabs (`tabs`)

**Rules:**
- The trigger says what is inside and how much ("Three archived projects"), in sentence case.
- `CollapsibleTrigger` comes unstyled: give it a row's shape, a hover and the focus ring (`focus-visible:shadow-(--focus-ring)`).
- Turn the chevron from the trigger's open state (`group-data-panel-open/c:rotate-180` on a named group), never from state of your own.
- Start it open with `defaultOpen`, or control it with `open` and `onOpenChange`.
- The panel opens and shuts on the glide in --dur-glide, from its top: never a fixed height.

**Import:**
```tsx
import { ChevronDownIcon } from "@/components/icons"
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "@/components/ui/collapsible"
```

**Usage:**
```tsx
<Collapsible className="w-full max-w-sm">
  <CollapsibleTrigger className="group/c flex h-10 w-full cursor-pointer items-center justify-between rounded-[12px] px-3 text-[14px] font-medium outline-none hover:bg-background focus-visible:shadow-(--focus-ring)">
    Three archived projects
    <ChevronDownIcon className="size-4 text-ink-4 transition-transform duration-(--dur-slide) ease-glide group-data-panel-open/c:rotate-180" />
  </CollapsibleTrigger>
  <CollapsiblePanel>
    <ul className="grid gap-1 px-3 pt-1 pb-2 text-[13px] text-ink-3">
      <li>Website 2024</li>
      <li>Price list</li>
      <li>Old portal</li>
    </ul>
  </CollapsiblePanel>
</Collapsible>
```

## Badge (`badge`)

Section: Badges, avatars and cards. File: `components/ui/badge.tsx`. Page: /consistency/badge

A small pill that labels something with a word or a number: a status, a count or a tag.

**Use case.** Use a badge for a record's status where it is scanned in a list: an invoice that is Paid, Due soon or Overdue in a table row, an order In review, a quote still a Draft. The four state variants (success, warning, error, info) say where something stands at a glance; the quiet default labels without judging, and outline suits a count beside a title (3,820 members). It is a label, not a message: a sentence about a problem belongs in an Alert.

**Use it when:**
- An invoice's or an order's status in a table cell
- A short tag beside a title (Draft, New)
- A count next to a name or a heading
- A small pill that links somewhere, through render

**Use something else when:**
- The state needs a sentence and stays on the page (the site is down): use Alert (`alert`)
- Something just happened and the person should know (the invoice was sent): use Toast (`toast`)
- Showing a keyboard shortcut: use Keyboard key (`kbd`)
- The pill switches a filter or a view on and off: use Toggle group (`toggle-group`)

**Rules:**
- The variant carries meaning: success for done or paid, warning for soon or needs a look, error for late or failed, info for in progress or in review. secondary, the default, labels anything without a state.
- Map each status to its variant in one object (a TONE map), so the same status wears the same colour on every screen.
- default, the ink fill, is for the one label that matters on a screen, not for every badge.
- One or two words in sentence case. Sizes are sm, default and lg; a table uses the default.
- A plain badge is a span and does nothing on press; to make it a link or a button pass render, which gives it press feedback.

**Import:**
```tsx
import { Badge } from "@/components/ui/badge"
```

**Usage:**
```tsx
const TONE = {
  Paid: "success",
  "Due soon": "warning",
  Overdue: "error",
  "In review": "info",
  Draft: "secondary",
} as const

export function InvoiceStatus({ status }: { status: keyof typeof TONE }) {
  return <Badge variant={TONE[status]}>{status}</Badge>
}

export function MemberCount() {
  return <Badge variant="outline">3,820 members</Badge>
}
```

## Avatar (`avatar`)

Section: Badges, avatars and cards. File: `components/ui/avatar.tsx`. Page: /consistency/avatar

A person's round picture, or their own colour gradient when there is no photo.

**Use case.** Use an avatar wherever a teammate appears: the person assigned to an order in a table row, the author of a note on a client's history, the account button in the top bar. With no photo, AvatarFallback takes the person's email as seed and draws their gradient, the same one on every screen. Several people on one job overlap as a group.

**Use it when:**
- A teammate beside their name in a row or a comment
- Who an order or a task is assigned to
- The signed-in person's account button
- Several assignees at once, overlapped

**Use something else when:**
- Switching between teams or workspaces: use Team selector (`team-selector`)
- Showing more about the person when the pointer rests on them: use Preview card (`preview-card`)
- The person's row is still loading: use Skeleton (`skeleton`)
- Labelling a row with a status or a category, not a person: use Badge (`badge`)

**Rules:**
- A person with no photo is drawn with AvatarFallback seed= (their email, else their name): their gradient from lib/avatar-gradient.ts, never initials in a grey circle.
- Keep AvatarFallback beside AvatarImage: it shows while the photo loads and if it fails.
- Size it with a size class on Avatar: size-6, size-8 (the default) or size-10.
- In a group each avatar carries ring-2 ring-surface and the row overlaps them with -space-x-2; a +3 count is a plain round span on an opaque ground (bg-surface-3), never a translucent fill the face behind would show through.
- AvatarImage takes alt="" when the name is printed beside it, so it is not read twice.

**Import:**
```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
```

**Usage:**
```tsx
export function Assignee({ name, email, photo }: { name: string; email: string; photo?: string }) {
  return (
    <div className="flex items-center gap-2">
      <Avatar className="size-8">
        {photo ? <AvatarImage src={photo} alt="" /> : null}
        <AvatarFallback seed={email} />
      </Avatar>
      <span className="text-[13px]">{name}</span>
    </div>
  )
}

export function Assignees({ emails, max = 4 }: { emails: string[]; max?: number }) {
  const more = emails.length - max
  return (
    <div className="flex -space-x-2">
      {emails.slice(0, max).map((email) => (
        <Avatar key={email} className="size-8 ring-2 ring-surface">
          <AvatarFallback seed={email} />
        </Avatar>
      ))}
      {more > 0 && (
        <span className="relative grid size-8 place-items-center rounded-full bg-surface-3 text-[11px] font-medium text-ink-3 tabular-nums ring-2 ring-surface">
          +{more}
        </span>
      )}
    </div>
  )
}
```

## Card (`card`)

Section: Badges, avatars and cards. File: `components/ui/card.tsx`. Page: /consistency/card

A white surface with rounded corners and a soft shadow that holds one subject.

**Use case.** Use a card for one thing read at a glance: a dashboard figure (what is outstanding across live invoices), a client's summary, a booking with Confirm and Move It. CardHeader carries the title, a muted line and an optional CardAction at the right (a menu button); CardPanel holds the body; CardFooter holds the buttons. Several related panels under one heading are a Frame instead.

**Use it when:**
- A figure on a dashboard with its title and its change
- One record's summary on its page
- A short task with its buttons (book, confirm)
- A block of settings that stands alone

**Use something else when:**
- Several panels grouped under one quiet heading: use Frame (`frame`)
- Many records compared by the same columns: use Table (`table`)
- A message about a condition on the page: use Alert (`alert`)
- Content that opens over the page to be filled in: use Dialog (`dialog`)

**Rules:**
- Parts in order: CardHeader (CardTitle, CardDescription, CardAction), then CardPanel, then CardFooter. Leave out what is not needed; the spacing adjusts to the parts present.
- CardAction holds one control, usually an icon button with an aria-label.
- Figures in a card are tabular-nums.
- Footer buttons go secondary first, primary last.
- CardPanel is also exported as CardContent; it is the same part.

**Import:**
```tsx
import { MoreHorizontalIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardDescription, CardHeader, CardPanel, CardTitle } from "@/components/ui/card"
```

**Usage:**
```tsx
export function OutstandingCard() {
  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-[15px] font-medium">Outstanding</CardTitle>
        <CardDescription className="text-[13px] text-ink-4">Across 3 live invoices</CardDescription>
        <CardAction>
          <Button size="icon-sm" variant="ghost" aria-label="More">
            <MoreHorizontalIcon />
          </Button>
        </CardAction>
      </CardHeader>
      <CardPanel>
        <p className="text-[30px] font-medium tracking-[-0.02em] tabular-nums">$58,834.75</p>
        <p className="mt-1 text-[12.5px] text-success-foreground tabular-nums">+1,204.60, 2.1% this week</p>
      </CardPanel>
    </Card>
  )
}
```

## Frame (`frame`)

Section: Badges, avatars and cards. File: `components/ui/frame.tsx`. Page: /consistency/frame

A grey tray that holds one or more white panels under a quiet heading.

**Use case.** Use a frame to group what belongs together under one small heading: the next steps on a client's page, the panels of one integration's settings, two stacked panels of the same record. The heading sits on the tray (FrameHeader, FrameTitle, FrameDescription) and each FramePanel is a white sheet inside it.

**Use it when:**
- A short list under a heading on a record's page
- Two or more panels that belong to one subject
- A settings group with its own title and a footer note

**Use something else when:**
- One subject that stands alone, with its own title and buttons: use Card (`card`)
- Sections a person opens and closes: use Accordion (`accordion`)
- Splitting content inside one panel: use Separator (`separator`)
- Rows of records with columns: use Table (`table`)

**Rules:**
- Content goes in a FramePanel; the tray itself holds only FrameHeader, the panels and FrameFooter.
- The tray is the surface tone: put the frame on the white page ground (bg-background), where it reads as a tray.
- Panels stack on their own with a 4px gap; do not add margins between them.
- The title is quiet: 13px, medium, text-ink-3, as in the showcase.

**Import:**
```tsx
import { Frame, FrameHeader, FramePanel, FrameTitle } from "@/components/ui/frame"
```

**Usage:**
```tsx
export function NextSteps() {
  return (
    <Frame className="w-full max-w-md">
      <FrameHeader>
        <FrameTitle className="text-[13px] font-medium text-ink-3">Next steps</FrameTitle>
      </FrameHeader>
      <FramePanel className="grid gap-2 text-[13.5px]">
        <span>Renew the domain</span>
        <span>Pay the invoice</span>
        <span>Call the landlord</span>
      </FramePanel>
    </Frame>
  )
}
```

## Keyboard key (`kbd`)

Section: Badges, avatars and cards. File: `components/ui/kbd.tsx`. Page: /consistency/kbd

A keycap that shows a key or a keyboard shortcut.

**Use case.** Use it to teach a shortcut where the person already looks: ⌘ K inside the search field, a key-hint footer (↵ to open, Esc to close), a tooltip naming a button's shortcut. Keys pressed together sit in one KbdGroup, one Kbd per key.

**Use it when:**
- A shortcut hint in a search field or a footer
- A tooltip that names a button's shortcut
- Help text that says which key to press

**Use something else when:**
- A shortcut on a dropdown menu's row (MenuShortcut): use Dropdown menu (`menu`)
- A shortcut on a command bar's row (CommandShortcut): use Command bar (`command`)
- A status or a count: use Badge (`badge`)

**Rules:**
- It only shows a key: it is not pressable (pointer-events-none) and listens for nothing; the shortcut needs its own key handler.
- One Kbd per key; keys pressed together go in one KbdGroup.
- Write keys as their symbols or short names (⌘, ⇧, ↵, Esc) and letters in capitals.
- Menu and command rows have their own shortcut slots; use those there, not a Kbd.

**Import:**
```tsx
import { Kbd, KbdGroup } from "@/components/ui/kbd"
```

**Usage:**
```tsx
export function SearchHint() {
  return (
    <p className="flex items-center gap-1.5 text-[12.5px] text-ink-4">
      Press
      <KbdGroup>
        <Kbd>⌘</Kbd>
        <Kbd>K</Kbd>
      </KbdGroup>
      to search, <Kbd>Esc</Kbd> to close
    </p>
  )
}
```

## Separator (`separator`)

Section: Badges, avatars and cards. File: `components/ui/separator.tsx`. Page: /consistency/separator

A one-pixel line that divides content, across or upright.

**Use case.** Use it inside one card or panel to split two blocks (an invoice's lines from its totals, two sections of a form), or upright between inline actions (Edit, Share, Delete). It is Base UI's separator, so it carries role="separator" and its orientation for assistive tech.

**Use it when:**
- Between two blocks inside the same card or panel
- Upright between inline actions or facts on one line
- Above a totals line or a footer note

**Use something else when:**
- Separating two whole subjects: use Card (`card`)
- Grouping several panels under a heading: use Frame (`frame`)
- Dividing items in a dropdown menu (MenuSeparator): use Dropdown menu (`menu`)
- Rows of records, which draw their own lines: use Table (`table`)

**Rules:**
- Horizontal is the default and spans the full width.
- orientation="vertical" stretches to the height of its flex row; give the row a height (h-5 in the showcase) or the separator its own h- class.
- Its colour is the border token, the same as every hairline in the library.
- Menus and tables draw their own dividers; a Separator is for content you lay out yourself.

**Import:**
```tsx
import { Separator } from "@/components/ui/separator"
```

**Usage:**
```tsx
export function InvoiceActions() {
  return (
    <div className="grid w-full max-w-sm gap-4 text-[13.5px]">
      <span>Three lines, $4,885.00 before tax</span>
      <Separator />
      <div className="flex h-5 items-center gap-4 text-ink-3">
        <span>Edit</span>
        <Separator orientation="vertical" />
        <span>Share</span>
        <Separator orientation="vertical" />
        <span>Delete</span>
      </div>
    </div>
  )
}
```

## Scroll area (`scroll-area`)

Section: Badges, avatars and cards. File: `components/ui/scroll-area.tsx`. Page: /consistency/scroll-area

A box that scrolls its content behind a thin scrollbar and fades the edge that has more to show.

**Use case.** Use it for any list that can outgrow its box: the invoices in a side panel, a client's notes, a long list inside a popover you build yourself. Give it a height and it fades at the top or the bottom only where more is hidden, which is the library's scroll rule. A table that scrolls sideways does not need it: its own container already fades.

**Use it when:**
- A list with a fixed height that may grow long
- A panel's body that scrolls under a fixed header
- Any region that would otherwise need overflow-y-auto

**Use something else when:**
- A table wider than its box: use Table (`table`)
- The app's navigation rail: use Sidebar (`sidebar`)
- Options to pick from in a list that opens: use Select (`select`)
- A few sections that can fold instead of scroll: use Accordion (`accordion`)

**Rules:**
- Give it a bounded height (h-56, or a flex child with min-h-0); without one nothing scrolls.
- Leave scrollFade on, its default: nothing that scrolls may cut a row in half at a hard edge.
- Never a bare overflow-y-auto: use ScrollArea, or put scroll-fade (or scroll-fade-x) on the element that overflows.
- The frame (corner, ground, shadow) goes on ScrollArea's className: the fade is on the inner viewport, so the frame never fades.
- overscrollContain keeps a scroll inside it from scrolling the page behind; scrollbarGutter keeps the content clear of the scrollbar.

**Import:**
```tsx
import { ScrollArea } from "@/components/ui/scroll-area"
```

**Usage:**
```tsx
type Invoice = { id: string; number: string; amount: string }

export function InvoiceList({ invoices }: { invoices: Invoice[] }) {
  return (
    <ScrollArea className="h-56 w-full max-w-xs rounded-[20px] bg-background shadow-(--shadow-rest)">
      <ul className="grid p-2">
        {invoices.map((invoice) => (
          <li key={invoice.id} className="flex h-9 items-center justify-between rounded-[10px] px-3 text-[13px] hover:bg-surface">
            <span>{invoice.number}</span>
            <span className="text-ink-4 tabular-nums">{invoice.amount}</span>
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}
```

## Table (`table`)

Section: Tables and figures. File: `components/ui/table.tsx`. Page: /consistency/table

Rows and columns of records under a header row, scrolling sideways with a fade when wider than its box.

**Use case.** Use a table when each row is one record and the columns compare the same facts: invoices by number, client, status and amount, orders by date and total, clients by city and balance. Status cells hold a Badge and amounts sit right-aligned in tabular figures. Wrap it in a rounded white box with overflow-hidden for its frame.

**Use it when:**
- A list of invoices, orders or clients with the same facts per row
- Figures a person compares down a column
- Rows that open a record when clicked
- A short summary with a totals row (TableFooter)

**Use something else when:**
- One record's facts on its own page: use Card (`card`)
- Moving through a long list page by page, under the table: use Pagination (`pagination`)
- The rows are still loading: use Skeleton (`skeleton`)
- There are no rows yet: use Empty state (`empty`)

**Rules:**
- The frame goes on a wrapper (rounded-[20px], overflow-hidden, bg-background, the rest shadow), never on Table: its container scrolls sideways with scroll-fade-x, and the fade would erase the frame.
- Numbers are tabular-nums, and amounts are right-aligned (text-right on the head and on the cell).
- Inside the frame the first column takes ps-5 and the last pe-5.
- A status is a Badge, its variant mapped from one TONE object.
- Headers are short and in sentence case (Invoice, Client, Status, Amount); a selected row carries data-state="selected".

**Import:**
```tsx
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
```

**Usage:**
```tsx
const TONE = { Paid: "success", "Due soon": "warning", Overdue: "error", Draft: "secondary" } as const

type Row = { number: string; client: string; status: keyof typeof TONE; amount: string }

export function InvoicesTable({ rows }: { rows: Row[] }) {
  return (
    <div className="overflow-hidden rounded-[20px] bg-background shadow-(--shadow-rest)">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead className="ps-5">Invoice</TableHead>
            <TableHead>Client</TableHead>
            <TableHead>Status</TableHead>
            <TableHead className="pe-5 text-right">Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.number}>
              <TableCell className="ps-5 font-medium tabular-nums">{row.number}</TableCell>
              <TableCell className="text-ink-2">{row.client}</TableCell>
              <TableCell>
                <Badge variant={TONE[row.status]}>{row.status}</Badge>
              </TableCell>
              <TableCell className="pe-5 text-right tabular-nums">{row.amount}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}
```

## Progress (`progress`)

Section: Tables and figures. File: `components/ui/progress.tsx`. Page: /consistency/progress

A bar that fills as a task moves toward done.

**Use case.** Use it while something advances to an end you can count: a file uploading, an import of 400 clients from a spreadsheet, a batch of invoices sent one by one. ProgressLabel says what is happening and ProgressValue prints the percentage above the track. With value={null} it is indeterminate and draws an empty track, so for a wait of unknown length a Spinner says more.

**Use it when:**
- An upload or an import of a known size
- A batch where you can count what is done
- A setup in steps that reports how far it got

**Use something else when:**
- A measure within a range that does not finish (storage used, hours left this month): use Meter (`meter`)
- A wait of unknown length: use Spinner (`spinner`)
- Content is loading and its shape is known: use Skeleton (`skeleton`)
- Saying the task finished: use Toast (`toast`)

**Rules:**
- value runs from min to max (0 to 100 by default) and only moves forward with the task; the indicator eases to each new value.
- Name the task with ProgressLabel; ProgressValue prints the percentage in tabular figures.
- With no label in view, give Progress an aria-label.
- Progress with no children draws the bare track and indicator.

**Import:**
```tsx
import { Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue } from "@/components/ui/progress"
```

**Usage:**
```tsx
export function UploadProgress({ name, percent }: { name: string; percent: number }) {
  return (
    <Progress value={percent}>
      <div className="flex justify-between text-[12.5px]">
        <ProgressLabel className="text-ink-3">Uploading {name}</ProgressLabel>
        <ProgressValue className="tabular-nums text-ink-4" />
      </div>
      <ProgressTrack className="mt-2">
        <ProgressIndicator />
      </ProgressTrack>
    </Progress>
  )
}

export function ImportProgress({ done, total }: { done: number; total: number }) {
  return <Progress value={done} max={total} aria-label="Importing clients" />
}
```

## Meter (`meter`)

Section: Tables and figures. File: `components/ui/meter.tsx`. Page: /consistency/meter

A bar that shows where a measure sits within its range.

**Use case.** Use it for an amount against a limit that is not a task: storage used of the plan, hours of maintenance used this month out of the agreement, seats taken in a subscription, a budget spent against its cap. Set max to the real limit and print the figure in its unit with MeterValue's function (7.4 of 10 GB).

**Use it when:**
- Storage, seats or hours used against a limit
- A budget spent against its cap
- A score within a known range

**Use something else when:**
- A task moving toward done (an upload, an import): use Progress (`progress`)
- Picking a value along a range: use Slider (`slider`)
- The limit is passed and someone must act: use Alert (`alert`)

**Rules:**
- min and max are the real range (max={10} for 10 GB); the bar shows value within it.
- MeterValue takes a function that returns the words a person reads (7.4 of 10 GB), in tabular figures; without one it prints a percentage.
- Give Meter getAriaValueText with the same words, so a screen reader hears the unit too.
- A meter moves only when the measure changes; it never animates on its own.

**Import:**
```tsx
import { Meter, MeterIndicator, MeterLabel, MeterTrack, MeterValue } from "@/components/ui/meter"
```

**Usage:**
```tsx
export function StorageMeter({ used, limit }: { used: number; limit: number }) {
  const words = `${used} of ${limit} GB`
  return (
    <Meter value={used} max={limit} getAriaValueText={() => words}>
      <div className="flex justify-between text-[12.5px]">
        <MeterLabel className="text-ink-3">Storage</MeterLabel>
        <MeterValue className="tabular-nums text-ink-4">{() => words}</MeterValue>
      </div>
      <MeterTrack className="mt-2">
        <MeterIndicator />
      </MeterTrack>
    </Meter>
  )
}
```

## Stat (`stat`)

Section: Charts. File: `components/ui/stat.tsx`. Page: /consistency/stat

One figure that matters: its label, its value, how it moved against a named period, and an optional sparkline under it.

**Use case.** Use it across the top of a dashboard or a report: the month's sales, the orders this week, the average order, the return rate. Give it the change in percent and the period it is compared with (vs last month), and say which way is good with goodWhen, so more returns read as bad even though the number went up. Put a Sparkline in trend for the shape of the last weeks. Four sit across a wide page, in cards (chart-card.stat).

**Use it when:**
- A dashboard's headline figures, four across
- A figure with its change against the period before
- A figure whose last weeks matter as much as today's value

**Use something else when:**
- The figure is a share of a goal (80 % of the month's target): use Progress ring (`progress-ring`)
- Several figures that belong to one record (an order's total, paid, owing): use Card (`card`)
- The value over the whole period is the point, not one number: use Area chart (`area-chart`)

**Rules:**
- Always say what the change is compared with (period); a percentage with no period means nothing.
- goodWhen decides the colour: a rise is green only when a rise is good. The arrow and the sign say the direction whatever the colour.
- The value uses the font's proportional figures at this size; keep tabular-nums for columns.
- Format the value in the client's language before passing it (128 460 $ in French, $128,460 in English) and pass locale for the change.

**Import:**
```tsx
import { Sparkline } from "@/components/ui/sparkline"
import { Stat } from "@/components/ui/stat"
```

**Usage:**
```tsx
<Card className="p-5">
  <Stat
    label="Ventes"
    value="128 460 $"
    change={12.6}
    period="vs 30 jours avant"
    trend={<Sparkline values={lastTwelveWeeks} aria-label="Ventes, 12 dernières semaines" />}
  />
</Card>
```

## Sparkline (`sparkline`)

Section: Charts. File: `components/ui/sparkline.tsx`. Page: /consistency/sparkline

A trend drawn as a small line beside or under a figure, with no axis: the past in grey, the latest stretch and its point in blue.

**Use case.** Use it inside a Stat or a table row to show the shape of recent values at a glance: sales over the last 12 weeks, a client's orders per month, stock over the last days. Hovering shows the value under the pointer; the figure beside it carries the number.

**Use it when:**
- Under a Stat, the last 12 periods
- In a table row, a client's recent trend
- Anywhere the shape matters more than the values

**Use something else when:**
- The values themselves must be read, with dates: use Area chart (`area-chart`)
- One amount against a limit: use Meter (`meter`)

**Rules:**
- Always next to a figure or a label that says what it is; a sparkline alone is a squiggle.
- Twelve points or so; the latest is the blue point.
- Give it an aria-label that names the measure and the span.
- Every value shown on hover is also reachable without it: a label, the legend with values, or the card's table view (ChartTable).

**Import:**
```tsx
import { Sparkline } from "@/components/ui/sparkline"
```

**Usage:**
```tsx
<Sparkline values={[31, 38, 34, 42, 40, 47, 44, 52, 49, 55, 58, 61]} format={(n) => `${n} commandes`} aria-label="Commandes, 12 dernières semaines" />
```

## Area chart (`area-chart`)

Section: Charts. File: `components/ui/area-chart.tsx`. Page: /consistency/area-chart

A value over time: one 2px line per series, the first washed underneath, the period before as a quiet dashed line, a crosshair that finds the nearest date.

**Use case.** Use it for anything measured over days, weeks or months: sales over the period against the period before, visits, hours worked, cash on hand. The crosshair snaps to the nearest date and the readout lists every series there; the arrows walk it from the keyboard. When the period changes the lines glide to their new shape. In a card it sits under a ChartLegend, with a switch to ChartTable (chart-card.trend).

**Use it when:**
- A value per day, week or month
- This period against the period before
- Two or three series of the same unit over the same dates

**Use something else when:**
- Amounts per category, or parts of each period: use Bar chart (`bar-chart`)
- The shape only, beside a figure: use Sparkline (`sparkline`)
- Two measures of different units: never a second axis, make two charts: use Area chart (`area-chart`)

**Rules:**
- One axis. Two measures with different units are two charts, never a second y-axis.
- Mark a comparison series compare: it draws quiet and dashed and never takes a series colour.
- A series may leave rows out (no key, or an empty string): its line stops and starts again, so a past and a plan are two series meeting at today; `mark={{ at, label }}` draws today as a quiet upright line.
- Pass format for the readout and formatAxis for the short ticks (128 k$); the axis starts at zero.
- Two series or more need a ChartLegend above the chart; one series is named by the card's title.
- Colours come from the series order (SERIES in components/ui/chart.tsx: blue, orange, aqua, yellow, magenta), never chosen by hand; a sixth series folds into Other. Colour follows what a series is, so the same series keeps its colour when a filter removes another.
- Words and numbers stay in the text inks; a short key beside them carries the colour. Never colour a label with its series.
- Every value shown on hover is also reachable without it: a label, the legend with values, or the card's table view (ChartTable).

**Import:**
```tsx
import { AreaChart } from "@/components/ui/area-chart"
import { COMPARE, ChartLegend, SERIES } from "@/components/ui/chart"
```

**Usage:**
```tsx
<ChartLegend
  items={[
    { label: "Cette période", color: SERIES[0], shape: "line" },
    { label: "Période d’avant", color: COMPARE, shape: "line", dashed: true },
  ]}
/>
<AreaChart
  data={days}
  index="label"
  series={[
    { key: "now", name: "Cette période" },
    { key: "before", name: "Période d’avant", compare: true },
  ]}
  format={money}
  formatAxis={moneyShort}
  aria-label="Ventes dans le temps"
/>
```

## Bar chart (`bar-chart`)

Section: Charts. File: `components/ui/bar-chart.tsx`. Page: /consistency/bar-chart

Amounts as columns from one baseline: one series, several side by side, or stacked as the parts of each period.

**Use case.** Use it for amounts per category or per period: orders per day by status (stacked), sales per month, hours per employee, invoices per week paid and unpaid. Columns are at most 24px wide with a rounded top, 2px of surface between touching marks, the tallest labelled at its cap. Each column's whole band answers the pointer and the keyboard with every value and the total. When the data changes the columns glide; the first time they grow from the baseline (chart-card.stacked).

**Use it when:**
- An amount per day, week or month
- The parts of each period (stacked)
- A handful of categories compared side by side

**Use something else when:**
- A long list of categories ranked: use Bar list (`bar-list`)
- A continuous value over many dates: use Area chart (`area-chart`)
- The parts of one whole, not of each period: use Donut chart (`donut-chart`)

**Rules:**
- Stack only parts that add up to a meaningful total; otherwise put them side by side (stacked={false}).
- Give a series its colour by what it is when the order of the stack is not the order of meaning (late is the warm orange whatever its place).
- Three series at most stacked; more is a table.
- Colours come from the series order (SERIES in components/ui/chart.tsx: blue, orange, aqua, yellow, magenta), never chosen by hand; a sixth series folds into Other. Colour follows what a series is, so the same series keeps its colour when a filter removes another.
- Words and numbers stay in the text inks; a short key beside them carries the colour. Never colour a label with its series.
- Every value shown on hover is also reachable without it: a label, the legend with values, or the card's table view (ChartTable).

**Import:**
```tsx
import { BarChart } from "@/components/ui/bar-chart"
import { ChartLegend, SERIES } from "@/components/ui/chart"
```

**Usage:**
```tsx
const series = [
  { key: "ready", name: "Prêtes", color: SERIES[2] },
  { key: "making", name: "En production", color: SERIES[0] },
  { key: "late", name: "En retard", color: SERIES[1] },
]

<ChartLegend items={series.map((s) => ({ label: s.name, color: s.color }))} />
<BarChart data={days} index="label" series={series} stacked aria-label="Commandes par état" />
```

## Bar list (`bar-list`)

Section: Charts. File: `components/ui/bar-list.tsx`. Page: /consistency/bar-list

A ranking: one row per name, longest first, a thin bar of its share of the first, the value in text, its share of the whole on hover.

**Use case.** Use it for a top list: the best clients by sales, the products sold most, the cities orders come from, the reasons for returns. Every value is printed, so it reads without hovering; limit shows the first few. One series, so one colour (chart-card.ranking).

**Use it when:**
- The top clients, products or places
- A long list of categories, sorted
- Labels too long for a column chart

**Use something else when:**
- A few categories over time: use Bar chart (`bar-chart`)
- The parts of one whole, five or fewer: use Donut chart (`donut-chart`)
- The rows need several columns of facts: use Table (`table`)

**Rules:**
- Sorted longest first (it sorts for you); never alphabetical.
- The value is always in text at the end of the row.
- One colour, the blue; a ranking is one series.

**Import:**
```tsx
import { BarList } from "@/components/ui/bar-list"
```

**Usage:**
```tsx
<BarList
  items={clients.map((c) => ({ name: c.name, value: c.sales }))}
  format={money}
  limit={6}
  aria-label="Meilleurs clients"
/>
```

## Donut chart (`donut-chart`)

Section: Charts. File: `components/ui/donut-chart.tsx`. Page: /consistency/donut-chart

The parts of one whole as a ring, five at most (more fold into Other), the total in the middle and a legend with each value and share.

**Use case.** Use it when the share of a whole is the point: sales by category, orders by channel, time by kind of work. Hovering a segment or its legend row puts that part in the middle and quiets the others; the legend already prints every value and share, so nothing depends on hovering or on colour (chart-card.breakdown).

**Use it when:**
- Sales by category or channel
- Where the time or the money went, in five parts or fewer

**Use something else when:**
- More than five parts that all matter: use Bar list (`bar-list`)
- The parts of each period over time: use Bar chart (`bar-chart`)
- Progress toward one goal: use Progress ring (`progress-ring`)

**Rules:**
- Five parts at most; the rest fold into Other (otherLabel in the client's language).
- Parts of one whole only: they must add up to the total in the middle.
- The legend is always there, with value and share.
- Colours come from the series order (SERIES in components/ui/chart.tsx: blue, orange, aqua, yellow, magenta), never chosen by hand; a sixth series folds into Other. Colour follows what a series is, so the same series keeps its colour when a filter removes another.

**Import:**
```tsx
import { DonutChart } from "@/components/ui/donut-chart"
```

**Usage:**
```tsx
<DonutChart parts={categories} format={moneyShort} totalLabel="Total" otherLabel="Autres" aria-label="Ventes par catégorie" />
```

## Progress ring (`progress-ring`)

Section: Charts. File: `components/ui/progress-ring.tsx`. Page: /consistency/progress-ring

How far toward a goal: a ring whose track is a lighter blue, filled in blue from the top, the share in the middle.

**Use case.** Use it for one goal with a target: the month's sales against the objective, a project's steps done, seats filled for an event. Say what is done of the target in words beside it (128,5 k$ sur 160 k$). It glides to a new value and fills from empty the first time (chart-card.goal).

**Use it when:**
- Sales against the month's objective
- A count of steps done out of all
- One goal on a dashboard

**Use something else when:**
- An amount against a limit in a list or a form: use Meter (`meter`)
- A task moving toward done (an upload): use Progress (`progress`)
- Several goals compared: use Bar list (`bar-list`)

**Rules:**
- Always the done and the target in words beside it, in the unit.
- One ring per card; several goals are a list.
- It moves only when the value changes.

**Import:**
```tsx
import { ProgressRing } from "@/components/ui/progress-ring"
```

**Usage:**
```tsx
<ProgressRing value={128460} max={160000} aria-label="Objectif du mois" />
<p className="text-[13px] text-ink-4">128,5 k$ sur 160 k$</p>
```

## Heatmap (`heatmap`)

Section: Charts. File: `components/ui/heatmap.tsx`. Page: /consistency/heatmap

When things happen: a grid of cells, one blue from light to dark for how many, the empty cell grey, the scale under it.

**Use case.** Use it for a count by two times: orders by weekday and hour, visits by week and day, calls by hour. Each cell answers the pointer and the keyboard with its row, column and value and lifts with a ring. One hue only, light to dark: a heatmap is never a rainbow (chart-card.heatmap).

**Use it when:**
- Orders or calls by weekday and hour
- Activity by week across a year

**Use something else when:**
- A total per day only: use Bar chart (`bar-chart`)
- A value over continuous time: use Area chart (`area-chart`)

**Rules:**
- One hue, light to dark (SEQUENTIAL); never a categorical palette.
- Label every row; label every few columns with columnStep when they crowd.
- Pass less and more in the client's language for the scale.

**Import:**
```tsx
import { Heatmap } from "@/components/ui/heatmap"
```

**Usage:**
```tsx
<Heatmap
  rows={["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."]}
  columns={hours}
  values={ordersByDayAndHour}
  columnStep={2}
  less="Moins"
  more="Plus"
  aria-label="Quand les commandes arrivent"
/>
```

## Toast (`toast`)

Section: Feedback and empty states. File: `components/ui/toast.tsx`. Page: /consistency/toast

A short message that slides in at the corner to say something just happened, then goes away on its own.

**Use case.** Use a toast to confirm an action the person just took when the page itself does not show it: the invoice was sent, the quote was saved, the card was declined. Call toastManager.add({ title, description, type }) from the handler; type picks the icon (success, error, warning, info, loading), and actionProps adds one button such as Undo. It needs ToastProvider mounted once around the app.

**Use it when:**
- Confirming a send, a save or a delete
- An error from an action the person just took
- Offering Undo right after a change
- News that does not need the person to stop

**Use something else when:**
- A condition that lasts while the page is open (the site is down, a certificate runs out): use Alert (`alert`)
- Asking before an action that cannot be undone: use Alert dialog and confirm (`alert-dialog`)
- The status of a record in a list: use Badge (`badge`)
- Explaining a control when the pointer rests on it: use Tooltip (`tooltip`)

**Rules:**
- Mount ToastProvider once, high in the layout (bottom-right by default); toastManager.add then works from any handler.
- A toast goes away on its own (5 seconds by default) and can be swiped away: never use one for something the person must act on later, which is an Alert.
- The title says what happened in a few words, in sentence case (Invoice sent); the description adds one sentence at most.
- type matches the outcome: success, error, warning, info, or loading while the work runs, then toastManager.update(id, ...) turns that same toast into the result.
- An action is one button, its label in Title Case (Undo), passed as actionProps: { children, onClick }.

**Import:**
```tsx
import type { ReactNode } from "react"
import { Button } from "@/components/ui/button"
import { ToastProvider, toastManager } from "@/components/ui/toast"
```

**Usage:**
```tsx
export function Providers({ children }: { children: ReactNode }) {
  return <ToastProvider>{children}</ToastProvider>
}

export function SendInvoiceButton({ onUndo }: { onUndo: () => void }) {
  return (
    <Button
      onClick={() =>
        toastManager.add({
          title: "Invoice sent",
          description: "The client will get it in a minute.",
          type: "success",
          actionProps: { children: "Undo", onClick: onUndo },
        })
      }
    >
      Send Invoice
    </Button>
  )
}
```

## Alert (`alert`)

Section: Feedback and empty states. File: `components/ui/alert.tsx`. Page: /consistency/alert

A block in the page, tinted by its meaning, that states a condition and what to do about it.

**Use case.** Use an alert for something true while the page is open: a client's site is down, a certificate runs out in 9 days, the bank connection needs reconnecting, this invoice is overdue. It sits where the problem is, at the top of the page or of the card it concerns, and stays until the condition changes. Its AlertAction holds the one button that fixes or opens it.

**Use it when:**
- A problem with the record on screen (overdue, blocked, down)
- A warning about something coming (a renewal, an expiry)
- A note that explains how the page behaves (backups run nightly)
- A good state worth stating (everything is up)

**Use something else when:**
- Saying an action just finished: use Toast (`toast`)
- Stopping the person to confirm a destructive action: use Alert dialog and confirm (`alert-dialog`)
- A one-word status in a list: use Badge (`badge`)
- A place with nothing in it yet: use Empty state (`empty`)

**Rules:**
- The variant matches the meaning and its icon: default or info (InfoIcon), success (CheckCircle2Icon), warning (AlertTriangleIcon), error (OctagonAlertIcon).
- Order: the icon as the first child, then AlertTitle, AlertDescription and AlertAction. The title is one short sentence, the description the detail.
- One action at most, size sm; on an error alert it is the destructive button.
- With a title the action spans both lines; a one-line alert with an action centres the icon, the words and the button on one line.
- Alert carries role="alert", so a screen reader announces it when it appears: keep it for what matters.

**Import:**
```tsx
import { OctagonAlertIcon } from "@/components/icons"
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
```

**Usage:**
```tsx
export function SiteDown({ since }: { since: string }) {
  return (
    <Alert variant="error">
      <OctagonAlertIcon />
      <AlertTitle>The site is down</AlertTitle>
      <AlertDescription>Since {since}, from every region.</AlertDescription>
      <AlertAction>
        <Button size="sm" variant="destructive">
          Open It
        </Button>
      </AlertAction>
    </Alert>
  )
}
```

## Empty state (`empty`)

Section: Feedback and empty states. File: `components/ui/empty.tsx`. Page: /consistency/empty

A centred message that says a place has nothing in it yet and offers the way to fill it.

**Use case.** Use it where a list or a page has no content: the client list before the first client, an order's files before any upload, a search that matched nothing. The title says what is missing, one sentence says what to do, and EmptyContent holds the one button that fixes it (Add a Client).

**Use it when:**
- A list or a table with no rows yet
- A search or a filter that matched nothing
- A section of a page waiting for its first item

**Use something else when:**
- The content is on its way: use Skeleton (`skeleton`)
- Something went wrong reading it: use Alert (`alert`)
- A short note under one field: use Field, label and form (`field`)

**Rules:**
- The title names what is missing (No files yet); the description is one sentence that says what to do.
- One action, the one that fills the place, in Title Case (Add a File); a search that matched nothing offers to clear the search rather than to create.
- EmptyMedia variant="icon" draws a lucide icon on a small stacked tile; pick the icon of the thing that is missing.
- Draw it where the rows would be, inside the card or table frame; inside a card, lower its padding (py-8 md:py-10, as in the showcase).

**Import:**
```tsx
import { FolderOpenIcon, PlusIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
```

**Usage:**
```tsx
export function NoFiles({ onAdd }: { onAdd: () => void }) {
  return (
    <Empty className="py-8 md:py-10">
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <FolderOpenIcon />
        </EmptyMedia>
        <EmptyTitle>No files yet</EmptyTitle>
        <EmptyDescription>Drop a file here, or add one from your computer.</EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button onClick={onAdd}>
          <PlusIcon />
          Add a File
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

## Skeleton (`skeleton`)

Section: Feedback and empty states. File: `components/ui/skeleton.tsx`. Page: /consistency/skeleton

A grey shape with a soft shimmer that stands where content will appear while it loads.

**Use case.** Use skeletons while a page's data comes from the server and you know its shape: a client's row with its avatar and two lines, a card's figure, the first rows of the invoices table. Drawn in the same sizes as the real content, the page does not jump when it lands.

**Use it when:**
- A list, a table or a card loading for the first time
- A record's page while its data is read
- Any wait where the shape of the result is known

**Use something else when:**
- A short wait with no shape (saving, sending): use Spinner (`spinner`)
- A task with a known amount done: use Progress (`progress`)
- The data came back empty: use Empty state (`empty`)

**Rules:**
- Mirror the real layout: the same sizes and places as the content (a round size-10 for a face, h-3.5 and h-3 lines at 2/3 and 1/2 width).
- Size and shape come from className; the corner is 8px by default, rounded-full for a face.
- Keep the real frame (the card, the row) and put the skeletons inside it.
- Never for a button's wait: Button's loading prop does that.

**Import:**
```tsx
import { Skeleton } from "@/components/ui/skeleton"
```

**Usage:**
```tsx
export function ClientRowSkeleton() {
  return (
    <div className="flex w-full max-w-sm items-center gap-3 rounded-[20px] bg-background p-4 shadow-(--shadow-rest)">
      <Skeleton className="size-10 rounded-full" />
      <div className="grid flex-1 gap-2">
        <Skeleton className="h-3.5 w-2/3" />
        <Skeleton className="h-3 w-1/2" />
      </div>
    </div>
  )
}
```

## Spinner (`spinner`)

Section: Feedback and empty states. File: `components/ui/spinner.tsx`. Page: /consistency/spinner

A small spinning circle that says something is working.

**Use case.** Use a spinner for a short wait with no shape to preview: checking a code, sending a message, refreshing a panel. When the wait starts from a button, pass loading to Button, which draws this spinner inside the button and disables it. It spins a little faster than usual, which makes the wait feel shorter.

**Use it when:**
- A short wait after a press (saving, sending, checking)
- A panel refreshing in place
- A small area waiting on one answer

**Use something else when:**
- The shape of what is loading is known: use Skeleton (`skeleton`)
- A task where you can count how much is done: use Progress (`progress`)
- Saying the work finished: use Toast (`toast`)

**Rules:**
- Size it with a size class: size-4, size-5 or size-6.
- On the button that started the work, pass loading to Button instead of placing a Spinner: it keeps the button's width and disables it.
- It carries role="status" and aria-label="Loading"; standing alone, pass an aria-label that names the wait (Checking the code).
- Remove it when the work ends and show the outcome (the result in place, or a toast).

**Import:**
```tsx
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
```

**Usage:**
```tsx
export function SaveButton({ saving, onSave }: { saving: boolean; onSave: () => void }) {
  return (
    <Button loading={saving} onClick={onSave}>
      Save Changes
    </Button>
  )
}

export function PanelRefreshing() {
  return (
    <div className="grid h-32 place-items-center">
      <Spinner className="size-5" aria-label="Refreshing the orders" />
    </div>
  )
}
```
