@yahoo/uds-create-config 4.51.3 → 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.
- package/dist/capture/node-ops.js +1 -11
- package/dist/configs/CanvasConfig.js +5 -1
- package/dist/configs/system.js +6 -0
- package/dist/entities/canvas/node-gestures.d.ts +2 -0
- package/dist/entities/canvas/node-gestures.js +616 -0
- package/dist/{capture → entities/canvas}/node-patches.d.ts +6 -4
- package/dist/{capture → entities/canvas}/node-patches.js +16 -3
- package/dist/entities/system/Component.d.ts +47 -32
- package/dist/entities/system/Component.js +181 -30
- package/dist/entities/system/LinkedSystem.d.ts +10 -3
- package/dist/entities/system/Motion.d.ts +5 -1
- package/dist/entities/system/Motion.js +7 -2
- package/dist/entities/system/component-forwards.d.ts +46 -0
- package/dist/entities/system/component-forwards.js +131 -0
- package/dist/entities/system/token-values.d.ts +9 -1
- package/dist/entities/system/token-values.js +4 -3
- package/dist/framework/Config.d.ts +1 -1
- package/dist/framework/Config.js +1 -1
- package/dist/framework/defineSubEntity.d.ts +10 -1
- package/dist/framework/defineSubEntity.js +19 -1
- package/dist/framework/rejection.js +16 -4
- package/dist/framework/schema-version.d.ts +1 -1
- package/dist/framework/schema-version.js +5 -5
- package/dist/framework/schemas.js +14 -3
- package/dist/index.d.ts +7 -6
- package/dist/index.js +5 -5
- package/dist/migrations/20261001112308_style_rules_as_list.d.ts +12 -0
- package/dist/migrations/20261001112308_style_rules_as_list.js +8 -0
- package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.d.ts +9 -0
- package/dist/migrations/20261001130958_motion_exit_and_element_duplicate.js +10 -0
- package/dist/migrations/20261001131000_layer_style_and_forward_ops.d.ts +11 -0
- package/dist/migrations/20261001131000_layer_style_and_forward_ops.js +10 -0
- package/dist/spec/slot.d.ts +19 -1
- package/dist/spec/slot.js +49 -11
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -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 };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Config, Patch } from "
|
|
2
|
-
import { Spec } from "
|
|
1
|
+
import { Config, Patch } from "../../framework/Config.js";
|
|
2
|
+
import { Spec } from "../../capture/capture-to-spec.js";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/entities/canvas/node-patches.d.ts
|
|
5
|
+
/** The key a placed node's own element takes when its rows read back as a spec. */
|
|
6
|
+
declare const PLACED_NODE_KEY = "root";
|
|
5
7
|
interface Placement {
|
|
6
8
|
readonly page: string;
|
|
7
9
|
readonly x?: number;
|
|
@@ -29,4 +31,4 @@ declare function specToNodePatches({
|
|
|
29
31
|
readonly placement: Placement;
|
|
30
32
|
}): Patch[];
|
|
31
33
|
//#endregion
|
|
32
|
-
export { specToNodePatches };
|
|
34
|
+
export { PLACED_NODE_KEY, specToNodePatches };
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { rejectData } from "../../framework/rejection.js";
|
|
2
|
+
import { ref } from "../../framework/utils/refs.js";
|
|
3
|
+
//#region src/entities/canvas/node-patches.ts
|
|
3
4
|
/**
|
|
4
5
|
* A spec as the node rows of one new placed node. A placed node's content is its subtree rows, one
|
|
5
6
|
* per element, at `<node>/<element key>`, each pointing at its children by ref; the root element's
|
|
6
7
|
* content lives on the placed node itself beside its placement.
|
|
7
8
|
*/
|
|
9
|
+
/** The system this canvas draws on, with the slug node types are qualified by. */
|
|
10
|
+
function attachedSystem(config) {
|
|
11
|
+
const [source] = config.list("system-source");
|
|
12
|
+
const system = source ? config.sourceFor(source.path) : void 0;
|
|
13
|
+
if (!source || !system) return rejectData("This canvas has no system attached, so there are no components to place on it.");
|
|
14
|
+
return {
|
|
15
|
+
slug: source.path,
|
|
16
|
+
system
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The key a placed node's own element takes when its rows read back as a spec. */
|
|
20
|
+
const PLACED_NODE_KEY = "root";
|
|
8
21
|
const INTRINSIC_TAG = /^[a-z][a-zA-Z0-9-]*$/;
|
|
9
22
|
const SPEC_PRIMITIVES = new Set(["Slot", "Fragment"]);
|
|
10
23
|
/** A spec element's `type` as the ref a node row stores, qualified with the system's slug. */
|
|
@@ -96,4 +109,4 @@ function specToNodePatches({ config, nodeId, spec, slug, placement }) {
|
|
|
96
109
|
return patches;
|
|
97
110
|
}
|
|
98
111
|
//#endregion
|
|
99
|
-
export { specToNodePatches };
|
|
112
|
+
export { PLACED_NODE_KEY, attachedSystem, elementTypeRef, postOrder, specToNodePatches };
|