@yahoo/uds-create-config 5.19.1 → 5.21.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 (56) 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 +8 -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 +45 -5
  7. package/dist/entities/system/LinkedSystem.js +55 -4
  8. package/dist/entities/system/Modifier.d.ts +5 -2
  9. package/dist/entities/system/Modifier.js +5 -2
  10. package/dist/entities/system/Motion.d.ts +578 -4
  11. package/dist/entities/system/Motion.js +233 -70
  12. package/dist/entities/system/css-motion-reach.d.ts +62 -0
  13. package/dist/entities/system/css-motion-reach.js +195 -0
  14. package/dist/entities/system/css-motion.d.ts +55 -0
  15. package/dist/entities/system/css-motion.js +445 -0
  16. package/dist/entities/system/css-timing.d.ts +40 -0
  17. package/dist/entities/system/css-timing.js +247 -0
  18. package/dist/entities/system/easings.d.ts +29 -0
  19. package/dist/entities/system/easings.js +99 -0
  20. package/dist/entities/system/motion-convert.d.ts +42 -0
  21. package/dist/entities/system/motion-convert.js +517 -0
  22. package/dist/entities/system/motion-presets.d.ts +9 -1
  23. package/dist/entities/system/motion-presets.js +27 -38
  24. package/dist/entities/system/motion-variants.js +66 -49
  25. package/dist/entities/system/reduced-motion.d.ts +20 -0
  26. package/dist/entities/system/reduced-motion.js +42 -0
  27. package/dist/framework/Config.d.ts +32 -2
  28. package/dist/framework/Config.js +117 -7
  29. package/dist/framework/class-names.d.ts +2 -1
  30. package/dist/framework/class-names.js +31 -13
  31. package/dist/framework/defineEntity.d.ts +3 -1
  32. package/dist/framework/defineEntity.js +2 -0
  33. package/dist/framework/registered.d.ts +8 -1
  34. package/dist/framework/registry.d.ts +38 -1
  35. package/dist/framework/registry.js +3 -1
  36. package/dist/framework/render-spec.js +7 -2
  37. package/dist/framework/schema-version.d.ts +1 -1
  38. package/dist/framework/schema-version.js +5 -5
  39. package/dist/framework/signature.js +4 -2
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/migrations/2.0.0/v1-artifact.d.ts +606 -2
  43. package/dist/migrations/20261009000514_css_motion_states.d.ts +11 -0
  44. package/dist/migrations/20261009000514_css_motion_states.js +10 -0
  45. package/dist/migrations/20261009022722_starting_style_modifier.d.ts +9 -0
  46. package/dist/migrations/20261009022722_starting_style_modifier.js +10 -0
  47. package/dist/migrations/20261009181635_css_motion_strict.d.ts +23 -0
  48. package/dist/migrations/20261009181635_css_motion_strict.js +152 -0
  49. package/dist/migrations/20261009193004_link_override_ref.d.ts +9 -0
  50. package/dist/migrations/20261009193004_link_override_ref.js +10 -0
  51. package/dist/spec/links.d.ts +2 -0
  52. package/dist/spec/links.js +2 -0
  53. package/dist/spec/motion.d.ts +7 -0
  54. package/dist/spec/motion.js +7 -0
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +10 -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 = 20261009193004;
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.
@@ -1323,6 +1328,31 @@ declare class Config {
1323
1328
  * only as a borrow the picker offered and the read then refused.
1324
1329
  */
1325
1330
  private extensionBody;
1331
+ /**
1332
+ * A source's body with a linked extension's `extend` laid over it, the one merge every read and
1333
+ * write of a borrowed row goes through. An `extend` whose `__ref` names another entity of the kind
1334
+ * (a link's `override: Motion.ref('css/scale')`) re-points the path: it reads as that entity, as
1335
+ * this config resolves it, with the rest of the `extend` over it. The kind's
1336
+ * {@link EntityKind.rebaseLinkedBody} then adapts the base before the merge.
1337
+ *
1338
+ * A target that resolves to nothing, names another kind, or leads back to this path through other
1339
+ * re-pointed paths leaves an empty body, which the kind's schema refuses, so the row reads as
1340
+ * nothing. The pin's invariants refuse the last two when they're written, and the build reports a
1341
+ * missing target (`brokenLinkRedirects`).
1342
+ */
1343
+ laidLinkedBody({
1344
+ kind,
1345
+ path,
1346
+ body,
1347
+ extend
1348
+ }: {
1349
+ kind: string;
1350
+ path: string;
1351
+ body: Record<string, unknown>;
1352
+ extend: Record<string, unknown>;
1353
+ }): Record<string, unknown>;
1354
+ /** The body of the entity a re-pointed path names, or `undefined` when there is none to read. */
1355
+ private redirectedBody;
1326
1356
  /**
1327
1357
  * Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
1328
1358
  * authored here, then parsed through this config's kind. Parsing on this side is what lets a React
@@ -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.
@@ -1784,7 +1800,12 @@ var Config = class Config {
1784
1800
  kind: patch.kind,
1785
1801
  path: pathOf(extension)
1786
1802
  }) : void 0;
1787
- const inherited = current ? extendBody(current.source, current.overlay) : resolved.toJSON();
1803
+ const inherited = current ? this.at(snapshot).laidLinkedBody({
1804
+ kind: patch.kind,
1805
+ path: pathOf(extension),
1806
+ body: current.source,
1807
+ extend: current.overlay
1808
+ }) : resolved.toJSON();
1788
1809
  const materialized = snapshot.withChanges([itemChange(entry, patch.kind, patch.path, inherited)]);
1789
1810
  const changed = this.applySub(materialized, patch, materialized.get(patch.kind, patch.path) ?? resolved, mode);
1790
1811
  const body = changed.get(patch.kind, patch.path)?.toJSON();
@@ -1868,14 +1889,24 @@ var Config = class Config {
1868
1889
  kind: patch.kind,
1869
1890
  path: pathOf(extension)
1870
1891
  }) : void 0;
1871
- const written = applyPathDelta(current ? extendBody(current.source, current.overlay) : resolved.toJSON(), delta, isField);
1892
+ const written = applyPathDelta(current ? this.at(snapshot).laidLinkedBody({
1893
+ kind: patch.kind,
1894
+ path: pathOf(extension),
1895
+ body: current.source,
1896
+ extend: current.overlay
1897
+ }) : resolved.toJSON(), delta, isField);
1872
1898
  const local = { ...current ? current.overlay : extension.extend };
1873
1899
  for (const field of touched) if (Object.hasOwn(delta, field) && delta[field] === null) delete local[field];
1874
1900
  else if (Object.hasOwn(written, field)) local[field] = written[field];
1875
1901
  else delete local[field];
1876
1902
  const sourceBody = this.configFor(extension)?.resolve(patch.kind, pathOf(extension))?.toJSON();
1877
1903
  if (!sourceBody) reject("NOT_FOUND", `Cannot edit ${patch.kind} "${patch.path}" — its source "${extension.__ref}" is unavailable`);
1878
- const candidate = extendBody(sourceBody, local);
1904
+ const candidate = this.at(snapshot).laidLinkedBody({
1905
+ kind: patch.kind,
1906
+ path: pathOf(extension),
1907
+ body: sourceBody,
1908
+ extend: local
1909
+ });
1879
1910
  const createSchema = wholeBodySchema(entry, { config: this });
1880
1911
  let merged = candidate;
1881
1912
  if (createSchema) {
@@ -1894,8 +1925,12 @@ var Config = class Config {
1894
1925
  if (!sourceKind || !write) reject("UNSUPPORTED", `Cannot edit ${patch.kind} "${patch.path}" — "${slug}" has no way to record the change`);
1895
1926
  const accessor = view.accessorOf(patch.kind) ?? patch.kind;
1896
1927
  const existing = view.sourceOverlay(slug, patch.kind, patch.path);
1928
+ const { target: redirect, rest } = redirectOf(local);
1897
1929
  const overlay = {
1898
- ...classifyOverlay(local, sourceBody),
1930
+ ...redirect === void 0 ? classifyOverlay(local, sourceBody) : {
1931
+ override: { __ref: redirect },
1932
+ ...Object.keys(rest).length > 0 ? { extend: rest } : {}
1933
+ },
1899
1934
  ...existing?.hidden ? { hidden: true } : {}
1900
1935
  };
1901
1936
  const data = view.directiveWrite({
@@ -2610,12 +2645,18 @@ var Config = class Config {
2610
2645
  if (!base) return void 0;
2611
2646
  const slug = sourceOf(extension);
2612
2647
  const owns = (kindName) => source?.ownedKinds().includes(kindName) ?? false;
2613
- const merged = extendBody(slug ? qualifyRefs({
2648
+ const inherited = slug ? qualifyRefs({
2614
2649
  value: base.toJSON(),
2615
2650
  source: slug,
2616
2651
  owns,
2617
2652
  namesItAlready: (target) => this.borrowsExactly(target, slug)
2618
- }) : base.toJSON(), extension.extend ?? {});
2653
+ }) : base.toJSON();
2654
+ const merged = this.laidLinkedBody({
2655
+ kind,
2656
+ path: pathOf(extension),
2657
+ body: inherited,
2658
+ extend: extension.extend ?? {}
2659
+ });
2619
2660
  return this.entityOf(kind)?.prepareLinkedBody?.({
2620
2661
  path: pathOf(extension),
2621
2662
  body: merged,
@@ -2624,6 +2665,46 @@ var Config = class Config {
2624
2665
  }) ?? merged;
2625
2666
  }
2626
2667
  /**
2668
+ * A source's body with a linked extension's `extend` laid over it, the one merge every read and
2669
+ * write of a borrowed row goes through. An `extend` whose `__ref` names another entity of the kind
2670
+ * (a link's `override: Motion.ref('css/scale')`) re-points the path: it reads as that entity, as
2671
+ * this config resolves it, with the rest of the `extend` over it. The kind's
2672
+ * {@link EntityKind.rebaseLinkedBody} then adapts the base before the merge.
2673
+ *
2674
+ * A target that resolves to nothing, names another kind, or leads back to this path through other
2675
+ * re-pointed paths leaves an empty body, which the kind's schema refuses, so the row reads as
2676
+ * nothing. The pin's invariants refuse the last two when they're written, and the build reports a
2677
+ * missing target (`brokenLinkRedirects`).
2678
+ */
2679
+ laidLinkedBody({ kind, path, body, extend }) {
2680
+ const { target, rest } = redirectOf(extend);
2681
+ const base = target === void 0 ? body : this.redirectedBody({
2682
+ kind,
2683
+ path,
2684
+ target
2685
+ }) ?? {};
2686
+ return extendBody(rebasedLinkedBody({
2687
+ entry: this.entityOf(kind),
2688
+ path,
2689
+ body: base,
2690
+ extend: rest
2691
+ }), rest);
2692
+ }
2693
+ /** The body of the entity a re-pointed path names, or `undefined` when there is none to read. */
2694
+ redirectedBody({ kind, path, target }) {
2695
+ if (kindOf(target) !== kind) return void 0;
2696
+ const reading = redirecting.get(this.state) ?? /* @__PURE__ */ new Set();
2697
+ const key = `${kind}:${path}`;
2698
+ if (reading.has(key)) return void 0;
2699
+ reading.add(key);
2700
+ redirecting.set(this.state, reading);
2701
+ try {
2702
+ return this.resolveRef(target)?.toJSON();
2703
+ } finally {
2704
+ reading.delete(key);
2705
+ }
2706
+ }
2707
+ /**
2627
2708
  * Resolve one locally extended linked item: the pinned source body, shallowly overlaid by the fields
2628
2709
  * authored here, then parsed through this config's kind. Parsing on this side is what lets a React
2629
2710
  * Native kind transform or reject a web value rather than trusting the source kind's rules.
@@ -3519,6 +3600,12 @@ var Config = class Config {
3519
3600
  },
3520
3601
  theirs
3521
3602
  });
3603
+ if (laid && redirectOf(laid).target !== void 0) return this.laidLinkedBody({
3604
+ kind,
3605
+ path,
3606
+ body: theirs,
3607
+ extend: laid
3608
+ });
3522
3609
  return laid ? {
3523
3610
  ...theirs,
3524
3611
  ...laid
@@ -5425,7 +5512,7 @@ var Config = class Config {
5425
5512
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5426
5513
  hydrateFrom(stored, options) {
5427
5514
  const claimed = schemaVersionOf(stored);
5428
- if (claimed > 20261008181849) throw new SchemaVersionTooNew(claimed);
5515
+ if (claimed > 20261009193004) throw new SchemaVersionTooNew(claimed);
5429
5516
  const wire = upgradeSerializedConfig(stored);
5430
5517
  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
5518
  const leftover = wire.options;
@@ -5590,6 +5677,29 @@ function extendBody(base, delta) {
5590
5677
  * differently, and why the source's body is a parameter. `extend: { values: [Token.ref('surface')] }`
5591
5678
  * means "alongside what they already accept", which has nothing to join without their list.
5592
5679
  */
5680
+ /** The paths each config is reading through a re-pointed link, for {@link Config.laidLinkedBody} to
5681
+ * find a loop by. Keyed on the config's state, which every read of one config shares. */
5682
+ const redirecting = /* @__PURE__ */ new WeakMap();
5683
+ /**
5684
+ * The entity an `extend` re-points its path to, and the rest of it: a link's `override` written as a
5685
+ * ref lands in the stored `extend` as its `__ref`, beside whatever the `extend` verb adds.
5686
+ */
5687
+ function redirectOf(extend) {
5688
+ const { __ref: target, ...rest } = extend;
5689
+ return typeof target === "string" ? {
5690
+ target,
5691
+ rest
5692
+ } : { rest: extend };
5693
+ }
5694
+ /** A source's body as an extension reads it before its `extend` merges in: the kind's
5695
+ * {@link EntityKind.rebaseLinkedBody}, where it declares one. */
5696
+ function rebasedLinkedBody({ entry, path, body, extend }) {
5697
+ return entry?.rebaseLinkedBody?.({
5698
+ path,
5699
+ body,
5700
+ extend
5701
+ }) ?? body;
5702
+ }
5593
5703
  function overlayBody({ overlay, theirs }) {
5594
5704
  if (!overlay) return void 0;
5595
5705
  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 };