@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/src/icons/build-icons.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|
|
46
|
-
'bun run --filter @ultimat3/ui icons
|
|
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
|
-
|
|
72
|
-
|
|
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(
|
|
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;
|
|
86
|
-
'bun run --filter @ultimat3/ui icons
|
|
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;
|
|
99
|
-
'bun run --filter @ultimat3/ui icons
|
|
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 =
|
|
138
|
-
if (
|
|
139
|
-
|
|
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
|
-
|
|
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(
|
|
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@${
|
|
155
|
-
|
|
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
|
|
170
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
317
|
-
|
|
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,
|
|
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:
|
|
42
|
-
* installs (saving the real binding), ui installs (saving ADMIN's factory), admin restores
|
|
43
|
-
* counters at 1 → the real binding is back), ui restores (its counter hits 0 → admin's
|
|
44
|
-
* reinstalled over it). The global ended up holding a harness the run had already torn
|
|
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
|
+
}
|