@yahoo/uds-create-config 5.25.2 → 5.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/configs/react-native-system.d.ts +22 -4
  2. package/dist/configs/system.js +4 -0
  3. package/dist/entities/native/NativeToken.d.ts +22 -4
  4. package/dist/entities/native/NativeToken.js +2 -1
  5. package/dist/entities/system/Component.d.ts +35 -2
  6. package/dist/entities/system/Component.js +224 -9
  7. package/dist/entities/system/LinkedSystem.js +3 -2
  8. package/dist/entities/system/Settings.js +1 -1
  9. package/dist/entities/system/StyleProperty.js +1 -1
  10. package/dist/entities/system/Token.d.ts +22 -4
  11. package/dist/entities/system/Token.js +4 -3
  12. package/dist/entities/system/component-module.d.ts +17 -1
  13. package/dist/entities/system/component-module.js +54 -2
  14. package/dist/entities/system/defineComponent.d.ts +53 -13
  15. package/dist/entities/system/defineComponent.js +1 -0
  16. package/dist/entities/system/forward-route-invariants.js +1 -1
  17. package/dist/entities/system/icon-domain-invariants.js +1 -1
  18. package/dist/entities/system/root-layer-invariants.js +1 -1
  19. package/dist/entities/system/token-values.js +1 -1
  20. package/dist/framework/Config.d.ts +1 -1
  21. package/dist/framework/Config.js +33 -2
  22. package/dist/framework/class-names.d.ts +27 -1
  23. package/dist/framework/class-names.js +56 -3
  24. package/dist/framework/component-states.d.ts +40 -0
  25. package/dist/framework/component-states.js +53 -0
  26. package/dist/framework/config-ops.d.ts +1 -1
  27. package/dist/framework/config-ops.js +25 -4
  28. package/dist/framework/defineSubEntity.d.ts +7 -0
  29. package/dist/framework/defineSubEntity.js +9 -0
  30. package/dist/framework/interaction-conditions.d.ts +48 -0
  31. package/dist/framework/interaction-conditions.js +104 -0
  32. package/dist/framework/layer-styles.d.ts +5 -1
  33. package/dist/framework/layer-styles.js +18 -4
  34. package/dist/framework/projections.js +44 -2
  35. package/dist/framework/render-spec.js +14 -4
  36. package/dist/framework/schema-version.d.ts +1 -1
  37. package/dist/framework/schema-version.js +5 -5
  38. package/dist/framework/utils/deprecated.d.ts +18 -0
  39. package/dist/framework/utils/deprecated.js +12 -0
  40. package/dist/framework/validate-spec.js +14 -0
  41. package/dist/index.d.ts +5 -3
  42. package/dist/index.js +6 -4
  43. package/dist/migrations/20261013154037_component_states.d.ts +19 -0
  44. package/dist/migrations/20261013154037_component_states.js +74 -0
  45. package/dist/migrations/20261013164300_token_overrides_list.d.ts +7 -0
  46. package/dist/migrations/20261013164300_token_overrides_list.js +10 -0
  47. package/dist/renderer/componentEntry.d.ts +7 -1
  48. package/dist/renderer/componentEntry.js +30 -1
  49. package/dist/renderer/forced-stamp.d.ts +26 -0
  50. package/dist/renderer/forced-stamp.js +50 -0
  51. package/dist/renderer/index.d.ts +3 -1
  52. package/dist/renderer/index.js +3 -1
  53. package/dist/renderer/use-forced-stamp.d.ts +11 -0
  54. package/dist/renderer/use-forced-stamp.js +50 -0
  55. package/dist/spec/specToJsx.d.ts +6 -0
  56. package/dist/spec/specToJsx.js +5 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +2 -2
@@ -75,6 +75,19 @@ function authoringSignature(schema, options) {
75
75
  const json = jsonSchema(schema);
76
76
  return json === void 0 ? void 0 : renderSignature(withoutMetadata(json), options);
77
77
  }
78
+ /**
79
+ * A real entry, spelled the way it is authored. A collection whose schema offers only a list (see
80
+ * `withDeprecated`) is stored as the record keyed by its members, so the example writes its members
81
+ * as that list, and a reader copying it never learns the deprecated record.
82
+ */
83
+ function authoredExample({ item, fields }) {
84
+ const body = item.toJSON();
85
+ const properties = fields?.properties;
86
+ if (!properties) return { [item.path]: body };
87
+ const authored = { ...body };
88
+ for (const [field, value] of Object.entries(body)) if (properties[field]?.type === "array" && value && typeof value === "object" && !Array.isArray(value)) authored[field] = Object.values(value);
89
+ return { [item.path]: authored };
90
+ }
78
91
  /** Every owned kind's authoring shape as signatures, with a real example where the config has one. */
79
92
  function authoringSignatures(config, { depth, memberDepth = depth } = {}) {
80
93
  return config.ownedKinds().flatMap((kind) => {
@@ -87,14 +100,18 @@ function authoringSignatures(config, { depth, memberDepth = depth } = {}) {
87
100
  if (signature) collections[field] = singleton ? `one section, no key (\`sub-update/${field}\`): ${signature}` : signature;
88
101
  }
89
102
  const first = config.list(kind)[0];
103
+ const create = dataSchemaOf(config, entity.schemas.create);
90
104
  return [{
91
105
  kind,
92
106
  label: entity.label,
93
107
  refHelper: entity.entityName,
94
- create: authoringSignature(dataSchemaOf(config, entity.schemas.create), { depth }),
108
+ create: authoringSignature(create, { depth }),
95
109
  collections,
96
110
  group: authoringSignature(entity.groupCreateSchema, { depth }),
97
- example: first ? { [first.path]: first.toJSON() } : void 0
111
+ example: first ? authoredExample({
112
+ item: first,
113
+ fields: jsonSchema(create)
114
+ }) : void 0
98
115
  }];
99
116
  });
100
117
  }
@@ -137,14 +154,18 @@ function authoringReference(config) {
137
154
  collections[field] = jsonSchema(dataSchemaOf(config, entity.schemas[`sub-create/${field}`]));
138
155
  }
139
156
  const first = config.list(kind)[0];
157
+ const fields = jsonSchema(dataSchemaOf(config, entity.schemas.create));
140
158
  return [{
141
159
  kind,
142
160
  label: entity.label,
143
161
  refHelper: entity.entityName,
144
- fields: jsonSchema(dataSchemaOf(config, entity.schemas.create)),
162
+ fields,
145
163
  collections,
146
164
  groupFields: jsonSchema(entity.groupCreateSchema),
147
- example: first ? { [first.path]: first.toJSON() } : void 0
165
+ example: first ? authoredExample({
166
+ item: first,
167
+ fields
168
+ }) : void 0
148
169
  }];
149
170
  }) };
150
171
  }
@@ -36,6 +36,9 @@ type SubEntityClass<F extends z.ZodTypeAny = z.ZodTypeAny> = CollectionSchema<F>
36
36
  * caller, so key and value can't drift, two members with the same identity can't both exist, and
37
37
  * an edit that changes the identity moves the entry. Omit when the key is arbitrary. */
38
38
  readonly key?: (member: unknown) => string;
39
+ /** The collection is authored as a list of its members, which carry their own `key`. The keyed
40
+ * record it is stored as is still read but no longer offered: see {@link orList}. */
41
+ readonly listed?: true;
39
42
  /** Whether a member DECLARES nothing — a style rule whose every layer bag is empty. Asked only
40
43
  * when a \`sub-update\` would bring the member into being: a delta made entirely of removals has
41
44
  * nothing to remove from a member that is not there, so creating one files an entry that emits
@@ -143,6 +146,7 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
143
146
  */
144
147
  base?: z.ZodTypeAny;
145
148
  key?: (member: unknown) => string;
149
+ listed?: true;
146
150
  vacant?: (member: unknown) => boolean;
147
151
  schemas?: UserSchemas;
148
152
  namespaces?: string;
@@ -161,6 +165,9 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
161
165
  * Two members with the same key are one member, the later one, as the key derivation on write
162
166
  * already treats a record whose keys disagree with their members — unless `merge` says how the two
163
167
  * combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
168
+ *
169
+ * A collection declared `listed` still takes the keyed record but offers only the list, see
170
+ * {@link withDeprecated}.
164
171
  */
165
172
  declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F>, {
166
173
  merge
@@ -1,6 +1,7 @@
1
1
  import { kindOf, memberOf, memberRef, pathOf } from "./utils/refs.js";
2
2
  import { resolveSchema } from "./schemas.js";
3
3
  import { titleCase } from "./registry.js";
4
+ import { withDeprecated } from "./utils/deprecated.js";
4
5
  import { z } from "zod";
5
6
  //#region src/framework/defineSubEntity.ts
6
7
  /**
@@ -78,6 +79,7 @@ function defineSubEntity(spec) {
78
79
  fields: spec.fields,
79
80
  base: spec.base,
80
81
  key: spec.key,
82
+ ...spec.listed ? { listed: true } : {},
81
83
  vacant: spec.vacant,
82
84
  schemas: spec.schemas,
83
85
  namespaces: spec.namespaces,
@@ -126,6 +128,9 @@ function defineSubEntity(spec) {
126
128
  * Two members with the same key are one member, the later one, as the key derivation on write
127
129
  * already treats a record whose keys disagree with their members — unless `merge` says how the two
128
130
  * combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
131
+ *
132
+ * A collection declared `listed` still takes the keyed record but offers only the list, see
133
+ * {@link withDeprecated}.
129
134
  */
130
135
  function orList(collection, { merge } = {}) {
131
136
  const key = collection.key;
@@ -142,6 +147,10 @@ function orList(collection, { merge } = {}) {
142
147
  }
143
148
  return keyed;
144
149
  });
150
+ if (collection.listed) return withDeprecated({
151
+ offered: listed,
152
+ deprecated: collection
153
+ });
145
154
  return z.union([listed, collection]);
146
155
  }
147
156
  //#endregion
@@ -0,0 +1,48 @@
1
+ import { Config } from "./Config.js";
2
+
3
+ //#region src/framework/interaction-conditions.d.ts
4
+ /** One interaction a rule keys on: the bag key, the modifier it resolves to, and whose it is. */
5
+ interface InteractionCondition {
6
+ /** As written in `when`: `_hover`. */
7
+ readonly key: string;
8
+ /** The modifier's path: `state/hover`. */
9
+ readonly modifier: string;
10
+ /** The layer whose interaction it is — the root for `true`. */
11
+ readonly layer: string;
12
+ }
13
+ /** Whether `modifier` (a path or leaf) can be a rule's interaction condition. */
14
+ declare function isInteractionModifier(config: Config, modifier: string): boolean;
15
+ /** The `when` key an interaction modifier is written under: its leaf, `_hover`. */
16
+ declare function interactionKey(modifier: string): string;
17
+ /** Every interaction modifier the config declares, by path. */
18
+ declare function interactionModifiers(config: Config): ReadonlySet<string>;
19
+ /**
20
+ * A rule's condition split in two: the props and states the render tests, and the interactions the
21
+ * stylesheet (or, for what CSS can't do, the module's own event tracking) holds it under.
22
+ */
23
+ declare function whenParts({
24
+ config,
25
+ when,
26
+ layers
27
+ }: {
28
+ config: Config;
29
+ when: Readonly<Record<string, unknown>> | null | undefined; /** The component's layers, for the root an interaction's `true` names. */
30
+ layers?: Readonly<Record<string, unknown>>;
31
+ }): {
32
+ props: Record<string, unknown>;
33
+ interactions: InteractionCondition[];
34
+ };
35
+ /** Whether `when` names any interaction — what tells the readers a rule holds only under one. */
36
+ declare function hasInteraction(config: Config, when: Readonly<Record<string, unknown>> | null | undefined): boolean;
37
+ /** One interaction a component's rules key on, with the selector that tells whether it holds. */
38
+ interface ComponentInteraction extends InteractionCondition {
39
+ /** The modifier's own selector, without its leading `&` (`:hover`): what `element.matches` asks. */
40
+ readonly selector: string;
41
+ }
42
+ /**
43
+ * Every interaction a component's rules key on, once per key and layer — what a surface that renders
44
+ * the component from its config, with no module to track them, watches on the live element.
45
+ */
46
+ declare function componentInteractions(config: Config, path: string): ComponentInteraction[];
47
+ //#endregion
48
+ export { ComponentInteraction, InteractionCondition, componentInteractions, hasInteraction, interactionKey, interactionModifiers, isInteractionModifier, whenParts };
@@ -0,0 +1,104 @@
1
+ import { modifierCategory } from "../entities/system/modifier-axis.js";
2
+ import { rootLayerOf } from "../entities/system/element.js";
3
+ import { createSliceMemo } from "./memo.js";
4
+ //#region src/framework/interaction-conditions.ts
5
+ /**
6
+ * A style rule's interaction conditions: `when: { _hover: true }`, "while the component is hovered".
7
+ *
8
+ * A rule's `when` names props and states, which the render knows. An interaction is the browser's —
9
+ * the pointer is over an element, it is pressed, it has focus — and a rule may key on one the same
10
+ * way, so the rule's whole reach (styles, an instance's props, content) applies while it holds. The
11
+ * value names whose interaction it is: `true` is the component's own root, and a layer name is that
12
+ * layer (`_hover: 'closeButton'`, "while the close button is hovered").
13
+ *
14
+ * Only an interaction modifier qualifies: a selector modifier that isn't a mode and names a state of
15
+ * the element itself (`:hover`, `[aria-checked="true"]`), never a pseudo-element (`::placeholder`)
16
+ * or a relation to another element (`[data-group]:hover &`).
17
+ */
18
+ /** Whether `modifier` (a path or leaf) can be a rule's interaction condition. */
19
+ function isInteractionModifier(config, modifier) {
20
+ return interactionModifiers(config).has(config.resolve("modifier", modifier)?.path ?? "");
21
+ }
22
+ /** The `when` key an interaction modifier is written under: its leaf, `_hover`. */
23
+ function interactionKey(modifier) {
24
+ return `_${modifier.slice(modifier.lastIndexOf("/") + 1)}`;
25
+ }
26
+ /** Every interaction modifier the config declares, by path. */
27
+ function interactionModifiers(config) {
28
+ return interactionMemo(config);
29
+ }
30
+ const interactionMemo = createSliceMemo({
31
+ kinds: ["modifier"],
32
+ compute: (config) => {
33
+ const paths = /* @__PURE__ */ new Set();
34
+ for (const modifier of config.list("modifier")) {
35
+ const { type, value } = modifier.toJSON() ?? {};
36
+ if (type !== "selector" || typeof value !== "string") continue;
37
+ if (modifierCategory(config, modifier.path) === "mode") continue;
38
+ const own = value.trim().replace(/^&/, "");
39
+ if (own.includes("::") || own.includes("&") || /\s/.test(own)) continue;
40
+ paths.add(modifier.path);
41
+ }
42
+ return paths;
43
+ }
44
+ });
45
+ /**
46
+ * A rule's condition split in two: the props and states the render tests, and the interactions the
47
+ * stylesheet (or, for what CSS can't do, the module's own event tracking) holds it under.
48
+ */
49
+ function whenParts({ config, when, layers }) {
50
+ const props = {};
51
+ const interactions = [];
52
+ const root = layers ? rootLayerOf(layers)[0] : "root";
53
+ for (const [key, value] of Object.entries(when ?? {})) {
54
+ const modifier = key.startsWith("_") ? config.resolve("modifier", key.slice(1)) : void 0;
55
+ if (modifier && interactionModifiers(config).has(modifier.path)) {
56
+ if (value === false || value === "false" || value == null) continue;
57
+ interactions.push({
58
+ key,
59
+ modifier: modifier.path,
60
+ layer: value === true || value === "true" ? root : String(value)
61
+ });
62
+ continue;
63
+ }
64
+ props[key] = value;
65
+ }
66
+ return {
67
+ props,
68
+ interactions
69
+ };
70
+ }
71
+ /** Whether `when` names any interaction — what tells the readers a rule holds only under one. */
72
+ function hasInteraction(config, when) {
73
+ return whenParts({
74
+ config,
75
+ when
76
+ }).interactions.length > 0;
77
+ }
78
+ /**
79
+ * Every interaction a component's rules key on, once per key and layer — what a surface that renders
80
+ * the component from its config, with no module to track them, watches on the live element.
81
+ */
82
+ function componentInteractions(config, path) {
83
+ const body = config.resolve("component", path)?.toJSON();
84
+ const out = [];
85
+ for (const rule of Object.values(body?.styles ?? {})) {
86
+ const { interactions } = whenParts({
87
+ config,
88
+ when: rule.when,
89
+ layers: body?.layers
90
+ });
91
+ for (const interaction of interactions) {
92
+ if (out.some((seen) => seen.key === interaction.key && seen.layer === interaction.layer)) continue;
93
+ const value = (config.resolve("modifier", interaction.modifier)?.toJSON())?.value;
94
+ if (typeof value !== "string") continue;
95
+ out.push({
96
+ ...interaction,
97
+ selector: value.trim().replace(/^&/, "")
98
+ });
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+ //#endregion
104
+ export { componentInteractions, hasInteraction, interactionKey, interactionModifiers, isInteractionModifier, whenParts };
@@ -2,7 +2,11 @@ import { Config } from "./Config.js";
2
2
 
3
3
  //#region src/framework/layer-styles.d.ts
4
4
  /** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
5
- * condition is canonicalized into the rule's key and a variant value is a string there. */
5
+ * condition is canonicalized into the rule's key and a variant value is a string there.
6
+ *
7
+ * An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
8
+ * `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
9
+ * hovered, so at rest the rule is out. */
6
10
  declare function ruleApplies(when: Record<string, unknown> | null | undefined, state: Record<string, unknown>): boolean;
7
11
  interface LayerStyleOptions {
8
12
  /**
@@ -2,9 +2,20 @@ import { isRef, refLeaf } from "./utils/refs.js";
2
2
  import { authoredBag } from "./projections.js";
3
3
  //#region src/framework/layer-styles.ts
4
4
  /** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
5
- * condition is canonicalized into the rule's key and a variant value is a string there. */
5
+ * condition is canonicalized into the rule's key and a variant value is a string there.
6
+ *
7
+ * An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
8
+ * `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
9
+ * hovered, so at rest the rule is out. */
6
10
  function ruleApplies(when, state) {
7
- return Object.entries(when ?? {}).every(([prop, value]) => String(state[prop]) === String(refLeaf(value)));
11
+ return Object.entries(when ?? {}).every(([prop, value]) => prop.startsWith("_") ? interactionHolds(state, prop, value) : String(state[prop]) === String(refLeaf(value)));
12
+ }
13
+ /** Whether an interaction holds: forced for the whole component (`_hover: true`, a surface's pin), or
14
+ * reported for the layer the condition names (`'_hover@close': true`, a live pointer). */
15
+ function interactionHolds(state, key, value) {
16
+ const on = (held) => held === true || held === "true";
17
+ if (on(state[key])) return true;
18
+ return on(state[`${key}@${value === true || value === "true" ? "root" : String(value)}`]);
8
19
  }
9
20
  /**
10
21
  * Merge a bag over another, flattening any forced modifier block as it goes.
@@ -14,15 +25,17 @@ function ruleApplies(when, state) {
14
25
  * with" for a panel and a matrix cell, and a picker's options are leaves.
15
26
  */
16
27
  function mergeBag(into, bag, forced) {
28
+ const forcedBlocks = [];
17
29
  for (const [key, value] of Object.entries(bag)) {
18
30
  const modifier = key.startsWith("_") ? key.slice(1) : void 0;
19
31
  if (modifier && value && typeof value === "object" && !isRef(value)) {
20
- if (forced.includes(modifier)) mergeBag(into, value, forced);
32
+ if (forced.includes(modifier)) forcedBlocks.push(value);
21
33
  else into[key] = value;
22
34
  continue;
23
35
  }
24
36
  into[key] = value;
25
37
  }
38
+ for (const block of forcedBlocks) mergeBag(into, block, forced);
26
39
  }
27
40
  /**
28
41
  * The resolved style bag per layer, for the props an instance was given.
@@ -36,7 +49,8 @@ function layerStyles(config, path, props = {}, options = {}) {
36
49
  ...defaultProps,
37
50
  ...props
38
51
  };
39
- const forced = options.forced ?? [];
52
+ const forced = (options.forced ?? []).map((name) => name.replace(/^_/, ""));
53
+ for (const name of forced) state[`_${name}`] = true;
40
54
  const applicable = Object.values(styles).filter((rule) => ruleApplies(rule.when, state)).sort((left, right) => Object.keys(left.when ?? {}).length - Object.keys(right.when ?? {}).length);
41
55
  const out = {};
42
56
  for (const rule of applicable) for (const [layer, bag] of Object.entries(rule.layers ?? {})) {
@@ -4,12 +4,13 @@ import { isOklchColor, renderOklch } from "../entities/system/color.js";
4
4
  import { isComposedValue, renderComposedValue } from "../entities/system/composed-values.js";
5
5
  import { modifierCategory } from "../entities/system/modifier-axis.js";
6
6
  import { forwardedPropName, forwardsOf, rootLayerOf } from "../entities/system/element.js";
7
+ import { createSliceMemo } from "./memo.js";
7
8
  import { fontFamilyStack } from "../entities/system/Font.js";
8
9
  import { listedIn } from "./utils/enumerated.js";
9
10
  import "../entities/system/Modifier.js";
10
- import { createSliceMemo } from "./memo.js";
11
11
  import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
12
12
  import { StyleProperty, leavesOfStylePropValue, numberUnitRefusal } from "../entities/system/StyleProperty.js";
13
+ import { componentStates } from "./component-states.js";
13
14
  import { isConditionalPreview, storedPreview, unconditionalPreviewProps } from "./preview-store.js";
14
15
  //#region src/framework/projections.ts
15
16
  /**
@@ -797,6 +798,10 @@ function authoredBag(config, bag) {
797
798
  continue;
798
799
  }
799
800
  }
801
+ if (isComposedValue(value)) {
802
+ out[key] = value;
803
+ continue;
804
+ }
800
805
  if (value && typeof value === "object" && !Array.isArray(value) && !isRef(value)) {
801
806
  const inner = authoredBag(config, value);
802
807
  out[key] = inner;
@@ -1792,8 +1797,34 @@ function validateComponentProps(config, componentPath, props) {
1792
1797
  value: props[LAYER_PROPS],
1793
1798
  where: componentPath
1794
1799
  });
1800
+ const states = Object.fromEntries(componentStates(config, componentPath).map((state) => [state.name, {
1801
+ type: state.type,
1802
+ value: state.values
1803
+ }]));
1804
+ const checkState = (name, value) => {
1805
+ if (name.startsWith("_")) {
1806
+ if (modifierCategory(config, name.slice(1)) !== "state") errors.push(`\`${name}\` is not a state modifier`);
1807
+ else if (!["true", "false"].includes(String(value))) errors.push(`\`${name}\` is \`true\` or \`false\`, not \`${String(value)}\``);
1808
+ return;
1809
+ }
1810
+ const state = states[name];
1811
+ if (!state) {
1812
+ errors.push(`\`${name}\` is not a state`);
1813
+ return;
1814
+ }
1815
+ const allowed = state.type === "variant" && Array.isArray(state.value) ? state.value.map(String) : ["true", "false"];
1816
+ if (!allowed.includes(String(value))) errors.push(`state \`${name}\` is ${allowed.map((v) => `\`${v}\``).join(" or ")}, not \`${String(value)}\``);
1817
+ };
1795
1818
  for (const [name, value] of Object.entries(props)) {
1796
1819
  if (name === "layerProps") continue;
1820
+ if (name === "__udsForceStates") {
1821
+ for (const [pinned, pin] of Object.entries(value ?? {})) checkState(pinned, pin);
1822
+ continue;
1823
+ }
1824
+ if (states[name]) {
1825
+ errors.push(`\`${name}\` is a state, not a prop — pin it as \`__udsForceStates: { ${name}: … }\``);
1826
+ continue;
1827
+ }
1797
1828
  const decl = declared[name];
1798
1829
  if (!decl) {
1799
1830
  errors.push(`unknown prop \`${name}\``);
@@ -1957,7 +1988,13 @@ function previewMatrix(config, path) {
1957
1988
  props[prop] = values;
1958
1989
  };
1959
1990
  for (const rule of Object.values(styles)) {
1960
- for (const [prop, stored] of Object.entries(rule.when ?? {})) nameAxisPoint(prop, stored);
1991
+ for (const [prop, stored] of Object.entries(rule.when ?? {})) {
1992
+ if (prop.startsWith("_")) {
1993
+ if (modifierCategory(config, prop.slice(1)) === "state") modifiers.add(prop);
1994
+ continue;
1995
+ }
1996
+ nameAxisPoint(prop, stored);
1997
+ }
1961
1998
  for (const bag of Object.values(rule.layers ?? {})) for (const key of Object.keys(bag ?? {})) {
1962
1999
  if (!key.startsWith("_")) continue;
1963
2000
  if (modifierCategory(config, key.slice(1)) === "state") modifiers.add(key);
@@ -1975,6 +2012,10 @@ function previewMatrix(config, path) {
1975
2012
  if (values.length > 0) props[name] = values;
1976
2013
  }
1977
2014
  }
2015
+ for (const state of componentStates(config, path)) {
2016
+ if (state.type === "boolean") props[state.name] = declaredFirst(["false", "true"], props[state.name] ?? []);
2017
+ if (state.type === "variant") props[state.name] = declaredFirst([...state.values ?? []], props[state.name] ?? []);
2018
+ }
1978
2019
  return {
1979
2020
  props,
1980
2021
  modifiers: [...modifiers]
@@ -1995,6 +2036,7 @@ function declaredFirst(declared, ruleNamed) {
1995
2036
  function propValueFromAxis(config, path, prop, value) {
1996
2037
  const declared = resolveComponentProps(config, path)[prop];
1997
2038
  if (declared?.type === "boolean") return value === "true";
2039
+ if (componentStates(config, path).some((state) => state.name === prop && state.type === "boolean")) return value === "true";
1998
2040
  if (declared?.type === "number") {
1999
2041
  const parsed = Number(value);
2000
2042
  return Number.isFinite(parsed) ? parsed : value;
@@ -1,6 +1,7 @@
1
1
  import { isRef, kindOf, memberOf, pathOf, ref, splitRef } from "./utils/refs.js";
2
2
  import { renderedElement } from "../entities/system/element.js";
3
3
  import { createSliceMemo } from "./memo.js";
4
+ import { componentStateDefaults, componentStates } from "./component-states.js";
4
5
  import { unconditionalPreviewProps } from "./preview-store.js";
5
6
  import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, refListOf, resolveComponentProps, stylePropertyPathFor } from "./projections.js";
6
7
  import { slotHostLayer } from "./slot-host.js";
@@ -8,7 +9,7 @@ import { forwardRoutes, forwardedLayers, intoName, isValueSlot, renderBindingsOf
8
9
  import { withRequiredContentHidden } from "../entities/system/required-content-layers.js";
9
10
  import { inlineMotionProps, jsMotionPresets } from "../entities/system/motion-presets.js";
10
11
  import { graftPreviewElements } from "../spec/preview-elements.js";
11
- import { componentLayerClass, componentPropClasses, componentRuleClasses, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
12
+ import { componentLayerClass, componentPropClasses, componentRuleClasses, componentStateAttributes, forcedModifierAttributes, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
12
13
  //#region src/framework/render-spec.ts
13
14
  /** A repeat source that is one prop binding: `{ "$state": "/stops" }`, settled by the cell alone. */
14
15
  function isStateSource(source) {
@@ -1202,11 +1203,14 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1202
1203
  const filled = {};
1203
1204
  for (const [from, fills] of Object.entries(instanceSlots ?? {})) if (fills.length > 0 && instanceProps[from] === void 0) filled[from] = true;
1204
1205
  const declaredDefaults = declaredPropDefaults(body.props);
1206
+ const pins = instanceProps["__udsForceStates"] ?? {};
1205
1207
  const state = {
1206
1208
  ...declaredDefaults,
1207
1209
  ...body.defaultProps,
1210
+ ...componentStateDefaults(config, path),
1208
1211
  ...filled,
1209
- ...instanceProps
1212
+ ...instanceProps,
1213
+ ...Object.fromEntries(Object.entries(pins).map(([key, value]) => key.startsWith("_") ? [key, value === true || value === "true"] : [key, value]))
1210
1214
  };
1211
1215
  const consumed = consumedProps(config, path, body);
1212
1216
  const { byLayer: targets } = slotTargets(config, path, body);
@@ -1222,8 +1226,12 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1222
1226
  ...body.defaultProps ?? {},
1223
1227
  ...instanceProps
1224
1228
  };
1229
+ const stateNames = new Set(componentStates(config, path).map(({ name }) => name));
1225
1230
  const bag = {};
1226
- for (const [name, value] of Object.entries(supplied)) if (!consumed.has(name)) bag[name] = value;
1231
+ for (const [name, value] of Object.entries(supplied)) {
1232
+ if (name === "__udsForceStates") continue;
1233
+ if (!consumed.has(name) && !stateNames.has(name)) bag[name] = value;
1234
+ }
1227
1235
  const { className: styleClasses, rest } = splitStyleProps({
1228
1236
  config,
1229
1237
  bag,
@@ -1352,6 +1360,8 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1352
1360
  ...forwarded[layer],
1353
1361
  ...motionPreset !== void 0 ? withoutKey(presetProps[motionPreset], "reducedMotion") : {},
1354
1362
  ...Object.fromEntries((valueRoutes.get(layer) ?? []).filter(({ from }) => state[from] !== void 0).map(({ from, into }) => [into, state[from]])),
1363
+ ...isRoot ? componentStateAttributes(componentStates(config, path), state) : {},
1364
+ ...isRoot || type === void 0 || kindOf(type) !== "component" ? forcedModifierAttributes(pins, path) : {},
1355
1365
  ...isRoot ? rest : {},
1356
1366
  ...slot && slotValue !== void 0 ? { [intoName(slot.into)]: slotValue } : {},
1357
1367
  ...layerProps[layer],
@@ -1365,7 +1375,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1365
1375
  ...authoredProps(config, node.props),
1366
1376
  [ANATOMY_MARKER]: key
1367
1377
  };
1368
- if (node.text !== void 0) props.children = node.text;
1378
+ if (node.text !== void 0 && props.children === void 0) props.children = node.text;
1369
1379
  for (const [name, value] of Object.entries(props)) {
1370
1380
  const settled = resolveValue(value, state, resolving);
1371
1381
  if (settled !== value) props[name] = settled;
@@ -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 = 20261012200606;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261013164300;
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 = 20261012200606;
16
+ const CURRENT_SCHEMA_VERSION = 20261013164300;
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 > 20261012200606) 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 > 20261013164300) 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 ?? 20261012200606;
105
+ const target = options?.target ?? 20261013164300;
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 ?? 20261012200606;
163
+ const target = options?.target ?? 20261013164300;
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 ?? 20261012200606;
182
+ const target = options?.target ?? 20261013164300;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A schema that still takes a deprecated form but no longer offers it.
3
+ *
4
+ * Parsing accepts `offered` or `deprecated`, so stored data and older config files keep loading.
5
+ * Rendered as JSON Schema, which is what tool signatures, `config/api`, the MCP's input schemas and
6
+ * the docs are built from, it shows only `offered`, so nothing that reads a schema learns the old
7
+ * form.
8
+ */
9
+ import { z } from 'zod';
10
+ /**
11
+ * `read` with every deprecated form shown in the JSON Schema it renders. The schema-version tripwire
12
+ * reads through this, since it guards everything a client accepts, offered or not.
13
+ */
14
+ export declare function includingDeprecated<T>(read: () => T): T;
15
+ export declare function withDeprecated<O extends z.ZodTypeAny, D extends z.ZodTypeAny>({ offered, deprecated, }: {
16
+ offered: O;
17
+ deprecated: D;
18
+ }): z.ZodUnion<readonly [O, D]>;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ function withDeprecated({ offered, deprecated }) {
3
+ const schema = z.union([offered, deprecated]);
4
+ const offeredOnly = () => {
5
+ const { $schema: _dialect, ...json } = z.toJSONSchema(offered, { io: "input" });
6
+ return json;
7
+ };
8
+ schema._zod.toJSONSchema = () => offeredOnly();
9
+ return schema;
10
+ }
11
+ //#endregion
12
+ export { withDeprecated };
@@ -1,6 +1,9 @@
1
1
  import { kindOf, memberOf, pathOf, splitRef } from "./utils/refs.js";
2
+ import { modifierCategory } from "../entities/system/modifier-axis.js";
2
3
  import { childrenPolicy } from "../entities/system/element.js";
4
+ import { UDS_FORCE_STATES, componentStates } from "./component-states.js";
3
5
  import { LAYER_PROPS, layerPropsErrors, resolveComponentProps } from "./projections.js";
6
+ import "./class-names.js";
4
7
  import { slotOf } from "../spec/slot.js";
5
8
  import { refResolves } from "./ref-integrity.js";
6
9
  //#region src/framework/validate-spec.ts
@@ -28,8 +31,19 @@ function walk(config, node, path, errors) {
28
31
  errors.push(`${path}: unknown ${kind} "${named}"`);
29
32
  } else if (kind === "component") {
30
33
  const props = resolveComponentProps(config, target);
34
+ const states = new Set(componentStates(config, target).map(({ name }) => name));
31
35
  for (const name of Object.keys(node.props ?? {})) {
32
36
  if (name === "layerProps") continue;
37
+ if (name === "__udsForceStates") {
38
+ for (const pinned of Object.keys(node.props?.["__udsForceStates"] ?? {})) if (pinned.startsWith("_")) {
39
+ if (modifierCategory(config, pinned.slice(1)) !== "state") errors.push(`${path}: "${pinned}" is not a state modifier`);
40
+ } else if (!states.has(pinned)) errors.push(`${path}: "${pinned}" is not a state of "${node.type}"`);
41
+ continue;
42
+ }
43
+ if (states.has(name)) {
44
+ errors.push(`${path}: "${name}" is a state of "${node.type}", not a prop — pin it as ${UDS_FORCE_STATES}: { ${name}: … }`);
45
+ continue;
46
+ }
33
47
  if (!(name in props)) errors.push(`${path}: unknown prop "${name}" on "${node.type}"`);
34
48
  }
35
49
  errors.push(...layerPropsErrors({