# Components

> The design system's components, each with a live example and its key props.

Every component below comes from `zidane-ui` and paints from the [theme tokens](https://zidane.si/docs/ui-theming.md), so it matches the desktop app in every theme. To import one module alone, see [Entry points](https://zidane.si/docs/ui-kit.md#entry-points).

```tsx
import { buttonClass, IconButton, Menu, MenuRow, Segmented } from 'zidane-ui'
```

## Buttons

`buttonClass({ variant, size })` returns a class string, so the same look fits a `<button>`, an `<a>` or a router link.

| Option | Values | Description |
| - | - | - |
| `variant` | `'secondary'` (default), `'primary'`, `'solid'`, `'ghost'`, `'danger'`, `'danger-solid'` | `solid`: accent fill, one per view (send, save). `primary`: accent wash. `danger`: red text, for the step that asks (Remove). `danger-solid`: red fill, for the destructive commit (Delete, Deny) |
| `size` | `'xs'`, `'sm'`, `'md'` (default), `'lg'` | `xs` toolbars (24px), `sm` rows (28px), `md` card actions, form submits, dialog footers (32px), `lg` page-level calls to action (36px) |

## Icon buttons and toggles

`IconButton` is the square icon control. `IconToggle` is its on/off variant for toolbars, placed in a `.zg-segment`.

| `IconButton` prop | Type | Description |
| - | - | - |
| `icon` | `string \| ReactNode` | An icon class (`i-ph-gear`) or a node (a `SwapIcon`) |
| `label` | `string` | Accessible name, and `title` unless you pass one |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | 20, 24, 28, 32px. Default `md` |
| `active` | `boolean` | Pressed or open state |
| `tone` | `'neutral' \| 'danger'` | |
| `children` | `ReactNode` | Overlays on the glyph: a badge, a dot |
| `render` | `(props) => ReactElement` | Render another element (a link) with the button's props |

`IconToggle` takes `active`, `onClick`, `icon` (a class), `label`, `badge?` and `size?` (`'xs' | 'sm'`, default `sm`).

## Segments and tabs

`Segmented` is a single choice on one track. `TabPill` is a closable tab in a `.zg-segment`, with a `CountBadge` in its trailing slot.

| `Segmented` prop | Type | Description |
| - | - | - |
| `value`, `onChange` | `T`, `(v: T) => void` | Controlled. `T` is a string union |
| `options` | `SegmentedOption<T>[]` | `{ value, label, icon?, title?, tooltip?, disabled? }` |
| `size` | `'sm' \| 'md'` | |
| `ariaLabel` | `string` | Name of the radio group |

| `TabPill` prop | Type | Description |
| - | - | - |
| `label`, `selected` | `ReactNode`, `boolean` | |
| `onSelect`, `onClose`, `closeLabel` | `() => void`, `() => void`, `string` | Close button shows on hover (always with `closeAlways`). Delete or Backspace closes the focused tab |
| `leading`, `trailing`, `marker` | `ReactNode` | An icon, a `CountBadge` (`text`, `tone: 'attention' \| 'info' \| 'neutral'`), a status mark |
| `size` | `'sm' \| 'md'` | |

## Surfaces and empty states

`Tray` is the bed. `Raised` is a card or control lifted off it. Both take `<div>` props plus `radius` (`'sm' | 'md' | 'lg' | 'xl'`, default `lg`). `Raised` adds `live` (one step up while a run streams).

Lighter surfaces are classes: `.zg-track-chip`, `.zg-well`, `.zg-fill`.

| Component | Props | Description |
| - | - | - |
| `SectionLabel` | `children` | The section label (sentence case) |
| `Meta` | `children`, `title?` | Metadata voice: mono, tabular, small, muted |
| `EmptyState` | `icon?`, `title`, `hint?`, `action?`, `framed?` | A panel's empty view, in a tray unless `framed={false}` |
| `PanelEmpty` | `icon?`, `title`, `hint?`, `warn?` | Compact empty or error line for a side panel. `PanelLoading` is its loading state |

### Error boundaries

`ErrorBoundary` keeps a throw in one region from blanking the others: `<ErrorBoundary region="Sidebar" resetKey={sessionId}>`. The region shows `ErrorFallback` (what failed, the message, Try again) until Try again or a new `resetKey`. `fallback` replaces it (`null` renders nothing), `actions` adds buttons, `framed={false}` drops the tray.

Each distinct error is logged once to the console through `reportRendererError(error, { source })`. `installRendererErrorHandlers()` does the same for the window's uncaught errors and unhandled rejections. `configureErrorReports({ sink, showStacks })` at boot sends every report to your log too, and shows the JS and component stacks in the fallback (a development build).

## Menus and rows

`MenuRow` is the row of every menu, list, picker and nav in the app. `Menu` gives rows their padding and role (`'menu' | 'listbox' | 'group'`). Put it in a `.zg-float` surface or a `Popover`. Your menu code handles the keyboard. Move `active` with the arrow keys.

| `MenuRow` prop | Type | Description |
| - | - | - |
| `title` | `ReactNode` | The label |
| `icon`, `description`, `trailing` | `ReactNode` | Leading glyph, second line, trailing keycap or value |
| `size` | `'sm' \| 'md' \| 'lg'` | `lg` stacks the description (two lines max) |
| `active` | `boolean` | Keyboard cursor |
| `checked` | `boolean` | Current choice: a trailing check |
| `toggle` | `boolean` | On/off option: a trailing switch (use `role="menuitemcheckbox"`) |
| `destructive` | `boolean` | Danger tone |
| `render` | `(props) => ReactElement` | Render a link with the row's look. `aria-current="page"` marks the current page |

`MenuSection` (`label`, `trailing`) groups rows and `MenuSeparator` divides them. `MenuNote` (`icon`) is a hint or empty state inside a menu. `MenuSwitch` (`on`) is the switch glyph alone.

## Floating surfaces

Floating surfaces use the overlay tone, position against their anchor (flipping and clamping to the viewport) and close on Escape or an outside click.

| Component | Key props | Description |
| - | - | - |
| `Popover` | `trigger(props, open)`, `children(close)`, `ariaLabel`, `side`, `align`, `width`, `role` | Opens on click. Spread `props` on your trigger |
| `HoverCard` | `trigger(props, open)`, `children`, `ariaLabel`, `side`, `align`, `width` | Opens on hover. Escape closes it |
| `Tooltip` | `label`, `side` (`'top' \| 'bottom'`), `align` (`'center' \| 'end'`), `portal`, `maxWidth` | Wraps its trigger. `portal` escapes a clipping scroller |
| `RowActionsMenu` | `open`, `onClose`, `items`, `anchorRef` or `position` | The 3-dots and right-click menu |

`side`: `'top' | 'bottom' | 'left' | 'right'`. `align`: `'start' | 'center' | 'end'`. `RowActionsMenu` items: `{ id, label, onSelect, destructive?, disabled?, shortcut? }`.

To build your own, use the parts of `Popover`: `FloatingSurface`, `usePopover` and `useAnchoredPosition`.

## Dialogs and overlays

`Overlay` owns the layer: portal, scrim, enter and exit, Escape, focus return, scroll lock. Put the state it passes on your surface's `data-state`. `ConfirmDialog` is a confirmation surface for it that focuses its confirm button on mount.

| `Overlay` prop | Type | Description |
| - | - | - |
| `open`, `onClose` | `boolean`, `() => void` | |
| `children` | `(state: 'open' \| 'closed') => ReactNode` | Rendered through the exit animation |
| `className`, `scrimClassName` | `string` | Position the layer (`fixed inset-0 …`), style the scrim |
| `closeOnEscape`, `trapFocus` | `boolean`, `boolean` | Both default to `true`. `trapFocus` moves focus in, keeps Tab inside and makes the rest of `body` inert. `exitMs` is deprecated: the exit lasts as long as its motion |
| `inertSelector` | `string` | Elements made `inert` while open (`main, footer`) |

| `ConfirmDialog` prop | Type | Description |
| - | - | - |
| `title`, `message` | `string`, `ReactNode` | The title has id `CONFIRM_TITLE_ID`, for your layer's `aria-labelledby` |
| `onConfirm`, `onCancel` | `() => void` | |
| `confirmLabel`, `cancelLabel` | `string` | Default to the `confirm` and `cancel` labels |
| `destructive` | `boolean` | Danger-toned confirm button |

`useLightbox()` returns `{ open(items, index, origin?), close, element }`. Render `element` once. Call `open` with `{ key, name, src?, body? }` items to page through images or text previews. The image grows out of the `origin` element.

An image in the lightbox shows in `ZoomableImage`, the kit's image viewer, which you can also use on its own. It fills its parent. The wheel and a trackpad pinch zoom toward the pointer, from fit up to 8× the image's real size. A drag pans while zoomed in, and a double-click switches between fit and the real size. While zoomed, a chip shows the share of the real size; click it to go back to fit. Right-click the image to copy it as an image (PNG), as Base64 or as a data URL. Its ref (`ZoomableImageHandle`) has `zoomIn`, `zoomOut`, `reset` and `zoomed`, for an owner that handles the keys: the lightbox maps + / − / 0 to them, and the arrow keys still step. `MediaNav` draws the lightbox's ‹ › chips (and an optional "n / N" chip) over any box, for your own media sets.

## Code, copy and keys

| Component | Key props | Description |
| - | - | - |
| `CodeFrame` | `code`, `children`, `caption?`, `icon?`, `copyLabel?` | Code panel with a copy button. `code` is the copied text. `children` is the highlighted markup, with no background of its own: the frame draws the code's tone and its edge, and a background would cover that edge |
| `CodePanel` | `children`, `wrap?`, `maxHeight?` | The bare panel, for raw output |
| `CopyButton` | `text`, `label?`, `variant?` (`'overlay' \| 'inline'`) | Copy with a check swap |
| `KeyCombo` | `spec`, `size?` (`'sm' \| 'md'`) | One chord from a spec (`cmd+shift+enter`), with platform glyphs |
| `Keycap` | `children` | A single key |

## Forms

The desktop app's settings layout: sections of rows, each row a label, an optional description and one control.

| Component | Key props | Description |
| - | - | - |
| `FormSection` | `title?`, `trailing?`, `layout?` (`'rows' \| 'cards'`) | A titled group of rows or cards |
| `FormCard` | `<div>` props | A raised card in a `cards` section |
| `SwitchRow` | `label`, `description?`, `on`, `onToggle` | The whole row toggles |
| `SegmentedRow` | `label`, `description?`, `value`, `onChange`, `options` | A `Segmented` on the right |
| `SliderRow` | `label`, `description?`, `value`, `min`, `max`, `step`, `format`, `onChange`, `onReset?` | |
| `NavRow` | `action: { label, description, icon, onSelect }` | Opens another page |
| `RowText` | `label`, `description?` | The label column, for your own rows |
| `ToggleSwitch` | `on`, `onChange`, `label`, `disabled?` | Standalone `role="switch"` |
| `Switch`, `Checkbox` | `on` / `checked` | Glyphs only. The surrounding control owns the state |
| `MetaChip` | `tone?` (`muted`, `text`, `accent`, `success`, `warn`, `danger`, `info`) | A value set into a surface |
| `ErrorNote`, `OpLine` | `children`. `OpLine` adds `kind` (`'busy' \| 'notice' \| 'error'`) | Inline outcome lines |

For your own fields: `INPUT_CLASS` and `TEXTAREA_CLASS`.

## List furniture

| Component | Key props | Description |
| - | - | - |
| `Breadcrumb` | `path`, `onFolder(dir)`, `onFile()` | File path that truncates from the left |
| `EditableTitle` | `text`, `editing`, `onStartEdit`, `onChange`, `onCommit`, `onCancel` | Rename in place. Enter commits, Escape cancels |
| `DiffCounts` | `added`, `removed` | `+14 −2` |
| `RowBadge` | `text`, `tone?` (`'neutral' \| 'diff' \| 'success' \| 'warn'`) | Small badge after a row title |
| `Highlighted` | `text`, `indices` | Fuzzy-match highlighting |
| `SegmentSpinner` | `size?` (`'xs' \| 'sm' \| 'md'`), `tone?` (`'inherit' \| 'accent' \| 'warn' \| 'muted'`), `label?` | Working indicator |
| `ZidaneMark` | `size?` (px), `speed?` (loops per second, `0` still), `tone?` (`'inherit' \| 'accent' \| 'warn' \| 'muted'`), `label?`, `decorative?` | The logo as the agent's working indicator: its bands flow across the sphere; still under reduced motion |

## Motion

Helpers for a status glyph swapping, a count rolling, a chip growing. All hold still when `html[data-motion]` is `off` or `reduced` ([motion switch](https://zidane.si/docs/ui-theming.md#motion)).

| Helper | Key props | Description |
| - | - | - |
| `Crossfade` | `activeKey`, `children`, `variant?` (`'scale' \| 'text'`), `align?` | Swaps content when `activeKey` changes |
| `Presence` | `show`, `children`, `variant?`, `display?` (`'inline' \| 'block'`) | Fades and scales content in and out |
| `SwapIcon` | `icons` (key → icon class), `current` | Swaps glyphs (copy → check) |
| `RollingText` | `text`, `value?` | Rolls changed characters. `value` sets the direction |
| `SmoothWidth` | `children` | Eases its width to its content's |
| `HeldWidth` | `holdKey`, `children` | Keeps its widest width until `holdKey` changes |
| `Grow` | `appear`, `as?`, `children` | Grows in and shrinks out, inside `GrowPresence` |
| `useReducedMotion()` | | True when motion is `off` or `reduced`. Skip your own JS animation then |

Lower-level motion modules, also exported from `zidane-ui`, for your own components:

| Module | Exports | Use |
| - | - | - |
| `motion/presence` | `usePresence(open, { enter?, exitMs? })`, `transitionState`, `useScrollLock`, `readPanelMs`, `readExitMs` | Keeps a node mounted through its CSS exit. The phase goes `entering`, `open`, `closing`, `closed`. Map it to `data-state` with `transitionState`. Pass `exitMs` as `readPanelMs` or `readExitMs` so the motion setting applies |
| `motion/collapse` | `useStaysMounted(open)` | True while open, or while the close still plays |
| `motion/swap` | `useCadencedValue(value, ms)` | Holds a fast-changing value to one change per `ms` |
| `motion/shimmer` | `syncShimmer` (a ref callback), `SHIMMER_MS` | Starts a `.zg-shimmer` label's sweep on a shared clock, so a swapped label keeps the band where it was |
| `motion/title-text` | `TitleText` | A title that writes itself in word by word when it changes |
| `motion/springs` | `useMotionTransition(name)`, `useWithFade`, `useMenuTransition`, `MOTIONS` | The named curves that every animation reads (`snap`, `pop`, `panel`, `slide`, `settle`, `exit`, `fade`, `fade-out`, `menu`, `menu-exit`) as `motion` transitions. Movement springs. Opacity, color and the menus never spring |
| `motion/provider` | `MotionProvider` (`level?`, `springs?`, `panelMs?`) | One motion policy for an app: writes the motion variables on `<html>` and wraps the tree in `motion`'s `MotionConfig` |

The springs and provider modules import the `motion` peer, as do `Grow`, `Overlay`, `FloatingSurface` and `Lightbox`.

## Hooks

From `zidane-ui`:

| Hook | Returns | Use |
| - | - | - |
| `usePersistedState(key, fallback, parse, serialize?)` | `[value, setValue]` | `useState` kept in `localStorage`: a stored value `parse` doesn't accept falls back, `null` removes the key, storage that throws is ignored. `readPersisted` / `writePersisted` are the same outside React; `oneOf(members)` and `parseBoolean` are ready-made `parse`s |
| `useMinuteTick()` | nothing | Re-renders once a minute for relative times ("5m"), on one shared interval that runs only while read and the window is visible |
| `useKeyedValue(store, key)` | the value or `null` | Reads one key of a `createKeyedStore()`: a write to one key re-renders only that key's readers |
| `useNearViewport(ref, margin?)` | `boolean` | Turns true once the element comes within `margin` of its scroll container's viewport, and stays true. Use it for lazy work such as code highlighting. It shares one `IntersectionObserver` per scroll root and margin. Where `IntersectionObserver` is missing, it is true at once |
| `useResizeDrag({ value, min, max, defaultValue, edge, axis?, target, onCommit })` | `{ handleProps, active }` | Resizes a panel by dragging its edge. Each frame writes the width into `target`'s CSS variable, with no React state. `onCommit` fires once on release. A double-click resets to `defaultValue`. `edge` is `end` for a panel on the left, `start` for one on the right. `axis: 'y'` resizes a height |

```tsx
import type { CSSProperties } from 'react'
import { useResizeDrag } from 'zidane-ui'
import { useRef, useState } from 'react'

export function Sidebar() {
  const el = useRef<HTMLDivElement>(null)
  const [width, setWidth] = useState(260)
  const drag = useResizeDrag({ value: width, min: 200, max: 480, defaultValue: 260, edge: 'end', target: { ref: el, cssVar: '--sidebar-w' }, onCommit: setWidth })
  return (
    <div ref={el} style={{ '--sidebar-w': `${width}px`, width: 'var(--sidebar-w)' } as CSSProperties}>
      <div {...drag.handleProps} data-active={drag.active} className="zg-resize-handle" />
    </div>
  )
}
```

## Path helpers

From `zidane-ui`, browser-safe (no `node:path`): `basename(path)`, `splitPath(path)` (`{ dir, base }`), `compactPath(path, maxChars?)` (`/…/agent/zidane`: the start collapses, the last segments stay), `formatPathForCwd(path, projectRoot, cwd)` (relative below `cwd`, absolute above it, never `../`), `tildePath(path, home)` (`/Users/ada/app` → `~/app`) and `tildeText(text, home)` (the same for each path in a line, such as `in /Users/ada/app`).

The kit's components show paths with their machine's home folder as `~` when you give them the home folders. Wrap the app in `PathDisplayProvider homes={{ local: '/Users/ada', [serverId]: '/home/ada' }}`. Keys are host ids (`HostView.id`, `'local'` for this computer). A component asks `useDisplayPath().path(path, hostId)`, and `PathDisplayScope hostId` names the host for everything below it (one session's transcript). This is display only: tooltips, copies and what goes to the agent keep the real path. Without a provider, paths show in full. A zidane host reports its home folder in its `ipc.hello` (`homeDir`).

## Reference

[`components/button`](https://zidane.si/docs/reference/ui-components-button.md), [`components/IconButton`](https://zidane.si/docs/reference/ui-components-icon-button.md), [`components/Menu`](https://zidane.si/docs/reference/ui-components-menu.md), [`components/Surface`](https://zidane.si/docs/reference/ui-components-surface.md), [`components/Segmented`](https://zidane.si/docs/reference/ui-components-segmented.md), [`components/TabPill`](https://zidane.si/docs/reference/ui-components-tab-pill.md), [`components/Form`](https://zidane.si/docs/reference/ui-components-form.md), [`components/floating/Popover`](https://zidane.si/docs/reference/ui-components-floating-popover.md), [`components/ConfirmDialog`](https://zidane.si/docs/reference/ui-components-confirm-dialog.md), [`components/CodeFrame`](https://zidane.si/docs/reference/ui-components-code-frame.md).
