@ultimat3/ui 24.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 +117 -13
- package/CLAUDE.md +14 -5
- package/README.md +109 -24
- package/package.json +6 -6
- package/src/a11y.ts +3 -3
- 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/Container.tsx +46 -9
- package/src/components/DataTable.module.scss +190 -0
- package/src/components/DataTable.tsx +124 -17
- 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.tsx +9 -1
- package/src/components/ErrorState.tsx +39 -12
- 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/PageHeader.tsx +5 -2
- package/src/components/Popover.tsx +2 -2
- 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.tsx +2 -2
- package/src/components/Text.tsx +41 -15
- package/src/components/Toaster.tsx +1 -1
- package/src/components/Toolbar.tsx +7 -6
- package/src/components/app-shell-view.ts +6 -4
- package/src/components/bar-chart-view.ts +5 -4
- 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 +1 -1
- 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/money-view.ts +2 -7
- package/src/form/form-binding.ts +23 -6
- package/src/form/use-form.ts +3 -3
- package/src/global.scss +9 -0
- package/src/icons/build-icons.ts +15 -6
- package/src/index.ts +45 -9
- package/src/jsx-probe.ts +5 -4
- package/src/theme/brand-declarations.ts +113 -0
- package/src/theme/brand.ts +98 -109
- 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 +1 -1
- 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/components/qr-reader.ts +0 -187
- package/src/fake-dom.ts +0 -206
- package/src/sass-probe.ts +0 -55
package/src/form/form-binding.ts
CHANGED
|
@@ -99,13 +99,19 @@ function declaredFields(fields: readonly string[]): ReadonlySet<string> {
|
|
|
99
99
|
return declared;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export function
|
|
102
|
+
export function formBinding<TValues, TResult>(
|
|
103
103
|
options: FormBindingOptions<TValues, TResult>,
|
|
104
104
|
): FormBinding<TValues, TResult> {
|
|
105
105
|
const fields = declaredFields(options.fields);
|
|
106
106
|
let state: FormState<TResult> = IDLE_FORM_STATE;
|
|
107
107
|
let touch: FormTouch = NO_FORM_TOUCH;
|
|
108
108
|
let inFlight: Promise<FormState<TResult>> | null = null;
|
|
109
|
+
/**
|
|
110
|
+
* The paths changed since the in-flight submit read the form, or `null` with none in flight.
|
|
111
|
+
* Every edit counts, a revert to `initial` included: the server now holds what was submitted,
|
|
112
|
+
* not the opening value, so the baseline cannot say whether an edit made after the read is lost.
|
|
113
|
+
*/
|
|
114
|
+
let editedInFlight: Set<string> | null = null;
|
|
109
115
|
|
|
110
116
|
/**
|
|
111
117
|
* A transition names the submit's own members; `touched`/`dirty` are added here, from the one
|
|
@@ -146,6 +152,9 @@ export function createFormBinding<TValues, TResult>(
|
|
|
146
152
|
const run = async (values: TValues): Promise<FormState<TResult>> => {
|
|
147
153
|
// Cleared, never carried: a stale message would mark a control invalid for a value the user
|
|
148
154
|
// has already changed, on the one screen where the user is watching for exactly that.
|
|
155
|
+
// Opened BEFORE the publish: `onState` is synchronous, so a subscriber can edit in reaction
|
|
156
|
+
// to `submitting` itself, and that edit already postdates what this submit read.
|
|
157
|
+
editedInFlight = new Set();
|
|
149
158
|
publish({ status: 'submitting', ...NO_FORM_ERRORS, result: undefined, issues: [] });
|
|
150
159
|
|
|
151
160
|
try {
|
|
@@ -159,12 +168,16 @@ export function createFormBinding<TValues, TResult>(
|
|
|
159
168
|
if (local.length > 0) return failed(local);
|
|
160
169
|
}
|
|
161
170
|
const result = await options.submit(values);
|
|
162
|
-
// The server accepted what the form held
|
|
163
|
-
//
|
|
164
|
-
|
|
171
|
+
// The server accepted what the form held WHEN IT SUBMITTED, so only what changed since is
|
|
172
|
+
// left to lose — clearing that too let a navigation guard drop an edit typed during the save.
|
|
173
|
+
// `touched` stays: the user has still visited those fields, and a hint that vanishes on save
|
|
174
|
+
// is a flicker.
|
|
175
|
+
touch = { touched: touch.touched, dirty: editedInFlight ?? NO_FORM_TOUCH.dirty };
|
|
165
176
|
return publish({ status: 'succeeded', ...NO_FORM_ERRORS, result, issues: [] });
|
|
166
177
|
} catch (rejection) {
|
|
167
178
|
return failed(issuesFromRejection(rejection));
|
|
179
|
+
} finally {
|
|
180
|
+
editedInFlight = null;
|
|
168
181
|
}
|
|
169
182
|
};
|
|
170
183
|
|
|
@@ -184,10 +197,14 @@ export function createFormBinding<TValues, TResult>(
|
|
|
184
197
|
pending: () => state.status === 'submitting',
|
|
185
198
|
firstInvalidField: () => firstInvalidField(state, fields),
|
|
186
199
|
touch: (path) => publishTouch(markTouched(touch, path)),
|
|
187
|
-
edit: (path, value) =>
|
|
188
|
-
|
|
200
|
+
edit: (path, value) => {
|
|
201
|
+
editedInFlight?.add(path);
|
|
202
|
+
publishTouch(markDirty(touch, path, !sameFieldValue(value, baseline(path))));
|
|
203
|
+
},
|
|
189
204
|
reset: () => {
|
|
190
205
|
touch = NO_FORM_TOUCH;
|
|
206
|
+
// A reset mid-flight discards what was typed before it, the in-flight window's share too.
|
|
207
|
+
if (editedInFlight !== null) editedInFlight = new Set();
|
|
191
208
|
publish(IDLE_FORM_STATE);
|
|
192
209
|
},
|
|
193
210
|
};
|
package/src/form/use-form.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// The reactive shell over `
|
|
1
|
+
// The reactive shell over `formBinding`, and the whole of what it adds: the state lands in a
|
|
2
2
|
// signal, so a `<Field error={form.errorFor('title')}>` re-renders when a submit answers.
|
|
3
3
|
//
|
|
4
4
|
// Reactivity goes through `solid()` like every other read in this package — a component that
|
|
5
5
|
// imported solid-js directly would make @ultimat3/ui depend on a runtime it deliberately does not.
|
|
6
6
|
|
|
7
7
|
import { solid } from '../theme/solid-adapter';
|
|
8
|
-
import {
|
|
8
|
+
import { type FormBinding, type FormBindingOptions, formBinding } from './form-binding';
|
|
9
9
|
import {
|
|
10
10
|
errorOf,
|
|
11
11
|
type FormState,
|
|
@@ -24,7 +24,7 @@ export function useForm<TValues, TResult>(
|
|
|
24
24
|
): FormBinding<TValues, TResult> {
|
|
25
25
|
const runtime = solid();
|
|
26
26
|
const [state, setState] = runtime.createSignal<FormState<TResult>>(IDLE_FORM_STATE);
|
|
27
|
-
const binding =
|
|
27
|
+
const binding = formBinding<TValues, TResult>({
|
|
28
28
|
...options,
|
|
29
29
|
onState: (next) => {
|
|
30
30
|
options.onState?.(next);
|
package/src/global.scss
CHANGED
|
@@ -5,3 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
@use 'tokens/theme';
|
|
7
7
|
@use 'tokens/reset';
|
|
8
|
+
|
|
9
|
+
// A held island's reveal that needs no script (`@ultimat3/render`'s `ISLAND_HOLD_REVEAL`, issue 506):
|
|
10
|
+
// its wrapper animates to this after the hold's cap, so JS off or a blocked runtime still shows
|
|
11
|
+
// the server's markup. The name is held equal to render's by `island-hold-replay.test.ts`.
|
|
12
|
+
@keyframes ultimate-hold-reveal {
|
|
13
|
+
to {
|
|
14
|
+
visibility: visible;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/icons/build-icons.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const LUCIDE_LATEST_URL = 'https://registry.npmjs.org/lucide-static/lates
|
|
|
23
23
|
/** The tail of every cause about data the PIN published: why re-running alone cannot help. */
|
|
24
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
25
|
|
|
26
|
-
export const GLYPHS_DIR = new URL('./glyphs/', import.meta.url)
|
|
26
|
+
export const GLYPHS_DIR = Bun.fileURLToPath(new URL('./glyphs/', import.meta.url));
|
|
27
27
|
|
|
28
28
|
/** `circle-alert` → `iconCircleAlert`. Prefixed because `delete`, `import` and `package` are icons
|
|
29
29
|
* and reserved words — one uniform rule beats three exceptions an agent has to remember. */
|
|
@@ -167,13 +167,22 @@ export function moduleSource(name: string, glyph: IconGlyph): string {
|
|
|
167
167
|
].join('\n');
|
|
168
168
|
}
|
|
169
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
|
+
|
|
170
181
|
/** Biome owns formatting in this repo, generated files included — so the generator asks it rather
|
|
171
182
|
* than imitating it, and a regenerated set never shows up as a lint diff. */
|
|
172
183
|
async function format(): Promise<void> {
|
|
173
|
-
const biome =
|
|
174
|
-
if (
|
|
175
|
-
throw runtimeMissingError('the biome binary', 'bun install');
|
|
176
|
-
}
|
|
184
|
+
const biome = biomeBinary();
|
|
185
|
+
if (biome === undefined) throw runtimeMissingError('the biome binary', 'bun install');
|
|
177
186
|
// A formatter over one directory of generated files; a wedged one is the refusal below.
|
|
178
187
|
const result = Bun.spawnSync([biome, 'format', '--write', GLYPHS_DIR], { timeout: 60_000 });
|
|
179
188
|
if (result.exitCode !== 0) {
|
|
@@ -235,7 +244,7 @@ async function bumpPin(): Promise<string> {
|
|
|
235
244
|
const body: unknown = await response.json();
|
|
236
245
|
const version =
|
|
237
246
|
typeof body === 'object' && body !== null && 'version' in body ? String(body.version) : '';
|
|
238
|
-
const file =
|
|
247
|
+
const file = import.meta.path;
|
|
239
248
|
await Bun.write(file, withPin(await Bun.file(file).text(), version));
|
|
240
249
|
return version;
|
|
241
250
|
}
|
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,6 +335,9 @@ export {
|
|
|
312
335
|
useUi,
|
|
313
336
|
} from './theme/context';
|
|
314
337
|
export { INERT_SOLID_RUNTIME } from './theme/inert-runtime';
|
|
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';
|
|
315
341
|
export type { UiProviderProps } from './theme/provider';
|
|
316
342
|
export { UiProvider } from './theme/provider';
|
|
317
343
|
// The slot is its own module so that registering a runtime does not drag `errors.ts` — and with it
|
|
@@ -334,7 +360,6 @@ export {
|
|
|
334
360
|
THEME_ATTRIBUTE,
|
|
335
361
|
THEME_DEFAULT_ATTRIBUTE,
|
|
336
362
|
THEME_MEDIA_QUERY,
|
|
337
|
-
THEME_STORAGE_KEY,
|
|
338
363
|
toggleTheme,
|
|
339
364
|
watchOsTheme,
|
|
340
365
|
} from './theme/theme';
|
|
@@ -352,8 +377,16 @@ export {
|
|
|
352
377
|
TOAST_MAX_VISIBLE,
|
|
353
378
|
} from './toast/toast-state';
|
|
354
379
|
export type { ToastEnv, ToastStore } from './toast/toast-store';
|
|
355
|
-
export { browserToastEnv,
|
|
380
|
+
export { browserToastEnv, INERT_TOAST_ENV, toastStore } from './toast/toast-store';
|
|
356
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';
|
|
357
390
|
export type { Channels } from './tokens/contrast';
|
|
358
391
|
export {
|
|
359
392
|
AA_LARGE,
|
|
@@ -365,11 +398,12 @@ export {
|
|
|
365
398
|
} from './tokens/contrast';
|
|
366
399
|
export type { ContrastPair } from './tokens/contrast-pairs';
|
|
367
400
|
export { CONTRAST_PAIRS } from './tokens/contrast-pairs';
|
|
368
|
-
export type { ColorRole, RadiusName, Theme } from './tokens/tokens';
|
|
401
|
+
export type { ChartRole, ColorRole, RadiusName, ShadowName, Theme } from './tokens/tokens';
|
|
369
402
|
// --- tokens ------------------------------------------------------------------
|
|
370
403
|
export {
|
|
371
404
|
assertColorRole,
|
|
372
405
|
breakpointTokens,
|
|
406
|
+
CHART_ROLES,
|
|
373
407
|
COLOR_ROLES,
|
|
374
408
|
color,
|
|
375
409
|
colorRgb,
|
|
@@ -381,8 +415,10 @@ export {
|
|
|
381
415
|
fontWeightTokens,
|
|
382
416
|
lineHeightTokens,
|
|
383
417
|
radiusTokens,
|
|
418
|
+
SHADOW_NAMES,
|
|
384
419
|
shadowTokens,
|
|
385
420
|
spaceTokens,
|
|
386
421
|
strokeTokens,
|
|
422
|
+
touchTokens,
|
|
387
423
|
zTokens,
|
|
388
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
|
+
}
|