@yahoo/uds-create-config 4.52.0 → 4.53.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 (30) hide show
  1. package/dist/capture/node-ops.js +1 -11
  2. package/dist/configs/CanvasConfig.js +5 -1
  3. package/dist/configs/system.js +4 -0
  4. package/dist/entities/canvas/node-gestures.d.ts +2 -0
  5. package/dist/entities/canvas/node-gestures.js +616 -0
  6. package/dist/{capture → entities/canvas}/node-patches.d.ts +6 -4
  7. package/dist/{capture → entities/canvas}/node-patches.js +16 -3
  8. package/dist/entities/system/Component.d.ts +35 -1
  9. package/dist/entities/system/Component.js +169 -18
  10. package/dist/entities/system/Motion.d.ts +5 -1
  11. package/dist/entities/system/Motion.js +7 -2
  12. package/dist/entities/system/component-forwards.d.ts +46 -0
  13. package/dist/entities/system/component-forwards.js +131 -0
  14. package/dist/entities/system/token-values.d.ts +9 -1
  15. package/dist/entities/system/token-values.js +4 -3
  16. package/dist/framework/Config.d.ts +1 -1
  17. package/dist/framework/Config.js +1 -1
  18. package/dist/framework/schema-version.d.ts +1 -1
  19. package/dist/framework/schema-version.js +5 -5
  20. package/dist/framework/schemas.js +14 -3
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +4 -4
  23. package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.d.ts +9 -0
  24. package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.js +10 -0
  25. package/dist/migrations/20261001131000_layer_style_and_forward_ops.d.ts +11 -0
  26. package/dist/migrations/20261001131000_layer_style_and_forward_ops.js +10 -0
  27. package/dist/spec/slot.d.ts +19 -1
  28. package/dist/spec/slot.js +49 -11
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +2 -2
@@ -3,7 +3,7 @@ import { pathOf } from "../framework/utils/refs.js";
3
3
  import { pathInput } from "../framework/schemas.js";
4
4
  import { CapturedNode } from "./captured-tree.js";
5
5
  import { specFromCapture } from "./capture-to-spec.js";
6
- import { specToNodePatches } from "./node-patches.js";
6
+ import { attachedSystem, specToNodePatches } from "../entities/canvas/node-patches.js";
7
7
  import { z } from "zod";
8
8
  //#region src/capture/node-ops.ts
9
9
  /**
@@ -40,16 +40,6 @@ const fromHtmlInput = z.object({
40
40
  path: pathInput("node"),
41
41
  data: fromHtmlData
42
42
  });
43
- /** The system this canvas draws on, with the slug node types are qualified by. */
44
- function attachedSystem(config) {
45
- const [source] = config.list("system-source");
46
- const system = source ? config.sourceFor(source.path) : void 0;
47
- if (!source || !system) return rejectData("This canvas has no system attached, so there is no component grammar to read a capture with.");
48
- return {
49
- slug: source.path,
50
- system
51
- };
52
- }
53
43
  function pageOf(body) {
54
44
  return body.page ? pathOf(body.page) : void 0;
55
45
  }
@@ -4,6 +4,7 @@ import { Package } from "../entities/system/Package.js";
4
4
  import { Component } from "../entities/system/Component.js";
5
5
  import { Icon } from "../entities/system/Icon.js";
6
6
  import { captureNodeOps } from "../capture/node-ops.js";
7
+ import { nodeGestureOps } from "../entities/canvas/node-gestures.js";
7
8
  import { Device } from "../entities/system/Device.js";
8
9
  import { Operation } from "../entities/system/Operation.js";
9
10
  import { Tool } from "../entities/system/Tool.js";
@@ -147,7 +148,10 @@ const Size = z.discriminatedUnion("type", [z.object({
147
148
  })]);
148
149
  const Node = defineEntity({
149
150
  kind: "node",
150
- schemas: captureNodeOps,
151
+ schemas: {
152
+ ...captureNodeOps,
153
+ ...nodeGestureOps
154
+ },
151
155
  fields: z.object({
152
156
  /** A real ref, never a bare name — dependents()/rename-cascade need a real edge, and a bare
153
157
  * `kind:path` string the framework never walks gives neither. Absent only on a placeholder
@@ -51,6 +51,8 @@ import { registryDirBesideComponentsMigration } from "../migrations/202609301522
51
51
  import { buildImportAliasesMigration } from "../migrations/20260930153059_build_import_aliases.js";
52
52
  import { canvasToolIconsAndDescriptionsMigration } from "../migrations/20260930183521_canvas_tool_icons_and_descriptions.js";
53
53
  import { styleRulesAsListMigration } from "../migrations/20261001112308_style_rules_as_list.js";
54
+ import { motionExitAndElementDuplicateMigration } from "../migrations/20261001130958_motion_exit_and_element_duplicate.js";
55
+ import { layerStyleAndForwardOpsMigration } from "../migrations/20261001131000_layer_style_and_forward_ops.js";
54
56
  const System = defineConfig({
55
57
  name: WEB_CONFIG_NAME,
56
58
  description: "A design system: the values, primitives and compositions an app is built from. A ref is written `{ \"__ref\": \"<kind>:<path>\" }`, so a token is `{ \"__ref\": \"token:<group>/<name>\" }` and a token group is `{ \"__ref\": \"token:<group>\" }`. Build it in this order. Tokens first, in groups by purpose (colors, spacing, radii, type sizes), since everything else names them. Then style properties, each with `values` holding one ref to the token group whose members it accepts (`{ \"__ref\": \"token:color\" }`), so a prop takes token names rather than raw CSS; add a string keyword only for a value no token holds. A style rule's bag holds a prop's values (`\"padding\": \"4\"`, `\"bg\": \"brand\"`), never raw CSS. Then a few primitive components, one layer each, whose props are style properties (a box for layout and surface, a text for type, a pressable for interaction). Then every other component composed from those primitives: its layers name a primitive or an element, its anatomy places them (a node renders a layer, a tag or `Slot` where a slot prop's content lands; `children` is the edge list; `visible` is `{ \"$state\": \"<prop>\" }`; a prop shown in a nested layer is a `forward` route, and `$state` never sits in a node's props), and its style rules set style-property values per layer, keyed by prop values when a variant changes them. Modes (a dark mode is `{ type: \"selector\", value: \".dark\" }`) change token values through a token's `overrides`, so a component never restyles for a mode; modifiers (hover, focus, disabled) name a state, and a style rule applies under one as a `_hover` block inside a layer bag. Composites bundle style values that recur. Last, register canvas roles: the canvas draws with them, one entity per tool (`frame` for the layout primitive, `text` for the text primitive, `image`) whose entries name the components that tool inserts, and a tool whose role is unregistered is disabled. Give each component a `preview` entry under `*` so it shows content, and never write a CSS property name where a style-property name belongs.",
@@ -113,5 +115,7 @@ if (!registeredSchemaMigrations().has(20260930152239)) registerSchemaMigrations(
113
115
  if (!registeredSchemaMigrations().has(20260930153059)) registerSchemaMigrations(buildImportAliasesMigration);
114
116
  if (!registeredSchemaMigrations().has(20260930183521)) registerSchemaMigrations(canvasToolIconsAndDescriptionsMigration);
115
117
  if (!registeredSchemaMigrations().has(20261001112308)) registerSchemaMigrations(styleRulesAsListMigration);
118
+ if (!registeredSchemaMigrations().has(20261001130958)) registerSchemaMigrations(motionExitAndElementDuplicateMigration);
119
+ if (!registeredSchemaMigrations().has(20261001131e3)) registerSchemaMigrations(layerStyleAndForwardOpsMigration);
116
120
  //#endregion
117
121
  export { System };
@@ -0,0 +1,2 @@
1
+ import { type UserSchemas } from '../../framework/schemas';
2
+ export declare const nodeGestureOps: UserSchemas;
@@ -0,0 +1,616 @@
1
+ import { rejectData } from "../../framework/rejection.js";
2
+ import { pathOf, ref } from "../../framework/utils/refs.js";
3
+ import { pathInput } from "../../framework/schemas.js";
4
+ import { childrenPolicy } from "../system/element.js";
5
+ import { surfaceProps } from "../../framework/prop-surface.js";
6
+ import { forceModeProp, forceModeProps } from "../../framework/class-names.js";
7
+ import { slotOf } from "../../spec/slot.js";
8
+ import { PLACED_NODE_KEY, attachedSystem, elementTypeRef, postOrder } from "./node-patches.js";
9
+ import { z } from "zod";
10
+ //#region src/entities/canvas/node-gestures.ts
11
+ const specSchema = z.object({
12
+ root: z.string().min(1),
13
+ elements: z.record(z.string(), z.unknown())
14
+ }).passthrough();
15
+ const insertData = z.object({
16
+ /** The subtree to graft, as a spec; its root becomes the node's last child unless `index` says. */
17
+ subtree: specSchema,
18
+ index: z.number().int().nonnegative().optional(),
19
+ /** A named slot of the node's component to fill instead of its children. */
20
+ slotProp: z.string().min(1).optional()
21
+ });
22
+ const insertInput = z.object({
23
+ path: pathInput("node"),
24
+ data: insertData
25
+ });
26
+ const wrapData = z.object({
27
+ /** The node's children to wrap, by path; they must all be its direct children. */
28
+ elements: z.array(z.string().min(1)).min(1),
29
+ /** The container: a component type as a spec spells it, with the props it starts with. */
30
+ container: z.object({
31
+ type: z.string().min(1),
32
+ props: z.record(z.string(), z.unknown()).optional(),
33
+ /** A named slot of the container to hold them, instead of its children. */
34
+ slotProp: z.string().min(1).optional()
35
+ })
36
+ });
37
+ const wrapInput = z.object({
38
+ path: pathInput("node"),
39
+ data: wrapData
40
+ });
41
+ const forceModeData = z.object({
42
+ /** A modifier group of the attached system whose modes are class-based. */
43
+ group: z.string().min(1),
44
+ /** The mode to force, or `null` to follow the page again. */
45
+ option: z.string().min(1).nullable()
46
+ });
47
+ const elementInput = z.object({ path: pathInput("node") });
48
+ const duplicateInput = z.object({
49
+ path: pathInput("node"),
50
+ data: z.object({
51
+ /** How many copies, each placed after the last. */
52
+ count: z.number().int().min(1).max(50).optional() }).optional()
53
+ });
54
+ const swapInput = z.object({
55
+ path: pathInput("node"),
56
+ data: z.object({
57
+ /** The component to render instead, as a spec spells a type. */
58
+ type: z.string().min(1) })
59
+ });
60
+ const moveInput = z.object({
61
+ path: pathInput("node"),
62
+ data: z.object({
63
+ /** The element to move it into, by path, inside the same placed node; its own parent when absent. */
64
+ parent: z.string().min(1).optional(),
65
+ /** A named slot of the parent to land in, instead of its children. */
66
+ slotProp: z.string().min(1).optional(),
67
+ /** Its position there, from 0; past the end is last, and the end when absent under a new parent. */
68
+ index: z.number().int().nonnegative().optional(),
69
+ /** How many places to move among its siblings, negative earlier, in place of `index`. */
70
+ offset: z.number().int().optional()
71
+ })
72
+ });
73
+ const reorderInput = z.object({
74
+ path: pathInput("node"),
75
+ data: z.object({
76
+ /** Every element of the list, by path, in the order to hold them. */
77
+ order: z.array(z.string().min(1)),
78
+ /** The named slot to reorder instead of the children. */
79
+ slotProp: z.string().min(1).optional()
80
+ })
81
+ });
82
+ const forceModeInput = z.object({
83
+ path: pathInput("node"),
84
+ data: forceModeData
85
+ });
86
+ function bodyOf(config, path, field) {
87
+ const node = config.resolve("node", path);
88
+ if (!node) return rejectData(`Node "${path}" does not exist.`, field);
89
+ return node.body;
90
+ }
91
+ /** The placed node a path belongs to: `card` for `card/title`. */
92
+ function rootOf(path) {
93
+ return path.split("/")[0] ?? path;
94
+ }
95
+ /**
96
+ * A minter of subtree paths under `root` that no row holds yet. The placed node's own key is taken
97
+ * too: a row at `<root>/root` would read back as the node's own element and replace it.
98
+ */
99
+ function freshPaths(config, root) {
100
+ const taken = new Set(config.list("node").map((node) => node.path));
101
+ taken.add(`${root}/${PLACED_NODE_KEY}`);
102
+ return (base) => {
103
+ let candidate = `${root}/${base}`;
104
+ let n = 0;
105
+ while (taken.has(candidate)) candidate = `${root}/${base}__${++n}`;
106
+ taken.add(candidate);
107
+ return candidate;
108
+ };
109
+ }
110
+ /** The node's children as refs, in order. */
111
+ function childrenOf(body) {
112
+ return [...body.children ?? []];
113
+ }
114
+ function insert(config, input) {
115
+ const { slug } = attachedSystem(config);
116
+ const parent = bodyOf(config, input.path, ["path"]);
117
+ const spec = input.data.subtree;
118
+ const elements = spec.elements;
119
+ if (!elements[spec.root]) return rejectData(`The subtree's root "${spec.root}" is not among its elements.`, ["data", "subtree"]);
120
+ const fresh = freshPaths(config, rootOf(input.path));
121
+ const order = postOrder(spec);
122
+ const pathFor = /* @__PURE__ */ new Map();
123
+ for (const key of order) pathFor.set(key, fresh(key));
124
+ const sub = (key) => {
125
+ const path = pathFor.get(key);
126
+ if (!path) return rejectData(`Element "${key}" is not among the subtree's elements.`, ["data", "subtree"]);
127
+ return ref("node", path);
128
+ };
129
+ const patches = [];
130
+ for (const key of order) {
131
+ const element = elements[key];
132
+ if (!element) continue;
133
+ const slots = {};
134
+ for (const [name, fills] of Object.entries(element.slots ?? {})) slots[name] = fills.map(sub);
135
+ patches.push({
136
+ kind: "node",
137
+ operation: "create",
138
+ path: pathFor.get(key),
139
+ data: {
140
+ type: elementTypeRef({
141
+ type: element.type,
142
+ slug
143
+ }),
144
+ props: element.props ?? {},
145
+ ...element.children?.length ? { children: element.children.map(sub) } : {},
146
+ ...Object.keys(slots).length > 0 ? { slots } : {},
147
+ ...element.visible !== void 0 ? { visible: element.visible } : {},
148
+ ...element.repeat !== void 0 ? { repeat: element.repeat } : {}
149
+ }
150
+ });
151
+ }
152
+ const slot = input.data.slotProp;
153
+ if (slot !== void 0) {
154
+ const component = componentOf({
155
+ config,
156
+ body: parent
157
+ });
158
+ const declared = component ? slotOf({
159
+ config: component.system,
160
+ path: component.path,
161
+ slot
162
+ }) : void 0;
163
+ if (!declared || declared.isChildren) return rejectData(`"${input.path}" has no slot "${slot}" to fill.`, ["data", "slotProp"]);
164
+ const rootType = elements[spec.root]?.type;
165
+ if (rootType && !declared.takes(rootType)) return rejectData(`The "${slot}" slot does not take ${rootType}.`, ["data", "slotProp"]);
166
+ }
167
+ const list = slot === void 0 ? childrenOf(parent) : [...parent.slots?.[slot] ?? []];
168
+ const at = Math.min(input.data.index ?? list.length, list.length);
169
+ list.splice(at, 0, sub(spec.root));
170
+ patches.push({
171
+ kind: "node",
172
+ operation: "update",
173
+ path: input.path,
174
+ data: slot === void 0 ? { children: list } : { slots: {
175
+ ...parent.slots ?? {},
176
+ [slot]: list
177
+ } }
178
+ });
179
+ return patches;
180
+ }
181
+ /** The component a node renders, with the system that declares it; `undefined` for any other type. */
182
+ function componentOf({ config, body }) {
183
+ if (!body.type) return void 0;
184
+ const { system } = attachedSystem(config);
185
+ const entity = system.resolve("component", pathOf(body.type));
186
+ return entity ? {
187
+ system,
188
+ path: entity.path
189
+ } : void 0;
190
+ }
191
+ function placeOf(config, path, field) {
192
+ const root = rootOf(path);
193
+ if (path === root) return rejectData(`"${path}" is a placed node; this acts on an element inside one.`, field);
194
+ const candidates = [config.resolve("node", root), ...config.members("node", root)];
195
+ for (const candidate of candidates) {
196
+ if (!candidate) continue;
197
+ const body = candidate.body;
198
+ const lists = [[null, childrenOf(body)], ...Object.entries(body.slots ?? {}).map(([name, fills]) => [name, [...fills]])];
199
+ for (const [slot, list] of lists) {
200
+ const index = list.findIndex((entry) => pathOf(entry) === path);
201
+ if (index !== -1) return {
202
+ parent: candidate.path,
203
+ body,
204
+ slot,
205
+ list,
206
+ index
207
+ };
208
+ }
209
+ }
210
+ return rejectData(`"${path}" is not inside a placed node.`, field);
211
+ }
212
+ /** The patch writing a changed list back to the parent it came from. */
213
+ function placeUpdate(place, list) {
214
+ return {
215
+ kind: "node",
216
+ operation: "update",
217
+ path: place.parent,
218
+ data: place.slot === null ? { children: list } : { slots: {
219
+ ...place.body.slots ?? {},
220
+ [place.slot]: list
221
+ } }
222
+ };
223
+ }
224
+ /** An element and everything beneath it, deepest first, so each create meets the rows it points at. */
225
+ function subtreeOf(config, path) {
226
+ const out = [];
227
+ const visit = (at) => {
228
+ const body = config.resolve("node", at)?.body;
229
+ if (!body) return;
230
+ for (const child of childrenOf(body)) visit(pathOf(child));
231
+ for (const fills of Object.values(body.slots ?? {})) for (const fill of fills) visit(pathOf(fill));
232
+ out.push(at);
233
+ };
234
+ visit(path);
235
+ return out;
236
+ }
237
+ function remove(config, input) {
238
+ const place = placeOf(config, input.path, ["path"]);
239
+ return [placeUpdate(place, place.list.filter((_, i) => i !== place.index)), ...subtreeOf(config, input.path).reverse().map((path) => ({
240
+ kind: "node",
241
+ operation: "delete",
242
+ path
243
+ }))];
244
+ }
245
+ function duplicate(config, input) {
246
+ const place = placeOf(config, input.path, ["path"]);
247
+ const fresh = freshPaths(config, rootOf(input.path));
248
+ const subtree = subtreeOf(config, input.path);
249
+ const count = input.data?.count ?? 1;
250
+ const patches = [];
251
+ const copies = [];
252
+ for (let n = 0; n < count; n++) {
253
+ const pathFor = new Map(subtree.map((path) => [path, fresh(path.split("/").at(-1) ?? path)]));
254
+ const remap = (entry) => {
255
+ const to = pathFor.get(pathOf(entry));
256
+ return to ? ref("node", to) : entry;
257
+ };
258
+ for (const path of subtree) {
259
+ const body = config.resolve("node", path)?.body;
260
+ patches.push({
261
+ kind: "node",
262
+ operation: "create",
263
+ path: pathFor.get(path),
264
+ data: {
265
+ ...body,
266
+ ...body.children ? { children: body.children.map(remap) } : {},
267
+ ...body.slots ? { slots: Object.fromEntries(Object.entries(body.slots).map(([name, fills]) => [name, fills.map(remap)])) } : {}
268
+ }
269
+ });
270
+ }
271
+ copies.push(ref("node", pathFor.get(input.path)));
272
+ }
273
+ const list = [...place.list];
274
+ list.splice(place.index + 1, 0, ...copies);
275
+ patches.push(placeUpdate(place, list));
276
+ return patches;
277
+ }
278
+ function unwrap(config, input) {
279
+ const place = placeOf(config, input.path, ["path"]);
280
+ const body = bodyOf(config, input.path, ["path"]);
281
+ const filled = Object.entries(body.slots ?? {}).find(([, fills]) => fills.length > 0);
282
+ if (filled) return rejectData(`"${input.path}" holds content in its "${filled[0]}" slot, which would have nowhere to go.`, ["path"]);
283
+ const children = childrenOf(body);
284
+ if (children.length === 0) return rejectData(`"${input.path}" holds nothing to keep.`, ["path"]);
285
+ const list = [...place.list];
286
+ list.splice(place.index, 1, ...children);
287
+ return [placeUpdate(place, list), {
288
+ kind: "node",
289
+ operation: "delete",
290
+ path: input.path
291
+ }];
292
+ }
293
+ function swap(config, input) {
294
+ const { slug, system } = attachedSystem(config);
295
+ const body = bodyOf(config, input.path, ["path"]);
296
+ const type = elementTypeRef({
297
+ type: input.data.type,
298
+ slug
299
+ });
300
+ const component = system.resolve("component", pathOf(type));
301
+ if (!component) return rejectData(`The system has no component "${input.data.type}".`, ["data", "type"]);
302
+ if (childrenOf(body).length > 0 && childrenPolicy(system, component.path) !== "nodes") return rejectData(`${component.path} takes no child elements, and "${input.path}" holds some.`, ["data", "type"]);
303
+ for (const [name, fills] of Object.entries(body.slots ?? {})) {
304
+ if (fills.length === 0) continue;
305
+ const slot = slotOf({
306
+ config: system,
307
+ path: component.path,
308
+ slot: name
309
+ });
310
+ if (!slot || fills.some((fill) => !slotTakes({
311
+ config,
312
+ slot,
313
+ fill
314
+ }))) return rejectData(`${component.path} has no "${name}" slot for what "${input.path}" holds there.`, ["data", "type"]);
315
+ }
316
+ const takes = new Set(surfaceProps(system, component.path).map((prop) => prop.name));
317
+ const props = Object.fromEntries(Object.entries(body.props ?? {}).filter(([name]) => takes.has(name)));
318
+ return [{
319
+ kind: "node",
320
+ operation: "update",
321
+ path: input.path,
322
+ data: {
323
+ type,
324
+ props
325
+ }
326
+ }];
327
+ }
328
+ /** Whether a slot takes the element a fill ref points at. */
329
+ function slotTakes({ config, slot, fill }) {
330
+ const type = (config.resolve("node", pathOf(fill))?.body)?.type;
331
+ return !type || slot.takes(type);
332
+ }
333
+ /** A move with no `parent` and no `slotProp` stays among its siblings; any other lands in a parent. */
334
+ function move(config, input) {
335
+ const { parent, slotProp, index, offset } = input.data;
336
+ if (index !== void 0 && offset !== void 0) return rejectData("Give `index` or `offset`, not both.", ["data", "offset"]);
337
+ if (parent !== void 0 || slotProp !== void 0) {
338
+ if (offset !== void 0) return rejectData("`offset` moves an element among its siblings; give `index` for a place under a parent.", ["data", "offset"]);
339
+ return reparent({
340
+ config,
341
+ path: input.path,
342
+ parent,
343
+ slotProp,
344
+ index
345
+ });
346
+ }
347
+ if (index === void 0 && offset === void 0) return rejectData("Say where to move it: `parent`, `index` or `offset`.", ["data"]);
348
+ const place = placeOf(config, input.path, ["path"]);
349
+ const list = [...place.list];
350
+ const [entry] = list.splice(place.index, 1);
351
+ if (!entry) return [];
352
+ const target = index ?? place.index + (offset ?? 0);
353
+ const at = Math.max(0, Math.min(target, list.length));
354
+ if (at === place.index) return [];
355
+ list.splice(at, 0, entry);
356
+ return [placeUpdate(place, list)];
357
+ }
358
+ function reorder(config, input) {
359
+ const body = bodyOf(config, input.path, ["path"]);
360
+ const { slotProp: slot, order } = input.data;
361
+ const held = (slot === void 0 ? childrenOf(body) : [...body.slots?.[slot] ?? []]).map(pathOf);
362
+ if (order.length !== held.length || new Set(order).size !== order.length || order.some((path) => !held.includes(path))) return rejectData(`The order has to name each of "${input.path}"'s ${slot ?? "children"} once: ${held.join(", ")}.`, ["data", "order"]);
363
+ const next = order.map((path) => ref("node", path));
364
+ return [{
365
+ kind: "node",
366
+ operation: "update",
367
+ path: input.path,
368
+ data: slot === void 0 ? { children: next } : { slots: {
369
+ ...body.slots ?? {},
370
+ [slot]: next
371
+ } }
372
+ }];
373
+ }
374
+ function reparent({ config, path, parent, slotProp: slot, index }) {
375
+ const place = placeOf(config, path, ["path"]);
376
+ const to = parent ?? place.parent;
377
+ if (rootOf(to) !== rootOf(path)) return rejectData(`"${to}" is in another placed node.`, ["data", "parent"]);
378
+ if (subtreeOf(config, path).includes(to)) return rejectData(`"${to}" is inside "${path}".`, ["data", "parent"]);
379
+ const target = bodyOf(config, to, ["data", "parent"]);
380
+ const moving = (config.resolve("node", path)?.body)?.type;
381
+ const component = componentOf({
382
+ config,
383
+ body: target
384
+ });
385
+ if (component) {
386
+ const declared = slotOf({
387
+ config: component.system,
388
+ path: component.path,
389
+ slot: slot ?? "children"
390
+ });
391
+ if (slot && (!declared || declared.isChildren)) return rejectData(`"${to}" has no slot "${slot}".`, ["data", "slotProp"]);
392
+ if (!slot && childrenPolicy(component.system, component.path) !== "nodes") return rejectData(`"${to}" takes no child elements.`, ["data", "parent"]);
393
+ if (declared && moving && !declared.takes(moving)) return rejectData(`The ${slot ?? "children"} of "${to}" do not take "${path}".`, ["data", "parent"]);
394
+ }
395
+ const entry = place.list[place.index];
396
+ const sameList = place.parent === to && place.slot === (slot ?? null);
397
+ const from = place.list.filter((_, i) => i !== place.index);
398
+ const into = sameList ? from : slot === void 0 ? childrenOf(target) : [...target.slots?.[slot] ?? []];
399
+ into.splice(Math.min(index ?? into.length, into.length), 0, entry);
400
+ if (sameList) return [placeUpdate(place, into)];
401
+ return [placeUpdate(place, from), {
402
+ kind: "node",
403
+ operation: "update",
404
+ path: to,
405
+ data: slot === void 0 ? { children: into } : { slots: {
406
+ ...target.slots ?? {},
407
+ [slot]: into
408
+ } }
409
+ }];
410
+ }
411
+ function wrap(config, input) {
412
+ const { slug } = attachedSystem(config);
413
+ const children = childrenOf(bodyOf(config, input.path, ["path"]));
414
+ const wrapped = new Set(input.data.elements);
415
+ for (const element of input.data.elements) if (!children.some((child) => pathOf(child) === element)) return rejectData(`"${element}" is not a child of "${input.path}"; wrap acts on a node's direct children.`, ["data", "elements"]);
416
+ const type = elementTypeRef({
417
+ type: input.data.container.type,
418
+ slug
419
+ });
420
+ const slotName = input.data.container.slotProp ?? "children";
421
+ const component = componentOf({
422
+ config,
423
+ body: { type }
424
+ });
425
+ const slot = component ? slotOf({
426
+ config: component.system,
427
+ path: component.path,
428
+ slot: slotName
429
+ }) : void 0;
430
+ if (input.data.container.slotProp && !slot) return rejectData(`${input.data.container.type} has no "${slotName}" slot.`, [
431
+ "data",
432
+ "container",
433
+ "slot"
434
+ ]);
435
+ for (const element of input.data.elements) {
436
+ const held = children.find((child) => pathOf(child) === element);
437
+ if (slot && held && !slotTakes({
438
+ config,
439
+ slot,
440
+ fill: held
441
+ })) return rejectData(`The "${slotName}" slot of ${input.data.container.type} does not take "${element}".`, ["data", "elements"]);
442
+ }
443
+ const base = pathOf(type).split("/").at(-1)?.toLowerCase() ?? "group";
444
+ const containerPath = freshPaths(config, rootOf(input.path))(base);
445
+ const first = children.findIndex((child) => wrapped.has(pathOf(child)));
446
+ const kept = children.filter((child) => !wrapped.has(pathOf(child)));
447
+ kept.splice(first, 0, ref("node", containerPath));
448
+ return [{
449
+ kind: "node",
450
+ operation: "create",
451
+ path: containerPath,
452
+ data: {
453
+ type,
454
+ props: input.data.container.props ?? {},
455
+ ...input.data.container.slotProp ? { slots: { [input.data.container.slotProp]: children.filter((child) => wrapped.has(pathOf(child))) } } : { children: children.filter((child) => wrapped.has(pathOf(child))) }
456
+ }
457
+ }, {
458
+ kind: "node",
459
+ operation: "update",
460
+ path: input.path,
461
+ data: { children: kept }
462
+ }];
463
+ }
464
+ function forceMode(config, input) {
465
+ const { system } = attachedSystem(config);
466
+ const body = bodyOf(config, input.path, ["path"]);
467
+ const { group, option } = input.data;
468
+ const members = system.members("modifier", group);
469
+ if (members.length === 0) return rejectData(`The system has no modifier group "${group}".`, ["data", "group"]);
470
+ const options = members.filter((member) => member.body.type === "selector").map((member) => member.path.split("/").at(-1) ?? member.path);
471
+ if (options.length === 0) return rejectData(`"${group}" has no class-based mode, so a layer cannot be forced into one.`, ["data", "group"]);
472
+ if (option !== null && !options.includes(option)) return rejectData(`"${option}" is not a mode of "${group}"; it offers ${options.join(", ")}.`, ["data", "option"]);
473
+ const props = { ...body.props ?? {} };
474
+ const key = forceModeProp(group);
475
+ if (option === null) delete props[key];
476
+ else props[key] = option;
477
+ for (const legacy of forceModeProps(group).slice(1)) delete props[legacy];
478
+ return [{
479
+ kind: "node",
480
+ operation: "update",
481
+ path: input.path,
482
+ data: { props }
483
+ }];
484
+ }
485
+ const applied = (config, patches) => patches.length === 0 ? config : config.applyAll(patches);
486
+ const nodeGestureOps = {
487
+ update: { description: "Change one or more properties of this node (\"make it rounder\", \"more padding\", \"center the content\", \"size large\"): `data` carries the fields to set, `props` among them. Anything the node's component accepts, in the values the system declares." },
488
+ "element-insert": {
489
+ input: insertInput,
490
+ readOnly: false,
491
+ label: "Insert into a node",
492
+ description: "Add a component inside this node (\"add a button\", \"add text\", \"insert an image\"): the `subtree` given in `data`, a spec, is grafted under the node as one of its children, the last unless `index` says otherwise, or into the slot `slotProp` names on the node's component. Every element of the subtree becomes a row of its own beneath the placed node, keyed collision-free, so the root of the subtree is the new child.",
493
+ scope: "item",
494
+ title: (entity) => `Insert into ${entity}`,
495
+ example: () => ({
496
+ path: "card",
497
+ data: { subtree: {
498
+ root: "title",
499
+ elements: { title: {
500
+ type: "Text",
501
+ props: { children: "Hello" }
502
+ } }
503
+ } }
504
+ }),
505
+ handler: (input, config) => applied(config, insert(config, insertInput.parse(input)))
506
+ },
507
+ "element-wrap": {
508
+ input: wrapInput,
509
+ readOnly: false,
510
+ label: "Wrap children in a container",
511
+ description: "Group or wrap elements (\"wrap these in a stack\", \"group the title and the body\", \"put it in a card\"): some of this node's direct children move into a new container element of the given component type, which takes their place among the node's children. They land in the container's children, or in the slot its `slotProp` names, and a slot that lists what it accepts refuses anything else.",
512
+ scope: "item",
513
+ title: (entity) => `Wrap children of ${entity}`,
514
+ example: () => ({
515
+ path: "card",
516
+ data: {
517
+ elements: ["card/title", "card/body"],
518
+ container: {
519
+ type: "VStack",
520
+ props: { gap: "2" }
521
+ }
522
+ }
523
+ }),
524
+ handler: (input, config) => applied(config, wrap(config, wrapInput.parse(input)))
525
+ },
526
+ "element-delete": {
527
+ input: elementInput,
528
+ readOnly: false,
529
+ label: "Remove an element",
530
+ description: "Delete this element and everything inside it (\"delete\", \"remove this\", \"get rid of it\"). It leaves the list it sat in, whether its parent's children or one of its parent's slots.",
531
+ scope: "item",
532
+ title: (entity) => `Remove ${entity}`,
533
+ example: () => ({ path: "card/title" }),
534
+ handler: (input, config) => applied(config, remove(config, elementInput.parse(input)))
535
+ },
536
+ "element-duplicate": {
537
+ input: duplicateInput,
538
+ readOnly: false,
539
+ 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.",
541
+ scope: "item",
542
+ title: (entity) => `Duplicate ${entity}`,
543
+ example: () => ({
544
+ path: "card/title",
545
+ data: { count: 2 }
546
+ }),
547
+ handler: (input, config) => applied(config, duplicate(config, duplicateInput.parse(input)))
548
+ },
549
+ "element-unwrap": {
550
+ input: elementInput,
551
+ readOnly: false,
552
+ label: "Unwrap an element",
553
+ description: "Remove this container but keep what it holds (\"unwrap\", \"ungroup\"): its children take its place among its siblings.",
554
+ scope: "item",
555
+ title: (entity) => `Unwrap ${entity}`,
556
+ example: () => ({ path: "card/row" }),
557
+ handler: (input, config) => applied(config, unwrap(config, elementInput.parse(input)))
558
+ },
559
+ "element-swap": {
560
+ input: swapInput,
561
+ readOnly: false,
562
+ label: "Swap an element for another component",
563
+ description: "Render this element as a different component (\"make this a VStack\", \"swap to an icon button\", \"turn it into a link\"): it keeps its place, its content and the props the new component takes.",
564
+ scope: "item",
565
+ title: (entity) => `Swap ${entity}`,
566
+ example: () => ({
567
+ path: "card/row",
568
+ data: { type: "VStack" }
569
+ }),
570
+ handler: (input, config) => applied(config, swap(config, swapInput.parse(input)))
571
+ },
572
+ "element-move": {
573
+ input: moveInput,
574
+ readOnly: false,
575
+ label: "Move an element",
576
+ description: "Move this element among its siblings (\"move up\", \"put it first\") or into another element of the same placed node (\"move it into the card\", \"put it in the footer\"). Without a `parent` or `slotProp` it stays among its siblings: `offset` moves it that many places, negative earlier, and `index` puts it at a position. With `parent`, `slotProp` or both, it lands in that element's children or the slot `slotProp` names, at `index` or the end. A parent that takes no children, a slot that refuses the element, or a parent inside the element itself is refused.",
577
+ scope: "item",
578
+ title: (entity) => `Move ${entity}`,
579
+ example: () => ({
580
+ path: "card/title",
581
+ data: { parent: "card/header" }
582
+ }),
583
+ handler: (input, config) => applied(config, move(config, moveInput.parse(input)))
584
+ },
585
+ "children-reorder": {
586
+ input: reorderInput,
587
+ readOnly: false,
588
+ label: "Reorder an element's children",
589
+ description: "Put this element's children, or the slot `slotProp` names, in a new order (\"reverse them\", \"sort by name\"): `order` names every one of them once, by path, in the order to hold them.",
590
+ scope: "item",
591
+ title: (entity) => `Reorder ${entity}`,
592
+ example: () => ({
593
+ path: "card",
594
+ data: { order: ["card/body", "card/title"] }
595
+ }),
596
+ handler: (input, config) => applied(config, reorder(config, reorderInput.parse(input)))
597
+ },
598
+ "force-mode": {
599
+ input: forceModeInput,
600
+ readOnly: false,
601
+ label: "Force a mode on a node",
602
+ description: "Switch this node to a color mode (\"dark mode\", \"make it light\", \"follow the page\"): the node and everything inside it is forced into one option of a color-mode group of the attached system, or `null` lets it follow the page's mode again. The option has to be a class-based mode the system declares.",
603
+ scope: "item",
604
+ title: (entity) => `Force a mode on ${entity}`,
605
+ example: () => ({
606
+ path: "card",
607
+ data: {
608
+ group: "colorMode",
609
+ option: "dark"
610
+ }
611
+ }),
612
+ handler: (input, config) => applied(config, forceMode(config, forceModeInput.parse(input)))
613
+ }
614
+ };
615
+ //#endregion
616
+ export { nodeGestureOps };