@yahoo/uds-create-config 5.60.0 → 5.60.2

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.
@@ -2501,7 +2501,7 @@ count: z.number().int().min(1).max(50).optional() }).prefault({})
2501
2501
  }),
2502
2502
  readOnly: false,
2503
2503
  label: "Set layer styles",
2504
- description: "Set or clear style properties on a component's layers for the instances a reach covers, the way the component editor's \"Apply to\" does. Each change names a `layer`, a style-prop `prop` and a `value` (`null` clears it). `when` pins variant, boolean or state props (`{ \"variant\": \"secondary\" }`) and the change lands in the rule with exactly that condition; leave it out for every instance. `state` is the state the change is made in (`\"hover\"`): an interaction becomes part of the rule's condition, any other modifier nests as a block in the layer's bag. `modifiers` nest further blocks, outermost first (`[\"dark\"]` for dark mode). Writes one `sub-update/styles` per rule touched, creating a rule its condition has none for.",
2504
+ description: "Set or clear style properties on a component's layers for the instances a reach covers, the way the component editor's \"Apply to\" does. Each change names a `layer`, a style-prop `prop` and a `value` (`null` clears it). `when` pins variant, boolean or state props (`{ \"variant\": \"secondary\" }`) and the change lands in the rule with exactly that condition; leave it out for every instance. `state` is the state the change is made in (`\"hover\"`): an interaction becomes part of the rule's condition, any other modifier nests as a block in the layer's bag. `modifiers` nest further blocks, outermost first (`[\"dark\"]` for dark mode). Writes one `sub-update/styles` per rule touched, creating a rule its condition has none for. A clear also removes the value from a broader rule that gives those instances it, and writes it back on the conditions that rule covers and the clear does not, so only the cleared instances change.",
2505
2505
  scope: "item",
2506
2506
  title: (entity) => `Set ${entity} layer styles`,
2507
2507
  handler: (input, config) => {
@@ -2518,6 +2518,7 @@ count: z.number().int().min(1).max(50).optional() }).prefault({})
2518
2518
  })
2519
2519
  });
2520
2520
  return config.run(styleSetEdits({
2521
+ config,
2521
2522
  path,
2522
2523
  changes,
2523
2524
  isInteraction: (modifier) => isInteractionModifier(config, modifier)
@@ -17,7 +17,7 @@ import { contentKey } from "../../framework/utils/content-key.js";
17
17
  * The shared compiler/runtime contract. Bump whenever codegen can produce different module bytes
18
18
  * from identical config-visible inputs (shim contracts, entry shape, transforms, and so on).
19
19
  */
20
- const COMPONENT_MODULE_BUILD_VERSION = 7;
20
+ const COMPONENT_MODULE_BUILD_VERSION = 8;
21
21
  /**
22
22
  * Generated web runtime inputs shared by every component module. These are intentionally separate
23
23
  * from the per-file closure: editing an unrelated component should not invalidate this module, but
@@ -1,4 +1,6 @@
1
1
  import { ConfigEdit } from "../../framework/config-op.js";
2
+ import { Config } from "../../framework/Config.js";
3
+
2
4
  //#region src/entities/system/component-style-set.d.ts
3
5
  /**
4
6
  * One style property set or cleared on one layer of a component, and the cells it reaches.
@@ -17,6 +19,12 @@ interface StyleSetChange {
17
19
  readonly when?: Readonly<Record<string, string | number | boolean>>;
18
20
  readonly state?: string;
19
21
  readonly modifiers?: readonly string[];
22
+ /**
23
+ * For a clear: remove only this rule's own entry, and leave the broader rules that give the same
24
+ * instances the value alone. A set over several cells releases the overrides inside its reach this
25
+ * way, since the set itself is what those cells should show.
26
+ */
27
+ readonly ownRuleOnly?: boolean;
20
28
  }
21
29
  /**
22
30
  * The `sub-update/styles` entries a set of style changes write: one per rule they touch, each a delta
@@ -25,12 +33,21 @@ interface StyleSetChange {
25
33
  * A rule's key is its condition, so a change at a condition with no rule yet creates one; `when` is
26
34
  * carried on every entry for that reason. A cleared interaction change also clears the address the
27
35
  * same change would have had as a nested block, so a value stored that way doesn't outlive the clear.
36
+ *
37
+ * With `config`, a clear also reaches the broader rules that give the cleared cells their value: a
38
+ * clear at `{ variant: 'tertiary', size: 'sm' }` removes the value from the `variant=tertiary` rule
39
+ * that holds it, and writes it back on the conditions that rule covers and the clear doesn't
40
+ * (`size=xs`, `size=md`, `size=lg` under tertiary), so only the cleared cells change. A broader rule
41
+ * whose split couldn't keep every instance it should, including one that leaves a prop unset, is
42
+ * left alone (see {@link complementOf}). A clear marked `ownRuleOnly` never reaches past its own rule.
28
43
  */
29
44
  declare function styleSetEdits({
45
+ config,
30
46
  path,
31
47
  changes,
32
48
  isInteraction
33
49
  }: {
50
+ config?: Config;
34
51
  path: string;
35
52
  changes: readonly StyleSetChange[]; /** Whether a state is an interaction a rule can be conditioned on, by modifier leaf. */
36
53
  isInteraction: (modifier: string) => boolean;
@@ -1,5 +1,7 @@
1
- import { canonicalCondition } from "../../framework/utils/refs.js";
1
+ import { canonicalCondition, refLeaf } from "../../framework/utils/refs.js";
2
2
  import { reject } from "../../framework/rejection.js";
3
+ import { propValueDomain } from "../../framework/projections.js";
4
+ import { ruleSpecificity } from "../../framework/layer-styles.js";
3
5
  //#region src/entities/system/component-style-set.ts
4
6
  /** A modifier as a style bag keys it: its leaf with a leading `_`. */
5
7
  function bagKey(modifier) {
@@ -15,6 +17,83 @@ function addressOf({ layer, modifiers, prop }) {
15
17
  prop
16
18
  ].join(".");
17
19
  }
20
+ /** A condition value as rules compare it: a value ref reads as its leaf. */
21
+ function leafKey(value) {
22
+ return String(refLeaf(value));
23
+ }
24
+ /** Whether every pin of `inner` holds in `outer`. */
25
+ function pinsHold({ inner, outer }) {
26
+ return Object.entries(inner).every(([axis, value]) => axis in outer && leafKey(outer[axis]) === leafKey(value));
27
+ }
28
+ /** Whether two conditions can hold at once: no axis is pinned to different values. */
29
+ function compatible(a, b) {
30
+ return Object.entries(a).every(([axis, value]) => !(axis in b) || leafKey(b[axis]) === leafKey(value));
31
+ }
32
+ /** The `_`-keyed entries of a condition: the states it holds under, which a split never crosses. */
33
+ function statesOf(condition) {
34
+ return canonicalCondition(Object.fromEntries(Object.entries(condition).filter(([axis]) => axis.startsWith("_"))));
35
+ }
36
+ function axisPins(condition) {
37
+ return Object.fromEntries(Object.entries(condition).filter(([axis]) => !axis.startsWith("_")));
38
+ }
39
+ function valueAt({ rule, address }) {
40
+ let at = rule;
41
+ for (const segment of address.split(".")) {
42
+ if (typeof at !== "object" || at === null) return void 0;
43
+ at = at[segment];
44
+ }
45
+ return at;
46
+ }
47
+ /**
48
+ * Every value a variant or boolean prop or state can take, or `undefined` when a split on it can't
49
+ * keep every instance: the values can't be listed, or a variant with no default can be left unset,
50
+ * which no condition on its values matches. A boolean always splits, since an unset one holds its
51
+ * `false` condition (`conditionHolds`).
52
+ */
53
+ function axisDomains({ config, path }) {
54
+ const body = config.resolve("component", path)?.toJSON();
55
+ return (axis) => {
56
+ const state = body?.states?.[axis];
57
+ const decl = body?.props?.[axis] ?? state;
58
+ if (decl?.type === "boolean") return [true, false];
59
+ const always = body?.defaultProps?.[axis] !== void 0 || state?.default !== void 0 || body?.props?.[axis]?.required === true;
60
+ if (decl?.type !== "variant" || !always) return void 0;
61
+ const leaves = propValueDomain(config, decl)?.leaves;
62
+ return leaves && leaves.length > 0 ? leaves : void 0;
63
+ };
64
+ }
65
+ /**
66
+ * The conditions under `within` that `selection` doesn't reach, as few as an axis-by-axis split
67
+ * gives: each axis a selected condition pins and `within` doesn't is split into its values, and a
68
+ * branch the selection covers whole, or doesn't touch, stops there. `undefined` when an axis to split
69
+ * on can't keep every instance the selection doesn't reach (see {@link axisDomains}).
70
+ */
71
+ function complementOf({ within, selection, domainOf }) {
72
+ const touching = selection.filter((selected) => compatible(selected, within));
73
+ if (touching.length === 0) return [within];
74
+ if (touching.some((selected) => pinsHold({
75
+ inner: selected,
76
+ outer: within
77
+ }))) return [];
78
+ const axis = touching.flatMap((selected) => Object.keys(selected)).find((candidate) => !(candidate in within));
79
+ if (axis === void 0) return [];
80
+ const domain = domainOf(axis);
81
+ if (!domain) return void 0;
82
+ const out = [];
83
+ for (const value of domain) {
84
+ const rest = complementOf({
85
+ within: {
86
+ ...within,
87
+ [axis]: value
88
+ },
89
+ selection: touching,
90
+ domainOf
91
+ });
92
+ if (!rest) return void 0;
93
+ out.push(...rest);
94
+ }
95
+ return out;
96
+ }
18
97
  /**
19
98
  * The `sub-update/styles` entries a set of style changes write: one per rule they touch, each a delta
20
99
  * of addresses, so nothing a change doesn't name is touched.
@@ -22,9 +101,17 @@ function addressOf({ layer, modifiers, prop }) {
22
101
  * A rule's key is its condition, so a change at a condition with no rule yet creates one; `when` is
23
102
  * carried on every entry for that reason. A cleared interaction change also clears the address the
24
103
  * same change would have had as a nested block, so a value stored that way doesn't outlive the clear.
104
+ *
105
+ * With `config`, a clear also reaches the broader rules that give the cleared cells their value: a
106
+ * clear at `{ variant: 'tertiary', size: 'sm' }` removes the value from the `variant=tertiary` rule
107
+ * that holds it, and writes it back on the conditions that rule covers and the clear doesn't
108
+ * (`size=xs`, `size=md`, `size=lg` under tertiary), so only the cleared cells change. A broader rule
109
+ * whose split couldn't keep every instance it should, including one that leaves a prop unset, is
110
+ * left alone (see {@link complementOf}). A clear marked `ownRuleOnly` never reaches past its own rule.
25
111
  */
26
- function styleSetEdits({ path, changes, isInteraction }) {
112
+ function styleSetEdits({ config, path, changes, isInteraction }) {
27
113
  const rules = /* @__PURE__ */ new Map();
114
+ const reaching = /* @__PURE__ */ new Set();
28
115
  const ruleAt = (condition) => {
29
116
  const when = Object.keys(condition).length > 0 ? { ...condition } : null;
30
117
  const key = canonicalCondition(when);
@@ -44,14 +131,17 @@ function styleSetEdits({ path, changes, isInteraction }) {
44
131
  const { layer, prop } = change;
45
132
  const clear = change.value === null;
46
133
  if (change.state && isInteraction(bagKey(change.state).slice(1))) {
47
- ruleAt({
134
+ const condition = {
48
135
  ...pins,
49
136
  [bagKey(change.state)]: true
50
- }).delta[addressOf({
137
+ };
138
+ const address = addressOf({
51
139
  layer,
52
140
  modifiers: nested,
53
141
  prop
54
- })] = change.value;
142
+ });
143
+ ruleAt(condition).delta[address] = change.value;
144
+ if (clear && !change.ownRuleOnly) reaching.add(`${canonicalCondition(condition)}\n${address}`);
55
145
  if (clear) ruleAt(pins).delta[addressOf({
56
146
  layer,
57
147
  modifiers: [change.state, ...nested],
@@ -59,13 +149,21 @@ function styleSetEdits({ path, changes, isInteraction }) {
59
149
  })] = null;
60
150
  continue;
61
151
  }
62
- const modifiers = change.state ? [change.state, ...nested] : nested;
63
- ruleAt(pins).delta[addressOf({
152
+ const address = addressOf({
64
153
  layer,
65
- modifiers,
154
+ modifiers: change.state ? [change.state, ...nested] : nested,
66
155
  prop
67
- })] = change.value;
156
+ });
157
+ ruleAt(pins).delta[address] = change.value;
158
+ if (clear && !change.ownRuleOnly) reaching.add(`${canonicalCondition(pins)}\n${address}`);
68
159
  }
160
+ if (config) releaseBroaderRules({
161
+ config,
162
+ path,
163
+ rules,
164
+ reaching,
165
+ ruleAt
166
+ });
69
167
  return [...rules].filter(([, rule]) => Object.keys(rule.delta).length > 0).map(([key, rule]) => ({
70
168
  kind: "component",
71
169
  operation: "sub-update",
@@ -79,6 +177,77 @@ function styleSetEdits({ path, changes, isInteraction }) {
79
177
  }));
80
178
  }
81
179
  /**
180
+ * Clears each cleared address from the stored rules broader than the clears that hold it, and writes
181
+ * the value back on the part of each such rule the clears don't cover. Clears of one address under
182
+ * the same states are one selection, so a clear over several cells splits a broader rule once.
183
+ */
184
+ function releaseBroaderRules({ config, path, rules, reaching, ruleAt }) {
185
+ const stored = (config.resolve("component", path)?.toJSON())?.styles;
186
+ if (!stored) return;
187
+ const selections = /* @__PURE__ */ new Map();
188
+ for (const [key, { when, delta }] of rules) for (const [address, value] of Object.entries(delta)) {
189
+ if (value !== null || !reaching.has(`${key}\n${address}`)) continue;
190
+ const condition = when ?? {};
191
+ const states = statesOf(condition);
192
+ const id = `${address}\n${states}`;
193
+ let selection = selections.get(id);
194
+ if (!selection) {
195
+ selection = {
196
+ address,
197
+ states,
198
+ conditions: []
199
+ };
200
+ selections.set(id, selection);
201
+ }
202
+ selection.conditions.push(axisPins(condition));
203
+ }
204
+ const domainOf = axisDomains({
205
+ config,
206
+ path
207
+ });
208
+ const written = (condition, address) => rules.get(canonicalCondition(condition))?.delta[address] !== void 0;
209
+ const ranked = Object.values(stored).map((rule, index) => ({
210
+ rule,
211
+ when: rule.when ?? {},
212
+ rank: [ruleSpecificity(rule.when), index]
213
+ }));
214
+ const outranks = (a, b) => a.rank[0] > b.rank[0] || a.rank[0] === b.rank[0] && a.rank[1] > b.rank[1];
215
+ for (const { address, states, conditions } of selections.values()) {
216
+ const holders = ranked.filter((entry) => statesOf(entry.when) === states && valueAt({
217
+ rule: entry.rule,
218
+ address
219
+ }) !== void 0);
220
+ for (const holder of holders) {
221
+ const { when } = holder;
222
+ if (written(when, address)) continue;
223
+ const pins = axisPins(when);
224
+ if (!conditions.some((outer) => pinsHold({
225
+ inner: pins,
226
+ outer
227
+ }))) continue;
228
+ const kept = complementOf({
229
+ within: pins,
230
+ selection: [...conditions, ...holders.filter((other) => outranks(other, holder)).map((other) => axisPins(other.when))],
231
+ domainOf
232
+ });
233
+ if (!kept) continue;
234
+ const value = valueAt({
235
+ rule: holder.rule,
236
+ address
237
+ });
238
+ const stateEntries = Object.fromEntries(Object.entries(when).filter(([axis]) => axis.startsWith("_")));
239
+ ruleAt(when).delta[address] = null;
240
+ for (const condition of kept) {
241
+ const at = {
242
+ ...condition,
243
+ ...stateEntries
244
+ };
245
+ if (!written(at, address)) ruleAt(at).delta[address] = value;
246
+ }
247
+ }
248
+ }
249
+ }
250
+ /**
82
251
  * Refuses a change that names a layer the component doesn't have, a prop the layer can't take, a
83
252
  * state or modifier the config doesn't register, or a pin no rule can be conditioned on: only a
84
253
  * variant or boolean prop, or a state, conditions a rule.
@@ -10,8 +10,7 @@ interface StoredRule {
10
10
  * unconditional `:where()` rules rank lower. Stable sorting preserves authored order among
11
11
  * conditions with different numbers of props. */
12
12
  declare function compareComponentRules(left: StoredRule, right: StoredRule): number;
13
- /** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
14
- * condition is canonicalized into the rule's key and a variant value is a string there.
13
+ /** Whether every prop a rule names holds the value it asks for ({@link conditionHolds}).
15
14
  *
16
15
  * An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
17
16
  * `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
@@ -29,14 +29,29 @@ import { resolveAppliedComposites } from "../entities/system/applied-composites.
29
29
  function compareComponentRules(left, right) {
30
30
  return Number(Object.keys(left.when ?? {}).length > 0) - Number(Object.keys(right.when ?? {}).length > 0);
31
31
  }
32
- /** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
33
- * condition is canonicalized into the rule's key and a variant value is a string there.
32
+ /**
33
+ * Whether a prop's value holds a condition's value. Compared as strings, because a condition is
34
+ * canonicalized into the rule's key and a variant value is a string there.
35
+ *
36
+ * A `false` condition also holds for a prop left unset, which renders as off: a rule written for a
37
+ * matrix cell's `loading: false` reaches an instance that never mentions `loading`. The emitted
38
+ * module tests the same way (`loading !== true`).
39
+ */
40
+ function conditionHolds({ actual, expected }) {
41
+ const leaf = refLeaf(expected);
42
+ if (leaf === false && actual == null) return true;
43
+ return String(actual) === String(leaf);
44
+ }
45
+ /** Whether every prop a rule names holds the value it asks for ({@link conditionHolds}).
34
46
  *
35
47
  * An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
36
48
  * `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
37
49
  * hovered, so at rest the rule is out. */
38
50
  function ruleApplies(when, state) {
39
- return Object.entries(when ?? {}).every(([prop, value]) => prop.startsWith("_") ? interactionHolds(state, prop, value) : String(state[prop]) === String(refLeaf(value)));
51
+ return Object.entries(when ?? {}).every(([prop, value]) => prop.startsWith("_") ? interactionHolds(state, prop, value) : conditionHolds({
52
+ actual: state[prop],
53
+ expected: value
54
+ }));
40
55
  }
41
56
  /** Whether an interaction holds: forced for the whole component (`_hover: true`, a surface's pin), or
42
57
  * reported for the layer the condition names (`'_hover@close': true`, a live pointer). */
@@ -138,4 +153,4 @@ function layerStyles(config, path, props = {}, options = {}) {
138
153
  return out;
139
154
  }
140
155
  //#endregion
141
- export { compareComponentRules, layerStyles, ruleApplies };
156
+ export { compareComponentRules, conditionHolds, layerStyles, ruleApplies, ruleSpecificity };
@@ -10,6 +10,7 @@ import { forwardRoutes, forwardedLayers, intoName, isValueSlot, renderBindingsOf
10
10
  import { withRequiredContentHidden } from "../entities/system/required-content-layers.js";
11
11
  import { inlineMotionProps, jsMotionPresets } from "../entities/system/motion-presets.js";
12
12
  import { graftPreviewElements } from "../spec/preview-elements.js";
13
+ import { conditionHolds } from "./layer-styles.js";
13
14
  import { componentLayerClass, componentPropClasses, componentRuleClasses, componentStateAttributes, forcedModifierAttributes, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
14
15
  //#region src/framework/render-spec.ts
15
16
  /** A repeat source that is one prop binding: `{ "$state": "/stops" }`, settled by the cell alone. */
@@ -1143,7 +1144,10 @@ function ruleKeysFor(config, path, layer, state, pins) {
1143
1144
  for (const rule of layerRules(config, path)) {
1144
1145
  const layerKeys = rule.keys.get(layer);
1145
1146
  if (!layerKeys) continue;
1146
- if (!rule.props.every(([prop, value]) => String(state[prop]) === String(value))) continue;
1147
+ if (!rule.props.every(([prop, value]) => conditionHolds({
1148
+ actual: state[prop],
1149
+ expected: value
1150
+ }))) continue;
1147
1151
  if (!rule.modifiers.every(([modifier, value]) => value === true && (pins[modifier] === true || pins[modifier] === "true"))) continue;
1148
1152
  for (const key of layerKeys) keys.add(key);
1149
1153
  }
@@ -1163,7 +1167,10 @@ function classesFor(config, path, layer, state, extra) {
1163
1167
  for (const rule of componentRuleClasses(config, path)) {
1164
1168
  if (rule.layer !== layer) continue;
1165
1169
  if (rule.composite) continue;
1166
- if (Object.entries(rule.when).every(([prop, value]) => String(state[prop]) === String(value))) parts.push(rule.className);
1170
+ if (Object.entries(rule.when).every(([prop, value]) => conditionHolds({
1171
+ actual: state[prop],
1172
+ expected: value
1173
+ }))) parts.push(rule.className);
1167
1174
  }
1168
1175
  parts.push(...extra);
1169
1176
  return parts.filter(Boolean).join(" ");
@@ -4,6 +4,7 @@ import { rootLayerOf } from "../entities/system/element.js";
4
4
  import { authoredValue, declaredPropDefaults } from "../framework/projections.js";
5
5
  import { forwardRoutes, isValueSlot } from "../framework/prop-surface.js";
6
6
  import { isPreviewElement } from "./preview-elements.js";
7
+ import { conditionHolds } from "../framework/layer-styles.js";
7
8
  import { entryKeyOf, resolveVisibility, slotAnatomyPropOf, slotTargetsOf } from "../framework/render-spec.js";
8
9
  import { Component } from "../entities/system/Component.js";
9
10
  //#region src/spec/detach.ts
@@ -35,7 +36,10 @@ function anatomyOf(body) {
35
36
  /** Whether a rule's condition holds under a prop bag, a ref compared by its leaf. */
36
37
  function ruleHolds(when, state) {
37
38
  if (!when) return true;
38
- return Object.entries(when).every(([prop, value]) => String(refLeaf(state[prop])) === String(refLeaf(value)));
39
+ return Object.entries(when).every(([prop, value]) => conditionHolds({
40
+ actual: refLeaf(state[prop]),
41
+ expected: value
42
+ }));
39
43
  }
40
44
  /** The rules in the order their bags apply: the unconditional ones, then by how many props a
41
45
  * condition pins, so a narrower rule writes over a wider one the way the stylesheet cascades them. */