@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.
Files changed (100) hide show
  1. package/CATALOG.md +119 -15
  2. package/CLAUDE.md +17 -8
  3. package/README.md +119 -33
  4. package/package.json +6 -6
  5. package/src/a11y.ts +27 -13
  6. package/src/catalog/build-catalog.ts +2 -2
  7. package/src/components/Accordion.tsx +8 -2
  8. package/src/components/AppShell.module.scss +105 -4
  9. package/src/components/AppShell.tsx +48 -10
  10. package/src/components/BarChart.module.scss +47 -9
  11. package/src/components/BarChart.tsx +152 -72
  12. package/src/components/Card.tsx +52 -16
  13. package/src/components/ChartFrame.module.scss +188 -0
  14. package/src/components/ChartFrame.tsx +155 -0
  15. package/src/components/CommandPalette.module.scss +8 -5
  16. package/src/components/Container.tsx +46 -9
  17. package/src/components/DataTable.module.scss +190 -0
  18. package/src/components/DataTable.tsx +161 -51
  19. package/src/components/Divider.tsx +0 -2
  20. package/src/components/DonutChart.module.scss +73 -0
  21. package/src/components/DonutChart.tsx +154 -0
  22. package/src/components/Dropzone.module.scss +3 -1
  23. package/src/components/Dropzone.tsx +17 -8
  24. package/src/components/ErrorState.tsx +39 -12
  25. package/src/components/FileInput.module.scss +3 -1
  26. package/src/components/FileInput.tsx +6 -2
  27. package/src/components/Form.tsx +17 -4
  28. package/src/components/Gauge.module.scss +48 -0
  29. package/src/components/Gauge.tsx +67 -0
  30. package/src/components/Grid.tsx +34 -13
  31. package/src/components/Image.tsx +2 -2
  32. package/src/components/InlineBar.module.scss +23 -0
  33. package/src/components/InlineBar.tsx +31 -0
  34. package/src/components/LineChart.module.scss +147 -0
  35. package/src/components/LineChart.tsx +169 -0
  36. package/src/components/Menu.tsx +9 -5
  37. package/src/components/Meter.tsx +3 -3
  38. package/src/components/PageHeader.tsx +5 -2
  39. package/src/components/Popover.module.scss +17 -20
  40. package/src/components/Popover.tsx +14 -7
  41. package/src/components/Section.tsx +45 -26
  42. package/src/components/Stack.tsx +59 -18
  43. package/src/components/Table.module.scss +30 -0
  44. package/src/components/Table.tsx +14 -3
  45. package/src/components/Tabs.module.scss +6 -0
  46. package/src/components/Tabs.tsx +2 -2
  47. package/src/components/Text.tsx +41 -15
  48. package/src/components/Textarea.tsx +7 -4
  49. package/src/components/Toaster.tsx +1 -1
  50. package/src/components/Toolbar.tsx +7 -6
  51. package/src/components/Tooltip.module.scss +8 -3
  52. package/src/components/app-shell-view.ts +6 -4
  53. package/src/components/bar-chart-view.ts +11 -6
  54. package/src/components/chart-frame-view.ts +219 -0
  55. package/src/components/chart-ticks-view.ts +173 -0
  56. package/src/components/data-table-view.ts +68 -0
  57. package/src/components/donut-chart-view.ts +120 -0
  58. package/src/components/file-input-view.ts +32 -13
  59. package/src/components/gauge-view.ts +43 -0
  60. package/src/components/heading-node.tsx +62 -0
  61. package/src/components/image-source.ts +3 -3
  62. package/src/components/line-chart-view.ts +153 -0
  63. package/src/components/link-target.ts +21 -1
  64. package/src/components/meter-view.ts +13 -0
  65. package/src/components/money-view.ts +2 -7
  66. package/src/components/qr-matrix.ts +16 -23
  67. package/src/components/relative-time-view.ts +11 -4
  68. package/src/form/form-binding.ts +32 -14
  69. package/src/form/use-form.ts +3 -3
  70. package/src/global.scss +9 -0
  71. package/src/icons/build-icons.ts +109 -24
  72. package/src/index.ts +47 -15
  73. package/src/jsx-probe.ts +5 -4
  74. package/src/theme/brand-declarations.ts +113 -0
  75. package/src/theme/brand.ts +104 -96
  76. package/src/theme/context.ts +1 -1
  77. package/src/theme/preset-scifi.ts +99 -0
  78. package/src/theme/preset-shape.ts +18 -0
  79. package/src/theme/presets.ts +15 -0
  80. package/src/theme/shadow-value.ts +39 -0
  81. package/src/theme/theme.ts +36 -11
  82. package/src/toast/toast-state.ts +0 -0
  83. package/src/toast/toast-store.ts +1 -1
  84. package/src/toast/use-toasts.ts +1 -1
  85. package/src/tokens/_colors.scss +20 -0
  86. package/src/tokens/_effects.scss +59 -0
  87. package/src/tokens/_index.scss +2 -0
  88. package/src/tokens/_mixins.scss +6 -4
  89. package/src/tokens/_shadow.scss +16 -0
  90. package/src/tokens/_touch.scss +39 -0
  91. package/src/tokens/_typography.scss +14 -0
  92. package/src/tokens/color-tokens.ts +133 -0
  93. package/src/tokens/colour-vision.ts +106 -0
  94. package/src/tokens/contrast-pairs.ts +6 -1
  95. package/src/tokens/contrast.ts +1 -1
  96. package/src/tokens/theme.scss +2 -0
  97. package/src/tokens/tokens.ts +41 -87
  98. package/src/fake-dom.ts +0 -206
  99. package/src/sass-probe.ts +0 -55
  100. package/src/theme/inline-script.ts +0 -49
@@ -11,9 +11,19 @@ import { invalidIconDataError, runtimeMissingError } from '../errors';
11
11
  /** Pinned: a floating version would silently redraw icons under an app that never asked. */
12
12
  export const LUCIDE_VERSION = '1.31.0';
13
13
 
14
- export const LUCIDE_ICON_NODES_URL = `https://cdn.jsdelivr.net/npm/lucide-static@${LUCIDE_VERSION}/icon-nodes.json`;
14
+ /** Where one pinned version's node table lives. */
15
+ export const iconNodesUrl = (version: string): string =>
16
+ `https://cdn.jsdelivr.net/npm/lucide-static@${version}/icon-nodes.json`;
15
17
 
16
- export const GLYPHS_DIR = new URL('./glyphs/', import.meta.url).pathname;
18
+ export const LUCIDE_ICON_NODES_URL = iconNodesUrl(LUCIDE_VERSION);
19
+
20
+ /** The registry's answer for the newest published `lucide-static` — what `--bump` pins to. */
21
+ export const LUCIDE_LATEST_URL = 'https://registry.npmjs.org/lucide-static/latest';
22
+
23
+ /** The tail of every cause about data the PIN published: why re-running alone cannot help. */
24
+ const PIN_REPEATS = `lucide-static@${LUCIDE_VERSION} is the pin that published it, so a re-run against the same pin repeats this; \`--bump\` raises LUCIDE_VERSION in packages/ui/src/icons/build-icons.ts to the latest release and regenerates`;
25
+
26
+ export const GLYPHS_DIR = Bun.fileURLToPath(new URL('./glyphs/', import.meta.url));
17
27
 
18
28
  /** `circle-alert` → `iconCircleAlert`. Prefixed because `delete`, `import` and `package` are icons
19
29
  * and reserved words — one uniform rule beats three exceptions an agent has to remember. */
@@ -27,7 +37,7 @@ export function identifierFor(name: string): string {
27
37
  * the network, so its shape is an assumption until it is checked.
28
38
  */
29
39
  export function parseIconNodes(text: string): ReadonlyMap<string, IconGlyph> {
30
- const parsed: unknown = JSON.parse(text);
40
+ const parsed = parseJson(text);
31
41
  // `typeof [] === 'object'`, and an array of icons is a different upstream format, not this one.
32
42
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
33
43
  throw invalidIconDataError(
@@ -42,8 +52,8 @@ export function parseIconNodes(text: string): ReadonlyMap<string, IconGlyph> {
42
52
  // writes, so `../../index` is a delete plus an overwrite of a hand-written module.
43
53
  if (!SAFE_ICON_NAME.test(name)) {
44
54
  throw invalidIconDataError(
45
- `carries ${renderCauseValue(name)} as an icon name, which is not the kebab-case shape every Lucide icon uses; the name becomes a file path and an exported identifier, so it cannot be escaped. lucide-static@${LUCIDE_VERSION} is the pin that published it, so a re-run against the same pin repeats this`,
46
- 'bun run --filter @ultimat3/ui icons # after raising LUCIDE_VERSION in packages/ui/src/icons/build-icons.ts',
55
+ `carries ${renderCauseValue(name)} as an icon name, which is not the kebab-case shape every Lucide icon uses; the name becomes a file path and an exported identifier, so it cannot be escaped. ${PIN_REPEATS}`,
56
+ 'bun run --filter @ultimat3/ui icons --bump',
47
57
  );
48
58
  }
49
59
  out.set(name, toGlyph(name, value));
@@ -51,6 +61,29 @@ export function parseIconNodes(text: string): ReadonlyMap<string, IconGlyph> {
51
61
  return out;
52
62
  }
53
63
 
64
+ /** The body as JSON, or the same coded refusal a wrong shape gets — never a bare `SyntaxError`. */
65
+ function parseJson(text: string): unknown {
66
+ try {
67
+ return JSON.parse(text);
68
+ } catch {
69
+ throw invalidIconDataError(
70
+ `is not JSON (${renderCauseValue(text.slice(0, 80))}…); ${LUCIDE_ICON_NODES_URL} served an error page or a truncated body`,
71
+ 'bun run --filter @ultimat3/ui icons',
72
+ );
73
+ }
74
+ }
75
+
76
+ /** A node that is not `[tag, attrs?]` with an object for attrs — refused, never skipped. */
77
+ function malformedNode(name: string, node: unknown): ReturnType<typeof invalidIconDataError> {
78
+ return invalidIconDataError(
79
+ `for the icon "${name}" carries ${renderCauseValue(node)} where a [tag, attributes] node belongs; skipping it would write a partial glyph. ${PIN_REPEATS}`,
80
+ 'bun run --filter @ultimat3/ui icons --bump',
81
+ );
82
+ }
83
+
84
+ const isAttrs = (value: unknown): value is Record<string, unknown> =>
85
+ typeof value === 'object' && value !== null && !Array.isArray(value);
86
+
54
87
  /**
55
88
  * An icon name, as characters: lowercase kebab-case, no leading, trailing or doubled hyphen. The
56
89
  * same allowlist-over-a-sink shape as `SAFE_ATTR_VALUE`, one layer up — the name is interpolated
@@ -68,10 +101,13 @@ export const SAFE_ATTR_VALUE = /^[\w\s.,+-]*$/;
68
101
 
69
102
  function toGlyph(name: string, value: unknown): IconGlyph {
70
103
  const glyph: (readonly [string, Readonly<Record<string, string>>])[] = [];
71
- for (const node of Array.isArray(value) ? (value as unknown[]) : []) {
72
- if (!Array.isArray(node) || typeof node[0] !== 'string') continue;
104
+ if (!Array.isArray(value)) throw malformedNode(name, value);
105
+ for (const node of value as unknown[]) {
106
+ if (!Array.isArray(node) || typeof node[0] !== 'string') throw malformedNode(name, node);
107
+ const rawAttrs: unknown = node[1] ?? {};
108
+ if (!isAttrs(rawAttrs)) throw malformedNode(name, node);
73
109
  const attrs: Record<string, string> = {};
74
- for (const [key, item] of Object.entries((node[1] ?? {}) as Record<string, unknown>)) {
110
+ for (const [key, item] of Object.entries(rawAttrs)) {
75
111
  // Upstream keys an element for its diff tooling; it is not an SVG attribute.
76
112
  if (key === 'key') continue;
77
113
  const text = String(item);
@@ -82,8 +118,8 @@ function toGlyph(name: string, value: unknown): IconGlyph {
82
118
  // no legitimate Lucide artwork.
83
119
  if (!SAFE_ATTR_VALUE.test(text)) {
84
120
  throw invalidIconDataError(
85
- `for the icon "${name}" carries ${renderCauseValue(text)} as "${key}", which is not glyph geometry; lucide-static@${LUCIDE_VERSION} is the pin that published it, so a re-run against the same pin repeats this`,
86
- 'bun run --filter @ultimat3/ui icons # after raising LUCIDE_VERSION in packages/ui/src/icons/build-icons.ts',
121
+ `for the icon "${name}" carries ${renderCauseValue(text)} as "${key}", which is not glyph geometry; ${PIN_REPEATS}`,
122
+ 'bun run --filter @ultimat3/ui icons --bump',
87
123
  );
88
124
  }
89
125
  attrs[key] = text;
@@ -95,8 +131,8 @@ function toGlyph(name: string, value: unknown): IconGlyph {
95
131
  iconElements(glyph);
96
132
  if (glyph.length === 0) {
97
133
  throw invalidIconDataError(
98
- `for the icon "${name}" carries no renderable node data; lucide-static@${LUCIDE_VERSION} is the pin that published it, so a re-run against the same pin repeats this`,
99
- 'bun run --filter @ultimat3/ui icons # after raising LUCIDE_VERSION in packages/ui/src/icons/build-icons.ts',
134
+ `for the icon "${name}" carries no renderable node data; ${PIN_REPEATS}`,
135
+ 'bun run --filter @ultimat3/ui icons --bump',
100
136
  );
101
137
  }
102
138
  return glyph;
@@ -131,28 +167,38 @@ export function moduleSource(name: string, glyph: IconGlyph): string {
131
167
  ].join('\n');
132
168
  }
133
169
 
170
+ /** The repo's own `node_modules/.bin`, where `bun install` links every tool binary. */
171
+ const REPO_BIN = Bun.fileURLToPath(new URL('../../../../node_modules/.bin', import.meta.url));
172
+
173
+ /**
174
+ * The biome executable in `binDir`, or `undefined`. Asked of `Bun.which` rather than spelled as
175
+ * `<bin>/biome`: on Windows the link is `biome.exe`, and a path with no extension is ENOENT there.
176
+ */
177
+ export function biomeBinary(binDir: string = REPO_BIN): string | undefined {
178
+ return Bun.which('biome', { PATH: binDir }) ?? undefined;
179
+ }
180
+
134
181
  /** Biome owns formatting in this repo, generated files included — so the generator asks it rather
135
182
  * than imitating it, and a regenerated set never shows up as a lint diff. */
136
183
  async function format(): Promise<void> {
137
- const biome = new URL('../../../../node_modules/.bin/biome', import.meta.url).pathname;
138
- if (!(await Bun.file(biome).exists())) {
139
- throw runtimeMissingError('the biome binary', 'bun install, then re-run this generator');
140
- }
141
- const result = Bun.spawnSync([biome, 'format', '--write', GLYPHS_DIR]);
184
+ const biome = biomeBinary();
185
+ if (biome === undefined) throw runtimeMissingError('the biome binary', 'bun install');
186
+ // A formatter over one directory of generated files; a wedged one is the refusal below.
187
+ const result = Bun.spawnSync([biome, 'format', '--write', GLYPHS_DIR], { timeout: 60_000 });
142
188
  if (result.exitCode !== 0) {
143
189
  throw runtimeMissingError(
144
190
  'a successful `biome format` over the generated glyphs',
145
- `run: ${biome} format --write ${GLYPHS_DIR}`,
191
+ 'bunx biome format --write packages/ui/src/icons/glyphs',
146
192
  );
147
193
  }
148
194
  }
149
195
 
150
- export async function buildIcons(): Promise<number> {
151
- const response = await fetch(LUCIDE_ICON_NODES_URL);
196
+ export async function buildIcons(version: string = LUCIDE_VERSION): Promise<number> {
197
+ const response = await fetch(iconNodesUrl(version));
152
198
  if (!response.ok) {
153
199
  throw runtimeMissingError(
154
- `lucide-static@${LUCIDE_VERSION} icon data (HTTP ${response.status})`,
155
- `check network access to ${LUCIDE_ICON_NODES_URL}, then re-run: bun run --filter @ultimat3/ui icons`,
200
+ `lucide-static@${version} icon data (HTTP ${response.status} from ${iconNodesUrl(version)})`,
201
+ 'bun run --filter @ultimat3/ui icons',
156
202
  );
157
203
  }
158
204
  const glyphs = parseIconNodes(await response.text());
@@ -165,8 +211,47 @@ export async function buildIcons(): Promise<number> {
165
211
  return glyphs.size;
166
212
  }
167
213
 
214
+ /** A plain `major.minor.patch` — the only shape allowed into a TypeScript string literal here. */
215
+ const SEMVER = /^\d+\.\d+\.\d+$/;
216
+
217
+ /** `source` with its `LUCIDE_VERSION` line moved to `version`. Pure; refuses rather than guesses. */
218
+ export function withPin(source: string, version: string): string {
219
+ if (!SEMVER.test(version)) {
220
+ throw invalidIconDataError(
221
+ `answered ${renderCauseValue(version)} as the latest lucide-static version, which is not plain semver; ${LUCIDE_LATEST_URL} served something else`,
222
+ 'bun run --filter @ultimat3/ui icons --bump',
223
+ );
224
+ }
225
+ const pin = /^export const LUCIDE_VERSION = '[^']*';$/m;
226
+ if (!pin.test(source)) {
227
+ throw invalidIconDataError(
228
+ 'has no `export const LUCIDE_VERSION = …` line in packages/ui/src/icons/build-icons.ts to move',
229
+ 'git checkout -- packages/ui/src/icons/build-icons.ts',
230
+ );
231
+ }
232
+ return source.replace(pin, `export const LUCIDE_VERSION = '${version}';`);
233
+ }
234
+
235
+ /** Moves the pin to the registry's latest release, in this very file, and answers the version. */
236
+ async function bumpPin(): Promise<string> {
237
+ const response = await fetch(LUCIDE_LATEST_URL);
238
+ if (!response.ok) {
239
+ throw runtimeMissingError(
240
+ `the latest lucide-static version (HTTP ${response.status} from ${LUCIDE_LATEST_URL})`,
241
+ 'bun run --filter @ultimat3/ui icons --bump',
242
+ );
243
+ }
244
+ const body: unknown = await response.json();
245
+ const version =
246
+ typeof body === 'object' && body !== null && 'version' in body ? String(body.version) : '';
247
+ const file = import.meta.path;
248
+ await Bun.write(file, withPin(await Bun.file(file).text(), version));
249
+ return version;
250
+ }
251
+
168
252
  if (import.meta.main) {
169
- const count = await buildIcons();
170
- const json = { ok: true, icons: count, lucide: LUCIDE_VERSION, dir: GLYPHS_DIR };
253
+ const version = Bun.argv.includes('--bump') ? await bumpPin() : LUCIDE_VERSION;
254
+ const count = await buildIcons(version);
255
+ const json = { ok: true, icons: count, lucide: version, dir: GLYPHS_DIR };
171
256
  console.log(Bun.argv.includes('--json') ? JSON.stringify(json) : `${count} icon modules written`);
172
257
  }
package/src/index.ts CHANGED
@@ -22,13 +22,13 @@ export type {
22
22
  export {
23
23
  announce,
24
24
  ariaBool,
25
- createFocusTrap,
26
- createRovingTabindex,
27
25
  FOCUSABLE_SELECTOR,
28
26
  focusableWithin,
27
+ focusTrap,
29
28
  LIVE_REGION_LEVELS,
30
29
  liveRegionAttrs,
31
- resetIdCounter,
30
+ resetUseIdCounter,
31
+ rovingTabindex,
32
32
  useId,
33
33
  } from './a11y';
34
34
  export type { AccordionItem, AccordionProps } from './components/Accordion';
@@ -70,6 +70,8 @@ export type { BarRect, ChartPoint } from './components/bar-chart-view';
70
70
  export { barRects } from './components/bar-chart-view';
71
71
  export type { CardProps, Elevation } from './components/Card';
72
72
  export { Card } from './components/Card';
73
+ export type { ChartFrameProps, ChartLegendItem } from './components/ChartFrame';
74
+ export { ChartFrame } from './components/ChartFrame';
73
75
  export type { CheckboxProps } from './components/Checkbox';
74
76
  export { Checkbox } from './components/Checkbox';
75
77
  export type { ComboboxProps } from './components/Combobox';
@@ -80,6 +82,17 @@ export type { ContainerProps, ContainerSize } from './components/Container';
80
82
  export { Container } from './components/Container';
81
83
  export type { CopyButtonProps } from './components/CopyButton';
82
84
  export { COPIED_MS, CopyButton } from './components/CopyButton';
85
+ export type {
86
+ ChartSeries,
87
+ ChartTable,
88
+ ChartTableInput,
89
+ ChartTableRow,
90
+ MarkerShape,
91
+ SeriesStyle,
92
+ } from './components/chart-frame-view';
93
+ export { CHART_SERIES_SLOTS, chartTable, seriesStyle } from './components/chart-frame-view';
94
+ export type { NiceScale, NiceTicksOptions } from './components/chart-ticks-view';
95
+ export { niceTicks } from './components/chart-ticks-view';
83
96
  export type { ComboboxOption } from './components/combobox-filter';
84
97
  export { filterOptions, normalizeQuery } from './components/combobox-filter';
85
98
  export type { CommandPaletteItem, PaletteKeyAction } from './components/command-palette-view';
@@ -93,6 +106,7 @@ export type {
93
106
  Column,
94
107
  DataTableCallbackProps,
95
108
  DataTableLinkProps,
109
+ DataTableNarrow,
96
110
  DataTableProps,
97
111
  } from './components/DataTable';
98
112
  export { DataTable } from './components/DataTable';
@@ -102,10 +116,13 @@ export type { DialogProps } from './components/Dialog';
102
116
  export { Dialog } from './components/Dialog';
103
117
  export type { DividerProps } from './components/Divider';
104
118
  export { Divider } from './components/Divider';
119
+ export type { DonutChartProps, DonutSegment } from './components/DonutChart';
120
+ export { DonutChart } from './components/DonutChart';
105
121
  export type { DrawerProps, DrawerSide } from './components/Drawer';
106
122
  export { Drawer } from './components/Drawer';
107
123
  export type { DropzoneProps } from './components/Dropzone';
108
124
  export { Dropzone } from './components/Dropzone';
125
+ export type { ColumnPriority } from './components/data-table-view';
109
126
  export type {
110
127
  DateStyle,
111
128
  DateTimeFormatter,
@@ -133,9 +150,11 @@ export type {
133
150
  FileTarget,
134
151
  } from './components/file-input-view';
135
152
  export {
136
- formatBytes,
153
+ formatFileSize,
137
154
  selectFiles,
138
155
  } from './components/file-input-view';
156
+ export type { GaugeProps } from './components/Gauge';
157
+ export { Gauge } from './components/Gauge';
139
158
  export type { GridProps } from './components/Grid';
140
159
  export { Grid } from './components/Grid';
141
160
  export type { HeadingLevel, HeadingTag } from './components/heading-level';
@@ -149,6 +168,8 @@ export type { ImageBaseProps, ImageDimensions, ImageProps } from './components/I
149
168
  export { Image } from './components/Image';
150
169
  export type { InfiniteScrollProps } from './components/InfiniteScroll';
151
170
  export { InfiniteScroll } from './components/InfiniteScroll';
171
+ export type { InlineBarProps } from './components/InlineBar';
172
+ export { InlineBar } from './components/InlineBar';
152
173
  export type { InputProps, InputType } from './components/Input';
153
174
  export { Input } from './components/Input';
154
175
  export type { IconElement, IconGlyph, IconTag } from './components/icon-glyph';
@@ -161,10 +182,12 @@ export type {
161
182
  ImageSources,
162
183
  ImageVariant,
163
184
  } from './components/image-source';
164
- export { boxFor, reservedRatio, sourceSetsFor, srcsetFor } from './components/image-source';
185
+ export { boxFor, reservedRatio, sourceSetsFor, variantSrcset } from './components/image-source';
165
186
  export type { LoadMoreInput, LoadMoreState } from './components/infinite-scroll-view';
166
187
  export type { KbdProps } from './components/Kbd';
167
188
  export { Kbd } from './components/Kbd';
189
+ export type { AreaChartProps, LineChartProps } from './components/LineChart';
190
+ export { AreaChart, LineChart } from './components/LineChart';
168
191
  export type { ButtonLinkProps, LinkProps, TextLinkProps } from './components/Link';
169
192
  export { Link } from './components/Link';
170
193
  export type { LocaleSwitcherProps } from './components/LocaleSwitcher';
@@ -265,7 +288,7 @@ export {
265
288
  export type { FieldPathSegment, IssuePathSegment } from './form/field-path';
266
289
  export { fieldSelector, formatFieldPath, MAX_FIELD_INDEX, parseFieldPath } from './form/field-path';
267
290
  export type { FormBinding, FormBindingOptions } from './form/form-binding';
268
- export { createFormBinding } from './form/form-binding';
291
+ export { formBinding } from './form/form-binding';
269
292
  export type {
270
293
  FormIssue,
271
294
  FormSchema,
@@ -312,12 +335,9 @@ export {
312
335
  useUi,
313
336
  } from './theme/context';
314
337
  export { INERT_SOLID_RUNTIME } from './theme/inert-runtime';
315
- export {
316
- THEME_INLINE_SCRIPT,
317
- themeInlineScriptCspSource,
318
- themeInlineScriptHash,
319
- themeInlineScriptTag,
320
- } from './theme/inline-script';
338
+ export type { ThemePreset } from './theme/preset-shape';
339
+ export type { ThemePresetName } from './theme/presets';
340
+ export { THEME_PRESET_NAMES, THEME_PRESETS } from './theme/presets';
321
341
  export type { UiProviderProps } from './theme/provider';
322
342
  export { UiProvider } from './theme/provider';
323
343
  // The slot is its own module so that registering a runtime does not drag `errors.ts` — and with it
@@ -330,6 +350,7 @@ export type { ThemeEnv } from './theme/theme';
330
350
  export {
331
351
  browserThemeEnv,
332
352
  clearTheme,
353
+ defaultTheme,
333
354
  initTheme,
334
355
  isTheme,
335
356
  osTheme,
@@ -337,8 +358,8 @@ export {
337
358
  setTheme,
338
359
  storedTheme,
339
360
  THEME_ATTRIBUTE,
361
+ THEME_DEFAULT_ATTRIBUTE,
340
362
  THEME_MEDIA_QUERY,
341
- THEME_STORAGE_KEY,
342
363
  toggleTheme,
343
364
  watchOsTheme,
344
365
  } from './theme/theme';
@@ -356,8 +377,16 @@ export {
356
377
  TOAST_MAX_VISIBLE,
357
378
  } from './toast/toast-state';
358
379
  export type { ToastEnv, ToastStore } from './toast/toast-store';
359
- export { browserToastEnv, createToastStore, INERT_TOAST_ENV } from './toast/toast-store';
380
+ export { browserToastEnv, INERT_TOAST_ENV, toastStore } from './toast/toast-store';
360
381
  export { useToasts } from './toast/use-toasts';
382
+ export type { ColourVision } from './tokens/colour-vision';
383
+ export {
384
+ CHART_DISTINCT_MIN,
385
+ COLOUR_VISIONS,
386
+ closestChartPair,
387
+ colourDistance,
388
+ simulateColourVision,
389
+ } from './tokens/colour-vision';
361
390
  export type { Channels } from './tokens/contrast';
362
391
  export {
363
392
  AA_LARGE,
@@ -369,11 +398,12 @@ export {
369
398
  } from './tokens/contrast';
370
399
  export type { ContrastPair } from './tokens/contrast-pairs';
371
400
  export { CONTRAST_PAIRS } from './tokens/contrast-pairs';
372
- export type { ColorRole, RadiusName, Theme } from './tokens/tokens';
401
+ export type { ChartRole, ColorRole, RadiusName, ShadowName, Theme } from './tokens/tokens';
373
402
  // --- tokens ------------------------------------------------------------------
374
403
  export {
375
404
  assertColorRole,
376
405
  breakpointTokens,
406
+ CHART_ROLES,
377
407
  COLOR_ROLES,
378
408
  color,
379
409
  colorRgb,
@@ -385,8 +415,10 @@ export {
385
415
  fontWeightTokens,
386
416
  lineHeightTokens,
387
417
  radiusTokens,
418
+ SHADOW_NAMES,
388
419
  shadowTokens,
389
420
  spaceTokens,
390
421
  strokeTokens,
422
+ touchTokens,
391
423
  zTokens,
392
424
  } from './tokens/tokens';
package/src/jsx-probe.ts CHANGED
@@ -38,10 +38,11 @@ function h(
38
38
  *
39
39
  * There may be exactly ONE such counter in the framework, and it is this one — `globalThis.React`
40
40
  * is a single property, so a second module counting its own depth over it restores in the wrong
41
- * order. `@ultimat3/admin`'s `inert-jsx.ts` kept a second pair and the two interleaved: admin
42
- * installs (saving the real binding), ui installs (saving ADMIN's factory), admin restores (both
43
- * counters at 1 → the real binding is back), ui restores (its counter hits 0 → admin's factory is
44
- * reinstalled over it). The global ended up holding a harness the run had already torn down.
41
+ * order. `@ultimat3/admin`'s `inert-jsx-fixture.ts` kept a second pair and the two interleaved:
42
+ * admin installs (saving the real binding), ui installs (saving ADMIN's factory), admin restores
43
+ * (both counters at 1 → the real binding is back), ui restores (its counter hits 0 → admin's
44
+ * factory is reinstalled over it). The global ended up holding a harness the run had already torn
45
+ * down.
45
46
  * `admin` is tier 5 and `ui` is tier 4, so admin imports these rather than declaring them.
46
47
  */
47
48
  let depth = 0;
@@ -0,0 +1,113 @@
1
+ // The validators behind `defineTheme()`: each slot's override checked against its canonical list
2
+ // and value grammar, then rendered as custom-property lines in that list's order — never the
3
+ // caller's — so a brand rendered twice is byte-identical and its CSP hash does not churn.
4
+
5
+ import { invalidBrandTokenError, unknownTokenError } from '../errors';
6
+ import { parseChannels } from '../tokens/contrast';
7
+ import {
8
+ COLOR_ROLES,
9
+ type ColorRole,
10
+ type RadiusName,
11
+ radiusTokens,
12
+ SHADOW_NAMES,
13
+ type ShadowName,
14
+ } from '../tokens/tokens';
15
+ import { FONT_SLOTS, type FontSlot } from './preset-shape';
16
+ import { isShadowValue, SHADOW_EXPECTED } from './shadow-value';
17
+
18
+ /** `0` or a number with a CSS length unit. No `calc()`, no `var()` — a scale rung is a value. */
19
+ const LENGTH_PATTERN = /^(0|\d+(\.\d+)?(px|rem|em|ch|%))$/;
20
+
21
+ /**
22
+ * Family names, quotes and separators only: everything a `font-family` list legitimately needs.
23
+ * The separator is a literal SPACE, never `\s`: the HTML parser rewrites a CR or CRLF inside a
24
+ * `<style>` to LF before the browser hashes it, so a stack carrying one served bytes whose sha256
25
+ * no longer matched the CSP hash computed here, and the brand's whole stylesheet was blocked.
26
+ */
27
+ const FONT_STACK_PATTERN = /^[\w ,'"-]{1,200}$/;
28
+
29
+ const LENGTH_EXPECTED = 'a bare CSS length such as "0.5rem", "4px" or "0"';
30
+ const STACK_EXPECTED = 'a font-family list such as "Inter, system-ui, sans-serif"';
31
+ const CHANNELS_EXPECTED = 'space-separated RGB channels such as "31 110 178"';
32
+
33
+ function assertKnown(
34
+ kind: string,
35
+ overrides: object,
36
+ known: readonly string[],
37
+ source?: string,
38
+ ): void {
39
+ for (const name of Object.keys(overrides)) {
40
+ if (!known.includes(name)) throw unknownTokenError(kind, name, known, source);
41
+ }
42
+ }
43
+
44
+ export function colorDeclarations(
45
+ overrides: Partial<Record<ColorRole, string>> | undefined,
46
+ scope: string,
47
+ ): string[] {
48
+ if (overrides === undefined) return [];
49
+ assertKnown('color', overrides, COLOR_ROLES);
50
+ const out: string[] = [];
51
+ for (const role of COLOR_ROLES) {
52
+ const value = overrides[role];
53
+ if (value === undefined) continue;
54
+ try {
55
+ parseChannels(value);
56
+ } catch {
57
+ throw invalidBrandTokenError(scope, role, value, CHANNELS_EXPECTED);
58
+ }
59
+ out.push(`--color-${role}: ${value};`);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ export function shadowDeclarations(
65
+ overrides: Partial<Record<ShadowName, string>> | undefined,
66
+ scope: string,
67
+ ): string[] {
68
+ if (overrides === undefined) return [];
69
+ assertKnown('shadow', overrides, SHADOW_NAMES, '_shadow.scss');
70
+ const out: string[] = [];
71
+ for (const name of SHADOW_NAMES) {
72
+ const value = overrides[name];
73
+ if (value === undefined) continue;
74
+ if (!isShadowValue(value)) throw invalidBrandTokenError(scope, name, value, SHADOW_EXPECTED);
75
+ out.push(`--shadow-${name}: ${value};`);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ export function radiusDeclarations(
81
+ overrides: Partial<Record<RadiusName, string>> | undefined,
82
+ ): string[] {
83
+ if (overrides === undefined) return [];
84
+ const known = Object.keys(radiusTokens) as RadiusName[];
85
+ assertKnown('radius', overrides, known, '_radius.scss');
86
+ const out: string[] = [];
87
+ for (const name of known) {
88
+ const value = overrides[name];
89
+ if (value === undefined) continue;
90
+ if (!LENGTH_PATTERN.test(value)) {
91
+ throw invalidBrandTokenError('radius', name, value, LENGTH_EXPECTED);
92
+ }
93
+ out.push(`--radius-${name}: ${value};`);
94
+ }
95
+ return out;
96
+ }
97
+
98
+ export function fontDeclarations(
99
+ overrides: Partial<Record<FontSlot, string>> | undefined,
100
+ ): string[] {
101
+ if (overrides === undefined) return [];
102
+ assertKnown('font', overrides, FONT_SLOTS, '_typography.scss');
103
+ const out: string[] = [];
104
+ for (const slot of FONT_SLOTS) {
105
+ const value = overrides[slot];
106
+ if (value === undefined) continue;
107
+ if (!FONT_STACK_PATTERN.test(value)) {
108
+ throw invalidBrandTokenError('font', slot, value, STACK_EXPECTED);
109
+ }
110
+ out.push(`--font-${slot}: ${value};`);
111
+ }
112
+ return out;
113
+ }