@yahoo/uds-create-config 5.15.1 → 5.16.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.
@@ -1,6 +1,6 @@
1
1
  import { Ref } from "./utils/refs.js";
2
2
  import { ItemOf } from "./defineEntity.js";
3
- import { StylePropLeaf, StyleProperty } from "../entities/system/StyleProperty.js";
3
+ import { StylePropLeaf, StylePropNumberUnit, StyleProperty } from "../entities/system/StyleProperty.js";
4
4
  import { StoredPreviewCondition, UNCONDITIONAL_PREVIEW_KEY, storedPreview, unconditionalPreviewProps } from "./preview-store.js";
5
5
  import { Config, ListOptions } from "./Config.js";
6
6
 
@@ -142,6 +142,89 @@ declare function stylePropValueLeaves(config: Config, path: string, opts?: ListO
142
142
  * when the leaf isn't in the prop's domain.
143
143
  */
144
144
  declare function stylePropValueFromLeaf(config: Config, path: string, leaf: string): string | undefined;
145
+ /**
146
+ * The leaves a style property's body declares AS NUMBERS: a numeric literal (`values: [1, 2]`) or a
147
+ * numeric alias (`{ alias: 3, value: 'repeat(3, minmax(0, 1fr))' }`). Only these match a bare number
148
+ * — `columns={3}` — while a token's name is a string however it reads (`spacing="4"`, never
149
+ * `spacing={4}`), as is a string alias.
150
+ */
151
+ declare function bodyNumberLeaves(stored: unknown): string[];
152
+ /** The leaves a style prop declares as numbers ({@link StylePropLeaf.numeric}), by path. */
153
+ declare function stylePropNumberLeaves(config: Config, path: string): string[];
154
+ /**
155
+ * The leaf a bare number names on a style prop — `columns={3}` → `'3'` — or `undefined` when the
156
+ * prop declares no such number (`spacing={4}`, whose `4` is a token's name) or the value isn't a
157
+ * number.
158
+ */
159
+ declare function stylePropNumberLeaf(config: Config, path: string, value: unknown): string | undefined;
160
+ /** The unit a style property's body reads a bare number in, or `undefined` when it takes none. */
161
+ declare function bodyNumberUnit(stored: unknown): StylePropNumberUnit | undefined;
162
+ /** The unit a style prop reads a bare number in (`maxWidth={600}` with `arbitrary: 'px'`), or
163
+ * `undefined` when it takes none. */
164
+ declare function stylePropNumberUnit(config: Config, path: string): StylePropNumberUnit | undefined;
165
+ /**
166
+ * The CSS a bare number on a style prop is written as, in the unit the prop declares (`arbitrary`):
167
+ * `maxWidth={600}` with `arbitrary: 'px'` is `600px`, `lineHeight={1.5}` with `arbitrary: 'unitless'` is
168
+ * `1.5`. `undefined` for a prop that declares none, for any other value, and for a number the
169
+ * prop declares as a value ({@link stylePropNumberLeaf}), which is that value. A token's name never
170
+ * reads as a number, so `spacing="4"` is the step wherever `spacing={4}` is a length.
171
+ *
172
+ * Read wherever a bag value meets a style prop: the canvas split, a rule's declarations, and the
173
+ * emitted runtime, which carries the same facts in its table.
174
+ */
175
+ declare function stylePropNumberCss(config: Config, path: string, value: unknown): string | undefined;
176
+ /**
177
+ * The value a bare number on a style prop spelled before a value's type decided what it names: `gap: 6`
178
+ * → `'6'`, where `'6'` is one of the prop's values (UDS-4828). `undefined` when the prop reads the
179
+ * number itself — a number it declares (`columns={3}`) or a length in its unit (`maxWidth={600}`) —
180
+ * and when no value is spelled that way, which validation refuses.
181
+ *
182
+ * Read where a design meets numbers written under the old reading: rows stored before the change,
183
+ * a model's unquoted YAML, the code view's `gap={6}`.
184
+ */
185
+ declare function stylePropNumberName(config: Config, path: string, value: unknown): string | undefined;
186
+ /**
187
+ * Why a style prop can't read a bare number in a unit, or `undefined` when it can — what an editor
188
+ * says beside its unit picker instead of offering one the config refuses.
189
+ */
190
+ declare function stylePropNumberUnitRefusal(config: Config, path: string): string | undefined;
191
+ /**
192
+ * A style prop's values whose names are numbers it doesn't declare as numbers — a spacing step `'4'`,
193
+ * an opacity `'50'`. On a prop with a unit these and a bare number part ways: `opacity="50"` is the
194
+ * value, `opacity={50}` is 50 in the unit. Empty when no value is named that way.
195
+ */
196
+ declare function stylePropNumberNamedLeaves(config: Config, path: string): string[];
197
+ /**
198
+ * `props` with `rewrite` applied to each bare number on a style prop — in modifier blocks, and in
199
+ * `layerProps` bags against the layer's own component. `rewrite` gets the style property the key writes
200
+ * and the number, and answers what to store instead, or `undefined` to keep it. The same object when
201
+ * nothing changed, so a reader can tell a row it rewrote from one it didn't.
202
+ */
203
+ declare function rewriteStyleNumbers(config: Config, elementType: string | undefined, props: Record<string, unknown>, rewrite: (path: string, value: number) => string | undefined, depth?: number): Record<string, unknown>;
204
+ /** `props` with each bare number spelled as the value it names ({@link stylePropNumberName}). */
205
+ declare function nameStyleNumbers(config: Config, elementType: string | undefined, props: Record<string, unknown>): Record<string, unknown>;
206
+ /**
207
+ * A bare number on a style prop as it painted while the prop read numbers in `before` — a unit, or none
208
+ * — written in a form that paints the same whatever unit the prop reads in: the value it names (`'6'`)
209
+ * or its bracket literal (`[6px]`). `undefined` when it painted nothing then, and for a number the prop
210
+ * declares (`columns={3}`), which reads the same under any unit.
211
+ *
212
+ * What a stored number becomes when its prop's unit changes, so the change doesn't reread it: `gap: 6`
213
+ * named the token `'6'` until `gap` took `px`, and stays that token after.
214
+ */
215
+ declare function styleNumberAsPainted(config: Config, path: string, value: number, before: StylePropNumberUnit | undefined): string | undefined;
216
+ /**
217
+ * The style properties whose bare-number reading differs between two versions of a config, each with
218
+ * the unit it read numbers in before (`undefined` for none) — what {@link pinStyleNumbers} takes. A
219
+ * property only one version holds isn't a change of reading.
220
+ */
221
+ declare function styleNumberReadingChanges(before: Config, after: Config): Map<string, StylePropNumberUnit | undefined>;
222
+ /**
223
+ * `props` with each bare number on a prop whose unit changed written as it painted before
224
+ * ({@link styleNumberAsPainted}). `before` maps each such style property's path to the unit it read
225
+ * numbers in before the change, `undefined` for none.
226
+ */
227
+ declare function pinStyleNumbers(config: Config, elementType: string | undefined, props: Record<string, unknown>, before: ReadonlyMap<string, StylePropNumberUnit | undefined>): Record<string, unknown>;
145
228
  /**
146
229
  * The token a style-prop leaf binds, as a qualified path — `('bg', 'brand')` → `color/brand`.
147
230
  * `undefined` when the leaf is a keyword the system declared rather than a token (`row`,
@@ -732,4 +815,4 @@ declare function propValueFromAxis(config: Config, path: string, prop: string, v
732
815
  /** The `value` a string, number or boolean prop declares as its default, per prop. */
733
816
  declare function declaredPropDefaults(props: Record<string, unknown> | undefined): Record<string, unknown>;
734
817
  //#endregion
735
- export { IconTool, IndexedPreviewCondition, PreviewMatrix, PropDeclaration, StyleDeclarationGroup, StylePropEntry, StylePropertyItem, VarNaming, authoredBag, authoredValue, className, cssPropValue, cssVar, cssVarRef, declarationKeyName, declaredPropDefaults, declaredPropRouting, declaresCssVariable, glyphComponentOf, glyphLibrariesOf, glyphPropsOf, heldLinked, holdingPreviewConditions, iconToolAxisOptions, iconToolFor, iconToolOf, iconToolsOf, isCustomPropertyKey, keyedPreviewConditions, linkedVarNaming, literalValueOf, memberFromLeaf, memberLeaves, negatedCssValue, opacityPercentage, previewConditionalProps, previewConditions, previewDefaults, previewMatrix, propOwner, propValueDomain, propValueFromAxis, refListOf, renderInterpolated, renderStyleValue, resolveComponentProps, resolveFieldValue, resolveTokenValue, resolveTokenValueUnder, routedBag, sourceVarPrefix, styleAliasesOf, styleDeclarationGroups, styleDeclarations, stylePropEntries, stylePropLeafForToken, stylePropNegates, stylePropTokenGroup, stylePropTokenPath, stylePropTokenValues, stylePropValueFromLeaf, stylePropValueLeaves, stylePropValues, stylePropertiesWriting, stylePropertyAccepts, stylePropertyFor, stylePropertyPathFor, tokenReference, validateComponentProps };
818
+ export { IconTool, IndexedPreviewCondition, PreviewMatrix, PropDeclaration, StyleDeclarationGroup, StylePropEntry, StylePropertyItem, VarNaming, authoredBag, authoredValue, bodyNumberLeaves, bodyNumberUnit, className, cssPropValue, cssVar, cssVarRef, declarationKeyName, declaredPropDefaults, declaredPropRouting, declaresCssVariable, glyphComponentOf, glyphLibrariesOf, glyphPropsOf, heldLinked, holdingPreviewConditions, iconToolAxisOptions, iconToolFor, iconToolOf, iconToolsOf, isCustomPropertyKey, keyedPreviewConditions, linkedVarNaming, literalValueOf, memberFromLeaf, memberLeaves, nameStyleNumbers, negatedCssValue, opacityPercentage, pinStyleNumbers, previewConditionalProps, previewConditions, previewDefaults, previewMatrix, propOwner, propValueDomain, propValueFromAxis, refListOf, renderInterpolated, renderStyleValue, resolveComponentProps, resolveFieldValue, resolveTokenValue, resolveTokenValueUnder, rewriteStyleNumbers, routedBag, sourceVarPrefix, styleAliasesOf, styleDeclarationGroups, styleDeclarations, styleNumberAsPainted, styleNumberReadingChanges, stylePropEntries, stylePropLeafForToken, stylePropNegates, stylePropNumberCss, stylePropNumberLeaf, stylePropNumberLeaves, stylePropNumberName, stylePropNumberNamedLeaves, stylePropNumberUnit, stylePropNumberUnitRefusal, stylePropTokenGroup, stylePropTokenPath, stylePropTokenValues, stylePropValueFromLeaf, stylePropValueLeaves, stylePropValues, stylePropertiesWriting, stylePropertyAccepts, stylePropertyFor, stylePropertyPathFor, tokenReference, validateComponentProps };
@@ -9,7 +9,7 @@ import { listedIn } from "./utils/enumerated.js";
9
9
  import "../entities/system/Modifier.js";
10
10
  import { createSliceMemo } from "./memo.js";
11
11
  import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
12
- import { StyleProperty, leavesOfStylePropValue } from "../entities/system/StyleProperty.js";
12
+ import { StyleProperty, leavesOfStylePropValue, numberUnitRefusal } from "../entities/system/StyleProperty.js";
13
13
  import { isConditionalPreview, storedPreview, unconditionalPreviewProps } from "./preview-store.js";
14
14
  //#region src/framework/projections.ts
15
15
  /**
@@ -457,6 +457,265 @@ function stylePropValueFromLeaf(config, path, leaf) {
457
457
  return stylePropEntries(config, path).find((entry) => entry.leaf === leaf)?.value;
458
458
  }
459
459
  /**
460
+ * The leaves a style property's body declares AS NUMBERS: a numeric literal (`values: [1, 2]`) or a
461
+ * numeric alias (`{ alias: 3, value: 'repeat(3, minmax(0, 1fr))' }`). Only these match a bare number
462
+ * — `columns={3}` — while a token's name is a string however it reads (`spacing="4"`, never
463
+ * `spacing={4}`), as is a string alias.
464
+ */
465
+ function bodyNumberLeaves(stored) {
466
+ const values = stored?.values ?? [];
467
+ const out = [];
468
+ for (const entry of values) if (typeof entry === "number") out.push(String(entry));
469
+ else if (entry && typeof entry === "object" && "alias" in entry && typeof entry.alias === "number") out.push(String(entry.alias));
470
+ return out;
471
+ }
472
+ /** The leaves a style prop declares as numbers ({@link StylePropLeaf.numeric}), by path. */
473
+ function stylePropNumberLeaves(config, path) {
474
+ return stylePropEntries(config, path).filter((entry) => entry.numeric).map((entry) => entry.leaf);
475
+ }
476
+ /**
477
+ * The leaf a bare number names on a style prop — `columns={3}` → `'3'` — or `undefined` when the
478
+ * prop declares no such number (`spacing={4}`, whose `4` is a token's name) or the value isn't a
479
+ * number.
480
+ */
481
+ function stylePropNumberLeaf(config, path, value) {
482
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
483
+ const leaf = String(value);
484
+ return stylePropNumberLeaves(config, path).includes(leaf) ? leaf : void 0;
485
+ }
486
+ /** The unit a style property's body reads a bare number in, or `undefined` when it takes none. */
487
+ function bodyNumberUnit(stored) {
488
+ const body = stored;
489
+ if (!body?.properties?.length || body.selector || Object.keys(body.declarations ?? {}).length > 0) return;
490
+ return typeof body.arbitrary === "string" ? body.arbitrary : void 0;
491
+ }
492
+ /** The unit a style prop reads a bare number in (`maxWidth={600}` with `arbitrary: 'px'`), or
493
+ * `undefined` when it takes none. */
494
+ function stylePropNumberUnit(config, path) {
495
+ return bodyNumberUnit(config.resolve("style-property", path)?.toJSON());
496
+ }
497
+ /**
498
+ * The CSS a bare number on a style prop is written as, in the unit the prop declares (`arbitrary`):
499
+ * `maxWidth={600}` with `arbitrary: 'px'` is `600px`, `lineHeight={1.5}` with `arbitrary: 'unitless'` is
500
+ * `1.5`. `undefined` for a prop that declares none, for any other value, and for a number the
501
+ * prop declares as a value ({@link stylePropNumberLeaf}), which is that value. A token's name never
502
+ * reads as a number, so `spacing="4"` is the step wherever `spacing={4}` is a length.
503
+ *
504
+ * Read wherever a bag value meets a style prop: the canvas split, a rule's declarations, and the
505
+ * emitted runtime, which carries the same facts in its table.
506
+ */
507
+ function stylePropNumberCss(config, path, value) {
508
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
509
+ const unit = stylePropNumberUnit(config, path);
510
+ if (unit === void 0) return void 0;
511
+ if (stylePropNumberLeaf(config, path, value) !== void 0) return void 0;
512
+ return unit === "unitless" ? String(value) : `${value}${unit}`;
513
+ }
514
+ /**
515
+ * The value a bare number on a style prop spelled before a value's type decided what it names: `gap: 6`
516
+ * → `'6'`, where `'6'` is one of the prop's values (UDS-4828). `undefined` when the prop reads the
517
+ * number itself — a number it declares (`columns={3}`) or a length in its unit (`maxWidth={600}`) —
518
+ * and when no value is spelled that way, which validation refuses.
519
+ *
520
+ * Read where a design meets numbers written under the old reading: rows stored before the change,
521
+ * a model's unquoted YAML, the code view's `gap={6}`.
522
+ */
523
+ function stylePropNumberName(config, path, value) {
524
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
525
+ if (stylePropNumberLeaf(config, path, value) !== void 0) return void 0;
526
+ if (stylePropNumberUnit(config, path) !== void 0) return void 0;
527
+ const leaf = String(value);
528
+ return stylePropEntries(config, path).some((entry) => entry.leaf === leaf) ? leaf : void 0;
529
+ }
530
+ /**
531
+ * Why a style prop can't read a bare number in a unit, or `undefined` when it can — what an editor
532
+ * says beside its unit picker instead of offering one the config refuses.
533
+ */
534
+ function stylePropNumberUnitRefusal(config, path) {
535
+ const body = config.resolve("style-property", path)?.toJSON();
536
+ if (!body) return void 0;
537
+ return numberUnitRefusal({
538
+ ...body,
539
+ properties: body.properties ?? []
540
+ });
541
+ }
542
+ /**
543
+ * A style prop's values whose names are numbers it doesn't declare as numbers — a spacing step `'4'`,
544
+ * an opacity `'50'`. On a prop with a unit these and a bare number part ways: `opacity="50"` is the
545
+ * value, `opacity={50}` is 50 in the unit. Empty when no value is named that way.
546
+ */
547
+ function stylePropNumberNamedLeaves(config, path) {
548
+ const numeric = new Set(stylePropNumberLeaves(config, path));
549
+ return stylePropEntries(config, path).map((entry) => entry.leaf).filter((leaf) => leaf !== "" && !numeric.has(leaf) && Number.isFinite(Number(leaf)) && String(Number(leaf)) === leaf);
550
+ }
551
+ /**
552
+ * `props` with `rewrite` applied to each bare number on a style prop — in modifier blocks, and in
553
+ * `layerProps` bags against the layer's own component. `rewrite` gets the style property the key writes
554
+ * and the number, and answers what to store instead, or `undefined` to keep it. The same object when
555
+ * nothing changed, so a reader can tell a row it rewrote from one it didn't.
556
+ */
557
+ function rewriteStyleNumbers(config, elementType, props, rewrite, depth = 0) {
558
+ let next;
559
+ const set = (key, value) => {
560
+ next ??= { ...props };
561
+ next[key] = value;
562
+ };
563
+ for (const [key, value] of Object.entries(props)) if (key === "layerProps") {
564
+ if (depth >= 5 || !isBag(value)) continue;
565
+ const layers = ((elementType ? config.resolve("component", pathOf(elementType)) : void 0)?.body)?.layers;
566
+ let layerProps;
567
+ for (const [layer, bag] of Object.entries(value)) {
568
+ if (!isBag(bag)) continue;
569
+ const element = layers?.[layer];
570
+ const written = rewriteStyleNumbers(config, isRef(element) ? pathOf(element) : void 0, bag, rewrite, depth + 1);
571
+ if (written !== bag) {
572
+ layerProps ??= { ...value };
573
+ layerProps[layer] = written;
574
+ }
575
+ }
576
+ if (layerProps) set(key, layerProps);
577
+ } else if (key.startsWith("_")) {
578
+ if (!isBag(value)) continue;
579
+ const written = rewriteStyleNumbers(config, elementType, value, rewrite, depth);
580
+ if (written !== value) set(key, written);
581
+ } else if (typeof value === "number" && Number.isFinite(value)) {
582
+ const path = stylePropertyPathFor({
583
+ config,
584
+ through: [elementType],
585
+ key
586
+ });
587
+ const written = path === void 0 ? void 0 : rewrite(path, value);
588
+ if (written !== void 0) set(key, written);
589
+ }
590
+ return next ?? props;
591
+ }
592
+ /** `props` with each bare number spelled as the value it names ({@link stylePropNumberName}). */
593
+ function nameStyleNumbers(config, elementType, props) {
594
+ return rewriteStyleNumbers(config, elementType, props, (path, value) => stylePropNumberName(config, path, value));
595
+ }
596
+ /**
597
+ * A bare number on a style prop as it painted while the prop read numbers in `before` — a unit, or none
598
+ * — written in a form that paints the same whatever unit the prop reads in: the value it names (`'6'`)
599
+ * or its bracket literal (`[6px]`). `undefined` when it painted nothing then, and for a number the prop
600
+ * declares (`columns={3}`), which reads the same under any unit.
601
+ *
602
+ * What a stored number becomes when its prop's unit changes, so the change doesn't reread it: `gap: 6`
603
+ * named the token `'6'` until `gap` took `px`, and stays that token after.
604
+ */
605
+ function styleNumberAsPainted(config, path, value, before) {
606
+ if (!Number.isFinite(value)) return void 0;
607
+ if (stylePropNumberLeaf(config, path, value) !== void 0) return void 0;
608
+ if (before !== void 0) return `[${value}${before === "unitless" ? "" : before}]`;
609
+ const leaf = String(value);
610
+ return stylePropEntries(config, path).some((entry) => entry.leaf === leaf) ? leaf : void 0;
611
+ }
612
+ /** A keyed collection with `rewrite` applied to each entry; the same object when none changed. */
613
+ function eachMember(collection, rewrite) {
614
+ if (!isBag(collection)) return collection;
615
+ let next;
616
+ for (const [key, entry] of Object.entries(collection)) {
617
+ const written = isBag(entry) ? rewrite(entry) : entry;
618
+ if (written !== entry) {
619
+ next ??= { ...collection };
620
+ next[key] = written;
621
+ }
622
+ }
623
+ return next ?? collection;
624
+ }
625
+ /**
626
+ * A stored component or canvas-role body with {@link pinStyleNumbers} applied to every bag it keeps: a
627
+ * component's style rules (each layer's bag against the component that layer renders), `defaultProps`
628
+ * and `preview` props, an `extend` delta's the same way, and the `defaultProps` a canvas role inserts its
629
+ * component with. The same object when nothing changed.
630
+ */
631
+ function pinBodyStyleNumbers(config, kind, body, before, self) {
632
+ if (kind === "canvas-role") {
633
+ if (!Array.isArray(body.entries)) return body;
634
+ let changed = false;
635
+ const entries = body.entries.map((entry) => {
636
+ if (!isBag(entry) || !isBag(entry.defaultProps)) return entry;
637
+ const defaultProps = pinStyleNumbers(config, isRef(entry.component) ? pathOf(entry.component) : void 0, entry.defaultProps, before);
638
+ if (defaultProps === entry.defaultProps) return entry;
639
+ changed = true;
640
+ return {
641
+ ...entry,
642
+ defaultProps
643
+ };
644
+ });
645
+ return changed ? {
646
+ ...body,
647
+ entries
648
+ } : body;
649
+ }
650
+ if (kind !== "component") return body;
651
+ if (isBag(body.extend) && isRef(body)) {
652
+ const extend = pinBodyStyleNumbers(config, kind, body.extend, before, pathOf(body));
653
+ return extend === body.extend ? body : {
654
+ ...body,
655
+ extend
656
+ };
657
+ }
658
+ const layers = isBag(body.layers) ? body.layers : {};
659
+ const layerType = (layer) => {
660
+ const element = layers[layer];
661
+ return isRef(element) ? pathOf(element) : void 0;
662
+ };
663
+ const pin = (type, bag) => isBag(bag) ? pinStyleNumbers(config, type, bag, before) : bag;
664
+ let next;
665
+ const set = (field, value) => {
666
+ if (value === body[field]) return;
667
+ next ??= { ...body };
668
+ next[field] = value;
669
+ };
670
+ set("styles", eachMember(body.styles, (rule) => {
671
+ if (!isBag(rule.layers)) return rule;
672
+ let written;
673
+ for (const [layer, bag] of Object.entries(rule.layers)) {
674
+ const pinned = pin(layerType(layer), bag);
675
+ if (pinned !== bag) {
676
+ written ??= { ...rule.layers };
677
+ written[layer] = pinned;
678
+ }
679
+ }
680
+ return written ? {
681
+ ...rule,
682
+ layers: written
683
+ } : rule;
684
+ }));
685
+ set("defaultProps", pin(self, body.defaultProps));
686
+ set("preview", eachMember(body.preview, (entry) => {
687
+ const props = pin(self, entry.props);
688
+ return props === entry.props ? entry : {
689
+ ...entry,
690
+ props
691
+ };
692
+ }));
693
+ return next ?? body;
694
+ }
695
+ /**
696
+ * The style properties whose bare-number reading differs between two versions of a config, each with
697
+ * the unit it read numbers in before (`undefined` for none) — what {@link pinStyleNumbers} takes. A
698
+ * property only one version holds isn't a change of reading.
699
+ */
700
+ function styleNumberReadingChanges(before, after) {
701
+ const changes = /* @__PURE__ */ new Map();
702
+ for (const prop of StyleProperty.list(after)) {
703
+ if (!before.resolve("style-property", prop.path)) continue;
704
+ const was = stylePropNumberUnit(before, prop.path);
705
+ if (was !== stylePropNumberUnit(after, prop.path)) changes.set(prop.path, was);
706
+ }
707
+ return changes;
708
+ }
709
+ /**
710
+ * `props` with each bare number on a prop whose unit changed written as it painted before
711
+ * ({@link styleNumberAsPainted}). `before` maps each such style property's path to the unit it read
712
+ * numbers in before the change, `undefined` for none.
713
+ */
714
+ function pinStyleNumbers(config, elementType, props, before) {
715
+ if (before.size === 0) return props;
716
+ return rewriteStyleNumbers(config, elementType, props, (path, value) => before.has(path) ? styleNumberAsPainted(config, path, value, before.get(path)) : void 0);
717
+ }
718
+ /**
460
719
  * The token a style-prop leaf binds, as a qualified path — `('bg', 'brand')` → `color/brand`.
461
720
  * `undefined` when the leaf is a keyword the system declared rather than a token (`row`,
462
721
  * `flex-start`), or isn't in the prop's domain at all.
@@ -1045,9 +1304,10 @@ function propDeclarations({ config, body, namespace }) {
1045
1304
  * A flag is written bare (`truncate: true`) and its entry sits under the empty leaf, which is the same
1046
1305
  * entry the class `.uds-truncate` is built from. `false` is the prop's absence and has no entry.
1047
1306
  */
1048
- function leafAuthoredFor(value) {
1307
+ function leafAuthoredFor(config, key, value) {
1049
1308
  if (typeof value === "boolean") return value ? "" : void 0;
1050
- if (typeof value === "string" || typeof value === "number") return String(value);
1309
+ if (typeof value === "string") return value;
1310
+ if (typeof value === "number") return stylePropNumberLeaf(config, key, value);
1051
1311
  }
1052
1312
  /**
1053
1313
  * The declarations a bag sets on the element itself. Declarations a prop scopes to a `selector` or a
@@ -1124,7 +1384,7 @@ function styleDeclarationGroups({ config, bag, namespace }) {
1124
1384
  const keyed = refOfKey(property);
1125
1385
  return keyed ? cssVar(config, "token", pathOf(keyed), namespace) : property;
1126
1386
  }) : [];
1127
- const authoredLeafValue = leafAuthoredFor(value);
1387
+ const authoredLeafValue = leafAuthoredFor(config, key, value);
1128
1388
  const entry = authoredLeafValue === void 0 ? void 0 : stylePropEntries(config, key).find((candidate) => candidate.leaf === authoredLeafValue);
1129
1389
  if (entry?.declarations) {
1130
1390
  for (const [key, declaredValue] of Object.entries(entry.declarations)) target[declarationKeyName({
@@ -1171,11 +1431,14 @@ function styleDeclarationGroups({ config, bag, namespace }) {
1171
1431
  }
1172
1432
  /** One bag value as CSS text, or `undefined` for a flag turned off. */
1173
1433
  function renderBagValue({ config, key, value, namespace }) {
1434
+ const number = stylePropNumberCss(config, key, value);
1435
+ if (number !== void 0) return number;
1174
1436
  let resolved = value;
1175
- if (typeof value === "string") {
1176
- const qualified = stylePropValueFromLeaf(config, key, value);
1437
+ const leaf = typeof value === "string" ? value : stylePropNumberLeaf(config, key, value);
1438
+ if (leaf !== void 0) {
1439
+ const qualified = stylePropValueFromLeaf(config, key, leaf);
1177
1440
  if (qualified !== void 0) resolved = qualified;
1178
- }
1441
+ } else if (typeof value === "number") return;
1179
1442
  if (typeof value === "boolean") {
1180
1443
  if (!value) return void 0;
1181
1444
  const bare = stylePropValueFromLeaf(config, key, "");
@@ -1536,6 +1799,15 @@ function validateComponentProps(config, componentPath, props) {
1536
1799
  errors.push(`unknown prop \`${name}\``);
1537
1800
  continue;
1538
1801
  }
1802
+ if (decl.type === "styleProperty" && isRef(decl.value) && stylePropNumberCss(config, splitRef(decl.value.__ref).qualifiedPath, value) !== void 0) continue;
1803
+ if (decl.type === "styleProperty" && typeof value === "number" && isRef(decl.value)) {
1804
+ const path = splitRef(decl.value.__ref).qualifiedPath;
1805
+ if (stylePropNumberLeaf(config, path, value) === void 0) {
1806
+ const declared = stylePropNumberLeaves(config, path);
1807
+ errors.push(declared.length > 0 ? `\`${name}\`: ${value} is not one of the numbers it takes (${declared.join(" | ")})` : `\`${name}\`: takes no number; a value named \`${value}\` is written as the string '${value}'`);
1808
+ }
1809
+ continue;
1810
+ }
1539
1811
  const domain = propValueDomain(config, decl);
1540
1812
  if (domain && !domain.accepts(String(value))) {
1541
1813
  const shown = domain.leaves.slice(0, 8).map((leaf) => `'${leaf}'`).join(" | ");
@@ -1729,4 +2001,4 @@ function declaredPropDefaults(props) {
1729
2001
  return out;
1730
2002
  }
1731
2003
  //#endregion
1732
- export { LAYER_PROPS, authoredBag, authoredValue, className, cssPropValue, cssVar, cssVarRef, declarationKeyName, declaredPropDefaults, declaredPropRouting, declaresCssVariable, fontStackOf, glyphComponentOf, glyphLibrariesOf, glyphPropsOf, glyphPropsOfBody, heldLinked, holdingPreviewConditions, iconToolAxisOptions, iconToolFor, iconToolOf, iconToolsOf, isCustomPropertyKey, keyedPreviewConditions, layerPropsErrors, linkedVarNaming, literalValueOf, memberFromLeaf, memberLeaves, negatedCssValue, opacityPercentage, previewConditionalProps, previewConditions, previewDefaults, previewMatrix, propOwner, propValueDomain, propValueFromAxis, refListOf, renderInterpolated, renderStyleValue, resolveComponentProps, resolveFieldValue, resolveTokenValue, resolveTokenValueUnder, routedBag, sourceVarPrefix, styleAliasesOf, styleDeclarationGroups, styleDeclarations, stylePropEntries, stylePropLeafForToken, stylePropNegates, stylePropTokenGroup, stylePropTokenPath, stylePropTokenValues, stylePropValueFromLeaf, stylePropValueLeaves, stylePropValues, stylePropertiesWriting, stylePropertyAccepts, stylePropertyFor, stylePropertyPathFor, takenByForward, tokenReference, validateComponentProps };
2004
+ export { LAYER_PROPS, authoredBag, authoredValue, bodyNumberLeaves, bodyNumberUnit, className, cssPropValue, cssVar, cssVarRef, declarationKeyName, declaredPropDefaults, declaredPropRouting, declaresCssVariable, fontStackOf, glyphComponentOf, glyphLibrariesOf, glyphPropsOf, glyphPropsOfBody, heldLinked, holdingPreviewConditions, iconToolAxisOptions, iconToolFor, iconToolOf, iconToolsOf, isCustomPropertyKey, keyedPreviewConditions, layerPropsErrors, linkedVarNaming, literalValueOf, memberFromLeaf, memberLeaves, nameStyleNumbers, negatedCssValue, opacityPercentage, pinBodyStyleNumbers, pinStyleNumbers, previewConditionalProps, previewConditions, previewDefaults, previewMatrix, propOwner, propValueDomain, propValueFromAxis, refListOf, renderInterpolated, renderStyleValue, resolveComponentProps, resolveFieldValue, resolveTokenValue, resolveTokenValueUnder, rewriteStyleNumbers, routedBag, sourceVarPrefix, styleAliasesOf, styleDeclarationGroups, styleDeclarations, styleNumberAsPainted, styleNumberReadingChanges, stylePropEntries, stylePropLeafForToken, stylePropNegates, stylePropNumberCss, stylePropNumberLeaf, stylePropNumberLeaves, stylePropNumberName, stylePropNumberNamedLeaves, stylePropNumberUnit, stylePropNumberUnitRefusal, stylePropTokenGroup, stylePropTokenPath, stylePropTokenValues, stylePropValueFromLeaf, stylePropValueLeaves, stylePropValues, stylePropertiesWriting, stylePropertyAccepts, stylePropertyFor, stylePropertyPathFor, takenByForward, tokenReference, validateComponentProps };
@@ -1216,6 +1216,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1216
1216
  routes.push(route);
1217
1217
  valueRoutes.set(route.layer, routes);
1218
1218
  }
1219
+ const forwardsClassName = [...valueRoutes.values()].some((routes) => routes.some((route) => route.from === "className"));
1219
1220
  const supplied = {
1220
1221
  ...declaredDefaults,
1221
1222
  ...body.defaultProps ?? {},
@@ -1356,7 +1357,8 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1356
1357
  ...layerProps[layer],
1357
1358
  className: classesFor(config, path, layer, state, [
1358
1359
  isRoot ? styleClasses : void 0,
1359
- isRoot ? instanceProps.className : void 0,
1360
+ isRoot && !forwardsClassName ? instanceProps.className : void 0,
1361
+ ...(valueRoutes.get(layer) ?? []).filter(({ into }) => into === "className").map(({ from }) => state[from]),
1360
1362
  layerProps[layer]?.className
1361
1363
  ]) || void 0
1362
1364
  } : {
@@ -15,7 +15,7 @@ import { Patch, SerializedConfig } from "./Config.js";
15
15
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
16
16
  * newer than the constant, and the assertion fails until the author re-mints.
17
17
  */
18
- declare const CURRENT_SCHEMA_VERSION = 20261007182932;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261007210309;
19
19
  /** What the walkers need to see of an envelope — loose on purpose, so a typed `SerializedConfig`
20
20
  * and a raw old-shape record both flow in without casts. */
21
21
  interface VersionStamped {
@@ -13,7 +13,7 @@ import { ConfigFormatError } from "./rejection.js";
13
13
  * sets this constant to its own older timestamp; the merge ref then carries a registered migration
14
14
  * newer than the constant, and the assertion fails until the author re-mints.
15
15
  */
16
- const CURRENT_SCHEMA_VERSION = 20261007182932;
16
+ const CURRENT_SCHEMA_VERSION = 20261007210309;
17
17
  /** The mint that moved `options` into the `settings` kind — restated here, rather than imported
18
18
  * from the migration that owns it, because that migration imports this module. */
19
19
  const SETTINGS_AS_KIND_VERSION = 20260908171448;
@@ -45,7 +45,7 @@ function registerSchemaMigrations(...migrations) {
45
45
  for (const migration of migrations) {
46
46
  if (migration.version !== 1 && !isMintTimestamp(migration.version)) throw new Error(`Schema migration version ${migration.version} is not a mint timestamp — mint one with \`date +%Y%m%d%H%M%S\`, like a supabase migration.`);
47
47
  if (REGISTERED.has(migration.version)) throw new Error(`A schema migration at version ${migration.version} is already registered.`);
48
- if (migration.version > 20261007182932) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
48
+ if (migration.version > 20261007210309) throw new Error(`Schema migration ${migration.version} is newer than CURRENT_SCHEMA_VERSION (${CURRENT_SCHEMA_VERSION}) — a stale branch merged, or the mint forgot to bump the constant. Re-mint: set CURRENT_SCHEMA_VERSION to the newest migration version.`);
49
49
  REGISTERED.set(migration.version, migration);
50
50
  }
51
51
  }
@@ -102,7 +102,7 @@ function pendingFor(version, target, options) {
102
102
  * Already-current input passes through untouched — same reference, so hot paths pay nothing.
103
103
  */
104
104
  function upgradeSerializedConfig(json, options) {
105
- const target = options?.target ?? 20261007182932;
105
+ const target = options?.target ?? 20261007210309;
106
106
  const version = detectedWireVersion(json);
107
107
  if (version > target) throw new SchemaVersionTooNew(version, target);
108
108
  const raw = json;
@@ -160,7 +160,7 @@ function upgradedSources(json, options) {
160
160
  * meaning under the current schema and replay skips it — reported by the caller, not swallowed here.
161
161
  */
162
162
  function upgradePatch(patch, writtenAt, options) {
163
- const target = options?.target ?? 20261007182932;
163
+ const target = options?.target ?? 20261007210309;
164
164
  if (writtenAt > target) throw new SchemaVersionTooNew(writtenAt, target);
165
165
  let current = patch;
166
166
  for (const migration of pendingFor(writtenAt, target, options)) {
@@ -179,7 +179,7 @@ function upgradePatch(patch, writtenAt, options) {
179
179
  * stamp would cause.
180
180
  */
181
181
  function upgradeDraftEntries(draft, options) {
182
- const target = options?.target ?? 20261007182932;
182
+ const target = options?.target ?? 20261007210309;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -1,6 +1,6 @@
1
1
  import { isRef, kindOf, memberOf, pathOf, ref, refSchema } from "./utils/refs.js";
2
2
  import { listedIn } from "./utils/enumerated.js";
3
- import { memberFromLeaf, memberLeaves, refListOf, stylePropValueFromLeaf, stylePropValueLeaves } from "./projections.js";
3
+ import { bodyNumberLeaves, bodyNumberUnit, memberFromLeaf, memberLeaves, refListOf, stylePropValueFromLeaf, stylePropValueLeaves } from "./projections.js";
4
4
  import { z } from "zod";
5
5
  //#region src/framework/value-domain.ts
6
6
  /**
@@ -132,12 +132,12 @@ function buildRefDomainSchema(target, config, seen) {
132
132
  if (enumerated === void 0) return z.unknown();
133
133
  const property = stylePropertyOf(target, config);
134
134
  const also = [];
135
- if (property?.arbitrary === true) also.push(arbitraryValue);
135
+ if (property?.arbitrary) also.push(arbitraryValue);
136
136
  if (isFlag(property)) also.push(z.boolean());
137
- if (property !== void 0) {
138
- const leaf = enumerated;
139
- also.push(z.number().transform(String).pipe(leaf));
140
- }
137
+ const numbered = oneOf(bodyNumberLeaves(property));
138
+ if (numbered !== void 0) also.push(z.number().transform(String).pipe(numbered));
139
+ const unit = bodyNumberUnit(property);
140
+ if (unit) also.push(z.number().finite());
141
141
  const own = config.entityOf(kindOf(target))?.authoredValue?.({
142
142
  config,
143
143
  target,
@@ -150,8 +150,9 @@ function buildRefDomainSchema(target, config, seen) {
150
150
  refSchema
151
151
  ], { error: () => {
152
152
  const forms = [`one of ${spellValues(leaves)}`];
153
- if (property?.arbitrary === true) forms.push("a bracketed literal such as `[13px]`");
154
- if (property !== void 0) forms.push("a number spelling one of those leaves");
153
+ if (property?.arbitrary) forms.push("a bracketed literal such as `[13px]`");
154
+ if (unit) forms.push(unit === "unitless" ? "a number, written as it is" : `a number of \`${unit}\` such as \`600\``);
155
+ else if (numbered !== void 0) forms.push("a number it declares as a value");
155
156
  if (isFlag(property)) forms.push("a boolean");
156
157
  return `${forms.join(", ")}, or a ref to one of those tokens (\`{ "__ref": "${target}/<name>" }\`)`;
157
158
  } }).transform((value) => typeof value === "string" ? refForLeaf(target, value, config) ?? value : value);