@yahoo/uds-create-config 5.19.1 → 5.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/configs/documentation.js +1 -1
  2. package/dist/configs/system.d.ts +607 -2
  3. package/dist/configs/system.js +6 -0
  4. package/dist/entities/documentation/Documentation.d.ts +1 -1
  5. package/dist/entities/system/Component.js +1 -2
  6. package/dist/entities/system/LinkedSystem.d.ts +17 -2
  7. package/dist/entities/system/Modifier.d.ts +5 -2
  8. package/dist/entities/system/Modifier.js +5 -2
  9. package/dist/entities/system/Motion.d.ts +578 -4
  10. package/dist/entities/system/Motion.js +233 -70
  11. package/dist/entities/system/css-motion-reach.d.ts +62 -0
  12. package/dist/entities/system/css-motion-reach.js +195 -0
  13. package/dist/entities/system/css-motion.d.ts +55 -0
  14. package/dist/entities/system/css-motion.js +445 -0
  15. package/dist/entities/system/css-timing.d.ts +40 -0
  16. package/dist/entities/system/css-timing.js +247 -0
  17. package/dist/entities/system/easings.d.ts +29 -0
  18. package/dist/entities/system/easings.js +99 -0
  19. package/dist/entities/system/motion-convert.d.ts +42 -0
  20. package/dist/entities/system/motion-convert.js +517 -0
  21. package/dist/entities/system/motion-presets.d.ts +9 -1
  22. package/dist/entities/system/motion-presets.js +27 -38
  23. package/dist/entities/system/motion-variants.js +66 -49
  24. package/dist/entities/system/reduced-motion.d.ts +20 -0
  25. package/dist/entities/system/reduced-motion.js +42 -0
  26. package/dist/framework/Config.d.ts +7 -2
  27. package/dist/framework/Config.js +47 -5
  28. package/dist/framework/class-names.d.ts +2 -1
  29. package/dist/framework/class-names.js +31 -13
  30. package/dist/framework/defineEntity.d.ts +3 -1
  31. package/dist/framework/defineEntity.js +2 -0
  32. package/dist/framework/registered.d.ts +8 -1
  33. package/dist/framework/registry.d.ts +38 -1
  34. package/dist/framework/registry.js +3 -1
  35. package/dist/framework/render-spec.js +7 -2
  36. package/dist/framework/schema-version.d.ts +1 -1
  37. package/dist/framework/schema-version.js +5 -5
  38. package/dist/framework/signature.js +4 -2
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/migrations/2.0.0/v1-artifact.d.ts +606 -2
  42. package/dist/migrations/20261009000514_css_motion_states.d.ts +11 -0
  43. package/dist/migrations/20261009000514_css_motion_states.js +10 -0
  44. package/dist/migrations/20261009022722_starting_style_modifier.d.ts +9 -0
  45. package/dist/migrations/20261009022722_starting_style_modifier.js +10 -0
  46. package/dist/migrations/20261009181635_css_motion_strict.d.ts +23 -0
  47. package/dist/migrations/20261009181635_css_motion_strict.js +152 -0
  48. package/dist/spec/motion.d.ts +7 -0
  49. package/dist/spec/motion.js +7 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +6 -2
@@ -71,6 +71,11 @@ const MOTION_VARIANT_STATES = [
71
71
  "whileDrag"
72
72
  ];
73
73
  const jsBody = (config, path) => jsMotionBody(config, path);
74
+ /** A named CSS preset's body. */
75
+ const cssBody = (config, path) => {
76
+ const body = config.resolve("motion", path)?.toJSON();
77
+ return body?.runtime === "css" ? body : void 0;
78
+ };
74
79
  /** The variant names one state holds: a name, a list of names, or none (values, `false`). */
75
80
  function stateVariantNames(value) {
76
81
  if (typeof value === "string") return [value];
@@ -123,46 +128,55 @@ const composedComponents = (config, path) => {
123
128
  */
124
129
  const motionVariantGroups = createSliceMemo({
125
130
  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);
131
+ compute: (config) => variantGroupsOf({
132
+ config,
133
+ placements: placedMotions(config)
134
+ })
135
+ });
136
+ /**
137
+ * The presets that share variant names among `placements`, closed over placement and composition as
138
+ * {@link motionVariantGroups} describes. The JS presets' groups and the CSS presets' are each this,
139
+ * over their own placements.
140
+ */
141
+ function variantGroupsOf({ config, placements }) {
142
+ const parent = /* @__PURE__ */ new Map();
143
+ const find = (node) => {
144
+ let root = node;
145
+ while (parent.has(root) && parent.get(root) !== root) root = parent.get(root);
146
+ parent.set(node, root);
147
+ return root;
148
+ };
149
+ const join = (a, b) => {
150
+ const ra = find(a);
151
+ const rb = find(b);
152
+ if (ra !== rb) parent.set(ra, rb);
153
+ };
154
+ const placing = /* @__PURE__ */ new Set();
155
+ for (const { component, preset } of placements) {
156
+ placing.add(component);
157
+ join(`c\u0000${component}`, `p\u0000${preset}`);
158
+ }
159
+ for (const component of placing) {
160
+ const seen = new Set([component]);
161
+ const queue = composedComponents(config, component);
162
+ while (queue.length > 0) {
163
+ const next = queue.shift();
164
+ if (seen.has(next)) continue;
165
+ seen.add(next);
166
+ if (placing.has(next)) join(`c\u0000${component}`, `c\u0000${next}`);
167
+ queue.push(...composedComponents(config, next));
162
168
  }
163
- return [...groups.values()];
164
169
  }
165
- });
170
+ const groups = /* @__PURE__ */ new Map();
171
+ for (const node of parent.keys()) {
172
+ if (node[0] !== "p") continue;
173
+ const root = find(node);
174
+ const group = groups.get(root) ?? /* @__PURE__ */ new Set();
175
+ group.add(node.slice(2));
176
+ groups.set(root, group);
177
+ }
178
+ return [...groups.values()];
179
+ }
166
180
  /** The presets that share variant names with `preset` — itself alone when no rule places it. */
167
181
  function motionVariantGroupOf(config, preset) {
168
182
  return motionVariantGroups(config).find((group) => group.has(preset)) ?? new Set([preset]);
@@ -181,22 +195,25 @@ function renamedBody(body, from, to) {
181
195
  }
182
196
  const mentions = (body, name) => declaredVariants(body).includes(name) || namedVariants(body).includes(name);
183
197
  /**
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.
198
+ * Rename a variant across every preset that shares names with `path` ({@link motionVariantGroupOf},
199
+ * or `groupOf` for a CSS preset, whose presets share names among themselves): the key in each
200
+ * `variants` that declares it, and each state that names it. Refused when `path` is no preset that
201
+ * declares `from`, or when a preset in the set already declares `to`, since the two would become one
202
+ * variant.
188
203
  */
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.`);
204
+ function renameMotionVariant(config, { path, from, to, groupOf }) {
205
+ const css = cssBody(config, path) !== void 0;
206
+ const bodyOf = css ? cssBody : jsBody;
207
+ const own = bodyOf(config, path);
208
+ if (!own) reject("NOT_FOUND", `"${path}" is not a motion preset.`);
192
209
  if (!declaredVariants(own).includes(from)) reject("NOT_FOUND", `"${path}" declares no variant "${from}".`);
193
210
  if (from === to) return config;
194
- const group = [...motionVariantGroupOf(config, path)];
195
- const taken = group.filter((preset) => declaredVariants(jsBody(config, preset)).includes(to));
211
+ const group = [...css && groupOf ? groupOf(config, path) : motionVariantGroupOf(config, path)];
212
+ const taken = group.filter((preset) => declaredVariants(bodyOf(config, preset)).includes(to));
196
213
  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
214
  let next = config;
198
215
  for (const preset of group) {
199
- const body = jsBody(next, preset);
216
+ const body = bodyOf(next, preset);
200
217
  if (!body || !mentions(body, from)) continue;
201
218
  const inline = inlineMotions(next).valid.find((entry) => entry.id === preset);
202
219
  if (!inline) {
@@ -267,4 +284,4 @@ function orphanedMotionVariants(config) {
267
284
  return out;
268
285
  }
269
286
  //#endregion
270
- export { MOTION_VARIANT_STATES, declaredVariants, inlineMotionId, inlineMotions, isInlineMotion, jsMotionBody, motionVariantGroups, orphanedMotionVariants, placedMotions, renameMotionVariant, stateVariantNames };
287
+ export { MOTION_VARIANT_STATES, declaredVariants, inlineMotionId, inlineMotions, isInlineMotion, jsMotionBody, motionVariantGroups, orphanedMotionVariants, placedMotions, renameMotionVariant, stateVariantNames, variantGroupsOf };
@@ -0,0 +1,20 @@
1
+ import { Config } from "../../framework/Config.js";
2
+
3
+ //#region src/entities/system/reduced-motion.d.ts
4
+ /** The attribute a layer carries its reduced motion as. */
5
+ declare const REDUCED_MOTION_ATTRIBUTE = "data-uds-reduced-motion";
6
+ /** What a layer's motion does under the viewer's reduced-motion setting. */
7
+ declare const REDUCED_MOTION_VALUES: readonly ["user", "always", "never"];
8
+ type ReducedMotion = (typeof REDUCED_MOTION_VALUES)[number];
9
+ /** A style rule setting a layer's reduced motion. */
10
+ interface ReducedMotionLayer {
11
+ readonly component: string;
12
+ /** The rule's key: its condition, `*` for the unconditional rule. */
13
+ readonly rule: string;
14
+ readonly layer: string;
15
+ readonly value: ReducedMotion;
16
+ }
17
+ /** Every style rule that sets a layer's reduced motion. */
18
+ declare function reducedMotionLayers(config: Config): readonly ReducedMotionLayer[];
19
+ //#endregion
20
+ export { REDUCED_MOTION_ATTRIBUTE, REDUCED_MOTION_VALUES, ReducedMotion, ReducedMotionLayer, reducedMotionLayers };
@@ -0,0 +1,42 @@
1
+ import { isPlainObject } from "../../framework/utils/field-path.js";
2
+ import { createSliceMemo } from "../../framework/memo.js";
3
+ //#region src/entities/system/reduced-motion.ts
4
+ /** The style rule key a layer's reduced motion is set by. */
5
+ const REDUCED_MOTION_KEY = "reducedMotion";
6
+ /** The attribute a layer carries its reduced motion as. */
7
+ const REDUCED_MOTION_ATTRIBUTE = "data-uds-reduced-motion";
8
+ /** What a layer's motion does under the viewer's reduced-motion setting. */
9
+ const REDUCED_MOTION_VALUES = [
10
+ "user",
11
+ "always",
12
+ "never"
13
+ ];
14
+ function isReducedMotion(value) {
15
+ return REDUCED_MOTION_VALUES.includes(value);
16
+ }
17
+ const resolveReducedMotion = createSliceMemo({
18
+ kinds: ["component"],
19
+ compute: (config) => {
20
+ const layers = [];
21
+ for (const component of config.list("component")) {
22
+ const { styles = {} } = component.toJSON();
23
+ for (const [rule, entry] of Object.entries(styles)) for (const [layer, bag] of Object.entries(entry?.layers ?? {})) {
24
+ if (!isPlainObject(bag) || !(REDUCED_MOTION_KEY in bag)) continue;
25
+ const value = bag[REDUCED_MOTION_KEY];
26
+ if (isReducedMotion(value)) layers.push({
27
+ component: component.path,
28
+ rule,
29
+ layer,
30
+ value
31
+ });
32
+ }
33
+ }
34
+ return layers;
35
+ }
36
+ });
37
+ /** Every style rule that sets a layer's reduced motion. */
38
+ function reducedMotionLayers(config) {
39
+ return resolveReducedMotion(config);
40
+ }
41
+ //#endregion
42
+ export { REDUCED_MOTION_ATTRIBUTE, REDUCED_MOTION_VALUES, isReducedMotion, reducedMotionLayers };
@@ -9,7 +9,7 @@ import { RefGraph, TraverseOptions } from "./ref-graph.js";
9
9
  import { FieldsSchema } from "./utils/field-schema.js";
10
10
  import { SubEntityClass } from "./defineSubEntity.js";
11
11
  import { LocalOverlay, SourceAnswer, SourceOverlay, SourceResolver, SourceState } from "./sources.js";
12
- import { EntityKind } from "./registry.js";
12
+ import { EntityKind, LayerKey } from "./registry.js";
13
13
  import { AuthoredEntry } from "./utils/group.js";
14
14
  import { EntityClass } from "./defineEntity.js";
15
15
  import { BreakPlan, LinkPlan, SourceRowState, UnlinkPlan } from "../entities/system/link-plan.js";
@@ -73,7 +73,7 @@ interface RecordedSource {
73
73
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
74
74
  * that replaced it (see `detectedWireVersion`).
75
75
  */
76
- declare const SERIALIZED_CONFIG_VERSION = 20261008181849;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261009181635;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -667,6 +667,11 @@ declare class Config {
667
667
  * was registered into this instance. `undefined` for a kind this config doesn't know. The single
668
668
  * accessor every internal read routes through. */
669
669
  entityOf(kind: string): EntityKind | undefined;
670
+ /** Every key a style rule's layer bag takes that a kind owns, with the kind that owns it. See
671
+ * {@link LayerKey}. */
672
+ layerKeys(): ReadonlyMap<string, LayerKey & {
673
+ readonly kind: string;
674
+ }>;
670
675
  /**
671
676
  * The value each of a kind's fields takes when a create leaves it out — `responsive: false` on a
672
677
  * style property, say.
@@ -55,6 +55,9 @@ function nameEdgeReaders(entities) {
55
55
  for (const [kind, entry] of entities) if (entry.nameEdges) readers.set(kind, entry.nameEdges);
56
56
  return readers;
57
57
  }
58
+ /** {@link Config.layerKeys}, once per set of registered kinds, which every config built on one
59
+ * registration shares. */
60
+ const layerKeysByEntities = /* @__PURE__ */ new WeakMap();
58
61
  /**
59
62
  * The kinds whose items name an external source — what a `@slug` in a ref resolves against.
60
63
  *
@@ -812,6 +815,19 @@ var Config = class Config {
812
815
  entityOf(kind) {
813
816
  return this.state.entities.get(kind);
814
817
  }
818
+ /** Every key a style rule's layer bag takes that a kind owns, with the kind that owns it. See
819
+ * {@link LayerKey}. */
820
+ layerKeys() {
821
+ const cached = layerKeysByEntities.get(this.state.entities);
822
+ if (cached) return cached;
823
+ const out = /* @__PURE__ */ new Map();
824
+ for (const [kind, entity] of this.state.entities) for (const [key, declared] of Object.entries(entity.layerKeys ?? {})) out.set(key, {
825
+ ...declared,
826
+ kind
827
+ });
828
+ layerKeysByEntities.set(this.state.entities, out);
829
+ return out;
830
+ }
815
831
  /**
816
832
  * The value each of a kind's fields takes when a create leaves it out — `responsive: false` on a
817
833
  * style property, say.
@@ -1868,14 +1884,24 @@ var Config = class Config {
1868
1884
  kind: patch.kind,
1869
1885
  path: pathOf(extension)
1870
1886
  }) : void 0;
1871
- const written = applyPathDelta(current ? extendBody(current.source, current.overlay) : resolved.toJSON(), delta, isField);
1887
+ const written = applyPathDelta(current ? extendBody(rebasedLinkedBody({
1888
+ entry,
1889
+ path: pathOf(extension),
1890
+ body: current.source,
1891
+ extend: current.overlay
1892
+ }), current.overlay) : resolved.toJSON(), delta, isField);
1872
1893
  const local = { ...current ? current.overlay : extension.extend };
1873
1894
  for (const field of touched) if (Object.hasOwn(delta, field) && delta[field] === null) delete local[field];
1874
1895
  else if (Object.hasOwn(written, field)) local[field] = written[field];
1875
1896
  else delete local[field];
1876
1897
  const sourceBody = this.configFor(extension)?.resolve(patch.kind, pathOf(extension))?.toJSON();
1877
1898
  if (!sourceBody) reject("NOT_FOUND", `Cannot edit ${patch.kind} "${patch.path}" — its source "${extension.__ref}" is unavailable`);
1878
- const candidate = extendBody(sourceBody, local);
1899
+ const candidate = extendBody(rebasedLinkedBody({
1900
+ entry,
1901
+ path: pathOf(extension),
1902
+ body: sourceBody,
1903
+ extend: local
1904
+ }), local);
1879
1905
  const createSchema = wholeBodySchema(entry, { config: this });
1880
1906
  let merged = candidate;
1881
1907
  if (createSchema) {
@@ -2610,12 +2636,19 @@ var Config = class Config {
2610
2636
  if (!base) return void 0;
2611
2637
  const slug = sourceOf(extension);
2612
2638
  const owns = (kindName) => source?.ownedKinds().includes(kindName) ?? false;
2613
- const merged = extendBody(slug ? qualifyRefs({
2639
+ const inherited = slug ? qualifyRefs({
2614
2640
  value: base.toJSON(),
2615
2641
  source: slug,
2616
2642
  owns,
2617
2643
  namesItAlready: (target) => this.borrowsExactly(target, slug)
2618
- }) : base.toJSON(), extension.extend ?? {});
2644
+ }) : base.toJSON();
2645
+ const extend = extension.extend ?? {};
2646
+ const merged = extendBody(rebasedLinkedBody({
2647
+ entry: this.entityOf(kind),
2648
+ path: pathOf(extension),
2649
+ body: inherited,
2650
+ extend
2651
+ }), extend);
2619
2652
  return this.entityOf(kind)?.prepareLinkedBody?.({
2620
2653
  path: pathOf(extension),
2621
2654
  body: merged,
@@ -5425,7 +5458,7 @@ var Config = class Config {
5425
5458
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5426
5459
  hydrateFrom(stored, options) {
5427
5460
  const claimed = schemaVersionOf(stored);
5428
- if (claimed > 20261008181849) throw new SchemaVersionTooNew(claimed);
5461
+ if (claimed > 20261009181635) throw new SchemaVersionTooNew(claimed);
5429
5462
  const wire = upgradeSerializedConfig(stored);
5430
5463
  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\`.`);
5431
5464
  const leftover = wire.options;
@@ -5590,6 +5623,15 @@ function extendBody(base, delta) {
5590
5623
  * differently, and why the source's body is a parameter. `extend: { values: [Token.ref('surface')] }`
5591
5624
  * means "alongside what they already accept", which has nothing to join without their list.
5592
5625
  */
5626
+ /** A source's body as an extension reads it before its `extend` merges in: the kind's
5627
+ * {@link EntityKind.rebaseLinkedBody}, where it declares one. */
5628
+ function rebasedLinkedBody({ entry, path, body, extend }) {
5629
+ return entry?.rebaseLinkedBody?.({
5630
+ path,
5631
+ body,
5632
+ extend
5633
+ }) ?? body;
5634
+ }
5593
5635
  function overlayBody({ overlay, theirs }) {
5594
5636
  if (!overlay) return void 0;
5595
5637
  return layerOverlay({
@@ -123,7 +123,8 @@ declare function motionClassName(config: Config, path: string): string;
123
123
  * rule declares. Without this the value went nowhere — `motion` names no style property so it isn't
124
124
  * a declaration, and it IS a declared prop of the layer's element so it isn't an attribute either.
125
125
  *
126
- * A JS preset is skipped: it animates in the client and has no class to wear.
126
+ * A JS preset is skipped: it animates in the client and has no class to wear. The classes come in the
127
+ * order the rules apply, so the last one that applies is the preset the layer plays.
127
128
  */
128
129
  declare function componentMotionClasses(config: Config, path: string): ComponentRuleClass[];
129
130
  /**
@@ -277,6 +277,12 @@ function componentCompositeClasses(config, path) {
277
277
  }
278
278
  return out;
279
279
  }
280
+ /** The bag keys a kind owns as props of a type, as an element's declared props spell them. */
281
+ function ownedLayerProps(config) {
282
+ const out = {};
283
+ for (const [key, owned] of config.layerKeys()) if (owned.propType !== void 0) out[key] = { type: owned.propType };
284
+ return out;
285
+ }
280
286
  /** The class a CSS motion preset is applied by. Namespaced, so it doesn't share a space with the
281
287
  * style-prop utilities; the `@keyframes` it runs keep their own name. */
282
288
  function motionClassName(config, path) {
@@ -293,25 +299,30 @@ function motionClassName(config, path) {
293
299
  * rule declares. Without this the value went nowhere — `motion` names no style property so it isn't
294
300
  * a declaration, and it IS a declared prop of the layer's element so it isn't an attribute either.
295
301
  *
296
- * A JS preset is skipped: it animates in the client and has no class to wear.
302
+ * A JS preset is skipped: it animates in the client and has no class to wear. The classes come in the
303
+ * order the rules apply, so the last one that applies is the preset the layer plays.
297
304
  */
298
305
  function componentMotionClasses(config, path) {
299
306
  const component = config.resolve("component", path);
300
307
  if (!component) return [];
301
308
  const { layers = {}, styles = {} } = component.toJSON();
302
309
  const out = [];
303
- for (const rule of Object.values(styles)) {
310
+ const rules = Object.values(styles);
311
+ const unconditional = (rule) => !rule.when || Object.keys(rule.when).length === 0;
312
+ for (const rule of [...rules.filter(unconditional), ...rules.filter((rule) => !unconditional(rule))]) {
304
313
  const when = Object.fromEntries(Object.entries(rule.when ?? {}).map(([prop, value]) => [prop, refLeaf(value)]));
305
314
  for (const [layer, bag] of Object.entries(rule.layers ?? {})) {
306
315
  const element = layers[layer];
307
- if (!isRef(element)) continue;
308
- const declared = resolveComponentProps(config, pathOf(element.__ref));
316
+ if (element === void 0) continue;
317
+ const declared = {
318
+ ...ownedLayerProps(config),
319
+ ...isRef(element) ? resolveComponentProps(config, pathOf(element.__ref)) : {}
320
+ };
309
321
  for (const [prop, value] of Object.entries(bag ?? {})) {
310
322
  if (declared[prop]?.type !== "motion") continue;
311
323
  const name = presetNamed(value);
312
324
  if (name === void 0) continue;
313
- const preset = config.resolve("motion", name)?.toJSON();
314
- if (preset?.runtime !== "css" || !preset.keyframes) continue;
325
+ if ((config.resolve("motion", name)?.toJSON())?.runtime !== "css") continue;
315
326
  out.push({
316
327
  layer,
317
328
  when,
@@ -530,8 +541,19 @@ function layerRoutedProps(config, path, state = {}) {
530
541
  const declared = isRef(element) ? resolveComponentProps(config, pathOf(element.__ref)) : {};
531
542
  for (const [prop, value] of Object.entries(bag ?? {})) {
532
543
  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;
544
+ const owned = config.layerKeys().get(prop);
545
+ if (owned) {
546
+ const reach = owned.reach({
547
+ config,
548
+ value,
549
+ element
550
+ });
551
+ if (reach === "none") continue;
552
+ if (reach !== "prop") {
553
+ out[layer] ??= {};
554
+ out[layer][reach.attribute] = value;
555
+ continue;
556
+ }
535
557
  }
536
558
  const styleKey = stylePropertyPathFor({
537
559
  config,
@@ -600,11 +622,7 @@ function styleRuleMotionClasses(config, path, state = {}) {
600
622
  ...state
601
623
  };
602
624
  const out = {};
603
- for (const rule of componentMotionClasses(config, path)) {
604
- if (!ruleApplies(rule.when, resolved)) continue;
605
- out[rule.layer] ??= [];
606
- if (!out[rule.layer].includes(rule.className)) out[rule.layer].push(rule.className);
607
- }
625
+ for (const rule of componentMotionClasses(config, path)) if (ruleApplies(rule.when, resolved)) out[rule.layer] = [rule.className];
608
626
  return out;
609
627
  }
610
628
  /**
@@ -355,7 +355,9 @@ declare function defineEntity<F extends FieldsSchema, G extends FieldsSchema = z
355
355
  * an edit to one has nowhere to land.
356
356
  */
357
357
  bulkOverlayWrite?: (args: BulkOverlayWrite) => Record<string, unknown>; /** Adapt a pinned source body before linked validation. */
358
- prepareLinkedBody?: EntityKind['prepareLinkedBody'];
358
+ prepareLinkedBody?: EntityKind['prepareLinkedBody']; /** See {@link EntityKind.rebaseLinkedBody}. */
359
+ rebaseLinkedBody?: EntityKind['rebaseLinkedBody']; /** See {@link EntityKind.layerKeys}. */
360
+ layerKeys?: EntityKind['layerKeys'];
359
361
  /**
360
362
  * Edges this kind's data implies by name rather than by ref, and how to rewrite one — the pair that
361
363
  * makes a record keyed by another kind's item names behave like a reference: reported by
@@ -124,6 +124,8 @@ function defineEntity(spec) {
124
124
  bulkDirectives: spec.bulkDirectives,
125
125
  bulkOverlayWrite: spec.bulkOverlayWrite,
126
126
  prepareLinkedBody: spec.prepareLinkedBody,
127
+ rebaseLinkedBody: spec.rebaseLinkedBody,
128
+ layerKeys: spec.layerKeys,
127
129
  nameEdges: spec.nameEdges,
128
130
  styleBags: spec.styleBags,
129
131
  rewriteName: spec.rewriteName,
@@ -74,6 +74,13 @@ interface RegisteredEntities {}
74
74
  * arguments a locally authored component would.
75
75
  */
76
76
  interface RegisteredComponentContracts {}
77
+ /**
78
+ * Qualified linked motion preset → the runtimes a link can read it as: its own, and the other one when
79
+ * the preset converts (`motion/convert`). The consumer's emitted `uds-env.d.ts` fills it from the
80
+ * installed source config, so a link that swaps a preset to a runtime it can't convert to is a type
81
+ * error at the swap. Unaugmented, a swap takes either runtime.
82
+ */
83
+ interface RegisteredMotionRuntimes {}
77
84
  /** The generated contract for one qualified linked component, or an open fallback before build. */
78
85
  type ComponentContractOf<P extends string> = P extends keyof RegisteredComponentContracts ? RegisteredComponentContracts[P] : {
79
86
  layers: Record<string, unknown>;
@@ -100,4 +107,4 @@ type PathOf<K extends string> = K extends keyof RegisteredPaths ? RegisteredPath
100
107
  /** The value type for a style prop — its registered union, else `string`. */
101
108
  type ValueOf<P extends string> = P extends keyof RegisteredStyleProps ? RegisteredStyleProps[P] extends string | number ? RegisteredStyleProps[P] : string : string;
102
109
  //#endregion
103
- export { ComponentContractOf, PathOf, RegisteredComponentContracts, RegisteredEntities, RegisteredModifiers, RegisteredPaths, RegisteredStyleProps, ValueOf };
110
+ export { ComponentContractOf, PathOf, RegisteredComponentContracts, RegisteredEntities, RegisteredModifiers, RegisteredMotionRuntimes, RegisteredPaths, RegisteredStyleProps, ValueOf };
@@ -47,8 +47,45 @@ type AuthoredValueForm = (ctx: {
47
47
  readonly target: string; /** The leaves the target offers, which is what the form has to answer with. */
48
48
  readonly leaves: readonly string[];
49
49
  }) => z.ZodTypeAny | undefined;
50
+ /**
51
+ * A key a style rule's layer bag takes that a kind owns, rather than a prop the element declares or a
52
+ * style property: `motion` names a preset, `reducedMotion` says how the layer treats the OS setting.
53
+ */
54
+ interface LayerKey {
55
+ /** What the key takes. `declared` is the element's own prop of the same name, where it declares
56
+ * one, which the key's value can also be. */
57
+ readonly schema: (declared: z.ZodTypeAny | undefined) => z.ZodTypeAny;
58
+ /**
59
+ * How the value reaches the layer's element: as the element's prop of the key's name (`'prop'`),
60
+ * as an attribute (`{ attribute }`), or by some other route the build writes, such as a class the
61
+ * stylesheet emits (`'none'`).
62
+ */
63
+ readonly reach: (args: {
64
+ readonly config: Config; /** The value as the rule stores it. */
65
+ readonly value: unknown; /** The layer's element: a component ref, or a tag. */
66
+ readonly element: unknown;
67
+ }) => 'prop' | 'none' | {
68
+ readonly attribute: string;
69
+ };
70
+ /** The prop type the key stands for on every element, for a reader that finds an element's props by
71
+ * type: `motion`'s is `'motion'`, so a layer whose element declares no `motion` prop still plays a
72
+ * preset a rule names. */
73
+ readonly propType?: string;
74
+ }
50
75
  interface EntityKind {
51
76
  readonly name: string;
77
+ /**
78
+ * A source's body as this config reads it under a linked extension, before the extension's `extend`
79
+ * merges in: for a kind whose extension can change what the rest of the body means, such as a motion
80
+ * preset whose `runtime` the extension sets.
81
+ */
82
+ readonly rebaseLinkedBody?: (args: {
83
+ readonly path: string;
84
+ readonly body: Record<string, unknown>;
85
+ readonly extend: Record<string, unknown>;
86
+ }) => Record<string, unknown>;
87
+ /** The keys a style rule's layer bag takes that this kind owns, by key. See {@link LayerKey}. */
88
+ readonly layerKeys?: Readonly<Record<string, LayerKey>>;
52
89
  /** Adapt a pinned source body before this kind validates a linked extension. */
53
90
  readonly prepareLinkedBody?: (args: {
54
91
  readonly path: string;
@@ -333,4 +370,4 @@ interface EntityKind {
333
370
  * kind def a `Config` holds (`config.entityOf(kind)`); `undefined` → no ops. */
334
371
  declare function opsForKind(entry: EntityKind | undefined, config?: Config): DerivedOp[];
335
372
  //#endregion
336
- export { AuthoredValueForm, ComputedFields, ComputedInput, EntityKind, Linkable, opsForKind };
373
+ export { AuthoredValueForm, ComputedFields, ComputedInput, EntityKind, LayerKey, Linkable, opsForKind };
@@ -32,7 +32,7 @@ function titleCase(kind) {
32
32
  /** Derive a kind's immutable metadata from its field schemas — the value a `defineEntity`/
33
33
  * `defineDerivedEntity` attaches to its class and a `Config.register` stores in `#entities`. */
34
34
  function buildKind(spec) {
35
- const { name, prepareLinkedBody, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants } = spec;
35
+ const { name, prepareLinkedBody, rebaseLinkedBody, layerKeys, fields, groupFields, subEntities = {}, computed, label, labelPlural, schemas: userSchemas, derived, sourceIdentity, sourceOrigin, authoredRefs, linkable, localGroupFields, emergentGroups, singleton = false, bareMember, sourceResolution, bulkAdopts, bulkOverlay, bulkDirectives, bulkOverlayWrite, nameEdges, rewriteName, styleBags, uniqueLeaves, authoredValue, mentionFields, invariants } = spec;
36
36
  const allComputed = {
37
37
  sources: ({ member, config }) => sourceSlugs(member, config),
38
38
  borrowedFrom: ({ config, path }) => config.borrowedFrom(name, path),
@@ -49,6 +49,8 @@ function buildKind(spec) {
49
49
  return {
50
50
  name,
51
51
  ...prepareLinkedBody ? { prepareLinkedBody } : {},
52
+ ...rebaseLinkedBody ? { rebaseLinkedBody } : {},
53
+ ...layerKeys ? { layerKeys } : {},
52
54
  label: resolvedLabel,
53
55
  entityName: spec.entityName ?? entityClassName(name),
54
56
  labelPlural: labelPlural ?? `${resolvedLabel}s`,
@@ -5,8 +5,8 @@ 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";
9
8
  import { withRequiredContentHidden } from "../entities/system/required-content-layers.js";
9
+ import { inlineMotionProps, jsMotionPresets } from "../entities/system/motion-presets.js";
10
10
  import { graftPreviewElements } from "../spec/preview-elements.js";
11
11
  import { componentLayerClass, componentPropClasses, componentRuleClasses, layerJsMotionPresets, layerRoutedProps, splitStyleProps, styleRuleMotionClasses } from "./class-names.js";
12
12
  //#region src/framework/render-spec.ts
@@ -1350,7 +1350,7 @@ function expandInstance(sink, path, instanceProps, content, hint, instanceSlots)
1350
1350
  "data-uds-component": path,
1351
1351
  "data-uds-layer": layer,
1352
1352
  ...forwarded[layer],
1353
- ...motionPreset !== void 0 ? presetProps[motionPreset] : {},
1353
+ ...motionPreset !== void 0 ? withoutKey(presetProps[motionPreset], "reducedMotion") : {},
1354
1354
  ...Object.fromEntries((valueRoutes.get(layer) ?? []).filter(({ from }) => state[from] !== void 0).map(({ from, into }) => [into, state[from]])),
1355
1355
  ...isRoot ? rest : {},
1356
1356
  ...slot && slotValue !== void 0 ? { [intoName(slot.into)]: slotValue } : {},
@@ -1511,5 +1511,10 @@ function sameKeys(a, b) {
1511
1511
  function expandComponent(config, path, props = {}, options = {}) {
1512
1512
  return expandSpec(config, instanceSpec(path, props), options);
1513
1513
  }
1514
+ /** A record without one of its keys. */
1515
+ function withoutKey(record, key) {
1516
+ const { [key]: _omitted, ...rest } = record ?? {};
1517
+ return rest;
1518
+ }
1514
1519
  //#endregion
1515
1520
  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 = 20261008181849;
18
+ declare const CURRENT_SCHEMA_VERSION = 20261009181635;
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 = 20261008181849;
16
+ const CURRENT_SCHEMA_VERSION = 20261009181635;
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 > 20261008181849) 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 > 20261009181635) 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 ?? 20261008181849;
105
+ const target = options?.target ?? 20261009181635;
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 ?? 20261008181849;
163
+ const target = options?.target ?? 20261009181635;
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 ?? 20261008181849;
182
+ const target = options?.target ?? 20261009181635;
183
183
  const entries = [];
184
184
  const held = [];
185
185
  const retired = [];