@yahoo/uds-create-config 5.6.0 → 5.7.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.
@@ -0,0 +1,73 @@
1
+ import { Config } from "../../framework/Config.js";
2
+
3
+ //#region src/entities/system/motion-variants.d.ts
4
+ /**
5
+ * A JS preset a component's style rule puts on one of its layers — `checkbox: { motion: 'press' }`.
6
+ * `preset` is the preset's path, or for one written in place the id {@link inlineMotionId} gives it.
7
+ */
8
+ interface PlacedMotion {
9
+ readonly component: string;
10
+ readonly layer: string;
11
+ readonly preset: string;
12
+ }
13
+ /**
14
+ * A JS preset written in place as a rule's `motion` on a layer. It has no path, so it is named by
15
+ * where it sits: the component, the rule's key (its condition, `*` for the unconditional rule) and
16
+ * the layer. `invalid` holds why a body the build cannot read was left out, for the build to report.
17
+ */
18
+ interface InlineMotion {
19
+ readonly id: string;
20
+ readonly component: string;
21
+ readonly rule: string;
22
+ readonly layer: string;
23
+ readonly body: Record<string, unknown>;
24
+ }
25
+ /** The id an in-place preset goes by — `Btn.root inline [disabled=true]`. Never a preset path, since a
26
+ * path holds no space. */
27
+ declare function inlineMotionId(component: string, rule: string, layer: string): string;
28
+ /** Whether a rule's `motion` value is a preset written in place, rather than a ref or a name. */
29
+ declare function isInlineMotion(value: unknown): value is Record<string, unknown>;
30
+ /** Every JS preset a rule writes in place, and every one the build cannot read as a JS preset. */
31
+ declare const inlineMotions: (config: Config) => {
32
+ readonly valid: readonly InlineMotion[];
33
+ readonly invalid: readonly (Omit<InlineMotion, 'body'> & {
34
+ reason: string;
35
+ })[];
36
+ };
37
+ /** A JS preset's body by its path, or an in-place one's by its id; `undefined` for anything else. */
38
+ declare function jsMotionBody(config: Config, preset: string): Record<string, unknown> | undefined;
39
+ /** The states a preset names a variant in. */
40
+ declare const MOTION_VARIANT_STATES: readonly ["initial", "animate", "exit", "whileHover", "whileTap", "whileFocus", "whileDrag"];
41
+ type Body = Record<string, unknown>;
42
+ /** The variant names one state holds: a name, a list of names, or none (values, `false`). */
43
+ declare function stateVariantNames(value: unknown): string[];
44
+ /** The variants a preset declares. */
45
+ declare function declaredVariants(body: Body | undefined): string[];
46
+ /** Every JS preset a style rule puts on a layer, across the system. */
47
+ declare const placedMotions: (config: Config) => readonly PlacedMotion[];
48
+ /**
49
+ * The presets that share variant names, each set closed over placement and composition.
50
+ *
51
+ * Two presets on one component's layers are in one set. So is a preset a component places and one a
52
+ * component it renders places, at any depth: the label passes through a component that places nothing
53
+ * on its way down, so the walk goes through those without joining them. A component that places
54
+ * nothing joins nothing, which keeps Button and Checkbox apart although both render Box.
55
+ */
56
+ declare const motionVariantGroups: (config: Config) => readonly ReadonlySet<string>[];
57
+ /** A variant a placed preset declares that nothing animating with it ever names. */
58
+ interface OrphanedMotionVariant {
59
+ readonly preset: string;
60
+ readonly variant: string;
61
+ }
62
+ /**
63
+ * Every variant a placed preset declares that no state in its set names — what a rename of the name
64
+ * on one side leaves on the other, and what `motion/react` then never animates to, silently.
65
+ *
66
+ * Only presets a style rule places are checked: an unplaced preset's variants may answer whatever the
67
+ * app renders around it, and neither is a set holding a component with a `motion` prop. A placed one
68
+ * may be driven from outside the system too, by an app ancestor animating to the same name, which is
69
+ * why this warns and never refuses.
70
+ */
71
+ declare function orphanedMotionVariants(config: Config): OrphanedMotionVariant[];
72
+ //#endregion
73
+ export { InlineMotion, MOTION_VARIANT_STATES, OrphanedMotionVariant, PlacedMotion, declaredVariants, inlineMotionId, inlineMotions, isInlineMotion, jsMotionBody, motionVariantGroups, orphanedMotionVariants, placedMotions, stateVariantNames };
@@ -0,0 +1,270 @@
1
+ import { reject } from "../../framework/rejection.js";
2
+ import { isRef, kindOf, pathOf } from "../../framework/utils/refs.js";
3
+ import { isPlainObject } from "../../framework/utils/field-path.js";
4
+ import { createSliceMemo } from "../../framework/memo.js";
5
+ import { inlineJsMotion } from "./Motion.js";
6
+ //#region src/entities/system/motion-variants.ts
7
+ /** The id an in-place preset goes by — `Btn.root inline [disabled=true]`. Never a preset path, since a
8
+ * path holds no space. */
9
+ function inlineMotionId(component, rule, layer) {
10
+ return `${component}.${layer} inline [${rule}]`;
11
+ }
12
+ /** Whether a rule's `motion` value is a preset written in place, rather than a ref or a name. */
13
+ function isInlineMotion(value) {
14
+ return isPlainObject(value) && !isRef(value);
15
+ }
16
+ /** Every JS preset a rule writes in place, and every one the build cannot read as a JS preset. */
17
+ const inlineMotions = createSliceMemo({
18
+ kinds: ["component"],
19
+ compute: (config) => {
20
+ const valid = [];
21
+ const invalid = [];
22
+ for (const component of config.list("component")) {
23
+ const { styles = {} } = component.toJSON();
24
+ for (const [rule, entry] of Object.entries(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
25
+ const value = bag?.motion;
26
+ if (!isInlineMotion(value)) continue;
27
+ const id = inlineMotionId(component.path, rule, layer);
28
+ const parsed = inlineJsMotion.safeParse(value);
29
+ if (!parsed.success) {
30
+ invalid.push({
31
+ id,
32
+ component: component.path,
33
+ rule,
34
+ layer,
35
+ reason: parsed.error.issues.map((issue) => [issue.path.join("."), issue.message].filter(Boolean).join(": ")).join("; ")
36
+ });
37
+ continue;
38
+ }
39
+ valid.push({
40
+ id,
41
+ component: component.path,
42
+ rule,
43
+ layer,
44
+ body: {
45
+ ...value,
46
+ runtime: "js"
47
+ }
48
+ });
49
+ }
50
+ }
51
+ return {
52
+ valid,
53
+ invalid
54
+ };
55
+ }
56
+ });
57
+ /** A JS preset's body by its path, or an in-place one's by its id; `undefined` for anything else. */
58
+ function jsMotionBody(config, preset) {
59
+ const named = config.resolve("motion", preset)?.toJSON();
60
+ if (named) return named.runtime === "js" ? named : void 0;
61
+ return inlineMotions(config).valid.find((entry) => entry.id === preset)?.body;
62
+ }
63
+ /** The states a preset names a variant in. */
64
+ const MOTION_VARIANT_STATES = [
65
+ "initial",
66
+ "animate",
67
+ "exit",
68
+ "whileHover",
69
+ "whileTap",
70
+ "whileFocus",
71
+ "whileDrag"
72
+ ];
73
+ const jsBody = (config, path) => jsMotionBody(config, path);
74
+ /** The variant names one state holds: a name, a list of names, or none (values, `false`). */
75
+ function stateVariantNames(value) {
76
+ if (typeof value === "string") return [value];
77
+ if (Array.isArray(value)) return value.filter((name) => typeof name === "string");
78
+ return [];
79
+ }
80
+ /** The variants a preset declares. */
81
+ function declaredVariants(body) {
82
+ const variants = body?.variants;
83
+ return variants && typeof variants === "object" ? Object.keys(variants) : [];
84
+ }
85
+ /** Every variant a preset's states name. */
86
+ function namedVariants(body) {
87
+ if (!body) return [];
88
+ return MOTION_VARIANT_STATES.flatMap((state) => stateVariantNames(body[state]));
89
+ }
90
+ /** Every JS preset a style rule puts on a layer, across the system. */
91
+ const placedMotions = createSliceMemo({
92
+ kinds: ["component", "motion"],
93
+ compute: (config) => {
94
+ const out = [];
95
+ for (const component of config.list("component")) {
96
+ const { styles = {} } = component.toJSON();
97
+ for (const [rule, entry] of Object.entries(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
98
+ const named = bag?.motion;
99
+ const preset = isRef(named) ? pathOf(named.__ref) : typeof named === "string" ? named : isInlineMotion(named) ? inlineMotionId(component.path, rule, layer) : void 0;
100
+ if (preset === void 0 || !jsBody(config, preset)) continue;
101
+ out.push({
102
+ component: component.path,
103
+ layer,
104
+ preset
105
+ });
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ });
111
+ /** The components a component's layers render, resolved to the path each lives at. */
112
+ const composedComponents = (config, path) => {
113
+ const { layers = {} } = config.resolve("component", path)?.toJSON() ?? {};
114
+ return Object.values(layers).flatMap((element) => isRef(element) && kindOf(element) === "component" ? [config.resolve("component", pathOf(element))?.path ?? pathOf(element)] : []);
115
+ };
116
+ /**
117
+ * The presets that share variant names, each set closed over placement and composition.
118
+ *
119
+ * Two presets on one component's layers are in one set. So is a preset a component places and one a
120
+ * component it renders places, at any depth: the label passes through a component that places nothing
121
+ * on its way down, so the walk goes through those without joining them. A component that places
122
+ * nothing joins nothing, which keeps Button and Checkbox apart although both render Box.
123
+ */
124
+ const motionVariantGroups = createSliceMemo({
125
+ kinds: ["component", "motion"],
126
+ compute: (config) => {
127
+ const parent = /* @__PURE__ */ new Map();
128
+ const find = (node) => {
129
+ let root = node;
130
+ while (parent.has(root) && parent.get(root) !== root) root = parent.get(root);
131
+ parent.set(node, root);
132
+ return root;
133
+ };
134
+ const join = (a, b) => {
135
+ const ra = find(a);
136
+ const rb = find(b);
137
+ if (ra !== rb) parent.set(ra, rb);
138
+ };
139
+ const placing = /* @__PURE__ */ new Set();
140
+ for (const { component, preset } of placedMotions(config)) {
141
+ placing.add(component);
142
+ join(`c\u0000${component}`, `p\u0000${preset}`);
143
+ }
144
+ for (const component of placing) {
145
+ const seen = new Set([component]);
146
+ const queue = composedComponents(config, component);
147
+ while (queue.length > 0) {
148
+ const next = queue.shift();
149
+ if (seen.has(next)) continue;
150
+ seen.add(next);
151
+ if (placing.has(next)) join(`c\u0000${component}`, `c\u0000${next}`);
152
+ queue.push(...composedComponents(config, next));
153
+ }
154
+ }
155
+ const groups = /* @__PURE__ */ new Map();
156
+ for (const node of parent.keys()) {
157
+ if (node[0] !== "p") continue;
158
+ const root = find(node);
159
+ const group = groups.get(root) ?? /* @__PURE__ */ new Set();
160
+ group.add(node.slice(2));
161
+ groups.set(root, group);
162
+ }
163
+ return [...groups.values()];
164
+ }
165
+ });
166
+ /** The presets that share variant names with `preset` — itself alone when no rule places it. */
167
+ function motionVariantGroupOf(config, preset) {
168
+ return motionVariantGroups(config).find((group) => group.has(preset)) ?? new Set([preset]);
169
+ }
170
+ /** A preset's body with one variant name renamed, in its `variants` and in every state naming it. */
171
+ function renamedBody(body, from, to) {
172
+ const next = { ...body };
173
+ const variants = body.variants;
174
+ if (variants && Object.hasOwn(variants, from)) next.variants = Object.fromEntries(Object.entries(variants).map(([name, value]) => [name === from ? to : name, value]));
175
+ for (const state of MOTION_VARIANT_STATES) {
176
+ const value = body[state];
177
+ if (value === from) next[state] = to;
178
+ else if (Array.isArray(value)) next[state] = value.map((name) => name === from ? to : name);
179
+ }
180
+ return next;
181
+ }
182
+ const mentions = (body, name) => declaredVariants(body).includes(name) || namedVariants(body).includes(name);
183
+ /**
184
+ * Rename a variant across every preset that shares names with `path` ({@link motionVariantGroupOf}):
185
+ * the key in each `variants` that declares it, and each state that names it. Refused when `path` is
186
+ * not a JS preset that declares `from`, or when a preset in the set already declares `to`, since the
187
+ * two would become one variant.
188
+ */
189
+ function renameMotionVariant(config, { path, from, to }) {
190
+ const own = jsBody(config, path);
191
+ if (!own) reject("NOT_FOUND", `"${path}" is not a JS motion preset, so it has no variants.`);
192
+ if (!declaredVariants(own).includes(from)) reject("NOT_FOUND", `"${path}" declares no variant "${from}".`);
193
+ if (from === to) return config;
194
+ const group = [...motionVariantGroupOf(config, path)];
195
+ const taken = group.filter((preset) => declaredVariants(jsBody(config, preset)).includes(to));
196
+ if (taken.length > 0) reject("ALREADY_EXISTS", `"${to}" is already a variant of ${taken.map((preset) => `"${preset}"`).join(", ")}, which animate${taken.length === 1 ? "s" : ""} with "${path}". Renaming "${from}" onto it would make the two one variant.`);
197
+ let next = config;
198
+ for (const preset of group) {
199
+ const body = jsBody(next, preset);
200
+ if (!body || !mentions(body, from)) continue;
201
+ const inline = inlineMotions(next).valid.find((entry) => entry.id === preset);
202
+ if (!inline) {
203
+ next = next.apply({
204
+ kind: "motion",
205
+ operation: "replace",
206
+ path: preset,
207
+ data: renamedBody(body, from, to)
208
+ });
209
+ continue;
210
+ }
211
+ const component = next.resolve("component", inline.component)?.toJSON();
212
+ const bag = component?.styles?.[inline.rule]?.layers?.[inline.layer];
213
+ if (!component || !bag || !isInlineMotion(bag.motion)) continue;
214
+ next = next.apply({
215
+ kind: "component",
216
+ operation: "replace",
217
+ path: inline.component,
218
+ data: {
219
+ ...component,
220
+ styles: {
221
+ ...component.styles,
222
+ [inline.rule]: {
223
+ ...component.styles?.[inline.rule],
224
+ layers: {
225
+ ...component.styles?.[inline.rule]?.layers,
226
+ [inline.layer]: {
227
+ ...bag,
228
+ motion: renamedBody(bag.motion, from, to)
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ });
235
+ }
236
+ return next;
237
+ }
238
+ const declaresMotionProp = (config, path) => {
239
+ const { props = {} } = config.resolve("component", path)?.toJSON() ?? {};
240
+ return Object.values(props).some((prop) => prop?.type === "motion");
241
+ };
242
+ /**
243
+ * Every variant a placed preset declares that no state in its set names — what a rename of the name
244
+ * on one side leaves on the other, and what `motion/react` then never animates to, silently.
245
+ *
246
+ * Only presets a style rule places are checked: an unplaced preset's variants may answer whatever the
247
+ * app renders around it, and neither is a set holding a component with a `motion` prop. A placed one
248
+ * may be driven from outside the system too, by an app ancestor animating to the same name, which is
249
+ * why this warns and never refuses.
250
+ */
251
+ function orphanedMotionVariants(config) {
252
+ const entries = placedMotions(config);
253
+ const placed = new Set(entries.map((entry) => entry.preset));
254
+ const open = new Set(entries.filter((entry) => declaresMotionProp(config, entry.component)).map((entry) => entry.preset));
255
+ const out = [];
256
+ for (const group of motionVariantGroups(config)) {
257
+ if ([...group].some((preset) => open.has(preset))) continue;
258
+ const named = new Set([...group].flatMap((preset) => namedVariants(jsBody(config, preset))));
259
+ for (const preset of group) {
260
+ if (!placed.has(preset)) continue;
261
+ for (const variant of declaredVariants(jsBody(config, preset))) if (!named.has(variant)) out.push({
262
+ preset,
263
+ variant
264
+ });
265
+ }
266
+ }
267
+ return out;
268
+ }
269
+ //#endregion
270
+ export { MOTION_VARIANT_STATES, declaredVariants, inlineMotionId, inlineMotions, isInlineMotion, jsMotionBody, motionVariantGroups, orphanedMotionVariants, placedMotions, renameMotionVariant, stateVariantNames };
@@ -72,7 +72,7 @@ interface RecordedSource {
72
72
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
73
73
  * that replaced it (see `detectedWireVersion`).
74
74
  */
75
- declare const SERIALIZED_CONFIG_VERSION = 20261005225949;
75
+ declare const SERIALIZED_CONFIG_VERSION = 20261006125609;
76
76
  interface SerializedConfig {
77
77
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
78
78
  readonly version: number;
@@ -5346,7 +5346,7 @@ var Config = class Config {
5346
5346
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5347
5347
  hydrateFrom(stored, options) {
5348
5348
  const claimed = schemaVersionOf(stored);
5349
- if (claimed > 20261005225949) throw new SchemaVersionTooNew(claimed);
5349
+ if (claimed > 20261006125609) throw new SchemaVersionTooNew(claimed);
5350
5350
  const wire = upgradeSerializedConfig(stored);
5351
5351
  if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating, or port the repo once with \`uds migrate\`.`);
5352
5352
  const leftover = wire.options;
@@ -199,6 +199,16 @@ declare function inlineRuleComposites(config: Config, element: unknown, bag: Rec
199
199
  * nothing until a rebuild, while a prop json-render applies on the spot.
200
200
  */
201
201
  declare function layerRoutedProps(config: Config, path: string, state?: Record<string, unknown>): Record<string, Record<string, unknown>>;
202
+ /**
203
+ * The JS preset each layer animates by, for one instance: the `motion` of the last style rule that
204
+ * applies and names a JS preset for the layer, the unconditional rule first and the conditional ones
205
+ * in declaration order, which is the order the emitted module routes them in. A CSS preset is a
206
+ * class and stays with {@link styleRuleMotionClasses}.
207
+ *
208
+ * What lets the canvas play a preset a rule put on a layer before any build: the expansion renders
209
+ * that layer through motion's factory and hands it the preset's props.
210
+ */
211
+ declare function layerJsMotionPresets(config: Config, path: string, state?: Record<string, unknown>): Record<string, string>;
202
212
  /**
203
213
  * The CSS MOTION classes a style rule puts on a layer, for one instance.
204
214
  *
@@ -337,4 +347,4 @@ declare function modifierUtilitiesUsed(config: Config): Set<string>;
337
347
  */
338
348
  declare function splitVariants(cls: string): string[];
339
349
  //#endregion
340
- export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
350
+ export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
@@ -4,6 +4,7 @@ import { createSliceMemo } from "./memo.js";
4
4
  import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
5
5
  import { storedPreview } from "./preview-store.js";
6
6
  import { authoredValue, className, isCustomPropertyKey, negatedCssValue, renderStyleValue, resolveComponentProps, stylePropEntries, stylePropLeafForToken, stylePropValueLeaves, stylePropertyPathFor } from "./projections.js";
7
+ import { inlineMotionId, isInlineMotion, jsMotionBody } from "../entities/system/motion-variants.js";
7
8
  import { ruleApplies } from "./layer-styles.js";
8
9
  //#region src/framework/class-names.ts
9
10
  /**
@@ -529,6 +530,9 @@ function layerRoutedProps(config, path, state = {}) {
529
530
  const declared = isRef(element) ? resolveComponentProps(config, pathOf(element.__ref)) : {};
530
531
  for (const [prop, value] of Object.entries(bag ?? {})) {
531
532
  if (prop.startsWith("_") || isCustomPropertyKey(prop)) continue;
533
+ if (prop === "motion") {
534
+ if ((config.resolve("motion", String(authoredValue(config, prop, value)))?.toJSON())?.runtime !== "css" || !isRef(element)) continue;
535
+ }
532
536
  const styleKey = stylePropertyPathFor({
533
537
  config,
534
538
  key: prop
@@ -543,6 +547,43 @@ function layerRoutedProps(config, path, state = {}) {
543
547
  return out;
544
548
  }
545
549
  /**
550
+ * The JS preset each layer animates by, for one instance: the `motion` of the last style rule that
551
+ * applies and names a JS preset for the layer, the unconditional rule first and the conditional ones
552
+ * in declaration order, which is the order the emitted module routes them in. A CSS preset is a
553
+ * class and stays with {@link styleRuleMotionClasses}.
554
+ *
555
+ * What lets the canvas play a preset a rule put on a layer before any build: the expansion renders
556
+ * that layer through motion's factory and hands it the preset's props.
557
+ */
558
+ function layerJsMotionPresets(config, path, state = {}) {
559
+ const { styles = {}, defaultProps = {} } = config.resolve("component", path)?.toJSON() ?? {};
560
+ const resolved = {
561
+ ...defaultProps,
562
+ ...state
563
+ };
564
+ const rules = Object.entries(styles);
565
+ const unconditional = ([, rule]) => !rule.when || Object.keys(rule.when).length === 0;
566
+ const ordered = [...rules.filter(unconditional), ...rules.filter((entry) => !unconditional(entry))];
567
+ const out = {};
568
+ for (const [key, rule] of ordered) {
569
+ if (!ruleApplies(rule.when, resolved)) continue;
570
+ for (const [layer, bag] of Object.entries(rule.layers ?? {})) {
571
+ const named = bag?.motion;
572
+ if (isInlineMotion(named)) {
573
+ const id = inlineMotionId(path, key, layer);
574
+ if (jsMotionBody(config, id)) out[layer] = id;
575
+ else delete out[layer];
576
+ continue;
577
+ }
578
+ const preset = isRef(named) ? pathOf(named.__ref) : typeof named === "string" ? named : void 0;
579
+ if (preset === void 0) continue;
580
+ if ((config.resolve("motion", preset)?.toJSON())?.runtime === "js") out[layer] = preset;
581
+ else delete out[layer];
582
+ }
583
+ }
584
+ return out;
585
+ }
586
+ /**
546
587
  * The CSS MOTION classes a style rule puts on a layer, for one instance.
547
588
  *
548
589
  * A preset's whole runtime is a class too, and a composite's route (forward it, and let the receiver
@@ -929,4 +970,4 @@ function splitVariants(cls) {
929
970
  return segments;
930
971
  }
931
972
  //#endregion
932
- export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
973
+ export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOwnClasses, styleRuleMotionClasses };
@@ -159,8 +159,13 @@ declare function defineSubEntity<F extends z.ZodTypeAny>(spec: {
159
159
  * every way into stored state takes it: `register`, an item's create or update, and op data.
160
160
  *
161
161
  * Two members with the same key are one member, the later one, as the key derivation on write
162
- * already treats a record whose keys disagree with their members.
162
+ * already treats a record whose keys disagree with their members — unless `merge` says how the two
163
+ * combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
163
164
  */
164
- declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F>): z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<F>, z.ZodTransform<Record<string, z.output<F>>, z.core.output<F>[]>>, SubEntityClass<F>]>;
165
+ declare function orList<F extends z.ZodTypeAny>(collection: SubEntityClass<F>, {
166
+ merge
167
+ }?: {
168
+ readonly merge?: (earlier: z.output<F>, later: z.output<F>) => z.output<F>;
169
+ }): z.ZodUnion<readonly [z.ZodPipe<z.ZodArray<F>, z.ZodTransform<Record<string, z.output<F>>, z.core.output<F>[]>>, SubEntityClass<F>]>;
165
170
  //#endregion
166
171
  export { SubEntityClass, defineSubEntity, orList };
@@ -124,14 +124,22 @@ function defineSubEntity(spec) {
124
124
  * every way into stored state takes it: `register`, an item's create or update, and op data.
125
125
  *
126
126
  * Two members with the same key are one member, the later one, as the key derivation on write
127
- * already treats a record whose keys disagree with their members.
127
+ * already treats a record whose keys disagree with their members — unless `merge` says how the two
128
+ * combine, in which case the member is `merge(earlier, later)`, placed where the later one was.
128
129
  */
129
- function orList(collection) {
130
+ function orList(collection, { merge } = {}) {
130
131
  const key = collection.key;
131
132
  if (!key) throw new Error(`orList: "${collection.name}" declares no \`key\`, so a list of its members has nothing to be keyed by.`);
132
133
  const listed = z.array(collection.valueType).transform((members) => {
133
134
  const keyed = {};
134
- for (const member of members) keyed[key(member)] = member;
135
+ for (const member of members) {
136
+ const at = key(member);
137
+ const earlier = keyed[at];
138
+ if (earlier !== void 0 && merge) {
139
+ delete keyed[at];
140
+ keyed[at] = merge(earlier, member);
141
+ } else keyed[at] = member;
142
+ }
135
143
  return keyed;
136
144
  });
137
145
  return z.union([listed, collection]);
@@ -476,6 +476,7 @@ function authoredValue(config, key, value) {
476
476
  function authoredLeaf(config, key, stored) {
477
477
  const path = pathOf(stored);
478
478
  if (kindOf(stored) === "token") return stylePropLeafForToken(config, key, path);
479
+ if (kindOf(stored) === "motion") return path;
479
480
  const slash = path.indexOf("/");
480
481
  return slash === -1 ? path : path.slice(slash + 1);
481
482
  }
@@ -92,6 +92,10 @@ interface ResolveContext {
92
92
  * one — since it names a derived thing, not an entity.
93
93
  */
94
94
  declare function wrappedElementKey(wrapper: string, type: string): string;
95
+ /** The factory a layer renders through when a style rule gives it a JS preset. The registry holds a
96
+ * pair of it with every element a layer renders, when the system has a JS preset (codegen's
97
+ * registry emitter). */
98
+ declare const MOTION_FACTORY = "package:motion/react#motion";
95
99
  /**
96
100
  * The node a tree ENTERS at — the one nothing else claims as a child.
97
101
  *
@@ -497,4 +501,4 @@ declare function expandSpec(config: Config, spec: RenderSpec, options?: ExpandOp
497
501
  */
498
502
  declare function expandComponent(config: Config, path: string, props?: Record<string, unknown>, options?: ExpandOptions): ExpansionResult;
499
503
  //#endregion
500
- export { ANATOMY_MARKER, ComponentSpec, ComponentSpecs, ComputedFunctions, ComputedValue, ExpandOptions, ExpansionResult, RenderElement, RenderSpec, RepeatRow, RepeatSpec, ResolveContext, SlotTarget, VisibilityOperator, VisibilityTerm, anatomyElementComponent, anatomyTextProp, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, routesOf, slotTargetsOf, specWithResolvedVisibility, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
504
+ export { ANATOMY_MARKER, ComponentSpec, ComponentSpecs, ComputedFunctions, ComputedValue, ExpandOptions, ExpansionResult, MOTION_FACTORY, RenderElement, RenderSpec, RepeatRow, RepeatSpec, ResolveContext, SlotTarget, VisibilityOperator, VisibilityTerm, anatomyElementComponent, anatomyTextProp, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, routesOf, slotTargetsOf, specWithResolvedVisibility, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
@@ -5,9 +5,10 @@ import { unconditionalPreviewProps } from "./preview-store.js";
5
5
  import { authoredBag, authoredValue, declaredPropDefaults, memberFromLeaf, refListOf, resolveComponentProps, stylePropertyPathFor } from "./projections.js";
6
6
  import { slotHostLayer } from "./slot-host.js";
7
7
  import { forwardRoutes, forwardedLayers, intoName, isValueSlot, renderBindingsOf, routesContent } from "./prop-surface.js";
8
+ import { inlineMotionProps, jsMotionPresets } from "../entities/system/motion-presets.js";
8
9
  import { withRequiredContentHidden } from "../entities/system/required-content-layers.js";
9
10
  import { graftPreviewElements } from "../spec/preview-elements.js";
10
- import { componentLayerClass, componentPropClasses, componentRuleClasses, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
11
+ import { componentLayerClass, componentPropClasses, componentRuleClasses, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
11
12
  //#region src/framework/render-spec.ts
12
13
  /** A repeat source that is one prop binding: `{ "$state": "/stops" }`, settled by the cell alone. */
13
14
  function isStateSource(source) {
@@ -49,6 +50,15 @@ function unwrapElementKey(key) {
49
50
  if (open === -1 || !key.endsWith(")")) return void 0;
50
51
  return key.slice(open + 1, -1);
51
52
  }
53
+ /** The factory a layer renders through when a style rule gives it a JS preset. The registry holds a
54
+ * pair of it with every element a layer renders, when the system has a JS preset (codegen's
55
+ * registry emitter). */
56
+ const MOTION_FACTORY = "package:motion/react#motion";
57
+ /** Whether a spec type is already a motion element, which animates by its own props, so a rule's
58
+ * preset would animate it twice: `package:motion/react#motion.span`, or a factory-wrapped pair. */
59
+ function isMotionElementType(type) {
60
+ return type.startsWith(`package:motion/react#motion.`) || type.startsWith(`package:motion/react#motion(`);
61
+ }
52
62
  /** The type a node renders as, wrapped when the node names a wrapper. */
53
63
  function wrappedTypeOf(node, type) {
54
64
  if (type === void 0) return void 0;
@@ -1220,6 +1230,11 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1220
1230
  });
1221
1231
  const layerProps = instanceProps.layerProps ?? {};
1222
1232
  const forwarded = layerRoutedProps(config, path, state);
1233
+ const layerMotions = layerJsMotionPresets(config, path, state);
1234
+ const presetProps = Object.keys(layerMotions).length > 0 ? {
1235
+ ...jsMotionPresets(config),
1236
+ ...inlineMotionProps(config)
1237
+ } : {};
1223
1238
  const entryNode = entryKeyOf(anatomy);
1224
1239
  const rootLayer = rootKeyOf(layers);
1225
1240
  const hostNode = Object.keys(anatomy).find((key) => layerOf(anatomy[key]?.element) === rootLayer) ?? entryNode;
@@ -1311,6 +1326,9 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1311
1326
  const bound = boundPropOf(node.element);
1312
1327
  if (bound) type = boundChoice(config, path, node.element, state[bound]);
1313
1328
  type = wrappedTypeOf(node, type);
1329
+ const motionWrapped = type !== void 0 ? wrappedElementKey(MOTION_FACTORY, type) : void 0;
1330
+ const motionPreset = layer !== void 0 && !refOf(node.wrapper) && type !== void 0 && motionWrapped !== void 0 && !isMotionElementType(type) && sink.rendered?.has(motionWrapped) ? layerMotions[layer] : void 0;
1331
+ if (motionPreset !== void 0) type = motionWrapped;
1314
1332
  if (!type) {
1315
1333
  sink.unresolved.add(bound ? `component:${path}#${key} (${bound})` : `component:${path}#${key}`);
1316
1334
  return;
@@ -1331,6 +1349,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1331
1349
  "data-uds-component": path,
1332
1350
  "data-uds-layer": layer,
1333
1351
  ...forwarded[layer],
1352
+ ...motionPreset !== void 0 ? presetProps[motionPreset] : {},
1334
1353
  ...Object.fromEntries((valueRoutes.get(layer) ?? []).filter(({ from }) => state[from] !== void 0).map(({ from, into }) => [into, state[from]])),
1335
1354
  ...isRoot ? rest : {},
1336
1355
  ...slot && slotValue !== void 0 ? { [intoName(slot.into)]: slotValue } : {},
@@ -1491,4 +1510,4 @@ function expandComponent(config, path, props = {}, options = {}) {
1491
1510
  return expandSpec(config, instanceSpec(path, props), options);
1492
1511
  }
1493
1512
  //#endregion
1494
- export { ANATOMY_MARKER, anatomyElementComponent, anatomyTextProp, andListOf, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, isExpression, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, routesOf, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, spelled, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
1513
+ export { ANATOMY_MARKER, MOTION_FACTORY, anatomyElementComponent, anatomyTextProp, andListOf, boundElementType, componentSpec, deriveSpec, entryKeyOf, expandComponent, expandSpec, instanceSpec, isExpression, layerTextProp, liveStateProps, previewSpec, propInto, propsIntoLayer, readStatePointer, resolveValue, resolveVisibility, routeOf, routesOf, slotAnatomyPropOf, slotTargetsOf, specWithResolvedVisibility, spelled, statePointerProp, valueForwardsOf, visibilityStateProps, visibilityTerms, withoutTruthyTerm, wrappedElementKey };
@@ -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 = 20261005225949;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261006125609;
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 = 20261005225949;
16
+ const CURRENT_SCHEMA_VERSION = 20261006125609;
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 > 20261005225949) 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 > 20261006125609) 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 ?? 20261005225949;
105
+ const target = options?.target ?? 20261006125609;
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 ?? 20261005225949;
163
+ const target = options?.target ?? 20261006125609;
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 ?? 20261005225949;
182
+ const target = options?.target ?? 20261006125609;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];