@ultimat3/ui 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CATALOG.md ADDED
@@ -0,0 +1,848 @@
1
+ <!-- GENERATED by `bun run catalog` from packages/ui/src. Do not edit by hand. -->
2
+
3
+ # @ultimat3/ui catalog
4
+
5
+ Every component and every token, projected from source. Import all of it from `@ultimat3/ui`.
6
+
7
+ 46 components: `Alert` · `AppShell` · `Avatar` · `Badge` · `Breadcrumb` · `Button` · `Card` · `Checkbox` · `Container` · `DataTable` · `DateTime` · `Dialog` · `Divider` · `Drawer` · `EmptyState` · `ErrorState` · `Field` · `Form` · `Grid` · `IconButton` · `Image` · `Input` · `Link` · `LocaleSwitcher` · `Menu` · `Money` · `PageHeader` · `Pagination` · `Popover` · `Radio` · `RelativeTime` · `Section` · `Select` · `Skeleton` · `Spinner` · `Stack` · `Switch` · `Table` · `Tabs` · `Text` · `Textarea` · `ThemeToggle` · `ToastRegion` · `Toast` · `Toolbar` · `Tooltip`
8
+
9
+ ## Vocabulary
10
+
11
+ One size scale, one tone scale, one variant scale — shared by every component that has them.
12
+
13
+ | Scale | Values |
14
+ |---|---|
15
+ | `Size` | `sm` · `md` · `lg` |
16
+ | `Tone` | `neutral` · `accent` · `success` · `warning` · `danger` · `info` |
17
+ | `ButtonVariant` | `primary` · `secondary` · `ghost` · `link` |
18
+ | `SpaceStep` | `0` · `1` · `2` · `3` · `4` · `5` · `6` · `8` · `10` · `12` · `16` |
19
+
20
+ ## Components
21
+
22
+ ### Alert
23
+
24
+ Inline message bound to the page, not the viewport. `danger`/`warning` use `role="alert"` (assertive); the rest use `role="status"`, so an informational banner never interrupts what a screen-reader user is doing.
25
+
26
+ | Prop | Type | Required | Notes |
27
+ |---|---|---|---|
28
+ | `title` | `string` | — | Already-translated heading. |
29
+ | `children` | `JSX.Element` | yes | |
30
+ | `tone` | `Tone` | — | |
31
+ | `icon` | `JSX.Element` | — | |
32
+ | `onDismiss` | `(() => void)` | — | Provide both to make the alert dismissible; the label is the a11y name. |
33
+ | `dismissLabel` | `string` | — | |
34
+ | `class` | `string` | — | |
35
+
36
+ ### AppShell
37
+
38
+ The page frame every app screen sits in: skip link, banner, navigation, main, contentinfo. Stateless on purpose — below `md` the sidebar becomes a band above the content instead of growing an open/closed flag, because an off-canvas menu is already `Drawer` and axiom 1 allows exactly one of those.
39
+
40
+ | Prop | Type | Required | Notes |
41
+ |---|---|---|---|
42
+ | `children` | `JSX.Element` | yes | The page. Rendered inside the one `<main>`, which is the skip link's target. |
43
+ | `header` | `JSX.Element` | — | |
44
+ | `sidebar` | `JSX.Element` | — | Rendered inside a `<nav>` landmark at the inline start. |
45
+ | `footer` | `JSX.Element` | — | |
46
+ | `sidebarLabel` | `string` | — | Accessible name for the sidebar landmark. Defaults to the translated `ui.navigation`. |
47
+ | `skipLabel` | `string` | — | Skip-link text. Defaults to the translated `ui.skip`. |
48
+ | `sidebarWidth` | `string` | — | Sidebar track width at `md` and up. Any CSS length. |
49
+ | `stickyHeader` | `boolean` | — | Keeps the header pinned while the main region scrolls. |
50
+ | `class` | `string` | — | |
51
+
52
+ ### Avatar
53
+
54
+ Identity chip. An avatar image always carries intrinsic dimensions and an empty alt (the name is rendered as text or the accessible label), so it can never shift layout or duplicate the name to a screen reader.
55
+
56
+ | Prop | Type | Required | Notes |
57
+ |---|---|---|---|
58
+ | `name` | `string` | yes | Already-localised display name. Drives initials and the accessible name. |
59
+ | `src` | `string` | — | |
60
+ | `size` | `Size \| 'xs' \| 'xl'` | — | |
61
+ | `shape` | `'circle' \| 'rounded'` | — | |
62
+ | `status` | `'online' \| 'busy' \| 'offline'` | — | Presence ring; the meaning must also be conveyed in text nearby. |
63
+ | `class` | `string` | — | |
64
+
65
+ ### Badge
66
+
67
+ Small status label. Tone maps straight onto the status colour roles so the same component is legible on both themes with no per-theme override.
68
+
69
+ | Prop | Type | Required | Notes |
70
+ |---|---|---|---|
71
+ | `children` | `JSX.Element` | yes | |
72
+ | `tone` | `Tone` | — | |
73
+ | `size` | `Size` | — | |
74
+ | `variant` | `'soft' \| 'solid' \| 'outline'` | — | |
75
+ | `dot` | `boolean` | — | Renders a leading dot; pair with a tone that carries the meaning. |
76
+ | `class` | `string` | — | |
77
+
78
+ ### Breadcrumb
79
+
80
+ Ancestor trail. The last item is the current page: rendered as text, never a link, and marked `aria-current="page"`. Separators are decorative CSS.
81
+
82
+ | Prop | Type | Required | Notes |
83
+ |---|---|---|---|
84
+ | `items` | `readonly BreadcrumbItem[]` | yes | |
85
+ | `label` | `string` | — | Overrides the translated `ui.breadcrumb` landmark name. |
86
+ | `class` | `string` | — | |
87
+
88
+ ### Button
89
+
90
+ The one button. Variants and tones are token-driven, so dark mode and RTL need no extra rules; `loading` keeps the label mounted to avoid a layout jump.
91
+
92
+ | Prop | Type | Required | Notes |
93
+ |---|---|---|---|
94
+ | `children` | `JSX.Element` | yes | Visible label. Never hardcoded inside the component. |
95
+ | `variant` | `ButtonVariant` | — | |
96
+ | `tone` | `Tone` | — | |
97
+ | `size` | `Size` | — | |
98
+ | `type` | `'button' \| 'submit' \| 'reset'` | — | |
99
+ | `disabled` | `boolean` | — | |
100
+ | `loading` | `boolean` | — | Blocks interaction and announces progress via `aria-busy`. |
101
+ | `fullWidth` | `boolean` | — | |
102
+ | `iconStart` | `JSX.Element` | — | |
103
+ | `iconEnd` | `JSX.Element` | — | |
104
+ | `id` | `string` | — | |
105
+ | `class` | `string` | — | |
106
+ | `aria-label` | `string` | — | |
107
+ | `aria-controls` | `string` | — | |
108
+ | `aria-expanded` | `boolean` | — | |
109
+ | `onClick` | `JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>` | — | |
110
+
111
+ ### Card
112
+
113
+ Surface container. Elevation is a token rung, and because the shadow tokens are themed the same `elevation` reads correctly on light and dark.
114
+
115
+ | Prop | Type | Required | Notes |
116
+ |---|---|---|---|
117
+ | `children` | `JSX.Element` | yes | |
118
+ | `header` | `JSX.Element` | — | |
119
+ | `footer` | `JSX.Element` | — | |
120
+ | `elevation` | `Elevation` | — | |
121
+ | `padding` | `SpaceStep` | — | |
122
+ | `interactive` | `boolean` | — | Adds hover affordance. Only use when the whole card is a link/button. |
123
+ | `as` | `'div' \| 'article' \| 'section' \| 'li'` | — | |
124
+ | `class` | `string` | — | |
125
+
126
+ ### Checkbox
127
+
128
+ Native checkbox with a token-drawn indicator. The label element wraps the input, so the whole row is a hit target without a manual `for`/`id` pairing.
129
+
130
+ | Prop | Type | Required | Notes |
131
+ |---|---|---|---|
132
+ | `label` | `string` | yes | Already-translated label. Required — an unlabelled checkbox is a bug. |
133
+ | `id` | `string` | — | |
134
+ | `name` | `string` | — | |
135
+ | `value` | `string` | — | |
136
+ | `checked` | `boolean` | — | |
137
+ | `indeterminate` | `boolean` | — | Tri-state for "some children selected". Mirrored to `aria-checked`. |
138
+ | `disabled` | `boolean` | — | |
139
+ | `required` | `boolean` | — | |
140
+ | `description` | `string` | — | |
141
+ | `class` | `string` | — | |
142
+ | `aria-describedby` | `string` | — | |
143
+ | `aria-invalid` | `boolean` | — | |
144
+ | `onChange` | `JSX.EventHandlerUnion<HTMLInputElement, Event>` | — | |
145
+
146
+ ### Container
147
+
148
+ Centred measure with a gutter. `margin-inline: auto` and `min()` mean one declaration covers every viewport and both writing directions.
149
+
150
+ | Prop | Type | Required | Notes |
151
+ |---|---|---|---|
152
+ | `children` | `JSX.Element` | yes | |
153
+ | `size` | `ContainerSize` | — | |
154
+ | `gutter` | `SpaceStep` | — | |
155
+ | `as` | `'div' \| 'main' \| 'section' \| 'article' \| 'header' \| 'footer'` | — | |
156
+ | `class` | `string` | — | |
157
+
158
+ ### DataTable
159
+
160
+ Data-driven table: sortable headers, cursor pagination, and the four states a real list always has (loading, error, empty, data). The error state renders an UltimateError with the same code/cause/fix strings the terminal prints.
161
+
162
+ | Prop | Type | Required | Notes |
163
+ |---|---|---|---|
164
+ | `caption` | `string` | yes | |
165
+ | `columns` | `readonly Column<Row>[]` | yes | |
166
+ | `rows` | `readonly Row[]` | yes | |
167
+ | `rowKey` | `(row: Row) => string` | yes | |
168
+ | `sort` | `SortState` | — | |
169
+ | `onSortChange` | `(sort: SortState \| undefined) => void` | — | |
170
+ | `loading` | `boolean` | — | |
171
+ | `error` | `unknown` | — | An UltimateError (or anything shaped like one) from the query. |
172
+ | `onRetry` | `(() => void)` | — | |
173
+ | `emptyTitle` | `string` | — | |
174
+ | `emptyDescription` | `string` | — | |
175
+ | `nextCursor` | `string` | — | Opaque cursors from the query result; absent means no further page. |
176
+ | `prevCursor` | `string` | — | |
177
+ | `onCursor` | `((cursor: string, direction: 'next' \| 'prev') => void)` | — | |
178
+ | `stickyHeader` | `boolean` | — | |
179
+ | `density` | `'comfortable' \| 'compact'` | — | |
180
+ | `skeletonRows` | `number` | — | Placeholder row count while loading. Match the usual page size. |
181
+ | `class` | `string` | — | |
182
+
183
+ ### DateTime
184
+
185
+ Renders <time datetime="<ISO instant>"> with text formatted in the context time zone. The attribute is always the UTC instant so crawlers, tests, and other machines read the same value regardless of the viewer's zone.
186
+
187
+ | Prop | Type | Required | Notes |
188
+ |---|---|---|---|
189
+ | `value` | `TimeInput` | yes | |
190
+ | `timeZone` | `TimeZone` | — | Overrides the context zone. Use for "in the venue's local time" displays. |
191
+ | `locale` | `string` | — | |
192
+ | `dateStyle` | `DateStyle` | — | |
193
+ | `timeStyle` | `DateStyle` | — | |
194
+ | `format` | `DateTimeFormatter` | — | |
195
+ | `class` | `string` | — | |
196
+
197
+ ### Dialog
198
+
199
+ Modal built on native <dialog>: the platform gives us the top layer, the backdrop, inert background content, and Escape-to-close for free. We add the labelled heading, the close affordance, and body scroll locking.
200
+
201
+ | Prop | Type | Required | Notes |
202
+ |---|---|---|---|
203
+ | `open` | `boolean` | yes | |
204
+ | `title` | `string` | yes | Already-translated title. Becomes the dialog's accessible name. |
205
+ | `children` | `JSX.Element` | yes | |
206
+ | `footer` | `JSX.Element` | — | |
207
+ | `onClose` | `() => void` | yes | |
208
+ | `size` | `'sm' \| 'md' \| 'lg' \| 'full'` | — | |
209
+ | `dismissOnBackdrop` | `boolean` | — | Clicking the backdrop closes. Turn off for destructive confirmations. |
210
+ | `class` | `string` | — | |
211
+
212
+ ### Divider
213
+
214
+ A rule. Always an explicit `role="separator"` with an orientation; pass `label` to render a captioned separator, which is what a settings group actually needs.
215
+
216
+ | Prop | Type | Required | Notes |
217
+ |---|---|---|---|
218
+ | `orientation` | `'horizontal' \| 'vertical'` | — | |
219
+ | `label` | `string` | — | Visible, already-translated caption rendered inside the rule. |
220
+ | `class` | `string` | — | |
221
+
222
+ ### Drawer
223
+
224
+ Edge-anchored panel, also a native <dialog> so it inherits the top layer and inert background. `side` is logical (`inline-start`/`inline-end`), so a drawer pinned to the start edge lands on the right in an RTL locale automatically.
225
+
226
+ | Prop | Type | Required | Notes |
227
+ |---|---|---|---|
228
+ | `open` | `boolean` | yes | |
229
+ | `title` | `string` | yes | Already-translated title. Becomes the drawer's accessible name. |
230
+ | `children` | `JSX.Element` | yes | |
231
+ | `onClose` | `() => void` | yes | |
232
+ | `side` | `DrawerSide` | — | |
233
+ | `size` | `string` | — | |
234
+ | `footer` | `JSX.Element` | — | |
235
+ | `class` | `string` | — | |
236
+
237
+ ### EmptyState
238
+
239
+ The "nothing here yet" surface. Title and description arrive as already translated strings; the action slot takes a Button so there is one CTA shape.
240
+
241
+ | Prop | Type | Required | Notes |
242
+ |---|---|---|---|
243
+ | `title` | `string` | — | Already-translated. Falls back to the `ui.empty` catalog key. |
244
+ | `description` | `string` | — | |
245
+ | `icon` | `JSX.Element` | — | |
246
+ | `action` | `JSX.Element` | — | |
247
+ | `class` | `string` | — | |
248
+
249
+ ### ErrorState
250
+
251
+ Renders an UltimateError with the same three strings the terminal prints: code, cause, fix. Identical text in the CLI, the overlay, and `--json` is the whole point of the error contract — this component must not paraphrase.
252
+
253
+ | Prop | Type | Required | Notes |
254
+ |---|---|---|---|
255
+ | `error` | `unknown` | yes | An UltimateError, or any thrown value; unknown values get X_INTERNAL text. |
256
+ | `onRetry` | `(() => void)` | — | |
257
+ | `retryLabel` | `string` | — | Already-translated; falls back to the ui.* catalog keys. |
258
+ | `showDocs` | `boolean` | — | Link the code to its docs page. On by default. |
259
+ | `class` | `string` | — | |
260
+
261
+ ### Field
262
+
263
+ Label + hint + error, wired for a11y. Field owns the ids and hands them to the control through a render prop, so `aria-describedby` and `aria-invalid` can never drift out of sync with what is actually rendered.
264
+
265
+ | Prop | Type | Required | Notes |
266
+ |---|---|---|---|
267
+ | `label` | `string` | yes | Already-translated label. Rendered as a real <label for>. |
268
+ | `children` | `(control: FieldControl) => JSX.Element` | yes | |
269
+ | `hint` | `string` | — | |
270
+ | `error` | `string` | — | Present means invalid; the string is announced with the control. |
271
+ | `required` | `boolean` | — | |
272
+ | `markOptional` | `boolean` | — | Appends a translated "(optional)" marker instead of marking required. |
273
+ | `class` | `string` | — | |
274
+
275
+ ### Form
276
+
277
+ Form shell. Owns the one thing every form needs and always forgets: a top-of-form error summary that is focusable and announced on submit.
278
+
279
+ | Prop | Type | Required | Notes |
280
+ |---|---|---|---|
281
+ | `children` | `JSX.Element` | yes | |
282
+ | `error` | `string` | — | Already-translated summary shown above the fields when submit fails. |
283
+ | `errorTitle` | `string` | — | Already-translated heading for the error summary region. |
284
+ | `actions` | `JSX.Element` | — | |
285
+ | `gap` | `SpaceStep` | — | |
286
+ | `method` | `'get' \| 'post'` | — | |
287
+ | `action` | `string` | — | |
288
+ | `novalidate` | `boolean` | — | |
289
+ | `class` | `string` | — | |
290
+ | `aria-label` | `string` | — | |
291
+ | `onSubmit` | `JSX.EventHandlerUnion<HTMLFormElement, SubmitEvent>` | — | |
292
+
293
+ ### Grid
294
+
295
+ Grid layout primitive. The default is an intrinsic responsive grid (`auto-fit` + `minmax`) so most layouts need no breakpoint at all.
296
+
297
+ | Prop | Type | Required | Notes |
298
+ |---|---|---|---|
299
+ | `children` | `JSX.Element` | yes | |
300
+ | `columns` | `number` | — | Fixed track count. Omit for the intrinsic `auto-fit` behaviour. |
301
+ | `minColumn` | `string` | — | Minimum track width for the intrinsic grid. |
302
+ | `gap` | `SpaceStep` | — | |
303
+ | `rowGap` | `SpaceStep` | — | |
304
+ | `as` | `'div' \| 'ul' \| 'ol' \| 'section'` | — | |
305
+ | `class` | `string` | — | |
306
+
307
+ ### IconButton
308
+
309
+ A button whose only content is an icon, so `label` is mandatory: it becomes the accessible name and the tooltip. There is no unlabelled icon button.
310
+
311
+ | Prop | Type | Required | Notes |
312
+ |---|---|---|---|
313
+ | `label` | `string` | yes | Required accessible name — also used as the native tooltip. |
314
+ | `children` | `JSX.Element` | yes | |
315
+ | `variant` | `ButtonVariant` | — | |
316
+ | `tone` | `Tone` | — | |
317
+ | `size` | `Size` | — | |
318
+ | `disabled` | `boolean` | — | |
319
+ | `round` | `boolean` | — | |
320
+ | `id` | `string` | — | |
321
+ | `class` | `string` | — | |
322
+ | `aria-pressed` | `boolean` | — | |
323
+ | `aria-expanded` | `boolean` | — | |
324
+ | `aria-controls` | `string` | — | |
325
+ | `onClick` | `JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>` | — | |
326
+
327
+ ### Image
328
+
329
+ Zero-CLS image primitive: a plain <img>, no JS, no fetch, no client state. The build-time half of the pipeline in `docs/idea/07-rendering-seo.md` — reading real dimensions, encoding AVIF/WebP renditions, the data-URI blur placeholder — is NOT here. This component emits exactly what it is handed and fabricates nothing: no variants it was not given, no dimensions it did not measure.
330
+
331
+ | Prop | Type | Required | Notes |
332
+ |---|---|---|---|
333
+ | `src` | `string` | yes | |
334
+ | `alt` | `string` | yes | Required, with no default: an <Image> whose meaning is undescribed is a type error at the call site. Pass `alt=""` for a decorative image, deliberately. |
335
+ | `variants` | `readonly ImageVariant[]` | — | Renditions to offer; the descriptors and their order are derived, not written. |
336
+ | `sizes` | `string` | — | Layout width of the box, e.g. `(max-width: 700px) 100vw, 620px`. |
337
+ | `priority` | `boolean` | — | The LCP image, at most one per route: eager, high fetch priority. |
338
+ | `width` | `number` | — | Intrinsic dimensions, from the build step that measured them. Both or neither. |
339
+ | `height` | `number` | — | |
340
+ | `class` | `string` | — | |
341
+
342
+ ### Input
343
+
344
+ Single-line text control. No `type="number"` convenience wrapper: numeric input uses `inputmode` + a text type so locale decimal separators survive.
345
+
346
+ | Prop | Type | Required | Notes |
347
+ |---|---|---|---|
348
+ | `id` | `string` | — | |
349
+ | `name` | `string` | — | |
350
+ | `type` | `InputType` | — | |
351
+ | `value` | `string` | — | |
352
+ | `placeholder` | `string` | — | |
353
+ | `size` | `Size` | — | |
354
+ | `required` | `boolean` | — | |
355
+ | `disabled` | `boolean` | — | |
356
+ | `readonly` | `boolean` | — | |
357
+ | `autocomplete` | `string` | — | |
358
+ | `inputmode` | `'text' \| 'numeric' \| 'decimal' \| 'tel' \| 'email' \| 'url' \| 'search'` | — | |
359
+ | `maxlength` | `number` | — | |
360
+ | `prefix` | `JSX.Element` | — | Non-interactive adornments; keep them icons or units, never controls. |
361
+ | `suffix` | `JSX.Element` | — | |
362
+ | `class` | `string` | — | |
363
+ | `aria-label` | `string` | — | |
364
+ | `aria-describedby` | `string` | — | |
365
+ | `aria-invalid` | `boolean` | — | |
366
+ | `onInput` | `JSX.EventHandlerUnion<HTMLInputElement, InputEvent>` | — | |
367
+ | `onChange` | `JSX.EventHandlerUnion<HTMLInputElement, Event>` | — | |
368
+ | `onBlur` | `JSX.EventHandlerUnion<HTMLInputElement, FocusEvent>` | — | |
369
+
370
+ ### Link
371
+
372
+ Anchor primitive. External links get `rel` hardening and a translated "opens in a new tab" hint automatically — never a bare `target="_blank"`.
373
+
374
+ | Prop | Type | Required | Notes |
375
+ |---|---|---|---|
376
+ | `href` | `string` | yes | |
377
+ | `children` | `JSX.Element` | yes | |
378
+ | `external` | `boolean` | — | |
379
+ | `externalHint` | `string` | — | Announced suffix for external links; supply via `t()` at the call site. |
380
+ | `underline` | `'always' \| 'hover' \| 'none'` | — | |
381
+ | `tone` | `'accent' \| 'inherit'` | — | |
382
+ | `id` | `string` | — | |
383
+ | `class` | `string` | — | |
384
+ | `aria-current` | `'page' \| 'step' \| 'true' \| false` | — | |
385
+ | `onClick` | `JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>` | — | |
386
+
387
+ ### LocaleSwitcher
388
+
389
+ Locale picker. Option labels come from `Intl.DisplayNames` in each locale's own language (endonyms), so the list needs no translation catalog of its own.
390
+
391
+ | Prop | Type | Required | Notes |
392
+ |---|---|---|---|
393
+ | `locales` | `readonly Locale[]` | yes | |
394
+ | `value` | `Locale` | — | Defaults to the context locale. |
395
+ | `onLocaleChange` | `((locale: Locale) => void)` | — | |
396
+ | `hrefFor` | `((locale: Locale) => string)` | — | Render as links instead of a select, for a 0kb-JS `site/` route. |
397
+ | `class` | `string` | — | |
398
+
399
+ ### Menu
400
+
401
+ Action menu: a trigger plus a `role="menu"` list with roving tabindex. Distinct from Select — a menu runs commands, it does not hold a form value.
402
+
403
+ | Prop | Type | Required | Notes |
404
+ |---|---|---|---|
405
+ | `items` | `readonly MenuItem[]` | yes | |
406
+ | `open` | `boolean` | yes | |
407
+ | `onOpenChange` | `(open: boolean) => void` | yes | |
408
+ | `trigger` | `(control: { id: string; 'aria-haspopup': 'menu'; 'aria-expanded': boolean; 'aria-controls': string; }) => JSX.Element` | yes | |
409
+ | `label` | `string` | yes | Already-translated accessible name for the menu. |
410
+ | `align` | `'start' \| 'end'` | — | |
411
+ | `class` | `string` | — | |
412
+
413
+ ### Money
414
+
415
+ Formats through @ultimat3/money using the locale and currency from context — never a process-wide default, never a float, never a hardcoded symbol.
416
+
417
+ | Prop | Type | Required | Notes |
418
+ |---|---|---|---|
419
+ | `value` | `MoneyValue \| number` | yes | A Money value, or bare minor units in the context currency. |
420
+ | `currency` | `string` | — | Overrides the context currency for bare-number values. |
421
+ | `locale` | `string` | — | Overrides the context locale. Use only for side-by-side comparisons. |
422
+ | `signed` | `boolean` | — | Colour negatives with the danger role, e.g. in a ledger. |
423
+ | `options` | `FormatMoneyOptions` | — | Passed to @ultimat3/money: display, accounting, grouping, fractionDigits. |
424
+ | `format` | `MoneyFormatter` | — | |
425
+ | `class` | `string` | — | |
426
+
427
+ ### PageHeader
428
+
429
+ The top of a screen: breadcrumbs, the page's one heading, a description, and the actions that belong to the page rather than to a row in it. Hand-rolled, this is where the heading level, the landmark and the action alignment go wrong; here it is one component with one shape.
430
+
431
+ | Prop | Type | Required | Notes |
432
+ |---|---|---|---|
433
+ | `title` | `string` | yes | Already-translated page title. Rendered as the heading, once. |
434
+ | `description` | `string` | — | Already-translated supporting line under the title. |
435
+ | `actions` | `JSX.Element` | — | Trailing controls — a Button, a Toolbar. Wraps under the title on narrow viewports. |
436
+ | `breadcrumbs` | `readonly BreadcrumbItem[]` | — | |
437
+ | `level` | `HeadingLevel` | — | Heading level. 1 by default: a screen has exactly one h1, and this is it. |
438
+ | `media` | `JSX.Element` | — | Rendered before the title — an avatar, an icon, a status dot. |
439
+ | `class` | `string` | — | |
440
+
441
+ ### Pagination
442
+
443
+ Cursor-first pagination — the only shape a Postgres-backed list should use. Numbered mode exists for prerendered archives where the total is known.
444
+
445
+ | Prop | Type | Required | Notes |
446
+ |---|---|---|---|
447
+ | `nextCursor` | `string` | — | Opaque cursor for the following page; absent disables "next". |
448
+ | `prevCursor` | `string` | — | |
449
+ | `onCursor` | `((cursor: string, direction: 'next' \| 'prev') => void)` | — | |
450
+ | `page` | `number` | — | Numbered mode: 1-based page and total. Ignored when cursors are present. |
451
+ | `totalPages` | `number` | — | |
452
+ | `onPage` | `((page: number) => void)` | — | |
453
+ | `labelPrevious` | `string` | — | Already-translated; falls back to the ui.* catalog keys. |
454
+ | `labelNext` | `string` | — | |
455
+ | `class` | `string` | — | |
456
+
457
+ ### Popover
458
+
459
+ Non-modal anchored panel. Positioning is CSS-only (a relatively positioned anchor plus logical insets), so there is no measure/reflow loop and the panel flips sides automatically under `dir="rtl"`.
460
+
461
+ | Prop | Type | Required | Notes |
462
+ |---|---|---|---|
463
+ | `open` | `boolean` | yes | |
464
+ | `onOpenChange` | `(open: boolean) => void` | yes | |
465
+ | `trigger` | `(control: { id: string; 'aria-expanded': boolean; 'aria-controls': string; }) => JSX.Element` | yes | The anchor. Wire its onClick to `onOpenChange` in the caller. |
466
+ | `children` | `JSX.Element` | yes | |
467
+ | `label` | `string` | yes | Already-translated accessible name for the panel. |
468
+ | `placement` | `Placement` | — | |
469
+ | `align` | `'start' \| 'center' \| 'end'` | — | |
470
+ | `class` | `string` | — | |
471
+
472
+ ### Radio
473
+
474
+ Radio group. Rendered as a <fieldset>/<legend> so the group has a name, and as one native radio per option so arrow-key navigation is the platform's job.
475
+
476
+ | Prop | Type | Required | Notes |
477
+ |---|---|---|---|
478
+ | `legend` | `string` | yes | Already-translated group label, rendered as the <legend>. |
479
+ | `name` | `string` | yes | |
480
+ | `options` | `readonly RadioOption[]` | yes | |
481
+ | `value` | `string` | — | |
482
+ | `disabled` | `boolean` | — | |
483
+ | `required` | `boolean` | — | |
484
+ | `direction` | `'row' \| 'column'` | — | |
485
+ | `class` | `string` | — | |
486
+ | `aria-describedby` | `string` | — | |
487
+ | `onChange` | `JSX.EventHandlerUnion<HTMLInputElement, Event>` | — | |
488
+
489
+ ### RelativeTime
490
+
491
+ "3 minutes ago" with the absolute instant in both `datetime` and `title`, so hovering (or reading the DOM) always recovers the exact time.
492
+
493
+ | Prop | Type | Required | Notes |
494
+ |---|---|---|---|
495
+ | `value` | `TimeInput` | yes | |
496
+ | `now` | `TimeInput` | — | Pass explicitly for SSR so server and client render the same string. |
497
+ | `locale` | `string` | — | |
498
+ | `timeZone` | `TimeZone` | — | |
499
+ | `numeric` | `'always' \| 'auto'` | — | |
500
+ | `format` | `DateTimeFormatter` | — | |
501
+ | `class` | `string` | — | |
502
+
503
+ ### Section
504
+
505
+ A titled block inside a page. Exists so a screen's second-level structure is a labelled landmark with a real heading — `aria-labelledby` wired to the heading it renders — instead of a <div> with bold text, which is what an unassisted layout always becomes.
506
+
507
+ | Prop | Type | Required | Notes |
508
+ |---|---|---|---|
509
+ | `children` | `JSX.Element` | yes | |
510
+ | `title` | `string` | — | Already-translated section title. Omit for an unlabelled grouping. |
511
+ | `description` | `string` | — | Already-translated supporting line under the title. |
512
+ | `actions` | `JSX.Element` | — | Controls that act on this section only. |
513
+ | `level` | `HeadingLevel` | — | Heading level. 2 by default — the level under a PageHeader's h1. |
514
+ | `as` | `'section' \| 'article' \| 'aside'` | — | |
515
+ | `class` | `string` | — | |
516
+
517
+ ### Select
518
+
519
+ Native <select>. Deliberately not a custom listbox: the platform control wins on mobile, on keyboard, and with screen readers.
520
+
521
+ | Prop | Type | Required | Notes |
522
+ |---|---|---|---|
523
+ | `options` | `readonly SelectOption[]` | yes | |
524
+ | `id` | `string` | — | |
525
+ | `name` | `string` | — | |
526
+ | `value` | `string` | — | |
527
+ | `placeholder` | `string` | — | Rendered as a disabled first option, so it is never a submittable value. |
528
+ | `size` | `Size` | — | |
529
+ | `required` | `boolean` | — | |
530
+ | `disabled` | `boolean` | — | |
531
+ | `class` | `string` | — | |
532
+ | `aria-label` | `string` | — | |
533
+ | `aria-describedby` | `string` | — | |
534
+ | `aria-invalid` | `boolean` | — | |
535
+ | `onChange` | `JSX.EventHandlerUnion<HTMLSelectElement, Event>` | — | |
536
+
537
+ ### Skeleton
538
+
539
+ Loading placeholder. Sized by the caller so the real content lands in the same box — a skeleton that changes size on load is just a slower layout shift.
540
+
541
+ | Prop | Type | Required | Notes |
542
+ |---|---|---|---|
543
+ | `width` | `string` | — | CSS length; must match the real content's box to keep CLS at 0. |
544
+ | `height` | `string` | — | |
545
+ | `shape` | `'text' \| 'block' \| 'circle'` | — | |
546
+ | `lines` | `number` | — | Repeat as stacked lines, e.g. a paragraph placeholder. |
547
+ | `class` | `string` | — | |
548
+
549
+ ### Spinner
550
+
551
+ Indeterminate progress. `role="status"` plus a translated name, because a spinner with no accessible name is silence to a screen reader.
552
+
553
+ | Prop | Type | Required | Notes |
554
+ |---|---|---|---|
555
+ | `size` | `Size` | — | |
556
+ | `label` | `string` | — | Overrides the translated `ui.loading` default. |
557
+ | `decorative` | `boolean` | — | Hide from assistive tech when a parent already announces the busy state. |
558
+ | `class` | `string` | — | |
559
+
560
+ ### Stack
561
+
562
+ Flex layout primitive. Gap comes from the space scale as a custom property, so there is no class per step, and `row` uses `flex-direction: row` — which the browser already mirrors under `dir="rtl"`.
563
+
564
+ | Prop | Type | Required | Notes |
565
+ |---|---|---|---|
566
+ | `children` | `JSX.Element` | yes | |
567
+ | `direction` | `'row' \| 'column'` | — | |
568
+ | `gap` | `SpaceStep` | — | |
569
+ | `align` | `Align` | — | |
570
+ | `justify` | `Align` | — | |
571
+ | `wrap` | `boolean` | — | |
572
+ | `as` | `'div' \| 'ul' \| 'ol' \| 'nav' \| 'section' \| 'header' \| 'footer'` | — | Renders a semantic element instead of a div. |
573
+ | `class` | `string` | — | |
574
+
575
+ ### Switch
576
+
577
+ Boolean toggle with immediate effect (as opposed to Checkbox, which is part of a form submit). `role="switch"` on a native checkbox keeps keyboard behaviour.
578
+
579
+ | Prop | Type | Required | Notes |
580
+ |---|---|---|---|
581
+ | `label` | `string` | yes | Already-translated label. Required — the state alone is not a name. |
582
+ | `checked` | `boolean` | — | |
583
+ | `id` | `string` | — | |
584
+ | `name` | `string` | — | |
585
+ | `disabled` | `boolean` | — | |
586
+ | `labelPosition` | `'start' \| 'end'` | — | Put the label before the track, e.g. in a settings row. |
587
+ | `class` | `string` | — | |
588
+ | `aria-describedby` | `string` | — | |
589
+ | `onChange` | `JSX.EventHandlerUnion<HTMLInputElement, Event>` | — | |
590
+
591
+ ### Table
592
+
593
+ Presentational table. Owns two things every hand-rolled table gets wrong: a sticky header, and horizontal overflow contained inside the table's own scroll container so the page body never scrolls sideways.
594
+
595
+ | Prop | Type | Required | Notes |
596
+ |---|---|---|---|
597
+ | `caption` | `string` | yes | Already-translated caption. Required: a table needs an accessible name. |
598
+ | `children` | `JSX.Element` | yes | |
599
+ | `hideCaption` | `boolean` | — | Hide the caption visually while keeping it for assistive tech. |
600
+ | `stickyHeader` | `boolean` | — | |
601
+ | `density` | `'comfortable' \| 'compact'` | — | |
602
+ | `striped` | `boolean` | — | Zebra striping. Off by default — a border is usually enough. |
603
+ | `class` | `string` | — | |
604
+
605
+ ### Tabs
606
+
607
+ Tablist with roving tabindex: one Tab in the page tab order, arrows move between them, Home/End jump to the ends, and arrow direction follows `dir`.
608
+
609
+ | Prop | Type | Required | Notes |
610
+ |---|---|---|---|
611
+ | `items` | `readonly TabItem[]` | yes | |
612
+ | `value` | `string` | yes | Selected tab id. Controlled — the caller owns the state. |
613
+ | `onChange` | `(id: string) => void` | yes | |
614
+ | `label` | `string` | yes | Already-translated accessible name for the tablist. |
615
+ | `orientation` | `'horizontal' \| 'vertical'` | — | |
616
+ | `class` | `string` | — | |
617
+
618
+ ### Text
619
+
620
+ Typography primitive. Tone is a foreground or status colour role, size and weight are keys of the type scale — so body copy, captions and inline status text come from one component and stay legible on both themes.
621
+
622
+ | Prop | Type | Required | Notes |
623
+ |---|---|---|---|
624
+ | `children` | `JSX.Element` | yes | |
625
+ | `tone` | `TextTone` | — | |
626
+ | `size` | `TextSize` | — | |
627
+ | `weight` | `TextWeight` | — | |
628
+ | `as` | `'span' \| 'p' \| 'div' \| 'strong' \| 'em'` | — | Renders a semantic element instead of a span; `p` for flow text. `strong` and `em` carry meaning to assistive tech — `weight` and `tone` do not. |
629
+ | `class` | `string` | — | |
630
+
631
+ ### Textarea
632
+
633
+ Multi-line text control. `field-sizing: content` grows the box natively where supported, with `rows` as the floor — no resize observer, no JS.
634
+
635
+ | Prop | Type | Required | Notes |
636
+ |---|---|---|---|
637
+ | `id` | `string` | — | |
638
+ | `name` | `string` | — | |
639
+ | `value` | `string` | — | |
640
+ | `placeholder` | `string` | — | |
641
+ | `rows` | `number` | — | |
642
+ | `required` | `boolean` | — | |
643
+ | `disabled` | `boolean` | — | |
644
+ | `readonly` | `boolean` | — | |
645
+ | `maxlength` | `number` | — | |
646
+ | `autoGrow` | `boolean` | — | |
647
+ | `class` | `string` | — | |
648
+ | `aria-label` | `string` | — | |
649
+ | `aria-describedby` | `string` | — | |
650
+ | `aria-invalid` | `boolean` | — | |
651
+ | `onInput` | `JSX.EventHandlerUnion<HTMLTextAreaElement, InputEvent>` | — | |
652
+ | `onBlur` | `JSX.EventHandlerUnion<HTMLTextAreaElement, FocusEvent>` | — | |
653
+
654
+ ### ThemeToggle
655
+
656
+ Theme control. `toggle` flips light/dark; `select` also offers "system", which clears the stored choice so the OS takes over again. All strings come from the catalog, and the DOM work happens in theme.ts, never here.
657
+
658
+ | Prop | Type | Required | Notes |
659
+ |---|---|---|---|
660
+ | `mode` | `'toggle' \| 'select'` | — | |
661
+ | `initial` | `Theme` | — | Server-render value; the effect corrects it on the client before paint. |
662
+ | `env` | `ThemeEnv` | — | Injectable for tests and for non-DOM hosts. |
663
+ | `class` | `string` | — | |
664
+
665
+ ### ToastRegion
666
+
667
+ Transient notification. ToastRegion is the single live region for the app; individual Toasts are its children, so announcements are not duplicated and the region exists before the first message (screen readers require that).
668
+
669
+ | Prop | Type | Required | Notes |
670
+ |---|---|---|---|
671
+ | `children` | `JSX.Element` | yes | |
672
+ | `label` | `string` | yes | Already-translated landmark name, e.g. "Notifications". |
673
+ | `placement` | `'block-end-inline-end' \| 'block-start-inline-end' \| 'block-end-center'` | — | |
674
+ | `class` | `string` | — | |
675
+
676
+ ### Toast
677
+
678
+ Transient notification. ToastRegion is the single live region for the app; individual Toasts are its children, so announcements are not duplicated and the region exists before the first message (screen readers require that).
679
+
680
+ | Prop | Type | Required | Notes |
681
+ |---|---|---|---|
682
+ | `children` | `JSX.Element` | yes | Already-translated message. |
683
+ | `title` | `string` | — | |
684
+ | `tone` | `Tone` | — | |
685
+ | `action` | `JSX.Element` | — | |
686
+ | `onDismiss` | `(() => void)` | — | |
687
+ | `dismissLabel` | `string` | — | |
688
+ | `class` | `string` | — | |
689
+
690
+ ### Toolbar
691
+
692
+ The control strip above a table or a list: filters and search at the inline start, actions at the inline end. `role="toolbar"` with the same roving-tabindex helper Tabs uses, so arrow keys move between controls and the strip costs one Tab stop instead of a dozen.
693
+
694
+ | Prop | Type | Required | Notes |
695
+ |---|---|---|---|
696
+ | `children` | `JSX.Element` | yes | Leading controls — search, filters, a Select. |
697
+ | `actions` | `JSX.Element` | — | Trailing controls, pushed to the inline end. |
698
+ | `label` | `string` | yes | Already-translated accessible name. Required: an unnamed toolbar is an unnamed group. |
699
+ | `surface` | `boolean` | — | Renders the strip on a raised, bordered surface. |
700
+ | `class` | `string` | — | |
701
+
702
+ ### Tooltip
703
+
704
+ Supplementary hint, shown on hover AND on keyboard focus. CSS-only, so it costs no JS; the content is wired with `aria-describedby`, never `title`, because `title` is unreachable by keyboard and untranslatable by the platform.
705
+
706
+ | Prop | Type | Required | Notes |
707
+ |---|---|---|---|
708
+ | `content` | `string` | yes | Already-translated hint text. Never the element's only accessible name. |
709
+ | `children` | `(control: { 'aria-describedby': string }) => JSX.Element` | yes | |
710
+ | `placement` | `'block-start' \| 'block-end'` | — | |
711
+ | `class` | `string` | — | |
712
+
713
+ ## Tokens
714
+
715
+ Colour roles are the only colours that exist. Use them through SCSS (`t.role('accent')`) or
716
+ CSS (`rgb(var(--color-accent) / 0.12)`); `colorRgb(theme, role)` resolves one for canvas,
717
+ charts and email.
718
+
719
+ ### Colour roles
720
+
721
+ | Role | Custom property |
722
+ |---|---|
723
+ | `bg` | `--color-bg` |
724
+ | `bg-soft` | `--color-bg-soft` |
725
+ | `surface` | `--color-surface` |
726
+ | `surface-raised` | `--color-surface-raised` |
727
+ | `fg` | `--color-fg` |
728
+ | `fg-strong` | `--color-fg-strong` |
729
+ | `fg-muted` | `--color-fg-muted` |
730
+ | `line` | `--color-line` |
731
+ | `scrim` | `--color-scrim` |
732
+ | `accent` | `--color-accent` |
733
+ | `accent-strong` | `--color-accent-strong` |
734
+ | `accent-fg` | `--color-accent-fg` |
735
+ | `success` | `--color-success` |
736
+ | `success-soft` | `--color-success-soft` |
737
+ | `success-fg` | `--color-success-fg` |
738
+ | `warning` | `--color-warning` |
739
+ | `warning-soft` | `--color-warning-soft` |
740
+ | `warning-fg` | `--color-warning-fg` |
741
+ | `danger` | `--color-danger` |
742
+ | `danger-soft` | `--color-danger-soft` |
743
+ | `danger-fg` | `--color-danger-fg` |
744
+ | `info` | `--color-info` |
745
+ | `info-soft` | `--color-info-soft` |
746
+ | `info-fg` | `--color-info-fg` |
747
+
748
+ ### Space — `--space-*`
749
+
750
+ | Token | Value |
751
+ |---|---|
752
+ | `0` | `0` |
753
+ | `1` | `0.25rem` |
754
+ | `2` | `0.5rem` |
755
+ | `3` | `0.75rem` |
756
+ | `4` | `1rem` |
757
+ | `5` | `1.25rem` |
758
+ | `6` | `1.5rem` |
759
+ | `8` | `2rem` |
760
+ | `10` | `2.5rem` |
761
+ | `12` | `3rem` |
762
+ | `16` | `4rem` |
763
+
764
+ ### Radius — `--radius-*`
765
+
766
+ | Token | Value |
767
+ |---|---|
768
+ | `none` | `0` |
769
+ | `sm` | `0.25rem` |
770
+ | `md` | `0.5rem` |
771
+ | `lg` | `0.75rem` |
772
+ | `xl` | `1rem` |
773
+ | `pill` | `999px` |
774
+ | `full` | `50%` |
775
+
776
+ ### Font size — `--text-*`
777
+
778
+ | Token | Value |
779
+ |---|---|
780
+ | `xs` | `clamp(0.75rem, 0.73rem + 0.1vw, 0.8125rem)` |
781
+ | `sm` | `clamp(0.875rem, 0.85rem + 0.15vw, 0.9375rem)` |
782
+ | `md` | `clamp(1rem, 0.96rem + 0.2vw, 1.0625rem)` |
783
+ | `lg` | `clamp(1.125rem, 1.05rem + 0.35vw, 1.25rem)` |
784
+ | `xl` | `clamp(1.375rem, 1.2rem + 0.7vw, 1.75rem)` |
785
+ | `2xl` | `clamp(1.75rem, 1.4rem + 1.4vw, 2.5rem)` |
786
+ | `3xl` | `clamp(2.25rem, 1.6rem + 2.6vw, 3.5rem)` |
787
+
788
+ ### Font weight — `--weight-*`
789
+
790
+ | Token | Value |
791
+ |---|---|
792
+ | `normal` | `400` |
793
+ | `medium` | `500` |
794
+ | `semibold` | `600` |
795
+ | `bold` | `700` |
796
+
797
+ ### Line height — `--leading-*`
798
+
799
+ | Token | Value |
800
+ |---|---|
801
+ | `tight` | `1.2` |
802
+ | `snug` | `1.35` |
803
+ | `normal` | `1.55` |
804
+ | `loose` | `1.75` |
805
+
806
+ ### Duration — `--duration-*`
807
+
808
+ | Token | Value |
809
+ |---|---|
810
+ | `instant` | `0ms` |
811
+ | `fast` | `120ms` |
812
+ | `base` | `220ms` |
813
+ | `slow` | `400ms` |
814
+ | `slower` | `640ms` |
815
+
816
+ ### Easing — `--easing-*`
817
+
818
+ | Token | Value |
819
+ |---|---|
820
+ | `out` | `cubic-bezier(0.16, 1, 0.3, 1)` |
821
+ | `in` | `cubic-bezier(0.5, 0, 0.75, 0)` |
822
+ | `in-out` | `cubic-bezier(0.65, 0, 0.35, 1)` |
823
+ | `spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` |
824
+
825
+ ### Z-index — `--z-*`
826
+
827
+ | Token | Value |
828
+ |---|---|
829
+ | `base` | `0` |
830
+ | `raised` | `10` |
831
+ | `sticky` | `100` |
832
+ | `dropdown` | `200` |
833
+ | `drawer` | `300` |
834
+ | `dialog` | `400` |
835
+ | `popover` | `500` |
836
+ | `tooltip` | `600` |
837
+ | `toast` | `700` |
838
+ | `skip-nav` | `800` |
839
+
840
+ ### Breakpoints — `@include t.respond-to(<name>)`
841
+
842
+ | Token | Value |
843
+ |---|---|
844
+ | `sm` | `480px` |
845
+ | `md` | `768px` |
846
+ | `lg` | `1024px` |
847
+ | `xl` | `1280px` |
848
+ | `2xl` | `1536px` |