@yahoo/uds-create-config 5.32.10 → 5.34.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.
@@ -242,15 +242,41 @@ function remove(config, input) {
242
242
  path
243
243
  }))];
244
244
  }
245
+ /** The annotations on `paths`, oldest first, as a layer lists them. */
246
+ function annotationsOn({ config, paths }) {
247
+ return config.list("annotation").map((note) => ({
248
+ ...note.body,
249
+ path: note.path
250
+ })).filter((note) => paths.has(pathOf(note.target))).sort((left, right) => (left.authoredAt ?? "").localeCompare(right.authoredAt ?? "") || left.path.localeCompare(right.path));
251
+ }
252
+ /**
253
+ * Each copy of a duplicated element carries copies of the annotations on it. A copy names no author: the
254
+ * writer who duplicates is its author, and stamps it where the write lands.
255
+ */
245
256
  function duplicate(config, input) {
246
257
  const place = placeOf(config, input.path, ["path"]);
247
258
  const fresh = freshPaths(config, rootOf(input.path));
248
259
  const subtree = subtreeOf(config, input.path);
260
+ const notes = annotationsOn({
261
+ config,
262
+ paths: new Set(subtree)
263
+ });
249
264
  const count = input.data?.count ?? 1;
250
265
  const patches = [];
266
+ const annotationPatches = [];
251
267
  const copies = [];
252
268
  for (let n = 0; n < count; n++) {
253
269
  const pathFor = new Map(subtree.map((path) => [path, fresh(path.split("/").at(-1) ?? path)]));
270
+ for (const note of notes) annotationPatches.push({
271
+ kind: "annotation",
272
+ operation: "create",
273
+ path: crypto.randomUUID(),
274
+ data: {
275
+ target: ref("node", pathFor.get(pathOf(note.target))),
276
+ ...note.layer ? { layer: note.layer } : {},
277
+ content: note.content
278
+ }
279
+ });
254
280
  const remap = (entry) => {
255
281
  const to = pathFor.get(pathOf(entry));
256
282
  return to ? ref("node", to) : entry;
@@ -272,7 +298,7 @@ function duplicate(config, input) {
272
298
  }
273
299
  const list = [...place.list];
274
300
  list.splice(place.index + 1, 0, ...copies);
275
- patches.push(placeUpdate(place, list));
301
+ patches.push(placeUpdate(place, list), ...annotationPatches);
276
302
  return patches;
277
303
  }
278
304
  function unwrap(config, input) {
@@ -537,7 +563,7 @@ const nodeGestureOps = {
537
563
  input: duplicateInput,
538
564
  readOnly: false,
539
565
  label: "Duplicate an element",
540
- description: "Copy this element and everything inside it (\"duplicate\", \"make 3 more of these\"): each copy lands right after it among its siblings, `count` of them.",
566
+ description: "Copy this element and everything inside it (\"duplicate\", \"make 3 more of these\"): each copy lands right after it among its siblings, `count` of them, with a copy of each annotation on the copied elements.",
541
567
  scope: "item",
542
568
  title: (entity) => `Duplicate ${entity}`,
543
569
  example: () => ({
@@ -4,7 +4,8 @@ import { ReducedMotion } from "./reduced-motion.js";
4
4
  //#region src/entities/system/motion-presets.d.ts
5
5
  /**
6
6
  * A value a JS preset holds that `motion/react` cannot be handed as written: a token a mode or state
7
- * decides (`conditional`), which a preset animates as its base value; a token that comes to nothing a
7
+ * decides (`conditional`), or one a style rule sets that sits inside a composed value (`rule-set`),
8
+ * either of which a preset animates as its base value; a token that comes to nothing a
8
9
  * preset can carry (`unresolved`); or an `ease` that is no easing `motion/react` reads (`easing`),
9
10
  * from a token or written in place. The last two leave the field out.
10
11
  */
@@ -14,12 +15,13 @@ interface MotionPresetToken {
14
15
  readonly at: string;
15
16
  /** The token the value comes from; absent for an `ease` written in place. */
16
17
  readonly token?: string;
17
- readonly problem: 'conditional' | 'unresolved' | 'easing';
18
+ readonly problem: 'conditional' | 'rule-set' | 'unresolved' | 'easing';
18
19
  }
19
20
  /**
20
21
  * Every JS-runtime preset, by path, as the props the wrapper spreads onto the animated component.
21
22
  * A token a preset names reaches `motion/react` as its value — `Token.ref('springStiffness/snappy')`
22
- * as `400`, a `200ms` duration as `0.2` — since it reads numbers and seconds, not variables.
23
+ * as `400`, a `200ms` duration as `0.2` — since it reads numbers and seconds, not variables. A token
24
+ * a style rule sets is the one exception: it reaches `motion/react` as its `var()`.
23
25
  */
24
26
  declare function jsMotionPresets(config: Config): Record<string, Record<string, unknown>>;
25
27
  /** Every JS preset a style rule writes in place, by its id, resolved as {@link jsMotionPresets} are. */
@@ -1,4 +1,4 @@
1
- import { collectRefs, isRef, kindOf, pathOf } from "../../framework/utils/refs.js";
1
+ import { collectRefs, isRef, kindOf, pathOf, refOfKey } from "../../framework/utils/refs.js";
2
2
  import { composedValueFormOf } from "./composed-values.js";
3
3
  import { createSliceMemo } from "../../framework/memo.js";
4
4
  import { tokenValueRow } from "./token-values.js";
@@ -43,12 +43,48 @@ function motionEasing(value) {
43
43
  return easings.length > 0 && easings.every((e) => e !== void 0) ? easings : void 0;
44
44
  }
45
45
  }
46
+ /** The tokens under a bag's `{token:…}` keys, at any depth: a state's bag nests in the layer's. */
47
+ function collectKeyedTokens(bag, into) {
48
+ if (typeof bag !== "object" || bag === null || Array.isArray(bag)) return;
49
+ for (const [key, inner] of Object.entries(bag)) {
50
+ const keyed = refOfKey(key);
51
+ if (keyed && kindOf(keyed) === "token") into.add(pathOf(keyed));
52
+ collectKeyedTokens(inner, into);
53
+ }
54
+ }
55
+ /**
56
+ * The tokens whose variable a style rule or a global style sets, under the token's own key
57
+ * (`{ [Token.ref('switch/travel').cssVariable.name()]: … }`). Such a token holds a different value on
58
+ * each element a rule reaches, so no one literal stands for it.
59
+ */
60
+ const ruleSetTokens = createSliceMemo({
61
+ kinds: ["component", "global-style"],
62
+ compute: (config) => {
63
+ const tokens = /* @__PURE__ */ new Set();
64
+ for (const component of config.list("component")) {
65
+ const { styles = {} } = component.toJSON();
66
+ for (const entry of Object.values(styles)) collectKeyedTokens(entry?.layers, tokens);
67
+ }
68
+ for (const style of config.list("global-style")) {
69
+ const { declarations } = style.toJSON();
70
+ collectKeyedTokens(declarations, tokens);
71
+ }
72
+ return tokens;
73
+ }
74
+ });
46
75
  /**
47
76
  * A preset's value with every token it names resolved, or `undefined` when one of them resolves to
48
77
  * nothing. Values and keyframe lists are walked; a ref, a string with refs in it and a composed value
49
78
  * are each read to their literal. A token a mode decides is read as its base value and reported.
79
+ *
80
+ * With `variables`, a value a preset animates to that is a bare ref to a token a style rule sets
81
+ * ({@link ruleSetTokens}) is its `var()`, which `motion/react` reads off the element it animates as
82
+ * the animation starts, so it lands where the rule put it. In a composed value such a token is read as
83
+ * its base value and reported, since `motion/react` reads a `var()` only as the whole value. A
84
+ * `transition` is timing, not a value animated to, so a ref under one is read to its literal either
85
+ * way.
50
86
  */
51
- function resolvePresetValue({ config, value, at, report }) {
87
+ function resolvePresetValue({ config, value, at, report, variables = false }) {
52
88
  const refs = collectRefs(value).filter((target) => kindOf(target) === "token");
53
89
  const container = Array.isArray(value) || typeof value === "object" && value !== null && !isRef(value) && !composedValueFormOf(value);
54
90
  if (refs.length === 0 && !container) return { value };
@@ -57,6 +93,7 @@ function resolvePresetValue({ config, value, at, report }) {
57
93
  config,
58
94
  target: value.__ref
59
95
  });
96
+ if (variables && row?.variable && ruleSetTokens(config).has(row.path)) return { value: `var(${row.variable})` };
60
97
  if (row?.state.kind === "value") return { value: motionValue(row.state.value.css) };
61
98
  }
62
99
  if (Array.isArray(value)) {
@@ -66,7 +103,8 @@ function resolvePresetValue({ config, value, at, report }) {
66
103
  config,
67
104
  value: item,
68
105
  at: `${at}.${index}`,
69
- report
106
+ report,
107
+ variables
70
108
  });
71
109
  if (!resolved) return void 0;
72
110
  items.push(resolved.value);
@@ -80,7 +118,8 @@ function resolvePresetValue({ config, value, at, report }) {
80
118
  config,
81
119
  value: inner,
82
120
  at: `${at}.${key}`,
83
- report
121
+ report,
122
+ variables: variables && key !== "transition"
84
123
  });
85
124
  if (!resolved) continue;
86
125
  if (key !== "ease") {
@@ -105,14 +144,22 @@ function resolvePresetValue({ config, value, at, report }) {
105
144
  }
106
145
  return { value: fields };
107
146
  }
108
- for (const target of refs) if (tokenValueRow({
109
- config,
110
- target
111
- })?.state.kind === "conditional") report({
112
- at,
113
- token: pathOf(target),
114
- problem: "conditional"
115
- });
147
+ for (const target of refs) {
148
+ const row = tokenValueRow({
149
+ config,
150
+ target
151
+ });
152
+ if (variables && row !== void 0 && ruleSetTokens(config).has(row.path)) report({
153
+ at,
154
+ token: pathOf(target),
155
+ problem: "rule-set"
156
+ });
157
+ else if (row?.state.kind === "conditional") report({
158
+ at,
159
+ token: pathOf(target),
160
+ problem: "conditional"
161
+ });
162
+ }
116
163
  const literal = renderValue({
117
164
  config,
118
165
  value,
@@ -147,7 +194,8 @@ function resolveBody(config, preset, body, tokens) {
147
194
  report: (problem) => tokens.push({
148
195
  preset,
149
196
  ...problem
150
- })
197
+ }),
198
+ variables: field !== "transition"
151
199
  });
152
200
  if (resolved) props[field] = resolved.value;
153
201
  }
@@ -165,7 +213,8 @@ const resolveJsMotionPresets = createSliceMemo({
165
213
  "component",
166
214
  "token",
167
215
  "settings",
168
- "font"
216
+ "font",
217
+ "global-style"
169
218
  ],
170
219
  compute: (config) => {
171
220
  const presets = {};
@@ -187,7 +236,8 @@ const resolveJsMotionPresets = createSliceMemo({
187
236
  /**
188
237
  * Every JS-runtime preset, by path, as the props the wrapper spreads onto the animated component.
189
238
  * A token a preset names reaches `motion/react` as its value — `Token.ref('springStiffness/snappy')`
190
- * as `400`, a `200ms` duration as `0.2` — since it reads numbers and seconds, not variables.
239
+ * as `400`, a `200ms` duration as `0.2` — since it reads numbers and seconds, not variables. A token
240
+ * a style rule sets is the one exception: it reaches `motion/react` as its `var()`.
191
241
  */
192
242
  function jsMotionPresets(config) {
193
243
  return resolveJsMotionPresets(config).presets;
@@ -5864,7 +5864,7 @@ var Config = class Config {
5864
5864
  const claimed = schemaVersionOf(stored);
5865
5865
  if (claimed > 20261013201548) throw new SchemaVersionTooNew(claimed);
5866
5866
  const wire = upgradeSerializedConfig(stored);
5867
- 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\`.`);
5867
+ 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.`);
5868
5868
  const leftover = wire.options;
5869
5869
  if (isPlainObject(leftover) && Object.keys(leftover).length > 0) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" carries an \`options\` slot holding ${Object.keys(leftover).map((key) => `"${key}"`).join(", ")} after every migration ran — this config type has nowhere to keep them. Rebuild it: \`uds build\` and \`uds push\` for a system with a repo; for a system created in Create, reset the local database that holds it.`);
5870
5870
  const meta = this.metaDraft();
@@ -19,18 +19,16 @@ import { parseRawCss } from "./raw-css.js";
19
19
  import { System } from "../../configs/system.js";
20
20
  //#region src/migrations/2.0.0/v1-artifact.ts
21
21
  /**
22
- * The v1 → config-v2 reader, for `uds migrate` and nothing else.
22
+ * The v1 → config-v2 reader, run by the cutover schema migration in `configs/system.ts`.
23
23
  *
24
24
  * A v1 system's authoring vocabulary is gone, so its `uds.config.ts` cannot be imported — but what
25
- * that config DESCRIBED survives in the flat `config.json` its last build wrote. This reads that
26
- * artifact into a config-v2 `System`, which `uds migrate` then renders back out as v2 source.
25
+ * that config described survives in the flat `config.json` its last build wrote. This reads that
26
+ * artifact into a config-v2 `System`.
27
27
  *
28
- * **Runtime code must not come through here.** Every stored artifact is the config-v2 wire form and
29
- * `Config.fromJSON` is the one door to it; this exists for the one-time port of a repo that never
30
- * built on the new version. It is also best-effort in a way a runtime path must not be: the few old
31
- * shapes config-v2 doesn't model (color expressions, component `preview`) pass through or drop
32
- * rather than failing the load, so a config read through here is the
33
- * starting point for a migration, not a faithful copy of a system.
28
+ * Runtime code doesn't come through here directly. `Config.fromJSON` is the one door to a stored
29
+ * artifact, and it reaches this only for one stamped before config-v2. It is best-effort: the few
30
+ * old shapes config-v2 doesn't model (color expressions, component `preview`) pass through or drop
31
+ * rather than failing the load, so a config read through here is not a faithful copy of a system.
34
32
  */
35
33
  /**
36
34
  * Default authoring guidance seeded onto a system that declares none — the stop-gap that fills