@ultimat3/ui 23.0.0 → 25.0.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 +119 -15
- package/CLAUDE.md +17 -8
- package/README.md +119 -33
- package/package.json +6 -6
- package/src/a11y.ts +27 -13
- package/src/catalog/build-catalog.ts +2 -2
- package/src/components/Accordion.tsx +8 -2
- package/src/components/AppShell.module.scss +105 -4
- package/src/components/AppShell.tsx +48 -10
- package/src/components/BarChart.module.scss +47 -9
- package/src/components/BarChart.tsx +152 -72
- package/src/components/Card.tsx +52 -16
- package/src/components/ChartFrame.module.scss +188 -0
- package/src/components/ChartFrame.tsx +155 -0
- package/src/components/CommandPalette.module.scss +8 -5
- package/src/components/Container.tsx +46 -9
- package/src/components/DataTable.module.scss +190 -0
- package/src/components/DataTable.tsx +161 -51
- package/src/components/Divider.tsx +0 -2
- package/src/components/DonutChart.module.scss +73 -0
- package/src/components/DonutChart.tsx +154 -0
- package/src/components/Dropzone.module.scss +3 -1
- package/src/components/Dropzone.tsx +17 -8
- package/src/components/ErrorState.tsx +39 -12
- package/src/components/FileInput.module.scss +3 -1
- package/src/components/FileInput.tsx +6 -2
- package/src/components/Form.tsx +17 -4
- package/src/components/Gauge.module.scss +48 -0
- package/src/components/Gauge.tsx +67 -0
- package/src/components/Grid.tsx +34 -13
- package/src/components/Image.tsx +2 -2
- package/src/components/InlineBar.module.scss +23 -0
- package/src/components/InlineBar.tsx +31 -0
- package/src/components/LineChart.module.scss +147 -0
- package/src/components/LineChart.tsx +169 -0
- package/src/components/Menu.tsx +9 -5
- package/src/components/Meter.tsx +3 -3
- package/src/components/PageHeader.tsx +5 -2
- package/src/components/Popover.module.scss +17 -20
- package/src/components/Popover.tsx +14 -7
- package/src/components/Section.tsx +45 -26
- package/src/components/Stack.tsx +59 -18
- package/src/components/Table.module.scss +30 -0
- package/src/components/Table.tsx +14 -3
- package/src/components/Tabs.module.scss +6 -0
- package/src/components/Tabs.tsx +2 -2
- package/src/components/Text.tsx +41 -15
- package/src/components/Textarea.tsx +7 -4
- package/src/components/Toaster.tsx +1 -1
- package/src/components/Toolbar.tsx +7 -6
- package/src/components/Tooltip.module.scss +8 -3
- package/src/components/app-shell-view.ts +6 -4
- package/src/components/bar-chart-view.ts +11 -6
- package/src/components/chart-frame-view.ts +219 -0
- package/src/components/chart-ticks-view.ts +173 -0
- package/src/components/data-table-view.ts +68 -0
- package/src/components/donut-chart-view.ts +120 -0
- package/src/components/file-input-view.ts +32 -13
- package/src/components/gauge-view.ts +43 -0
- package/src/components/heading-node.tsx +62 -0
- package/src/components/image-source.ts +3 -3
- package/src/components/line-chart-view.ts +153 -0
- package/src/components/link-target.ts +21 -1
- package/src/components/meter-view.ts +13 -0
- package/src/components/money-view.ts +2 -7
- package/src/components/qr-matrix.ts +16 -23
- package/src/components/relative-time-view.ts +11 -4
- package/src/form/form-binding.ts +32 -14
- package/src/form/use-form.ts +3 -3
- package/src/global.scss +9 -0
- package/src/icons/build-icons.ts +109 -24
- package/src/index.ts +47 -15
- package/src/jsx-probe.ts +5 -4
- package/src/theme/brand-declarations.ts +113 -0
- package/src/theme/brand.ts +104 -96
- package/src/theme/context.ts +1 -1
- package/src/theme/preset-scifi.ts +99 -0
- package/src/theme/preset-shape.ts +18 -0
- package/src/theme/presets.ts +15 -0
- package/src/theme/shadow-value.ts +39 -0
- package/src/theme/theme.ts +36 -11
- package/src/toast/toast-state.ts +0 -0
- package/src/toast/toast-store.ts +1 -1
- package/src/toast/use-toasts.ts +1 -1
- package/src/tokens/_colors.scss +20 -0
- package/src/tokens/_effects.scss +59 -0
- package/src/tokens/_index.scss +2 -0
- package/src/tokens/_mixins.scss +6 -4
- package/src/tokens/_shadow.scss +16 -0
- package/src/tokens/_touch.scss +39 -0
- package/src/tokens/_typography.scss +14 -0
- package/src/tokens/color-tokens.ts +133 -0
- package/src/tokens/colour-vision.ts +106 -0
- package/src/tokens/contrast-pairs.ts +6 -1
- package/src/tokens/contrast.ts +1 -1
- package/src/tokens/theme.scss +2 -0
- package/src/tokens/tokens.ts +41 -87
- package/src/fake-dom.ts +0 -206
- package/src/sass-probe.ts +0 -55
- package/src/theme/inline-script.ts +0 -49
package/CATALOG.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Every component and every token, projected from source. Import all of it from `@ultimat3/ui`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
68 components: `Accordion` · `Alert` · `AppShell` · `AsyncRegion` · `Avatar` · `Badge` · `BarChart` · `Breadcrumb` · `Button` · `Card` · `ChartFrame` · `Checkbox` · `Combobox` · `CommandPalette` · `Container` · `CopyButton` · `DataTable` · `DateTime` · `Dialog` · `Divider` · `DonutChart` · `Drawer` · `Dropzone` · `EmptyState` · `ErrorState` · `Field` · `FileInput` · `Form` · `Gauge` · `Grid` · `Icon` · `IconButton` · `Image` · `InfiniteScroll` · `InlineBar` · `Input` · `Kbd` · `LineChart` · `AreaChart` · `Link` · `LocaleSwitcher` · `Menu` · `Meter` · `Money` · `PageHeader` · `Pagination` · `Popover` · `QrCode` · `Radio` · `RelativeTime` · `Section` · `Select` · `Skeleton` · `Sparkline` · `Spinner` · `Stack` · `StatTile` · `Switch` · `Table` · `Tabs` · `Text` · `Textarea` · `ThemeToggle` · `ToastRegion` · `Toast` · `Toaster` · `Toolbar` · `Tooltip`
|
|
8
8
|
|
|
9
9
|
## Vocabulary
|
|
10
10
|
|
|
@@ -47,17 +47,18 @@ Inline message bound to the page, not the viewport. `danger`/`warning` use `role
|
|
|
47
47
|
|
|
48
48
|
### AppShell
|
|
49
49
|
|
|
50
|
-
The page frame every app screen sits in: skip link, banner, navigation, main, contentinfo. Stateless
|
|
50
|
+
The page frame every app screen sits in: skip link, banner, navigation, main, contentinfo. Stateless, and scriptless: below `md` the sidebar `<nav>` is a native `popover` panel at the inline-start edge, opened by a `popovertarget` menu button — the browser holds the open state, Esc and light dismiss, so the frame on every page costs no island. Not `Drawer`: that is a modal `<dialog>` opened from an effect, so it needs a hydrated island, and a `<dialog>` cannot also be the in-flow `navigation` landmark at `md` and up. An engine with no popovers keeps the old band.
|
|
51
51
|
|
|
52
52
|
| Prop | Type | Required | Notes |
|
|
53
53
|
|---|---|---|---|
|
|
54
54
|
| `children` | `JSX.Element` | yes | The page. Rendered inside the one `<main>`, which is the skip link's target. |
|
|
55
55
|
| `header` | `JSX.Element` | — | |
|
|
56
|
-
| `sidebar` | `JSX.Element` | — | Rendered inside a `<nav>` landmark
|
|
56
|
+
| `sidebar` | `JSX.Element` | — | Rendered inside a `<nav>` landmark: the inline-start column at `md` and up, a panel below. |
|
|
57
57
|
| `footer` | `JSX.Element` | — | |
|
|
58
58
|
| `sidebarLabel` | `string` | — | Accessible name for the sidebar landmark. Defaults to the translated `ui.navigation`. |
|
|
59
59
|
| `skipLabel` | `string` | — | Skip-link text. Defaults to the translated `ui.skip`. |
|
|
60
|
-
| `
|
|
60
|
+
| `menuLabel` | `string` | — | The menu button's text below `md`. Defaults to the translated `ui.menu`. |
|
|
61
|
+
| `sidebarWidth` | `string` | — | Sidebar track width at `md` and up, and the panel's width below. Any CSS length. |
|
|
61
62
|
| `stickyHeader` | `boolean` | — | Keeps the header pinned while the main region scrolls. |
|
|
62
63
|
| `class` | `string` | — | |
|
|
63
64
|
|
|
@@ -103,15 +104,19 @@ Small status label. Tone maps straight onto the status colour roles so the same
|
|
|
103
104
|
|
|
104
105
|
### BarChart
|
|
105
106
|
|
|
106
|
-
A bar chart of one series — clicks per day, signups per week — or two stacked (done, and failed on top),
|
|
107
|
+
A bar chart of one series — clicks per day, signups per week — or two stacked (done, and failed on top): SVG bars, HTML axis labels. No charting library: the framework's own docs use a sparkline pulling one in as the cautionary example, and a route's JS budget counts raw minified bytes. `<rect>` bars cost nothing to hydrate, so the server-rendered shell IS the chart. The labels sit outside the svg because text inside a viewBox scales with it — a speck at phone width. It stands in a `ChartFrame`: a legend for two series, and a hidden table of the same numbers.
|
|
107
108
|
|
|
108
109
|
| Prop | Type | Required | Notes |
|
|
109
110
|
|---|---|---|---|
|
|
110
111
|
| `label` | `string` | yes | The accessible name for the whole chart, already translated. |
|
|
111
112
|
| `points` | `readonly ChartPoint[]` | yes | Oldest first. Every bar comes from exactly this array — the caller zero-fills gaps. |
|
|
112
113
|
| `highlightLast` | `boolean` | — | Draw the last bar at full strength — the eye lands where the live number is. Default on. |
|
|
113
|
-
| `seriesLabels` | `{ readonly primary: string; readonly secondary: string }` | — | Names of the two series, already translated, read by each rect's `<title
|
|
114
|
-
| `
|
|
114
|
+
| `seriesLabels` | `{ readonly primary: string; readonly secondary: string }` | — | Names of the two series, already translated, read by each rect's `<title>`, the legend and the data table. Give them when a point carries `secondary`: a stacked bar with no name for its top is two numbers and no key. |
|
|
115
|
+
| `keyLabel` | `string` | — | The key column's head in the data table, already translated — "Day". |
|
|
116
|
+
| `format` | `((value: number) => string)` | — | Formats every value. Default: `Intl.NumberFormat` in the page's locale. |
|
|
117
|
+
| `focusable` | `boolean` | — | Give every bar a tab stop and a hover/focus readout. Default off: no tab stops. |
|
|
118
|
+
| `showCaption` | `boolean` | — | Show the caption above the plot. Default off — it names the figure either way. |
|
|
119
|
+
| `class` | `string` | — | Lands on the root `<figure>`, which holds the svg and its HTML axis labels. |
|
|
115
120
|
|
|
116
121
|
### Breadcrumb
|
|
117
122
|
|
|
@@ -161,6 +166,19 @@ Surface container. Elevation is a token rung, and because the shadow tokens are
|
|
|
161
166
|
| `as` | `'div' \| 'article' \| 'section' \| 'li'` | — | |
|
|
162
167
|
| `class` | `string` | — | |
|
|
163
168
|
|
|
169
|
+
### ChartFrame
|
|
170
|
+
|
|
171
|
+
The frame every chart stands in: a `<figure>` named by its `<figcaption>`, the plot, a legend that wraps under the plot on a narrow container and stands beside it on a wide one, and a visually-hidden `<table>` of the same numbers — what a screen reader and a reader who cannot tell the colours apart get instead of the picture. Server-rendered HTML: no island, no JS.
|
|
172
|
+
|
|
173
|
+
| Prop | Type | Required | Notes |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `label` | `string` | yes | The chart's name, already translated: the `<figcaption>`, and the data table's caption. |
|
|
176
|
+
| `children` | `JSX.Element` | yes | The plot — the chart's own svg and HTML axis labels. |
|
|
177
|
+
| `showCaption` | `boolean` | — | Show the caption above the plot. Off by default: a chart usually sits under a Card's own heading, and the caption names the figure for assistive tech either way. |
|
|
178
|
+
| `legend` | `readonly ChartLegendItem[]` | — | One entry per series. Omitted or empty renders no legend. |
|
|
179
|
+
| `table` | `ChartTable` | — | The same data as rows, rendered visually hidden (`chartTable`). |
|
|
180
|
+
| `class` | `string` | — | Lands on the root `<figure>`. |
|
|
181
|
+
|
|
164
182
|
### Checkbox
|
|
165
183
|
|
|
166
184
|
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.
|
|
@@ -251,7 +269,7 @@ Copies one string to the clipboard and says so — the control beside a short UR
|
|
|
251
269
|
|
|
252
270
|
### DataTable
|
|
253
271
|
|
|
254
|
-
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. The four-way decision itself is NOT here — it is `asyncBranch`, shared with `AsyncRegion`, so a table and a card list cannot disagree about what "loading with stale rows" looks like. Only the PLACEHOLDER is local, because a table's is table-shaped: rows of cells, not lines of text.
|
|
272
|
+
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. The four-way decision itself is NOT here — it is `asyncBranch`, shared with `AsyncRegion`, so a table and a card list cannot disagree about what "loading with stale rows" looks like. Only the PLACEHOLDER is local, because a table's is table-shaped: rows of cells, not lines of text. A narrow table is the SAME table restyled as labelled cards (`DataTable.module.scss`), never a second copy: a duplicate is read twice or, hidden, is links the reader is told are not there.
|
|
255
273
|
|
|
256
274
|
| Prop | Type | Required | Notes |
|
|
257
275
|
|---|---|---|---|
|
|
@@ -268,6 +286,8 @@ Data-driven table: sortable headers, cursor pagination, and the four states a re
|
|
|
268
286
|
| `nextCursor` | `string` | — | Opaque cursors from the query result; absent means no further page. |
|
|
269
287
|
| `prevCursor` | `string` | — | |
|
|
270
288
|
| `stickyHeader` | `boolean` | — | |
|
|
289
|
+
| `stickyFirstColumn` | `boolean` | — | Pins the first column while the table scrolls sideways. |
|
|
290
|
+
| `narrow` | `DataTableNarrow` | — | What a table narrower than `sm` becomes. Default `cards`. |
|
|
271
291
|
| `density` | `'comfortable' \| 'compact'` | — | |
|
|
272
292
|
| `skeletonRows` | `number` | — | Placeholder row count while loading. Match the usual page size. |
|
|
273
293
|
| `class` | `string` | — | |
|
|
@@ -315,6 +335,23 @@ A rule. Always an explicit `role="separator"` with an orientation; pass `label`
|
|
|
315
335
|
| `label` | `string` | — | Visible, already-translated caption rendered inside the rule. |
|
|
316
336
|
| `class` | `string` | — | |
|
|
317
337
|
|
|
338
|
+
### DonutChart
|
|
339
|
+
|
|
340
|
+
Parts of one whole — traffic by device, runs by outcome — as a ring of segments with the total (or the caller's figure) in its centre, a legend giving each part's value and share, and a hidden data table. Each segment carries its series' marker shape and a gap from its neighbours, so the ring reads in greyscale. SVG geometry from `donut-chart-view.ts`; text is HTML.
|
|
341
|
+
|
|
342
|
+
| Prop | Type | Required | Notes |
|
|
343
|
+
|---|---|---|---|
|
|
344
|
+
| `label` | `string` | yes | The chart's name, already translated: the figure's caption and the svg's accessible name. |
|
|
345
|
+
| `segments` | `readonly DonutSegment[]` | yes | Clockwise from twelve o'clock, in the order the legend lists them. |
|
|
346
|
+
| `centreValue` | `string` | — | The figure in the ring's centre, already formatted. Default: the formatted total. |
|
|
347
|
+
| `centreLabel` | `string` | — | A short line under the centre figure, already translated — "Total", "Sessions". |
|
|
348
|
+
| `keyLabel` | `string` | — | The segment column's head in the data table, already translated — "Device". |
|
|
349
|
+
| `valueLabel` | `string` | — | The value column's head in the data table, already translated. Default: `label`. |
|
|
350
|
+
| `format` | `((value: number) => string)` | — | Formats every value. Default: `Intl.NumberFormat` in the page's locale. |
|
|
351
|
+
| `focusable` | `boolean` | — | Give every drawn segment a tab stop and a hover/focus readout. Default off. |
|
|
352
|
+
| `showCaption` | `boolean` | — | Show the caption above the chart. Default off — it names the figure either way. |
|
|
353
|
+
| `class` | `string` | — | Lands on the root `<figure>`. |
|
|
354
|
+
|
|
318
355
|
### Drawer
|
|
319
356
|
|
|
320
357
|
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.
|
|
@@ -341,11 +378,11 @@ A drop surface that is a real `<label>` around a real, visually-hidden `<input t
|
|
|
341
378
|
| `id` | `string` | — | |
|
|
342
379
|
| `name` | `string` | — | |
|
|
343
380
|
| `accept` | `string` | — | The `accept` attribute, and the same list a dropped file is partitioned against. |
|
|
344
|
-
| `multiple` | `boolean` | — |
|
|
381
|
+
| `multiple` | `boolean` | — | Without it the zone takes ONE file, dropped or picked — the rest are refused by `count`. |
|
|
345
382
|
| `required` | `boolean` | — | |
|
|
346
383
|
| `disabled` | `boolean` | — | |
|
|
347
384
|
| `maxBytes` | `number` | — | Client-side ceiling. The server enforces its own; this only spares a doomed transfer. |
|
|
348
|
-
| `maxFiles` | `number` | — |
|
|
385
|
+
| `maxFiles` | `number` | — | Read only with `multiple`: a single-file zone's ceiling is one. |
|
|
349
386
|
| `progress` | `number` | — | 0..1 while an upload runs. Absent renders no bar at all — never one stuck at zero. |
|
|
350
387
|
| `progressLabel` | `string` | — | Already-translated accessible name for the progress bar. |
|
|
351
388
|
| `class` | `string` | — | |
|
|
@@ -435,6 +472,19 @@ Form shell. Owns the one thing every form needs and always forgets: a top-of-for
|
|
|
435
472
|
| `aria-label` | `string` | — | |
|
|
436
473
|
| `onSubmit` | `JSX.EventHandlerUnion<HTMLFormElement, SubmitEvent>` | — | |
|
|
437
474
|
|
|
475
|
+
### Gauge
|
|
476
|
+
|
|
477
|
+
One value against its maximum as a 270° dial — CPU, a quota, a queue's fill — with the reading in its centre and its label under it. A `meter` role carries the value for assistive tech, the way `Meter` does; the readout is HTML over the svg so it keeps its size on a phone.
|
|
478
|
+
|
|
479
|
+
| Prop | Type | Required | Notes |
|
|
480
|
+
|---|---|---|---|
|
|
481
|
+
| `label` | `string` | yes | What is measured, already translated: shown under the dial, and the meter's name. |
|
|
482
|
+
| `value` | `number` | yes | |
|
|
483
|
+
| `max` | `number` | yes | |
|
|
484
|
+
| `tone` | `Tone` | — | The fill's colour role. Default `accent`; a caller picks `warning`/`danger` by threshold. |
|
|
485
|
+
| `format` | `((value: number, max: number) => string)` | — | The readout, from the value and max. Default: the share as a percentage in the page's locale. |
|
|
486
|
+
| `class` | `string` | — | Lands on the root `<figure>`. |
|
|
487
|
+
|
|
438
488
|
### Grid
|
|
439
489
|
|
|
440
490
|
Grid layout primitive. The default is an intrinsic responsive grid (`auto-fit` + `minmax`) so most layouts need no breakpoint at all.
|
|
@@ -511,6 +561,18 @@ An endless list that degrades to pagination: the foot is always a real `rel="nex
|
|
|
511
561
|
| `rootMargin` | `string` | — | How far below the fold loading starts. A CSS length, as IntersectionObserver takes it. |
|
|
512
562
|
| `class` | `string` | — | |
|
|
513
563
|
|
|
564
|
+
### InlineBar
|
|
565
|
+
|
|
566
|
+
A figure with a bar drawn to its share of a maximum — the in-cell bar a dashboard table uses to make a column of numbers scannable. The text is the fact and is what assistive tech reads; the bar is `Meter`'s decorative form, so the share rule and its clamping are written once. The figure arrives formatted, as `StatTile`'s does: this component never guesses a locale.
|
|
567
|
+
|
|
568
|
+
| Prop | Type | Required | Notes |
|
|
569
|
+
|---|---|---|---|
|
|
570
|
+
| `value` | `number` | yes | |
|
|
571
|
+
| `max` | `number` | yes | The whole the bar is a share of — usually the largest value in the column. |
|
|
572
|
+
| `text` | `string` | yes | The figure as the cell shows it, already formatted (`Intl.NumberFormat`, `formatMoney`). |
|
|
573
|
+
| `tone` | `Tone` | — | |
|
|
574
|
+
| `class` | `string` | — | |
|
|
575
|
+
|
|
514
576
|
### Input
|
|
515
577
|
|
|
516
578
|
Single-line text control. No `type="number"` convenience wrapper: numeric input uses `inputmode` + a text type so locale decimal separators survive.
|
|
@@ -548,6 +610,38 @@ A keyboard key or chord as the user reads it — "⌘K", "Esc", "/". A native `<
|
|
|
548
610
|
| `children` | `JSX.Element` | yes | The key or chord, already translated where a key name is a word. |
|
|
549
611
|
| `class` | `string` | — | |
|
|
550
612
|
|
|
613
|
+
### LineChart
|
|
614
|
+
|
|
615
|
+
A line chart of one or more series over the same keys — signups per day, p95 per hour — and `AreaChart`, the same chart with each series' area filled to the baseline. SVG lines on a nice 1-2-5 value axis, HTML axis labels (text inside a viewBox scales to a speck on a phone), a legend, and a hidden data table. Every series has its own dash and marker, never colour alone.
|
|
616
|
+
|
|
617
|
+
| Prop | Type | Required | Notes |
|
|
618
|
+
|---|---|---|---|
|
|
619
|
+
| `label` | `string` | yes | The chart's name, already translated: the figure's caption and the svg's accessible name. |
|
|
620
|
+
| `keys` | `readonly string[]` | yes | The x axis, oldest first, already formatted for display — "17 Sep", "14:00". |
|
|
621
|
+
| `series` | `readonly ChartSeries[]` | yes | One entry per line. `values[i]` belongs to `keys[i]`; `null` is a gap, never a zero. |
|
|
622
|
+
| `keyLabel` | `string` | — | The key column's head in the data table, already translated — "Day". |
|
|
623
|
+
| `format` | `((value: number) => string)` | — | Formats every value and tick. Default: `Intl.NumberFormat` in the page's locale. |
|
|
624
|
+
| `zero` | `boolean` | — | Start the value axis at zero. Default on; off fits the axis to the data. |
|
|
625
|
+
| `focusable` | `boolean` | — | Give every point a tab stop and a hover/focus readout. Default off: no tab stops. |
|
|
626
|
+
| `showCaption` | `boolean` | — | Show the caption above the plot. Default off — it names the figure either way. |
|
|
627
|
+
| `class` | `string` | — | Lands on the root `<figure>`. |
|
|
628
|
+
|
|
629
|
+
### AreaChart
|
|
630
|
+
|
|
631
|
+
A line chart of one or more series over the same keys — signups per day, p95 per hour — and `AreaChart`, the same chart with each series' area filled to the baseline. SVG lines on a nice 1-2-5 value axis, HTML axis labels (text inside a viewBox scales to a speck on a phone), a legend, and a hidden data table. Every series has its own dash and marker, never colour alone.
|
|
632
|
+
|
|
633
|
+
| Prop | Type | Required | Notes |
|
|
634
|
+
|---|---|---|---|
|
|
635
|
+
| `label` | `string` | yes | The chart's name, already translated: the figure's caption and the svg's accessible name. |
|
|
636
|
+
| `keys` | `readonly string[]` | yes | The x axis, oldest first, already formatted for display — "17 Sep", "14:00". |
|
|
637
|
+
| `series` | `readonly ChartSeries[]` | yes | One entry per line. `values[i]` belongs to `keys[i]`; `null` is a gap, never a zero. |
|
|
638
|
+
| `keyLabel` | `string` | — | The key column's head in the data table, already translated — "Day". |
|
|
639
|
+
| `format` | `((value: number) => string)` | — | Formats every value and tick. Default: `Intl.NumberFormat` in the page's locale. |
|
|
640
|
+
| `zero` | `boolean` | — | Start the value axis at zero. Default on; off fits the axis to the data. |
|
|
641
|
+
| `focusable` | `boolean` | — | Give every point a tab stop and a hover/focus readout. Default off: no tab stops. |
|
|
642
|
+
| `showCaption` | `boolean` | — | Show the caption above the plot. Default off — it names the figure either way. |
|
|
643
|
+
| `class` | `string` | — | Lands on the root `<figure>`. |
|
|
644
|
+
|
|
551
645
|
### Link
|
|
552
646
|
|
|
553
647
|
Anchor primitive. External links get `rel` hardening and a translated "opens in a new tab" hint automatically — never a bare `target="_blank"`. `appearance="button"` is Button's look on the same anchor: a control that navigates is a link.
|
|
@@ -595,7 +689,7 @@ Action menu: a trigger plus a `role="menu"` list with roving tabindex. Distinct
|
|
|
595
689
|
| `items` | `readonly MenuItem[]` | yes | |
|
|
596
690
|
| `open` | `boolean` | yes | |
|
|
597
691
|
| `onOpenChange` | `(open: boolean) => void` | yes | |
|
|
598
|
-
| `trigger` | `(control: { id: string; 'aria-haspopup': 'menu'; 'aria-expanded': boolean; 'aria-controls': string; }) => JSX.Element` | yes |
|
|
692
|
+
| `trigger` | `(control: { id: string; 'aria-haspopup': 'menu'; 'aria-expanded': boolean; 'aria-controls': string \| undefined; }) => JSX.Element` | yes | The menu button. `aria-controls` is present only while the list is mounted: an IDREF to an element not in the document names nothing. |
|
|
599
693
|
| `label` | `string` | yes | Already-translated accessible name for the menu. |
|
|
600
694
|
| `align` | `'start' \| 'end'` | — | |
|
|
601
695
|
| `class` | `string` | — | |
|
|
@@ -659,13 +753,13 @@ Cursor-first pagination — the only shape a Postgres-backed list should use. Nu
|
|
|
659
753
|
|
|
660
754
|
### Popover
|
|
661
755
|
|
|
662
|
-
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
|
|
756
|
+
Non-modal anchored panel. Positioning is CSS-only (a relatively positioned anchor plus logical insets and auto-margin centring), so there is no measure/reflow loop and the panel mirrors under `dir="rtl"` — `rtl-sheets.test.ts` holds that.
|
|
663
757
|
|
|
664
758
|
| Prop | Type | Required | Notes |
|
|
665
759
|
|---|---|---|---|
|
|
666
760
|
| `open` | `boolean` | yes | |
|
|
667
761
|
| `onOpenChange` | `(open: boolean) => void` | yes | |
|
|
668
|
-
| `trigger` | `(control: { id: string; 'aria-expanded': boolean; 'aria-controls': string; }) => JSX.Element` | yes | The anchor. Wire its onClick to `onOpenChange` in the caller. |
|
|
762
|
+
| `trigger` | `(control: { id: string; 'aria-expanded': boolean; 'aria-controls': string \| undefined; }) => JSX.Element` | yes | The anchor. Wire its onClick to `onOpenChange` in the caller. `aria-controls` is present only while the panel is mounted: an IDREF to an element not in the document names nothing. |
|
|
669
763
|
| `children` | `JSX.Element` | yes | |
|
|
670
764
|
| `label` | `string` | yes | Already-translated accessible name for the panel. |
|
|
671
765
|
| `placement` | `Placement` | — | |
|
|
@@ -827,7 +921,7 @@ Boolean toggle with immediate effect (as opposed to Checkbox, which is part of a
|
|
|
827
921
|
|
|
828
922
|
### Table
|
|
829
923
|
|
|
830
|
-
Presentational table. Owns
|
|
924
|
+
Presentational table. Owns what every hand-rolled table gets wrong: a sticky header, a first column that stays put while the rest scrolls, and horizontal overflow contained inside the table's own scroll container so the page body never scrolls sideways.
|
|
831
925
|
|
|
832
926
|
| Prop | Type | Required | Notes |
|
|
833
927
|
|---|---|---|---|
|
|
@@ -835,6 +929,8 @@ Presentational table. Owns two things every hand-rolled table gets wrong: a stic
|
|
|
835
929
|
| `children` | `JSX.Element` | yes | |
|
|
836
930
|
| `hideCaption` | `boolean` | — | Hide the caption visually while keeping it for assistive tech. |
|
|
837
931
|
| `stickyHeader` | `boolean` | — | |
|
|
932
|
+
| `stickyFirstColumn` | `boolean` | — | Pins the first column at the inline start while the table scrolls sideways. Off by default. |
|
|
933
|
+
| `explicitRoles` | `boolean` | — | Declares `role="table"` on the element itself. For a caller that restyles the table's `display` (DataTable's card view): WebKit drops a table's semantics once `display` changes, and the explicit role is what keeps it a table. The rows and cells are the caller's to mark. |
|
|
838
934
|
| `density` | `'comfortable' \| 'compact'` | — | |
|
|
839
935
|
| `striped` | `boolean` | — | Zebra striping. Off by default — a border is usually enough. |
|
|
840
936
|
| `class` | `string` | — | |
|
|
@@ -933,7 +1029,7 @@ The one way to render a toast queue: the store's VISIBLE slice, inside the regio
|
|
|
933
1029
|
|
|
934
1030
|
| Prop | Type | Required | Notes |
|
|
935
1031
|
|---|---|---|---|
|
|
936
|
-
| `store` | `ToastStore` | yes | The queue. One per app — `
|
|
1032
|
+
| `store` | `ToastStore` | yes | The queue. One per app — `toastStore()` in the island that mounts this. |
|
|
937
1033
|
| `label` | `string` | yes | Already-translated landmark name, e.g. "Notifications". |
|
|
938
1034
|
| `politeness` | `Politeness` | — | |
|
|
939
1035
|
| `placement` | `ToastPlacement` | — | |
|
|
@@ -997,6 +1093,14 @@ charts and email.
|
|
|
997
1093
|
| `info` | `--color-info` |
|
|
998
1094
|
| `info-soft` | `--color-info-soft` |
|
|
999
1095
|
| `info-fg` | `--color-info-fg` |
|
|
1096
|
+
| `chart-1` | `--color-chart-1` |
|
|
1097
|
+
| `chart-2` | `--color-chart-2` |
|
|
1098
|
+
| `chart-3` | `--color-chart-3` |
|
|
1099
|
+
| `chart-4` | `--color-chart-4` |
|
|
1100
|
+
| `chart-5` | `--color-chart-5` |
|
|
1101
|
+
| `chart-6` | `--color-chart-6` |
|
|
1102
|
+
| `chart-7` | `--color-chart-7` |
|
|
1103
|
+
| `chart-8` | `--color-chart-8` |
|
|
1000
1104
|
|
|
1001
1105
|
### Space — `--space-*`
|
|
1002
1106
|
|
package/CLAUDE.md
CHANGED
|
@@ -16,7 +16,7 @@ Tier 4. Imports `@ultimat3/core`, `i18n`, `money`, `time` — **not `schema`**,
|
|
|
16
16
|
- **Logical properties only.** `margin-inline`, `inset-inline-start`, `text-align: start`. A `left`/`right` in a stylesheet is a bug.
|
|
17
17
|
- **solid-js is a type-only import.** All reactive access goes through `src/theme/solid-adapter.ts`. Never `import { createSignal } from 'solid-js'`.
|
|
18
18
|
- **The runtime SLOT is `src/theme/runtime-slot.ts`; the RULE is `solid-adapter.ts`, and the split is a byte measurement.** `solid-adapter.ts` reaches `../errors` and so core's error registry; the slot imports nothing, so an island that only calls `setSolidRuntime` pays 72 B, not 5,719. `barrel-bytes.test.ts` holds the ceiling at 1 kB.
|
|
19
|
-
- **The barrel is the ONE import path; component subpath exports are refused on measurement** (issue #275). A barrel import and a deep path emit the same modules and bytes, so `@ultimat3/ui/button` would be a second idiom for zero bytes. The two subpaths that exist are not exceptions: `./icons/*` is 1,767 data modules no bundler can split, and `./jsx-probe` is test-only. `barrel-bytes.test.ts` compares the two paths and names `schema-error-codes.ts` (Bun 1.4.0's load-correlated shake,
|
|
19
|
+
- **The barrel is the ONE import path; component subpath exports are refused on measurement** (issue #275). A barrel import and a deep path emit the same modules and bytes, so `@ultimat3/ui/button` would be a second idiom for zero bytes. The two subpaths that exist are not exceptions: `./icons/*` is 1,767 data modules no bundler can split, and `./jsx-probe` is test-only. `barrel-bytes.test.ts` compares the two paths and names `schema-error-codes.ts` (Bun 1.4.0's load-correlated shake, issue #273) rather than allowing bytes for it. Its sibling #276 — a pure re-export entry shaken to its export clause — was fixed in Bun 1.4.1 (oven-sh/bun#40578), and barrels have been built as their own entry since.
|
|
20
20
|
- **`useUi()`'s server branch is a SLOT, and the module that fills it is browser-mapped** (issue #490). `context.ts` imports no value from `@ultimat3/i18n` or `@ultimat3/time`. `src/theme/ambient.ts` registers into `ambient-slot.ts` at import; `index.ts` imports it BARE above every re-export; `package.json`'s `browser` field maps it to `ambient.browser.ts`. A test reaching components by module path must `import './ambient'` itself. `errors.ts` is pure: `registerErrorCodes()` is `src/error-registry.ts`, imported bare.
|
|
21
21
|
- **`solid()` always answers off-DOM.** No registered runtime and no DOM is a *server render*, and it gets `INERT_SOLID_RUNTIME` (`src/theme/inert-runtime.ts`) — signals hold, memos recompute on read, effects never run, `useContext` returns the default. No DOM means no reactivity to lose; a **DOM** with no runtime is still `X_UI_RUNTIME_MISSING`, because that one is the theme toggle that does nothing. Never widen this to "no runtime, never throw": that is the silent degradation the split exists to prevent.
|
|
22
22
|
- **`useUi()` reads the request on the server**, via `ambientUiContext()` — `currentLocale()`, `currentTimeZone()`, `useI18n()`, which read **core's own `Ctx.locale` / `Ctx.tz`** (written once per request by `@ultimat3/http`'s `locale` stage). Never add a second ambient store here or in `i18n`/`time`. `theme`/`currency` have no ambient source at all.
|
|
@@ -26,12 +26,12 @@ Tier 4. Imports `@ultimat3/core`, `i18n`, `money`, `time` — **not `schema`**,
|
|
|
26
26
|
- **No `{...rest}` prop spreading.** Splitting props reactively needs solid's `splitProps` (a value import), so components declare explicit props and read `props.x` inside JSX.
|
|
27
27
|
- **One mode per use is a UNION of interfaces** (`LinkProps`, `PaginationProps`, `DataTableProps` — where the mode covers the pager AND the sort headers: `hrefFor` + `sortHrefFor`, or `onCursor` + `onSortChange`; a linked table never renders a button with no handler): a mode forbids the other's props with `x?: undefined`, and `catalog/parse-component.ts` merges the modes.
|
|
28
28
|
- **No hardcoded strings.** Label props, or `useUi().t(UI_KEYS.x)`. New built-in strings go in `src/i18n-keys.ts`.
|
|
29
|
-
- **Page layout is four composites**: `AppShell` (frame + skip link + landmarks), `PageHeader`, `Section`, `Toolbar`. A screen that hand-rolls a header grid is the bug they exist to prevent. `AppShell` holds no state
|
|
29
|
+
- **Page layout is four composites**: `AppShell` (frame + skip link + landmarks), `PageHeader`, `Section`, `Toolbar`. A screen that hand-rolls a header grid is the bug they exist to prevent. `AppShell` holds no state; its mobile nav is a no-JS `popover`, and `Drawer` stays the modal off-canvas.
|
|
30
30
|
- **Restyling goes through `defineTheme()`**, never a forked stylesheet and never an SCSS `with ()` override. Its values are validated, not escaped: the output lands in a `<style>` element.
|
|
31
31
|
- **`@include t.tone-classes`** emits the `.tone-*` custom-property blocks. Never hand-write a seventh copy; `$tones` in `_colors.scss` mirrors `TONES` and `variants.test.ts` gates it.
|
|
32
32
|
- **Icons are generated, never authored.** `src/icons/glyphs/*.ts` is one module per Lucide icon, written by `bun run icons` from `lucide-static@LUCIDE_VERSION`. Never hand-edit a glyph, never add a hand-drawn one, never introduce a second icon source. An upstream fix is a version bump plus a re-run. `lucide` itself is NOT a dependency — the data is committed, so the package still installs, typechecks and renders offline with zero runtime deps.
|
|
33
33
|
- **One icon, one module.** The `Icon` component takes a `glyph`, not a name: a `name → glyph` map would be one module holding 1767 icons, and no bundler can split that. Per-icon imports are the whole point (1 icon = 104 B minified, 50 = 8.9 kB).
|
|
34
|
-
- **
|
|
34
|
+
- **Server-rendered first, an island second.** Components run in islands (`examples/dummy`'s settings and feed islands render them), but every one must still be correct server-rendered and usable with scripting off — `<details>` for disclosure, `<input list>`+`<datalist>` for suggestions, a real `rel="next"` link for paging — with listeners and observers as additive extras. A component that renders nothing until JS runs does not ship. In an island the body runs ONCE: a branch decided there (an early `return <ErrorState>`) is decided for the island's life, so branches live inside the returned JSX (`AsyncRegion`, `DataTable`). A server-only spelling is gated on `typeof document` (`Textarea`'s parser newline). Ids minted in an island carry a per-copy scope (`a11y.ts`), because each island bundles its own counter.
|
|
35
35
|
- **`inert-render.test.ts` must not assume which factory its `.tsx` compiled to.** `@ultimat3/render` installs a process-global `.tsx` `onLoad` plugin at import, so in one `bun test` process a component may compile to render's `h`. The walker recognises both (`Symbol.for('ultimate.render.jsx')`), and the first test asserts a recognised node — otherwise the file renders `"[object Object]"` and passes by shard packing.
|
|
36
36
|
- **Components are not unit-tested through a renderer.** `.tsx` compiles to `@ultimat3/render`'s `h`, which this package may not import, so every rule lives in a pure module beside the component (`icon-glyph.ts`, `accordion-view.ts`, `combobox-filter.ts`, `infinite-scroll-view.ts`) and *that* is what the tests assert.
|
|
37
37
|
- Formatting logic lives in a pure `*-view.ts` next to the component (`money-view.ts`, `date-time-view.ts`) so it is testable with no renderer. Every other renderer-free core follows the same rule under its own name (`sort-state.ts`, `image-source.ts`) — the `.tsx` holds markup, never a rule.
|
|
@@ -42,8 +42,11 @@ Tier 4. Imports `@ultimat3/core`, `i18n`, `money`, `time` — **not `schema`**,
|
|
|
42
42
|
- **A toast dwell stops for THREE independent reasons** — hover, focus-within, `document.hidden` — so `ToastHold` is a set, never a boolean. Duration is a TOKEN (`TOAST_DWELL_MS`). Rules are pure (`src/toast/toast-state.ts`), the clock injected (`ToastEnv`); a server render gets `INERT_TOAST_ENV` and `ToastRegion` still emits its empty live region.
|
|
43
43
|
- **`announce()` writes into a region `AppShell` already rendered** (`liveRegionAttrs` is the one derivation). For a state change with no surface of its own; a notification is a `Toast` — never both for one message.
|
|
44
44
|
- **`loading` never sets the native `disabled` attribute** (`Button`): `aria-disabled` + a refused click keeps it focusable and readable; `Button.module.scss` restores full opacity under `[aria-busy='true']`. `<Form busy>` refuses the submit again.
|
|
45
|
-
- **A failed submit focuses the first invalid CONTROL, and the summary only when there is none.** `firstInvalidField` answers in DECLARATION order; `fieldSelector` is the allowlist between a caller's string and a selector.
|
|
45
|
+
- **A failed submit focuses the first invalid CONTROL, and the summary only when there is none.** `firstInvalidField` answers in DECLARATION order; `fieldSelector` is the allowlist between a caller's string and a selector. Once per failure: only a CHANGED `(invalidField, error)` moves focus (`Form.test.ts`, real Solid).
|
|
46
46
|
- **`defineTheme()` refuses a palette that fails WCAG 2.2 AA** (`X_UI_CONTRAST_INSUFFICIENT`), measured against `CONTRAST_PAIRS` on the RESOLVED palette, only for pairings the brand changed. AA, never APCA.
|
|
47
|
+
- **A preset is a BASE, never a mode.** `defineTheme({ preset })` merges `THEME_PRESETS[name]` under the app's overrides key by key (an `undefined` override is no override), then runs every existing check on the merged result. A preset states ALL colour roles in both themes, so `presets.test.ts` measures it alone against `CONTRAST_PAIRS`, the chart CVD floor and the scrim rule. A new preset is a row in `presets.ts` plus its file; no preset forces a theme or ships CSS.
|
|
48
|
+
- **Chart series are measured twice**: 3:1 on all four surfaces (`CONTRAST_PAIRS`), and `CHART_DISTINCT_MIN` ΔE apart under protan/deutan/tritan simulation (`colour-vision.ts`, `colour-vision.test.ts`). Moving one channel of one series can break the second check without touching the first.
|
|
49
|
+
- **A `defineTheme()` shadow is a GRAMMAR match** (`theme/shadow-value.ts`), not a pattern of allowed characters: layers of lengths plus `rgb(R G B / a)` or `rgb(var(--color-<known role>) / a)`. The glow and tinted rungs read `--color-accent`, so a brand's accent recolours them for free — `tokens.test.ts` refuses a channel literal there.
|
|
47
50
|
- **Live semantics belong to the container that outlives the message.** `ToastRegion`'s `<ol>` carries `aria-live`; a `Toast` is a plain `<li>`. A region created with its content already inside it is not announced, and a `role="status"` on the `<li>` also strips its `listitem` semantics.
|
|
48
51
|
- **`aria-checked` never mirrors a native `checked`.** ARIA outranks host state in the accessibility tree, and on the no-JS path this package supports there is nothing to rewrite the attribute after the user ticks the box. `Checkbox` writes only `'mixed'` (an IDL property with no attribute form, so ARIA is the only server-side lever); `Switch` writes none at all, over a `biome-ignore` that says why.
|
|
49
52
|
- **A form binds to an action; it never decides for one.** `useForm` (`src/form/`) is a binding over an existing `action`, not a ninth primitive and not a component — `submit` is REQUIRED and is the only producer of a `succeeded` state, and the client-side parse's **value** is discarded so that only its issues are read. A binding that submitted the locally-parsed value would let a browser choose what the server was asked to store; `FormSchema` has no output type for that reason.
|
|
@@ -64,15 +67,20 @@ Tier 4. Imports `@ultimat3/core`, `i18n`, `money`, `time` — **not `schema`**,
|
|
|
64
67
|
| `src/theme/runtime-slot.ts` | the module-scope slot holding the app's Solid runtime — and nothing else, so registering one costs an island 72 B |
|
|
65
68
|
| `src/theme/solid-adapter.ts` | the runtime's SHAPE, and the one rule that decides which runtime a render gets |
|
|
66
69
|
| `src/theme/inert-runtime.ts` | `INERT_SOLID_RUNTIME` — what a server render IS, not a stub of what it lacks |
|
|
67
|
-
| `src/theme/theme.ts` | resolution: stored choice → OS; all side effects via injected `ThemeEnv` |
|
|
70
|
+
| `src/theme/theme.ts` | resolution: stored choice → the theme the boot stamped on `<html>` (`ThemeEnv.current()`) → the app default (`data-theme-default`, `ThemeEnv.appDefault()`) → OS; clearing and the OS listener answer the app default too; all side effects via injected `ThemeEnv` |
|
|
68
71
|
| `src/theme/ambient-slot.ts` | the module-scope slot holding the reader `useUi()` falls back to on the server — imports nothing, so a browser chunk pays nothing for it |
|
|
69
72
|
| `src/theme/ambient.ts` | the server's reader (`ambientUiContext`: request locale, zone, direction, translator), registered at import; `index.ts` imports it bare, and the `browser` field maps it to `ambient.browser.ts`, which answers the defaults and registers nothing |
|
|
70
73
|
| `src/error-registry.ts` | the `registerErrorCodes()` call, alone, so `errors.ts` is pure and the barrel's re-export of it costs a chunk nothing |
|
|
71
|
-
| `src/
|
|
72
|
-
| `src/components/` | 62 components, `PascalCase.tsx` (component convention overrides the repo's kebab-case) |
|
|
74
|
+
| `src/components/` | 67 components, `PascalCase.tsx` (component convention overrides the repo's kebab-case) |
|
|
73
75
|
| `src/icons/glyphs/` | GENERATED: 1767 per-icon modules, `@ultimat3/ui/icons/<name>` → `icon<Name>` |
|
|
74
76
|
| `src/icons/build-icons.ts` | the generator + the pinned `LUCIDE_VERSION`; `LICENSE.lucide` is upstream's ISC text |
|
|
75
|
-
| `src/theme/brand.ts` | `defineTheme()` — the ONE brand-override seam; there is no SCSS `@use ... with ()` path |
|
|
77
|
+
| `src/theme/brand.ts` | `defineTheme()` — the ONE brand-override seam; there is no SCSS `@use ... with ()` path. Merges a preset under the overrides, measures, emits at every level `theme.scss` does |
|
|
78
|
+
| `src/theme/brand-declarations.ts` | each slot's validation + its custom-property lines, in canonical order (colours, shadows, radius, font) |
|
|
79
|
+
| `src/theme/shadow-value.ts` | the shadow-value grammar a brand override must match |
|
|
80
|
+
| `src/theme/presets.ts` · `preset-shape.ts` · `preset-scifi.ts` | `THEME_PRESETS` (`scifi`), the `ThemePreset` shape + `FONT_SLOTS`, and the scifi data |
|
|
81
|
+
| `src/tokens/color-tokens.ts` | `COLOR_ROLES`, `CHART_ROLES`, the channel mirror — split from `tokens.ts` by size, re-exported by it |
|
|
82
|
+
| `src/tokens/colour-vision.ts` | Machado 2009 dichromacy simulation + CIE76 ΔE; `closestChartPair()` answers the pair to move |
|
|
83
|
+
| `src/tokens/_touch.scss` · `_effects.scss` | `--touch-target` + `touch-target` mixin; `glow-edge` and `grid-texture` (CSS only, static, withdrawn under `prefers-contrast: more` / `forced-colors`) |
|
|
76
84
|
| `src/tokens/contrast.ts` | WCAG ratios over the channel tokens; `contrast.test.ts` gates AA in both themes |
|
|
77
85
|
| `src/catalog/` | parses `components/*.tsx` into `CATALOG.md`; `bun run catalog` writes it, `catalog.test.ts` fails on drift |
|
|
78
86
|
| `src/roving.ts` | the pure rules of a keyboard group: navigable set, tab stop, who keeps their own arrows |
|
|
@@ -107,6 +115,7 @@ bun test # from the repo root
|
|
|
107
115
|
bun run --filter @ultimat3/ui typecheck
|
|
108
116
|
bun run --filter @ultimat3/ui catalog # after any prop change — CATALOG.md is gated
|
|
109
117
|
bun run --filter @ultimat3/ui icons # regenerate the glyph set (network; dev-only)
|
|
118
|
+
bun run --filter @ultimat3/ui icons --bump # move LUCIDE_VERSION to the latest lucide-static, then regenerate
|
|
110
119
|
```
|
|
111
120
|
|
|
112
121
|
## Deep import
|