@yahoo/uds-create-config 5.5.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;
@@ -1092,6 +1092,13 @@ declare class Config {
1092
1092
  * batch wrote once it has landed, every kind after a registration. Never called on hydration.
1093
1093
  */
1094
1094
  private assertInvariants;
1095
+ /**
1096
+ * One sentence per item of `kind` that extends itself through local extensions — `A` extending `B`
1097
+ * extending `A`. Each base resolves only once the one it extends has, so a loop has no body at all,
1098
+ * and a read answers nothing for every item on it. Refused where it's written instead, naming the
1099
+ * loop. An extension through a linked system can't close one: the source is its own config.
1100
+ */
1101
+ private extensionCycles;
1095
1102
  /** The {@link EntityKind.uniqueLeaves} half of {@link assertInvariants} alone. */
1096
1103
  private assertUniqueLeaves;
1097
1104
  /** One sentence per leaf that two items of `kind` hold, since a leaf has to name one item. */
@@ -1618,10 +1618,41 @@ var Config = class Config {
1618
1618
  const kinds = kind === void 0 ? [...this.state.entities.keys()] : [kind];
1619
1619
  for (const name of kinds) {
1620
1620
  const entry = this.entityOf(name);
1621
- const problems = [...entry?.uniqueLeaves ? this.duplicateLeaves(name) : [], ...entry?.invariants?.(this) ?? []];
1621
+ const problems = [
1622
+ ...entry?.uniqueLeaves ? this.duplicateLeaves(name) : [],
1623
+ ...this.extensionCycles(name),
1624
+ ...entry?.invariants?.(this) ?? []
1625
+ ];
1622
1626
  if (problems.length > 0) rejectData(`Invalid ${name}: ${problems.join(" ")}`);
1623
1627
  }
1624
1628
  }
1629
+ /**
1630
+ * One sentence per item of `kind` that extends itself through local extensions — `A` extending `B`
1631
+ * extending `A`. Each base resolves only once the one it extends has, so a loop has no body at all,
1632
+ * and a read answers nothing for every item on it. Refused where it's written instead, naming the
1633
+ * loop. An extension through a linked system can't close one: the source is its own config.
1634
+ */
1635
+ extensionCycles(kind) {
1636
+ const items = this.state.snapshot.slice(kind).items;
1637
+ const localBase = (path) => {
1638
+ const stored = items.get(path)?.toJSON();
1639
+ if (!isExtendedRef(stored) || sourceOf(stored) !== void 0) return;
1640
+ return pathOf(stored);
1641
+ };
1642
+ const problems = [];
1643
+ for (const path of items.keys()) {
1644
+ const chain = [path];
1645
+ for (let base = localBase(path); base !== void 0; base = localBase(base)) {
1646
+ if (base === path) {
1647
+ problems.push(`"${path}" extends itself (${[...chain, path].join(" → ")}); an extension needs a base that doesn't extend it.`);
1648
+ break;
1649
+ }
1650
+ if (chain.includes(base)) break;
1651
+ chain.push(base);
1652
+ }
1653
+ }
1654
+ return problems;
1655
+ }
1625
1656
  /** The {@link EntityKind.uniqueLeaves} half of {@link assertInvariants} alone. */
1626
1657
  assertUniqueLeaves(kind) {
1627
1658
  if (!this.entityOf(kind)?.uniqueLeaves) return;
@@ -3600,6 +3631,7 @@ var Config = class Config {
3600
3631
  const extension = stored.toJSON();
3601
3632
  if (!isExtendedRef(extension)) continue;
3602
3633
  if (slug !== void 0 && sourceOf(extension) !== slug) continue;
3634
+ if (only === void 0 && sourceOf(extension) === void 0) continue;
3603
3635
  const resolved = this.resolveExtension({
3604
3636
  kind,
3605
3637
  path,
@@ -5314,7 +5346,7 @@ var Config = class Config {
5314
5346
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5315
5347
  hydrateFrom(stored, options) {
5316
5348
  const claimed = schemaVersionOf(stored);
5317
- if (claimed > 20261005225949) throw new SchemaVersionTooNew(claimed);
5349
+ if (claimed > 20261006125609) throw new SchemaVersionTooNew(claimed);
5318
5350
  const wire = upgradeSerializedConfig(stored);
5319
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\`.`);
5320
5352
  const leftover = wire.options;
@@ -5596,7 +5628,6 @@ function comparableBody(value) {
5596
5628
  function extensionChange(entry, kind, path, raw, validate = false) {
5597
5629
  if (kindOf(raw) !== kind) rejectData(`Invalid ${kind} extension: base ref "${raw.__ref}" names ${kindOf(raw)}, not ${kind}`);
5598
5630
  if (memberOf(raw) !== void 0) rejectData(`Invalid ${kind} extension: base ref "${raw.__ref}" names a member; an item extension needs a whole-entity ref`);
5599
- if (sourceOf(raw) === void 0) rejectData(`Invalid ${kind} extension: base ref "${raw.__ref}" is local; extend a ref through a declared linked system`);
5600
5631
  let delta = raw.extend;
5601
5632
  if (validate === "author") {
5602
5633
  const parsed = dataSchema(entry.schemas, "update")?.safeParse(delta);
@@ -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 };