@yahoo/uds-create-config 4.6.0 → 4.8.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 (71) hide show
  1. package/dist/configs/CanvasConfig.d.ts +900 -80
  2. package/dist/configs/react-native-system.d.ts +603 -55
  3. package/dist/configs/system.d.ts +603 -55
  4. package/dist/configs/system.js +7 -1
  5. package/dist/entities/system/Component.d.ts +480 -39
  6. package/dist/entities/system/Component.js +264 -93
  7. package/dist/entities/system/File.d.ts +1 -1
  8. package/dist/entities/system/LinkedSystem.d.ts +2 -2
  9. package/dist/entities/system/Operation.d.ts +1 -1
  10. package/dist/entities/system/StyleProperty.d.ts +8 -0
  11. package/dist/entities/system/StyleProperty.js +12 -1
  12. package/dist/entities/system/Tool.d.ts +1 -1
  13. package/dist/entities/system/component-module.js +1 -1
  14. package/dist/entities/system/defineComponent.d.ts +45 -7
  15. package/dist/entities/system/defineComponent.js +52 -25
  16. package/dist/framework/Config.d.ts +2 -2
  17. package/dist/framework/Config.js +59 -7
  18. package/dist/framework/class-names.d.ts +4 -2
  19. package/dist/framework/class-names.js +56 -17
  20. package/dist/framework/defineDerivedEntity.d.ts +1 -1
  21. package/dist/framework/defineEntity.d.ts +2 -2
  22. package/dist/framework/defineSubEntity.d.ts +3 -2
  23. package/dist/framework/preview-store.d.ts +19 -0
  24. package/dist/framework/preview-store.js +18 -0
  25. package/dist/framework/projections.d.ts +28 -8
  26. package/dist/framework/projections.js +50 -16
  27. package/dist/framework/prop-surface.d.ts +21 -1
  28. package/dist/framework/prop-surface.js +99 -21
  29. package/dist/framework/render-spec.d.ts +103 -3
  30. package/dist/framework/render-spec.js +285 -37
  31. package/dist/framework/schema-version.d.ts +1 -1
  32. package/dist/framework/schema-version.js +5 -5
  33. package/dist/framework/utils/refs.js +10 -1
  34. package/dist/framework/validate-spec.d.ts +2 -0
  35. package/dist/framework/validate-spec.js +13 -0
  36. package/dist/index.d.ts +14 -11
  37. package/dist/index.js +6 -5
  38. package/dist/migrations/2.0.0/v1-artifact.d.ts +305 -29
  39. package/dist/migrations/20260919051027_prop_conditions_and_anatomy_positions_at_rest.d.ts +22 -0
  40. package/dist/migrations/20260919051027_prop_conditions_and_anatomy_positions_at_rest.js +10 -0
  41. package/dist/migrations/20260920004019_preview_conditions_keyed.d.ts +27 -0
  42. package/dist/migrations/20260920004019_preview_conditions_keyed.js +148 -0
  43. package/dist/migrations/20260920161309_prefixed_token_group.d.ts +12 -0
  44. package/dist/migrations/20260920161309_prefixed_token_group.js +10 -0
  45. package/dist/renderer/wrapRegistry.js +8 -2
  46. package/dist/renderer/wrappers/component-refs.d.ts +14 -0
  47. package/dist/renderer/wrappers/component-refs.js +79 -0
  48. package/dist/renderer/wrappers/hex-normalize.d.ts +5 -7
  49. package/dist/renderer/wrappers/hex-normalize.js +11 -10
  50. package/dist/spec/cell.d.ts +171 -0
  51. package/dist/spec/cell.js +361 -0
  52. package/dist/spec/detach.d.ts +125 -0
  53. package/dist/spec/detach.js +511 -0
  54. package/dist/spec/empty-node-slots.d.ts +11 -23
  55. package/dist/spec/empty-node-slots.js +12 -39
  56. package/dist/spec/index.d.ts +5 -3
  57. package/dist/spec/index.js +3 -3
  58. package/dist/spec/instance.d.ts +77 -0
  59. package/dist/spec/instance.js +80 -0
  60. package/dist/spec/preview-content.d.ts +95 -0
  61. package/dist/spec/preview-content.js +149 -0
  62. package/dist/spec/preview-elements.d.ts +54 -15
  63. package/dist/spec/preview-elements.js +109 -29
  64. package/dist/spec/prop-conditions.d.ts +28 -0
  65. package/dist/spec/prop-conditions.js +39 -0
  66. package/dist/spec/slot.d.ts +206 -0
  67. package/dist/spec/slot.js +542 -0
  68. package/dist/spec/visibility.d.ts +82 -0
  69. package/dist/spec/visibility.js +109 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +18 -2
@@ -0,0 +1,511 @@
1
+ import { isRef, kindOf, pathOf, refLeaf } from "../framework/utils/refs.js";
2
+ import { rootLayerOf } from "../entities/system/element.js";
3
+ import { declaredPropDefaults } from "../framework/projections.js";
4
+ import { isPreviewElement } from "./preview-elements.js";
5
+ import { entryKeyOf, resolveVisibility, slotTargetsOf } from "../framework/render-spec.js";
6
+ import { Component } from "../entities/system/Component.js";
7
+ //#region src/spec/detach.ts
8
+ /**
9
+ * Detaching a placed instance: the layer that renders a component becomes that component's anatomy,
10
+ * inlined as the edited component's own layers and baked at the instance's props.
11
+ *
12
+ * An instance shows its component under the props the node sets. Detaching keeps what it showed and
13
+ * gives the person the layers to restyle: each source node that renders under those props becomes a
14
+ * node here, its layer's element kept, the style rules that held folded into the node's props, a
15
+ * gate the props decide resolved away, a forwarded value written where it landed, and the content
16
+ * the instance held moved under the layer its slot routed to. What the instance's props no longer
17
+ * decide is gone with the instance; a gate on a runtime state stays.
18
+ */
19
+ function bodyOf(config, path) {
20
+ return config.resolve("component", path)?.body;
21
+ }
22
+ /** The layer a node renders, or `undefined` for a node that renders something else (a `Slot`). */
23
+ function layerOf(element) {
24
+ return isRef(element) && kindOf(element.__ref) === "layer" ? pathOf(element.__ref) : void 0;
25
+ }
26
+ /** The anatomy a component stores, or the one node a component without one is. */
27
+ function anatomyOf(body) {
28
+ if (body.anatomy && Object.keys(body.anatomy).length > 0) return body.anatomy;
29
+ const [root] = rootLayerOf(body.layers ?? {});
30
+ return { root: { element: { __ref: `layer:${root}` } } };
31
+ }
32
+ /** Whether a rule's condition holds under a prop bag, a ref compared by its leaf. */
33
+ function ruleHolds(when, state) {
34
+ if (!when) return true;
35
+ return Object.entries(when).every(([prop, value]) => String(refLeaf(state[prop])) === String(refLeaf(value)));
36
+ }
37
+ /** The rules in the order their bags apply: the unconditional ones, then by how many props a
38
+ * condition pins, so a narrower rule writes over a wider one the way the stylesheet cascades them. */
39
+ function orderedRules(styles) {
40
+ return Object.values(styles ?? {}).sort((a, b) => Object.keys(a.when ?? {}).length - Object.keys(b.when ?? {}).length);
41
+ }
42
+ /** The prop a forward lands its value under on the target layer. */
43
+ function intoName(into) {
44
+ if (typeof into === "string") return into.split("/").pop() ?? into;
45
+ if (isRef(into)) return into.__ref.split("#props/").pop() ?? "children";
46
+ return "children";
47
+ }
48
+ /** A layer key no layer of the component holds yet: `base` itself, else `base` numbered from 2. */
49
+ function freeLayerKey(layers, base) {
50
+ if (!(base in layers)) return base;
51
+ let n = 2;
52
+ while (`${base}${n}` in layers) n += 1;
53
+ return `${base}${n}`;
54
+ }
55
+ /** A layer key spelled after a component: its leaf, lower-cased at the front (`Badge` → `badge`). */
56
+ function layerKeyFor(componentPath) {
57
+ const leaf = componentPath.split("/").pop() ?? componentPath;
58
+ return `${leaf.charAt(0).toLowerCase()}${leaf.slice(1)}`;
59
+ }
60
+ /** The scalar props of a preview element, which is what an anatomy node's props hold. */
61
+ function scalarProps(props) {
62
+ const out = {};
63
+ for (const [name, value] of Object.entries(props ?? {})) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") out[name] = value;
64
+ return out;
65
+ }
66
+ /**
67
+ * Preview elements as subtree nodes, keyed under `prefix` so they join the baked subtree without
68
+ * colliding with the source's node names. A child that is itself a component stays an instance
69
+ * node; only the element being detached is baked.
70
+ */
71
+ function previewNodes(elements, prefix, into) {
72
+ return elements.map((element, index) => {
73
+ const key = `${prefix}${index}`;
74
+ const node = {
75
+ element: element.element,
76
+ ...element.text !== void 0 ? { text: element.text } : {}
77
+ };
78
+ const props = scalarProps(element.props);
79
+ if (Object.keys(props).length > 0) node.props = props;
80
+ into.set(key, node);
81
+ const children = previewNodes(element.children ?? [], `${key}_`, into);
82
+ if (children.length > 0) node.children = children;
83
+ for (const [prop, value] of Object.entries(element.props ?? {})) {
84
+ if (!isPreviewElement(value)) continue;
85
+ const [held] = previewNodes([value], `${key}_${prop}_`, into);
86
+ if (held) node.children = [...node.children ?? [], held];
87
+ }
88
+ return key;
89
+ });
90
+ }
91
+ /**
92
+ * The instance's component's anatomy under the instance's props: each source node that renders
93
+ * under those props, its layer's element kept, the style rules that held folded into its props, a
94
+ * gate the props decide resolved away, a forwarded value or text written where it landed, and a
95
+ * drilled override kept on its layer. `null` when the component stores nothing or its entry node
96
+ * does not render.
97
+ */
98
+ function bakeInstance({ config, source, instanceProps, layerProps, filled }) {
99
+ const sourcePath = pathOf(source);
100
+ const owner = config.configFor(source) ?? config;
101
+ const sourceBody = bodyOf(owner, sourcePath);
102
+ if (!sourceBody) return null;
103
+ const sourceLayers = sourceBody.layers ?? {};
104
+ const sourceAnatomy = anatomyOf(sourceBody);
105
+ const state = {
106
+ ...declaredPropDefaults(sourceBody.props),
107
+ ...sourceBody.defaultProps ?? {},
108
+ ...instanceProps
109
+ };
110
+ for (const from of filled) if (state[from] === void 0) state[from] = true;
111
+ const kept = /* @__PURE__ */ new Map();
112
+ for (const [key, sourceNode] of Object.entries(sourceAnatomy)) {
113
+ const shown = resolveVisibility(sourceNode.visible, state);
114
+ if (shown === false) continue;
115
+ const sourceLayer = layerOf(sourceNode.element);
116
+ if (sourceLayer === void 0) continue;
117
+ kept.set(key, {
118
+ element: sourceLayers[sourceLayer] ?? "div",
119
+ ...sourceNode.props ? { props: { ...sourceNode.props } } : {},
120
+ ...shown === void 0 && sourceNode.visible !== void 0 ? { visible: sourceNode.visible } : {},
121
+ ...sourceNode.text !== void 0 ? { text: sourceNode.text } : {}
122
+ });
123
+ }
124
+ for (const [key, sourceNode] of Object.entries(sourceAnatomy)) {
125
+ const inserted = kept.get(key);
126
+ if (!inserted) continue;
127
+ const children = (sourceNode.children ?? []).filter((child) => kept.has(child));
128
+ if (children.length > 0) inserted.children = children;
129
+ }
130
+ const keyOfLayer = (sourceLayer) => {
131
+ for (const [key, sourceNode] of Object.entries(sourceAnatomy)) if (layerOf(sourceNode.element) === sourceLayer && kept.has(key)) return key;
132
+ };
133
+ const nodeOfLayer = (sourceLayer) => {
134
+ const key = keyOfLayer(sourceLayer);
135
+ return key === void 0 ? void 0 : kept.get(key);
136
+ };
137
+ const setProp = (target, name, value) => {
138
+ target.props = {
139
+ ...target.props,
140
+ [name]: value
141
+ };
142
+ };
143
+ for (const rule of orderedRules(sourceBody.styles)) {
144
+ if (!ruleHolds(rule.when, state)) continue;
145
+ for (const [sourceLayer, bag] of Object.entries(rule.layers ?? {})) {
146
+ const target = nodeOfLayer(sourceLayer);
147
+ if (!target) continue;
148
+ for (const [name, value] of Object.entries(bag)) setProp(target, name, value);
149
+ }
150
+ }
151
+ const [sourceRoot] = rootLayerOf(sourceLayers);
152
+ const routes = slotTargetsOf(owner, sourcePath);
153
+ const declared = sourceBody.props ?? {};
154
+ for (const [name, value] of Object.entries(instanceProps)) {
155
+ if (value === void 0) continue;
156
+ const decl = declared[name];
157
+ const scalar = typeof value === "string" || typeof value === "number" || typeof value === "boolean";
158
+ if (decl?.type === "forward" && isRef(decl.target) && scalar) {
159
+ const target = nodeOfLayer(pathOf(decl.target.__ref));
160
+ const into = intoName(decl.into);
161
+ if (!target) continue;
162
+ if (into === "children") target.text = String(value);
163
+ else setProp(target, into, value);
164
+ continue;
165
+ }
166
+ if (decl?.type === "slot" && typeof value === "string") {
167
+ const target = nodeOfLayer(routes.find((r) => r.from === name)?.layer ?? sourceRoot);
168
+ if (target) target.text = value;
169
+ continue;
170
+ }
171
+ if (decl === void 0 || decl.type === "styleProperty") {
172
+ const target = nodeOfLayer(sourceRoot);
173
+ if (target && scalar) setProp(target, name, value);
174
+ }
175
+ }
176
+ for (const [sourceLayer, bag] of Object.entries(layerProps)) {
177
+ const target = nodeOfLayer(sourceLayer);
178
+ if (!target) continue;
179
+ for (const [name, value] of Object.entries(bag)) if (value !== void 0 && value !== null) setProp(target, name, value);
180
+ }
181
+ const entry = entryKeyOf(sourceAnatomy);
182
+ if (!kept.has(entry)) return null;
183
+ return {
184
+ entry,
185
+ nodes: kept,
186
+ keyOfLayer,
187
+ routes,
188
+ sourceRoot
189
+ };
190
+ }
191
+ /**
192
+ * The edits that detach the instance the layer `layer` of `path` renders: one `element-insert` of
193
+ * the instance's component's anatomy under the instance's parent at its index, a move for each
194
+ * element the instance held in a slot, and the instance's deletion last. `null` when the layer
195
+ * renders no component, or nothing places it.
196
+ */
197
+ function detachInstanceEdit({ config, path, layer }) {
198
+ const body = bodyOf(config, path);
199
+ if (!body) return null;
200
+ const layers = body.layers ?? {};
201
+ const element = layers[layer];
202
+ if (!isRef(element) || kindOf(element.__ref) !== "component") return null;
203
+ const anatomy = anatomyOf(body);
204
+ const node = anatomy[layer];
205
+ if (!node) return null;
206
+ let placed;
207
+ for (const [parentKey, parent] of Object.entries(anatomy)) {
208
+ const index = parent.children?.indexOf(layer) ?? -1;
209
+ if (index >= 0) {
210
+ placed = {
211
+ parent: parentKey,
212
+ index
213
+ };
214
+ break;
215
+ }
216
+ for (const [slotProp, fills] of Object.entries(parent.slots ?? {})) {
217
+ const at = fills.indexOf(layer);
218
+ if (at >= 0) {
219
+ placed = {
220
+ parent: parentKey,
221
+ index: at,
222
+ slotProp
223
+ };
224
+ break;
225
+ }
226
+ }
227
+ if (placed) break;
228
+ }
229
+ if (!placed) return null;
230
+ const instanceProps = { ...node.props ?? {} };
231
+ const layerProps = instanceProps.layerProps ?? {};
232
+ delete instanceProps.layerProps;
233
+ const filled = /* @__PURE__ */ new Set();
234
+ for (const [from, fills] of Object.entries(node.slots ?? {})) if (fills.length > 0) filled.add(from);
235
+ if (node.children?.length) filled.add("children");
236
+ const baked = bakeInstance({
237
+ config,
238
+ source: element.__ref,
239
+ instanceProps,
240
+ layerProps,
241
+ filled
242
+ });
243
+ if (!baked) return null;
244
+ const { entry, nodes: kept, keyOfLayer, routes, sourceRoot } = baked;
245
+ const newKey = freeLayerKey(layers, layer);
246
+ const grafted = (local) => local === entry ? newKey : `${newKey}__${local}`;
247
+ const edits = [{
248
+ kind: "component",
249
+ operation: "element-insert",
250
+ path,
251
+ key: newKey,
252
+ data: {
253
+ parent: placed.parent,
254
+ index: placed.index,
255
+ ...placed.slotProp ? { slotProp: placed.slotProp } : {},
256
+ subtree: {
257
+ root: entry,
258
+ elements: Object.fromEntries(kept)
259
+ }
260
+ }
261
+ }];
262
+ const moveTo = (fills, from) => {
263
+ const local = keyOfLayer(routes.find((r) => r.from === from)?.layer ?? sourceRoot) ?? entry;
264
+ for (const fill of fills) edits.push({
265
+ kind: "component",
266
+ operation: "element-move",
267
+ path,
268
+ key: fill,
269
+ data: { parent: grafted(local) }
270
+ });
271
+ };
272
+ moveTo(node.children ?? [], "children");
273
+ for (const [from, fills] of Object.entries(node.slots ?? {})) moveTo(fills, from);
274
+ edits.push({
275
+ kind: "component",
276
+ operation: "element-delete",
277
+ path,
278
+ key: layer
279
+ });
280
+ return edits;
281
+ }
282
+ /**
283
+ * The edits that detach one element of a slot's content: the element the address names in the
284
+ * content the cell shows becomes anatomy under the layer its slot routes to, baked at the props
285
+ * the element sets, and the content loses it. Its own children and the elements it hands to its
286
+ * slots come along as instance nodes under the layers the element's component routes them to.
287
+ * `null` for an address inside another element, or one naming no component.
288
+ */
289
+ function detachContentEdit({ config, path, cell, address }) {
290
+ const body = bodyOf(config, path);
291
+ if (!body) return null;
292
+ const slot = Component.cell({
293
+ config,
294
+ path,
295
+ cell
296
+ }).slot(address.prop);
297
+ const held = slot?.content[address.index];
298
+ const element = held ? elementAt(held, address.path) : void 0;
299
+ if (!slot || !element) return null;
300
+ if (!isRef(element.element) || kindOf(element.element.__ref) !== "component") return null;
301
+ if (address.path.length > 0) {
302
+ if (detachContentRefusal({
303
+ config,
304
+ path,
305
+ cell,
306
+ address
307
+ })) return null;
308
+ const baked = detachedPreviewElement({
309
+ config,
310
+ source: element.element.__ref,
311
+ element
312
+ });
313
+ return baked ? slot.replaceAt({
314
+ address,
315
+ element: baked
316
+ }) : null;
317
+ }
318
+ const anatomy = anatomyOf(body);
319
+ const hostKey = Object.keys(anatomy).find((key) => layerOf(anatomy[key]?.element) === slot.layer);
320
+ if (!hostKey) return null;
321
+ const instanceProps = scalarProps(element.props);
322
+ if (element.text !== void 0) instanceProps.children = element.text;
323
+ const filled = /* @__PURE__ */ new Set();
324
+ if (element.children?.length) filled.add("children");
325
+ for (const [prop, value] of Object.entries(element.props ?? {})) if (isPreviewElement(value)) filled.add(prop);
326
+ const baked = bakeInstance({
327
+ config,
328
+ source: element.element.__ref,
329
+ instanceProps,
330
+ layerProps: {},
331
+ filled
332
+ });
333
+ if (!baked) return null;
334
+ const { entry, nodes, keyOfLayer, routes, sourceRoot } = baked;
335
+ const attach = (held, from) => {
336
+ if (held.length === 0) return;
337
+ const local = keyOfLayer(routes.find((r) => r.from === from)?.layer ?? sourceRoot) ?? entry;
338
+ const target = nodes.get(local);
339
+ if (!target) return;
340
+ const keys = previewNodes(held, `${from}_`, nodes);
341
+ target.children = [...target.children ?? [], ...keys];
342
+ };
343
+ attach(element.children ?? [], "children");
344
+ for (const [prop, value] of Object.entries(element.props ?? {})) if (isPreviewElement(value)) attach([value], prop);
345
+ return [{
346
+ kind: "component",
347
+ operation: "element-insert",
348
+ path,
349
+ key: freeLayerKey(body.layers ?? {}, layerKeyFor(pathOf(element.element.__ref))),
350
+ data: {
351
+ parent: hostKey,
352
+ index: anatomy[hostKey]?.children?.length ?? 0,
353
+ subtree: {
354
+ root: entry,
355
+ elements: Object.fromEntries(nodes)
356
+ }
357
+ }
358
+ }, ...slot.removeAt(address)];
359
+ }
360
+ /**
361
+ * The anatomy an instance of `source` renders under `props`, as the elements a page document
362
+ * stores: the layer's element as the spec `type` (a component as its `component:` marker), the baked
363
+ * bag and any text as props, the kept edges as children. What a design-canvas Detach splices in
364
+ * place of the instance; the content the instance held goes under `slotHosts[<prop>]`.
365
+ */
366
+ function detachedElementSpec({ config, source, props, filled = /* @__PURE__ */ new Set() }) {
367
+ const instanceProps = { ...props };
368
+ const layerProps = instanceProps.layerProps ?? {};
369
+ delete instanceProps.layerProps;
370
+ const baked = bakeInstance({
371
+ config,
372
+ source,
373
+ instanceProps,
374
+ layerProps,
375
+ filled
376
+ });
377
+ if (!baked) return null;
378
+ const elements = {};
379
+ for (const [key, node] of baked.nodes) {
380
+ const bag = { ...node.props };
381
+ if (node.text !== void 0) bag.children = node.text;
382
+ elements[key] = {
383
+ type: isRef(node.element) ? node.element.__ref : String(node.element),
384
+ ...Object.keys(bag).length > 0 ? { props: bag } : {},
385
+ ...node.children?.length ? { children: [...node.children] } : {}
386
+ };
387
+ }
388
+ const slotHosts = {};
389
+ for (const route of baked.routes) slotHosts[route.from] = baked.keyOfLayer(route.layer) ?? baked.entry;
390
+ if (slotHosts.children === void 0) slotHosts.children = baked.keyOfLayer(baked.sourceRoot) ?? baked.entry;
391
+ return {
392
+ root: baked.entry,
393
+ elements,
394
+ slotHosts
395
+ };
396
+ }
397
+ /** The element an address's path reaches inside `root`: a number steps into a child, a string into an
398
+ * element-valued prop. */
399
+ function elementAt(root, steps) {
400
+ let current = root;
401
+ for (const step of steps) {
402
+ if (!current) return void 0;
403
+ const next = typeof step === "number" ? current.children?.[step] : current.props?.[step];
404
+ current = isPreviewElement(next) ? next : void 0;
405
+ }
406
+ return current;
407
+ }
408
+ /**
409
+ * A preview element that renders a component, as the tree its component is made of, baked at the
410
+ * props the element sets: each kept source node becomes an element (a component as its ref, a tag
411
+ * as itself), the baked bag as its props, its text as text, its edges as children. The children
412
+ * and element-valued props the original held ride along under the elements the component routes
413
+ * them to. `null` when the component stores nothing.
414
+ */
415
+ function detachedPreviewElement({ config, source, element }) {
416
+ const instanceProps = scalarProps(element.props);
417
+ if (element.text !== void 0) instanceProps.children = element.text;
418
+ const filled = /* @__PURE__ */ new Set();
419
+ if (element.children?.length) filled.add("children");
420
+ for (const [prop, value] of Object.entries(element.props ?? {})) if (isPreviewElement(value)) filled.add(prop);
421
+ const baked = bakeInstance({
422
+ config,
423
+ source,
424
+ instanceProps,
425
+ layerProps: {},
426
+ filled
427
+ });
428
+ if (!baked) return null;
429
+ const built = /* @__PURE__ */ new Map();
430
+ const build = (key) => {
431
+ const hit = built.get(key);
432
+ if (hit) return hit;
433
+ const node = baked.nodes.get(key);
434
+ if (!node) return void 0;
435
+ const out = { element: isRef(node.element) ? { __ref: node.element.__ref } : String(node.element) };
436
+ const props = {};
437
+ for (const [name, value] of Object.entries(node.props ?? {})) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isPreviewElement(value) || isRef(value)) props[name] = value;
438
+ if (Object.keys(props).length > 0) out.props = props;
439
+ if (node.text !== void 0) out.text = node.text;
440
+ const children = (node.children ?? []).map(build).filter((child) => child !== void 0);
441
+ if (children.length > 0) out.children = children;
442
+ built.set(key, out);
443
+ return out;
444
+ };
445
+ const root = build(baked.entry);
446
+ if (!root) return null;
447
+ const attach = (held, from) => {
448
+ if (held.length === 0) return;
449
+ const route = baked.routes.find((r) => r.from === from);
450
+ const local = baked.keyOfLayer(route?.layer ?? baked.sourceRoot) ?? baked.entry;
451
+ const target = built.get(local);
452
+ if (!target) return;
453
+ target.children = [...target.children ?? [], ...held];
454
+ };
455
+ attach(element.children ?? [], "children");
456
+ for (const [prop, value] of Object.entries(element.props ?? {})) if (isPreviewElement(value)) attach([value], prop);
457
+ return root;
458
+ }
459
+ /** The root element a detached instance of `source` would render, under `props`. */
460
+ function detachedRootElement({ config, source, props }) {
461
+ const baked = bakeInstance({
462
+ config,
463
+ source,
464
+ instanceProps: props,
465
+ layerProps: {},
466
+ filled: /* @__PURE__ */ new Set()
467
+ });
468
+ const root = baked?.nodes.get(baked.entry)?.element;
469
+ if (typeof root === "string") return root;
470
+ return isRef(root) ? root : void 0;
471
+ }
472
+ /**
473
+ * Why the element an address names inside another element cannot detach in place: the slot of the
474
+ * parent's component that holds it refuses what the element becomes. `null` when it can, or when
475
+ * the address is not inside another element.
476
+ */
477
+ function detachContentRefusal({ config, path, cell, address }) {
478
+ if (address.path.length === 0) return null;
479
+ const held = Component.cell({
480
+ config,
481
+ path,
482
+ cell
483
+ }).slot(address.prop)?.content[address.index];
484
+ if (!held) return null;
485
+ const parentSteps = address.path.slice(0, -1);
486
+ const last = address.path[address.path.length - 1];
487
+ const parent = elementAt(held, parentSteps);
488
+ const element = parent ? elementAt(parent, [last]) : void 0;
489
+ if (!parent || !element) return null;
490
+ if (!isRef(parent.element) || kindOf(parent.element.__ref) !== "component") return null;
491
+ if (!isRef(element.element) || kindOf(element.element.__ref) !== "component") return null;
492
+ const slot = Component.slot({
493
+ config,
494
+ path: pathOf(parent.element.__ref),
495
+ slot: typeof last === "number" ? "children" : last
496
+ });
497
+ if (!slot) return null;
498
+ const props = scalarProps(element.props);
499
+ const becomes = detachedRootElement({
500
+ config,
501
+ source: element.element.__ref,
502
+ props
503
+ });
504
+ if (becomes === void 0 || slot.takes(becomes)) return null;
505
+ return {
506
+ slot,
507
+ becomes
508
+ };
509
+ }
510
+ //#endregion
511
+ export { detachContentEdit, detachContentRefusal, detachInstanceEdit, detachedElementSpec, detachedPreviewElement, detachedRootElement };
@@ -1,34 +1,22 @@
1
1
  import { Config } from "../framework/Config.js";
2
- import { SlotTarget } from "../framework/render-spec.js";
3
2
  import { Spec } from "@json-render/core";
4
3
 
5
4
  //#region src/spec/empty-node-slots.d.ts
6
- /**
7
- * The component's named node slots — `slotTargetsOf` minus text slots and
8
- * minus `children`. `children` is the open content hole every container has
9
- * (a structural insert, not a named fill), and a text slot is an inline-edit
10
- * surface, not a place a node goes.
11
- *
12
- * Text-ness is checked on BOTH ends of the route: the component's own decl
13
- * (`title: { type: 'slot', text: true }`) and the prop the route lands on — a
14
- * `forward` into `Text#children` is typed in place, not filled with a dropped
15
- * node, and only the far end says so. A route whose host component doesn't
16
- * declare the landing prop is dropped too: the fill op would refuse it, so it
17
- * must never be offered as a target.
18
- */
19
- declare function namedSlotTargets(config: Config, componentPath: string): SlotTarget[];
20
5
  /**
21
6
  * Materialize every EMPTY layer-bound node slot of `spec`'s root as a `Slot`
22
7
  * element named in the root's `slots` record, so an unfilled slot is a stable
23
8
  * element in the spec rather than an absent entry — the studio canvas needs a
24
9
  * drop target, the layers panel a row, and the inspector an "Empty" state.
25
- * Slots the spec already fills are left as-is. Returns `spec` unchanged when
26
- * there's nothing to inject (no node slots, or all of them filled).
10
+ * Slots the spec already fills are left as-is, whether the fill is a `slots`
11
+ * entry or a value authored on the prop itself (a string title). Rendering
12
+ * routes a `slots` entry onto the prop it names, so a placeholder injected over
13
+ * an authored value would replace it for as long as the placeholder is there.
14
+ * Returns `spec` unchanged when there's nothing to inject (no node slots, or
15
+ * all of them filled).
27
16
  *
28
- * `slotNames` is the component's named node slots — {@link namedSlotTargets}'
29
- * `from` names. Callers pass names rather than a props record because which
30
- * props are node slots is route-dependent (a `forward` into a text prop is
31
- * not one), and only the config can answer that.
17
+ * `slotNames` is the component's named node slots — `Slot.named`'s `from` names. Callers pass
18
+ * names rather than a props record because which props are node slots is route-dependent (a
19
+ * `forward` into a text prop is not one), and only the config can answer that.
32
20
  */
33
21
  declare function withEmptyNodeSlots(spec: Spec, slotNames: readonly string[]): Spec;
34
22
  /**
@@ -84,7 +72,7 @@ declare function withContainerChildSlots(spec: Spec, acceptsChildNodes: AcceptsC
84
72
  /**
85
73
  * Compose both slot injections into the canvas's render view: layer-bound node
86
74
  * slots on a component instance's root (`withEmptyNodeSlots`, keyed by the
87
- * component's `namedSlotTargets`) plus a trailing insertion `Slot` in every container,
75
+ * component's `Slot.named`) plus a trailing insertion `Slot` in every container,
88
76
  * tree-wide (`withContainerChildSlots`). This is the spec the studio canvas
89
77
  * renders so empty slots show as drop targets — it is NEVER persisted. Stored
90
78
  * specs stay slot-free; `stripInjectedSlots` is the inverse, applied at every
@@ -111,4 +99,4 @@ declare function withRenderSlots(spec: Spec, slotNames: readonly string[] | null
111
99
  */
112
100
  declare function stripInjectedSlots(spec: Spec): Spec;
113
101
  //#endregion
114
- export { AcceptsChildNodes, ChildNodesClassification, ClassifyChildNodes, buildAcceptsChildNodes, buildClassifyChildNodes, namedSlotTargets, stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
102
+ export { AcceptsChildNodes, ChildNodesClassification, ClassifyChildNodes, buildAcceptsChildNodes, buildClassifyChildNodes, stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
@@ -1,50 +1,22 @@
1
- import { pathOf } from "../framework/utils/refs.js";
2
1
  import { childrenPolicy } from "../entities/system/element.js";
3
- import { resolveComponentProps } from "../framework/projections.js";
4
- import { slotTargetsOf } from "../framework/render-spec.js";
5
2
  import { Component } from "../entities/system/Component.js";
6
3
  //#region src/spec/empty-node-slots.ts
7
4
  const SLOT_TYPE = "Slot";
8
5
  /**
9
- * The component's named node slots — `slotTargetsOf` minus text slots and
10
- * minus `children`. `children` is the open content hole every container has
11
- * (a structural insert, not a named fill), and a text slot is an inline-edit
12
- * surface, not a place a node goes.
13
- *
14
- * Text-ness is checked on BOTH ends of the route: the component's own decl
15
- * (`title: { type: 'slot', text: true }`) and the prop the route lands on — a
16
- * `forward` into `Text#children` is typed in place, not filled with a dropped
17
- * node, and only the far end says so. A route whose host component doesn't
18
- * declare the landing prop is dropped too: the fill op would refuse it, so it
19
- * must never be offered as a target.
20
- */
21
- function namedSlotTargets(config, componentPath) {
22
- const component = Component.get(config, componentPath);
23
- const props = component?.props;
24
- const layers = component?.layers;
25
- return slotTargetsOf(config, componentPath).filter((target) => {
26
- if (target.from === "children") return false;
27
- const declared = props?.[target.from];
28
- if (declared?.type === "slot" && declared.text === true) return false;
29
- const hostRef = layers?.[target.layer];
30
- if (!hostRef || !Component.get(config, hostRef)) return true;
31
- const into = resolveComponentProps(config.configFor(hostRef) ?? config, pathOf(hostRef))[target.into];
32
- if (!into) return false;
33
- return !(into.type === "slot" && into.text === true);
34
- });
35
- }
36
- /**
37
6
  * Materialize every EMPTY layer-bound node slot of `spec`'s root as a `Slot`
38
7
  * element named in the root's `slots` record, so an unfilled slot is a stable
39
8
  * element in the spec rather than an absent entry — the studio canvas needs a
40
9
  * drop target, the layers panel a row, and the inspector an "Empty" state.
41
- * Slots the spec already fills are left as-is. Returns `spec` unchanged when
42
- * there's nothing to inject (no node slots, or all of them filled).
10
+ * Slots the spec already fills are left as-is, whether the fill is a `slots`
11
+ * entry or a value authored on the prop itself (a string title). Rendering
12
+ * routes a `slots` entry onto the prop it names, so a placeholder injected over
13
+ * an authored value would replace it for as long as the placeholder is there.
14
+ * Returns `spec` unchanged when there's nothing to inject (no node slots, or
15
+ * all of them filled).
43
16
  *
44
- * `slotNames` is the component's named node slots — {@link namedSlotTargets}'
45
- * `from` names. Callers pass names rather than a props record because which
46
- * props are node slots is route-dependent (a `forward` into a text prop is
47
- * not one), and only the config can answer that.
17
+ * `slotNames` is the component's named node slots — `Slot.named`'s `from` names. Callers pass
18
+ * names rather than a props record because which props are node slots is route-dependent (a
19
+ * `forward` into a text prop is not one), and only the config can answer that.
48
20
  */
49
21
  function withEmptyNodeSlots(spec, slotNames) {
50
22
  const names = slotNames;
@@ -57,6 +29,7 @@ function withEmptyNodeSlots(spec, slotNames) {
57
29
  let injected = false;
58
30
  for (const name of names) {
59
31
  if (root.slots?.[name] !== void 0) continue;
32
+ if (root.props?.[name] !== void 0) continue;
60
33
  const key = `${rootKey}__${name}__slot`;
61
34
  elements[key] = {
62
35
  type: SLOT_TYPE,
@@ -143,7 +116,7 @@ function withContainerChildSlots(spec, acceptsChildNodes) {
143
116
  /**
144
117
  * Compose both slot injections into the canvas's render view: layer-bound node
145
118
  * slots on a component instance's root (`withEmptyNodeSlots`, keyed by the
146
- * component's `namedSlotTargets`) plus a trailing insertion `Slot` in every container,
119
+ * component's `Slot.named`) plus a trailing insertion `Slot` in every container,
147
120
  * tree-wide (`withContainerChildSlots`). This is the spec the studio canvas
148
121
  * renders so empty slots show as drop targets — it is NEVER persisted. Stored
149
122
  * specs stay slot-free; `stripInjectedSlots` is the inverse, applied at every
@@ -192,4 +165,4 @@ function stripInjectedSlots(spec) {
192
165
  return next;
193
166
  }
194
167
  //#endregion
195
- export { buildAcceptsChildNodes, buildClassifyChildNodes, namedSlotTargets, stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
168
+ export { buildAcceptsChildNodes, buildClassifyChildNodes, stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
@@ -1,7 +1,9 @@
1
- import { AcceptsChildNodes, ChildNodesClassification, ClassifyChildNodes, buildAcceptsChildNodes, buildClassifyChildNodes, namedSlotTargets, stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots } from "./empty-node-slots.js";
2
- import { GraftableElement, GraftableSpec, PreviewElement, PreviewSpecNode, PreviewValue, graftPreviewElements, hasPreviewElementDeep, isPreviewElement, previewElementNodes, previewElementType } from "./preview-elements.js";
1
+ import { GraftableElement, GraftableSpec, PreviewElement, PreviewRef, PreviewSpecNode, PreviewValue, graftPreviewElements, graftSlotContent, hasPreviewElementDeep, isComponentRef, isPreviewElement, isPreviewRef, previewElementNodes, previewElementType } from "./preview-elements.js";
2
+ import { Slot, SlotCell } from "./slot.js";
3
+ import { Cell, CellSlot } from "./cell.js";
3
4
  import { DocumentRename, RewritableSpec, SpecRewrite, documentRenamesFromPatches, rewriteSpecRefs } from "./rewrite-refs.js";
4
5
  import { applyForcedModifiers, collectForcedModifierProps } from "./apply-forced-modifiers.js";
6
+ import { AcceptsChildNodes, ChildNodesClassification, ClassifyChildNodes, buildAcceptsChildNodes, buildClassifyChildNodes, stripInjectedSlots, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots } from "./empty-node-slots.js";
5
7
  import { AssetComponentMap, AssetJsxForm, AssetNodeResolver, assetJsxForm, jsxIdentifier, parseAssetType } from "./asset-jsx.js";
6
8
  import { CollapseTextLabelsResult, CollapsedTextLabel, collapseWrappedTextLabels } from "./collapse-text-labels.js";
7
9
  import { JsxToSpecOptions, UNRESOLVED, hasUnresolvedProps, jsxToSpec, stripUnresolvedProps } from "./jsxToSpec.js";
@@ -26,4 +28,4 @@ import { SpecToJsxOptions, SpecToJsxResult, specToJsx } from "./specToJsx.js";
26
28
  * element, without changing the rendered layer hierarchy. */
27
29
  declare const SYNTHETIC_TEXT_SLOT_PROP = "data-uds-synthetic-text-slot";
28
30
  //#endregion
29
- export { type AcceptsChildNodes, type AssetComponentMap, type AssetJsxForm, type AssetNodeResolver, type ChildNodesClassification, type ClassifyChildNodes, type CollapseTextLabelsResult, type CollapsedTextLabel, type DocumentRename, type GraftableElement, type GraftableSpec, type JsxToSpecOptions, type PreviewElement, type PreviewSpecNode, type PreviewValue, type RewritableSpec, SYNTHETIC_TEXT_SLOT_PROP, type SpecRewrite, type SpecToJsxOptions, type SpecToJsxResult, UNRESOLVED, applyForcedModifiers, assetJsxForm, buildAcceptsChildNodes, buildClassifyChildNodes, collapseWrappedTextLabels, collectForcedModifierProps, documentRenamesFromPatches, graftPreviewElements, hasPreviewElementDeep, hasUnresolvedProps, isPreviewElement, jsxIdentifier, jsxToSpec, namedSlotTargets, parseAssetType, previewElementNodes, previewElementType, rewriteSpecRefs, specToJsx, stripInjectedSlots, stripUnresolvedProps, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };
31
+ export { type AcceptsChildNodes, type AssetComponentMap, type AssetJsxForm, type AssetNodeResolver, type Cell, type CellSlot, type ChildNodesClassification, type ClassifyChildNodes, type CollapseTextLabelsResult, type CollapsedTextLabel, type DocumentRename, type GraftableElement, type GraftableSpec, type JsxToSpecOptions, type PreviewElement, type PreviewRef, type PreviewSpecNode, type PreviewValue, type RewritableSpec, SYNTHETIC_TEXT_SLOT_PROP, type Slot, type SlotCell, type SpecRewrite, type SpecToJsxOptions, type SpecToJsxResult, UNRESOLVED, applyForcedModifiers, assetJsxForm, buildAcceptsChildNodes, buildClassifyChildNodes, collapseWrappedTextLabels, collectForcedModifierProps, documentRenamesFromPatches, graftPreviewElements, graftSlotContent, hasPreviewElementDeep, hasUnresolvedProps, isComponentRef, isPreviewElement, isPreviewRef, jsxIdentifier, jsxToSpec, parseAssetType, previewElementNodes, previewElementType, rewriteSpecRefs, specToJsx, stripInjectedSlots, stripUnresolvedProps, withContainerChildSlots, withEmptyNodeSlots, withRenderSlots };