@elytracms/studio 0.0.64 → 0.0.66

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 (182) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/account/self-account-view-source.js +9 -2
  5. package/dist/components/account/self-account-view-source.js.map +1 -1
  6. package/dist/components/app-views/default-modules.d.ts +28 -1
  7. package/dist/components/app-views/default-modules.js +47 -2
  8. package/dist/components/app-views/default-modules.js.map +1 -1
  9. package/dist/components/assets/asset-thumb.d.ts +4 -2
  10. package/dist/components/assets/asset-thumb.js +26 -4
  11. package/dist/components/assets/asset-thumb.js.map +1 -1
  12. package/dist/components/assets/assets-module.js +48 -2
  13. package/dist/components/assets/assets-module.js.map +1 -1
  14. package/dist/components/assets/assets-store.d.ts +11 -1
  15. package/dist/components/assets/assets-store.js +18 -1
  16. package/dist/components/assets/assets-store.js.map +1 -1
  17. package/dist/components/auth-context.js +32 -1
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/blocks-model.d.ts +11 -0
  20. package/dist/components/builder/blocks-model.js +30 -10
  21. package/dist/components/builder/blocks-model.js.map +1 -1
  22. package/dist/components/builder/canvas-chrome-css.js +90 -8
  23. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  24. package/dist/components/builder/canvas-dnd.js +4 -2
  25. package/dist/components/builder/canvas-dnd.js.map +1 -1
  26. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  27. package/dist/components/builder/canvas-inline-editing.js +3 -1
  28. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  29. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  30. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  31. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  32. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  33. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  34. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  35. package/dist/components/builder/canvas.js +61 -12
  36. package/dist/components/builder/canvas.js.map +1 -1
  37. package/dist/components/builder/composition-field-editor.js +22 -3
  38. package/dist/components/builder/composition-field-editor.js.map +1 -1
  39. package/dist/components/builder/delete-key.d.ts +38 -0
  40. package/dist/components/builder/delete-key.js +46 -0
  41. package/dist/components/builder/delete-key.js.map +1 -0
  42. package/dist/components/builder/drag-session.d.ts +8 -0
  43. package/dist/components/builder/drag-session.js +15 -0
  44. package/dist/components/builder/drag-session.js.map +1 -1
  45. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  46. package/dist/components/builder/insert-affordance-context.js +7 -0
  47. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  48. package/dist/components/builder/insert-context.d.ts +40 -0
  49. package/dist/components/builder/insert-context.js +97 -5
  50. package/dist/components/builder/insert-context.js.map +1 -1
  51. package/dist/components/builder/node-title.d.ts +60 -0
  52. package/dist/components/builder/node-title.js +135 -0
  53. package/dist/components/builder/node-title.js.map +1 -0
  54. package/dist/components/builder/page-tree.js +25 -5
  55. package/dist/components/builder/page-tree.js.map +1 -1
  56. package/dist/components/builder/prop-selection.d.ts +52 -0
  57. package/dist/components/builder/prop-selection.js +38 -0
  58. package/dist/components/builder/prop-selection.js.map +1 -0
  59. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  60. package/dist/components/builder/prop-sync-context.js +23 -0
  61. package/dist/components/builder/prop-sync-context.js.map +1 -0
  62. package/dist/components/builder/property-inspector.js +24 -1
  63. package/dist/components/builder/property-inspector.js.map +1 -1
  64. package/dist/components/builder/reuse-context.js +1 -1
  65. package/dist/components/builder/reuse-context.js.map +1 -1
  66. package/dist/components/builder/selection-overlay.d.ts +79 -0
  67. package/dist/components/builder/selection-overlay.js +244 -0
  68. package/dist/components/builder/selection-overlay.js.map +1 -0
  69. package/dist/components/builder/spacing-handles.d.ts +57 -1
  70. package/dist/components/builder/spacing-handles.js +142 -26
  71. package/dist/components/builder/spacing-handles.js.map +1 -1
  72. package/dist/components/cms/asset-field.js +21 -5
  73. package/dist/components/cms/asset-field.js.map +1 -1
  74. package/dist/components/cms/asset-picker.js +11 -5
  75. package/dist/components/cms/asset-picker.js.map +1 -1
  76. package/dist/components/cms/bulk-actions.d.ts +22 -3
  77. package/dist/components/cms/bulk-actions.js +27 -3
  78. package/dist/components/cms/bulk-actions.js.map +1 -1
  79. package/dist/components/cms/cms-store.js +19 -0
  80. package/dist/components/cms/cms-store.js.map +1 -1
  81. package/dist/components/cms/detail-editor.js +18 -19
  82. package/dist/components/cms/detail-editor.js.map +1 -1
  83. package/dist/components/cms/prop-fields.js +23 -16
  84. package/dist/components/cms/prop-fields.js.map +1 -1
  85. package/dist/components/cms/publish-clean.d.ts +21 -0
  86. package/dist/components/cms/publish-clean.js +30 -0
  87. package/dist/components/cms/publish-clean.js.map +1 -0
  88. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  89. package/dist/components/cms/rich-text/block-style.js +30 -0
  90. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  91. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  92. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  93. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  94. package/dist/components/cms/rich-text/extensions.js +4 -0
  95. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  96. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  97. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  98. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  99. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  100. package/dist/components/cms/rich-text/font-size.js +40 -0
  101. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  102. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  103. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  104. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  105. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  106. package/dist/components/cms/rich-text/toolbar.js +17 -5
  107. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  108. package/dist/components/cms/use-live-url.d.ts +24 -1
  109. package/dist/components/cms/use-live-url.js +26 -4
  110. package/dist/components/cms/use-live-url.js.map +1 -1
  111. package/dist/components/cms/use-publish-controller.js +8 -6
  112. package/dist/components/cms/use-publish-controller.js.map +1 -1
  113. package/dist/components/collection-view/cms-collection-view-source.js +15 -7
  114. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  115. package/dist/components/components/component-detail.js +3 -3
  116. package/dist/components/components/component-detail.js.map +1 -1
  117. package/dist/components/components/component-preview.js +1 -1
  118. package/dist/components/components/component-preview.js.map +1 -1
  119. package/dist/components/components/registry-browser.js +2 -2
  120. package/dist/components/components/registry-browser.js.map +1 -1
  121. package/dist/components/components/taxonomy.js +3 -2
  122. package/dist/components/components/taxonomy.js.map +1 -1
  123. package/dist/components/components/transfer-dialog.js +1 -1
  124. package/dist/components/components/transfer-dialog.js.map +1 -1
  125. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  126. package/dist/lib/assets/backfill-thumbs.js +120 -0
  127. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  128. package/dist/lib/assets/delete-asset.d.ts +2 -1
  129. package/dist/lib/assets/delete-asset.js +12 -0
  130. package/dist/lib/assets/delete-asset.js.map +1 -1
  131. package/dist/lib/assets/index.d.ts +2 -0
  132. package/dist/lib/assets/index.js +2 -0
  133. package/dist/lib/assets/index.js.map +1 -1
  134. package/dist/lib/assets/thumbnail.d.ts +145 -0
  135. package/dist/lib/assets/thumbnail.js +220 -0
  136. package/dist/lib/assets/thumbnail.js.map +1 -0
  137. package/dist/lib/assets/upload.d.ts +9 -0
  138. package/dist/lib/assets/upload.js +31 -0
  139. package/dist/lib/assets/upload.js.map +1 -1
  140. package/dist/lib/auth/auth-adapter.d.ts +22 -0
  141. package/dist/lib/auth/auth-adapter.js.map +1 -1
  142. package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
  143. package/dist/lib/auth/convex-auth-adapter.js +69 -0
  144. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  145. package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
  146. package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
  147. package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
  148. package/dist/lib/auth/index.d.ts +1 -1
  149. package/dist/lib/auth/index.js.map +1 -1
  150. package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
  151. package/dist/lib/auth/local-auth-adapter.js +7 -0
  152. package/dist/lib/auth/local-auth-adapter.js.map +1 -1
  153. package/dist/lib/auth-errors.d.ts +45 -3
  154. package/dist/lib/auth-errors.js +46 -5
  155. package/dist/lib/auth-errors.js.map +1 -1
  156. package/dist/lib/clipboard/paste-target.js +5 -2
  157. package/dist/lib/clipboard/paste-target.js.map +1 -1
  158. package/dist/lib/dnd/drop-rules.d.ts +13 -0
  159. package/dist/lib/dnd/drop-rules.js +15 -0
  160. package/dist/lib/dnd/drop-rules.js.map +1 -1
  161. package/dist/lib/i18n/messages.de.js +19 -0
  162. package/dist/lib/i18n/messages.de.js.map +1 -1
  163. package/dist/lib/i18n/messages.en.d.ts +16 -0
  164. package/dist/lib/i18n/messages.en.js +24 -0
  165. package/dist/lib/i18n/messages.en.js.map +1 -1
  166. package/dist/lib/persistence/convex-adapter.js +4 -0
  167. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  168. package/dist/lib/persistence/index.js +6 -3
  169. package/dist/lib/persistence/index.js.map +1 -1
  170. package/dist/lib/persona/labels.d.ts +9 -5
  171. package/dist/lib/persona/labels.js +13 -8
  172. package/dist/lib/persona/labels.js.map +1 -1
  173. package/dist/lib/references/references-store.js +7 -1
  174. package/dist/lib/references/references-store.js.map +1 -1
  175. package/dist/lib/session-expiry.d.ts +19 -6
  176. package/dist/lib/session-expiry.js +13 -1
  177. package/dist/lib/session-expiry.js.map +1 -1
  178. package/dist/lib/session-revalidation.d.ts +81 -0
  179. package/dist/lib/session-revalidation.js +94 -0
  180. package/dist/lib/session-revalidation.js.map +1 -0
  181. package/package.json +2 -2
  182. package/src/styles/studio.css +13 -0
@@ -23,3 +23,43 @@ import type { InsertContext } from './insert-menu.js';
23
23
  export declare function resolveInsertContext(builder: {
24
24
  present: ComponentNode;
25
25
  }, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
26
+ /**
27
+ * An in-canvas insertion position relative to a reference node (EC-405) — the
28
+ * DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the
29
+ * insertion affordances offer: `before`/`after` a sibling, or `into` an (empty)
30
+ * container. `left`/`right` (the wrap-into-columns gesture) are DnD-only.
31
+ */
32
+ export type InsertPosition = 'before' | 'after' | 'into';
33
+ /**
34
+ * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
35
+ * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an
36
+ * affordance click at a chosen edge lands the insert at exactly that spot rather
37
+ * than the panel re-deriving it from the selection.
38
+ *
39
+ * The slot resolution reuses the shared DnD rules — `into` uses
40
+ * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
41
+ * node accepts no children), `before`/`after` use the reference's parent slot at
42
+ * `index`/`index + 1` — so this surface and drag-insert can never disagree about
43
+ * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
44
+ * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
45
+ */
46
+ export declare function resolveInsertContextAt(builder: {
47
+ present: ComponentNode;
48
+ }, nodeId: string, position: InsertPosition, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertContext | null;
49
+ /**
50
+ * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
51
+ * a position is offered only when it structurally exists AND at least one registry
52
+ * component may land there (the SAME slot-rule filter the tree/DnD and the blocks
53
+ * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
54
+ * blocked spot shows no dead button.
55
+ *
56
+ * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
57
+ * is "add the first child", and an empty container often renders zero-height, so
58
+ * its own before/after edges are degenerate and would pile the three "+" buttons
59
+ * onto one point. A filled container and every leaf get before/after instead (each
60
+ * child already carries its own edge affordances, so a filled container needs no
61
+ * center "+").
62
+ */
63
+ export declare function insertablePositions(builder: {
64
+ present: ComponentNode;
65
+ }, nodeId: string, fieldAllow?: readonly string[], studio?: StudioRegistry): InsertPosition[];
@@ -1,7 +1,8 @@
1
1
  import { getNode, getParentInfo } from '@elytracms/core/project-graph';
2
2
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
3
3
  import { shortId } from '@elytracms/studio/lib/persona/labels';
4
- import { intersectAllow } from './blocks-model.js';
4
+ import { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules';
5
+ import { hasInsertableComponent, intersectAllow } from './blocks-model.js';
5
6
  /**
6
7
  * Where an insert lands: the canvas insert-context resolution, shared by every
7
8
  * surface that mounts the canvas engine.
@@ -26,13 +27,16 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
26
27
  const node = getNode(root, nodeId);
27
28
  if (!node)
28
29
  return null;
30
+ // EC-386: resolve against the merged slot vocabulary (`manifestRules` folds
31
+ // blocks-prop regions in), through the SAME `primaryInsertSlot` rule a DnD
32
+ // `into` uses — so a `project.Section.body`-style region receives the insert
33
+ // instead of silently degrading to a sibling insert.
29
34
  const manifest = studio.registry.getManifest(node.componentId);
30
- const childSlot = manifest?.slots.find((s) => s.name === 'children') ??
31
- manifest?.slots.find((s) => s.name !== 'outlet');
35
+ const childSlot = primaryInsertSlot(manifestRules(manifest));
32
36
  const ctx = childSlot
33
37
  ? {
34
38
  target: { parentId: nodeId, slot: childSlot.name },
35
- slotAllow: manifest.slots.find((s) => s.name === childSlot.name)?.allow,
39
+ slotAllow: childSlot.allow,
36
40
  destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,
37
41
  }
38
42
  : resolveSiblingContext(studio, root, nodeId);
@@ -48,7 +52,9 @@ export function resolveInsertContext(builder, nodeId, fieldAllow, studio = getSt
48
52
  function resolveSiblingContext(studio, root, nodeId) {
49
53
  const info = getParentInfo(root, nodeId);
50
54
  if (info) {
51
- const pManifest = studio.registry.getManifest(info.parent.componentId);
55
+ // EC-386: the parent slot may be a blocks-prop region — look it up on the
56
+ // merged vocabulary so its `allow` leash still filters sibling inserts.
57
+ const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
52
58
  const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow;
53
59
  return {
54
60
  target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },
@@ -58,4 +64,90 @@ function resolveSiblingContext(studio, root, nodeId) {
58
64
  }
59
65
  return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' };
60
66
  }
67
+ /**
68
+ * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`
69
+ * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an
70
+ * affordance click at a chosen edge lands the insert at exactly that spot rather
71
+ * than the panel re-deriving it from the selection.
72
+ *
73
+ * The slot resolution reuses the shared DnD rules — `into` uses
74
+ * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the
75
+ * node accepts no children), `before`/`after` use the reference's parent slot at
76
+ * `index`/`index + 1` — so this surface and drag-insert can never disagree about
77
+ * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains
78
+ * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.
79
+ */
80
+ export function resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio = getStudioRegistry()) {
81
+ const root = builder.present;
82
+ const node = getNode(root, nodeId);
83
+ if (!node)
84
+ return null;
85
+ let ctx;
86
+ if (position === 'into') {
87
+ // EC-386: `manifestRules` folds blocks-prop regions into the slot set, so
88
+ // an affordance/drop `into` a Section-style body lands in that region.
89
+ const manifest = studio.registry.getManifest(node.componentId);
90
+ const slot = primaryInsertSlot(manifestRules(manifest));
91
+ if (!slot)
92
+ return null; // the node accepts no children — no `into` position
93
+ ctx = {
94
+ target: {
95
+ parentId: nodeId,
96
+ slot: slot.name,
97
+ index: (node.slots?.[slot.name] ?? []).length,
98
+ },
99
+ slotAllow: slot.allow,
100
+ destinationLabel: `${shortId(node.componentId)} · ${slot.name}`,
101
+ };
102
+ }
103
+ else {
104
+ const info = getParentInfo(root, nodeId);
105
+ if (!info)
106
+ return null; // the root has no siblings — no before/after position
107
+ // EC-386: merged vocabulary — a blocks-prop parent region keeps its leash.
108
+ const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId));
109
+ ctx = {
110
+ target: {
111
+ parentId: info.parent.id,
112
+ slot: info.slot,
113
+ index: info.index + (position === 'after' ? 1 : 0),
114
+ },
115
+ slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,
116
+ destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,
117
+ };
118
+ }
119
+ if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {
120
+ return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) };
121
+ }
122
+ return ctx;
123
+ }
124
+ /**
125
+ * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):
126
+ * a position is offered only when it structurally exists AND at least one registry
127
+ * component may land there (the SAME slot-rule filter the tree/DnD and the blocks
128
+ * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a
129
+ * blocked spot shows no dead button.
130
+ *
131
+ * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there
132
+ * is "add the first child", and an empty container often renders zero-height, so
133
+ * its own before/after edges are degenerate and would pile the three "+" buttons
134
+ * onto one point. A filled container and every leaf get before/after instead (each
135
+ * child already carries its own edge affordances, so a filled container needs no
136
+ * center "+").
137
+ */
138
+ export function insertablePositions(builder, nodeId, fieldAllow, studio = getStudioRegistry()) {
139
+ // Empty insertable container → into only.
140
+ const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio);
141
+ if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {
142
+ return ['into'];
143
+ }
144
+ // Otherwise the sibling edges that accept something.
145
+ const out = [];
146
+ for (const position of ['before', 'after']) {
147
+ const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio);
148
+ if (ctx && hasInsertableComponent(studio, ctx.slotAllow))
149
+ out.push(position);
150
+ }
151
+ return out;
152
+ }
61
153
  //# sourceMappingURL=insert-context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\n\n/**\n * Where an insert lands: the canvas insert-context resolution, shared by every\n * surface that mounts the canvas engine.\n *\n * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS\n * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without\n * importing the dead shell. No UI here — pure graph/registry resolution.\n */\n\n/**\n * Resolve where an insert lands: into the node's child slot, else as a sibling.\n *\n * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary\n * (`field.allow`). It constrains ONLY inserts that land as a direct child of the\n * active page root — the top level of a composition field value — where it is\n * intersected into `slotAllow` so forbidden components are never even offered,\n * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep\n * following each component's own slot `allow`. Callers that omit `fieldAllow` get\n * an unconstrained top level.\n */\nexport function resolveInsertContext(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n const manifest = studio.registry.getManifest(node.componentId)\n const childSlot =\n manifest?.slots.find((s) => s.name === 'children') ??\n manifest?.slots.find((s) => s.name !== 'outlet')\n const ctx: InsertContext = childSlot\n ? {\n target: { parentId: nodeId, slot: childSlot.name },\n slotAllow: manifest!.slots.find((s) => s.name === childSlot.name)?.allow,\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n : resolveSiblingContext(studio, root, nodeId)\n\n // The field vocabulary governs only the top level (a direct child of the page\n // root). `length > 0` matches the validator, which treats an empty allow as\n // \"allow any\" (validate.ts).\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/** Sibling-or-root insert context for a node with no insertable child slot. */\nfunction resolveSiblingContext(\n studio: StudioRegistry,\n root: ComponentNode,\n nodeId: string,\n): InsertContext {\n const info = getParentInfo(root, nodeId)\n if (info) {\n const pManifest = studio.registry.getManifest(info.parent.componentId)\n const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n slotAllow: allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' }\n}\n"]}
1
+ {"version":3,"file":"insert-context.js","sourceRoot":"","sources":["../../../src/components/builder/insert-context.ts"],"names":[],"mappings":"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","sourcesContent":["import { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { shortId } from '@elytracms/studio/lib/persona/labels'\nimport { manifestRules, primaryInsertSlot } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { hasInsertableComponent, intersectAllow } from './blocks-model'\nimport type { InsertContext } from './insert-menu'\n\n/**\n * Where an insert lands: the canvas insert-context resolution, shared by every\n * surface that mounts the canvas engine.\n *\n * Extracted from the retired standalone `BuilderShell` (EC-210) so the in-CMS\n * Canvas editor (`composition-field-editor.tsx`, EC-188/AD-12) can own it without\n * importing the dead shell. No UI here — pure graph/registry resolution.\n */\n\n/**\n * Resolve where an insert lands: into the node's child slot, else as a sibling.\n *\n * `fieldAllow` (EC-188) is the composition field's TOP-LEVEL vocabulary\n * (`field.allow`). It constrains ONLY inserts that land as a direct child of the\n * active page root — the top level of a composition field value — where it is\n * intersected into `slotAllow` so forbidden components are never even offered,\n * mirroring the top-level leash in `validateCompositionValue`. Deeper inserts keep\n * following each component's own slot `allow`. Callers that omit `fieldAllow` get\n * an unconstrained top level.\n */\nexport function resolveInsertContext(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n // EC-386: resolve against the merged slot vocabulary (`manifestRules` folds\n // blocks-prop regions in), through the SAME `primaryInsertSlot` rule a DnD\n // `into` uses — so a `project.Section.body`-style region receives the insert\n // instead of silently degrading to a sibling insert.\n const manifest = studio.registry.getManifest(node.componentId)\n const childSlot = primaryInsertSlot(manifestRules(manifest))\n const ctx: InsertContext = childSlot\n ? {\n target: { parentId: nodeId, slot: childSlot.name },\n slotAllow: childSlot.allow,\n destinationLabel: `${shortId(node.componentId)} · ${childSlot.name}`,\n }\n : resolveSiblingContext(studio, root, nodeId)\n\n // The field vocabulary governs only the top level (a direct child of the page\n // root). `length > 0` matches the validator, which treats an empty allow as\n // \"allow any\" (validate.ts).\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/** Sibling-or-root insert context for a node with no insertable child slot. */\nfunction resolveSiblingContext(studio: StudioRegistry, root: ComponentNode, nodeId: string): InsertContext {\n const info = getParentInfo(root, nodeId)\n if (info) {\n // EC-386: the parent slot may be a blocks-prop region — look it up on the\n // merged vocabulary so its `allow` leash still filters sibling inserts.\n const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId))\n const allow = pManifest?.slots.find((s) => s.name === info.slot)?.allow\n return {\n target: { parentId: info.parent.id, slot: info.slot, index: info.index + 1 },\n slotAllow: allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n return { target: { parentId: nodeId, slot: 'children' }, destinationLabel: 'root' }\n}\n\n/**\n * An in-canvas insertion position relative to a reference node (EC-405) — the\n * DnD position vocabulary (`lib/dnd/drop-rules`), narrowed to the three the\n * insertion affordances offer: `before`/`after` a sibling, or `into` an (empty)\n * container. `left`/`right` (the wrap-into-columns gesture) are DnD-only.\n */\nexport type InsertPosition = 'before' | 'after' | 'into'\n\n/**\n * Resolve an `InsertContext` for an EXPLICIT position relative to `nodeId`\n * (EC-405) — the position-carrying twin of {@link resolveInsertContext}, so an\n * affordance click at a chosen edge lands the insert at exactly that spot rather\n * than the panel re-deriving it from the selection.\n *\n * The slot resolution reuses the shared DnD rules — `into` uses\n * {@link primaryInsertSlot} (the same slot a drop `into` picks; `null` when the\n * node accepts no children), `before`/`after` use the reference's parent slot at\n * `index`/`index + 1` — so this surface and drag-insert can never disagree about\n * where a position lands or what it allows. `fieldAllow` (EC-188) still constrains\n * ONLY a direct child of the composition root, mirroring `resolveInsertContext`.\n */\nexport function resolveInsertContextAt(\n builder: { present: ComponentNode },\n nodeId: string,\n position: InsertPosition,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertContext | null {\n const root = builder.present\n const node = getNode(root, nodeId)\n if (!node) return null\n\n let ctx: InsertContext\n if (position === 'into') {\n // EC-386: `manifestRules` folds blocks-prop regions into the slot set, so\n // an affordance/drop `into` a Section-style body lands in that region.\n const manifest = studio.registry.getManifest(node.componentId)\n const slot = primaryInsertSlot(manifestRules(manifest))\n if (!slot) return null // the node accepts no children — no `into` position\n ctx = {\n target: {\n parentId: nodeId,\n slot: slot.name,\n index: (node.slots?.[slot.name] ?? []).length,\n },\n slotAllow: slot.allow,\n destinationLabel: `${shortId(node.componentId)} · ${slot.name}`,\n }\n } else {\n const info = getParentInfo(root, nodeId)\n if (!info) return null // the root has no siblings — no before/after position\n // EC-386: merged vocabulary — a blocks-prop parent region keeps its leash.\n const pManifest = manifestRules(studio.registry.getManifest(info.parent.componentId))\n ctx = {\n target: {\n parentId: info.parent.id,\n slot: info.slot,\n index: info.index + (position === 'after' ? 1 : 0),\n },\n slotAllow: pManifest?.slots.find((s) => s.name === info.slot)?.allow,\n destinationLabel: `${shortId(info.parent.componentId)} · ${info.slot}`,\n }\n }\n\n if (fieldAllow && fieldAllow.length > 0 && ctx.target.parentId === root.id) {\n return { ...ctx, slotAllow: intersectAllow(ctx.slotAllow, fieldAllow) }\n }\n return ctx\n}\n\n/**\n * Which affordance positions are actually insertable at `nodeId` (EC-405 scope 3):\n * a position is offered only when it structurally exists AND at least one registry\n * component may land there (the SAME slot-rule filter the tree/DnD and the blocks\n * panel apply — {@link hasInsertableComponent} over the resolved `slotAllow`), so a\n * blocked spot shows no dead button.\n *\n * An EMPTY, insertable container surfaces ONLY `into` — the meaningful action there\n * is \"add the first child\", and an empty container often renders zero-height, so\n * its own before/after edges are degenerate and would pile the three \"+\" buttons\n * onto one point. A filled container and every leaf get before/after instead (each\n * child already carries its own edge affordances, so a filled container needs no\n * center \"+\").\n */\nexport function insertablePositions(\n builder: { present: ComponentNode },\n nodeId: string,\n fieldAllow?: readonly string[],\n studio: StudioRegistry = getStudioRegistry(),\n): InsertPosition[] {\n // Empty insertable container → into only.\n const into = resolveInsertContextAt(builder, nodeId, 'into', fieldAllow, studio)\n if (into && (into.target.index ?? 0) === 0 && hasInsertableComponent(studio, into.slotAllow)) {\n return ['into']\n }\n // Otherwise the sibling edges that accept something.\n const out: InsertPosition[] = []\n for (const position of ['before', 'after'] as const) {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow, studio)\n if (ctx && hasInsertableComponent(studio, ctx.slotAllow)) out.push(position)\n }\n return out\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { ComponentNode } from '@elytracms/core/project-graph';
2
+ import type { ComponentManifest } from '@elytracms/core/component-registry';
3
+ /**
4
+ * Node display titles (EC-407) — the ONE helper behind both the page-tree row and
5
+ * the EC-404 selection-overlay frame label.
6
+ *
7
+ * A placed instance shows a derived **instance title** (its own text content, e.g. a
8
+ * heading's words) as the primary label, with the localized **component name** as
9
+ * muted secondary context; a node with no derivable instance title shows the
10
+ * component name alone. This replaces the old tree row's redundant `shortId` (mono
11
+ * id) + `title` pair — the raw id stays one hover away in the inspector header +
12
+ * the tree row `title=` attribute, so agent-inspectability is not lost.
13
+ *
14
+ * Placement: this lives in the studio (not `component-registry`) because the
15
+ * cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,
16
+ * which the DAG bars `component-registry` from importing. The pure derivation is
17
+ * mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +
18
+ * fallback), not shared with it.
19
+ */
20
+ /**
21
+ * Prop names, in priority order, whose value best titles an instance (EC-407). The
22
+ * name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first
23
+ * (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media
24
+ * captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).
25
+ */
26
+ export declare const INSTANCE_TITLE_AFFINITY: readonly ["title", "heading", "label", "text", "name", "value", "caption", "alt"];
27
+ /** Max characters of a derived instance title before it is truncated for a row/frame. */
28
+ export declare const INSTANCE_TITLE_MAX = 64;
29
+ export interface NodeDisplayTitle {
30
+ /** The main label a surface shows: the instance title when derivable, else the component name. */
31
+ primary: string;
32
+ /** The localized component name — shown as muted secondary context ONLY when an instance title distinguishes `primary`. */
33
+ secondary?: string;
34
+ /** The localized component TYPE name (always present). */
35
+ componentName: string;
36
+ /** The derived per-instance title (already truncated), when one could be derived. */
37
+ instanceTitle?: string;
38
+ }
39
+ /**
40
+ * Derive a placed instance's raw (untruncated) title from its effective prop values
41
+ * (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS
42
+ * `resolveDocumentTitle` precedence:
43
+ *
44
+ * 1. A declared `nodeTitle` **function** — composed from the values (throws are
45
+ * swallowed so a bad pattern never breaks the tree).
46
+ * 2. A declared `nodeTitle` **prop name** — that prop's display text.
47
+ * 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop
48
+ * that carries display text.
49
+ * 4. **Fallback** — the first declared text-ish prop (declaration order) with a
50
+ * non-URL value.
51
+ */
52
+ export declare function deriveInstanceTitle(manifest: ComponentManifest | undefined, node: ComponentNode): string | undefined;
53
+ /**
54
+ * The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay
55
+ * frame. `primary` is the derived instance title when one exists (truncated), else
56
+ * the localized component name; `secondary` is the localized component name, present
57
+ * only when an instance title distinguishes the row. `locale` is optional — omit it
58
+ * to resolve the component name against the active studio-UI locale.
59
+ */
60
+ export declare function nodeDisplayTitle(manifest: ComponentManifest | undefined, node: ComponentNode, locale?: string): NodeDisplayTitle;
@@ -0,0 +1,135 @@
1
+ import { effectivePropValues } from '@elytracms/core/component-registry';
2
+ import { richTextPlainText } from '@elytracms/core/rich-text';
3
+ import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
4
+ /**
5
+ * Node display titles (EC-407) — the ONE helper behind both the page-tree row and
6
+ * the EC-404 selection-overlay frame label.
7
+ *
8
+ * A placed instance shows a derived **instance title** (its own text content, e.g. a
9
+ * heading's words) as the primary label, with the localized **component name** as
10
+ * muted secondary context; a node with no derivable instance title shows the
11
+ * component name alone. This replaces the old tree row's redundant `shortId` (mono
12
+ * id) + `title` pair — the raw id stays one hover away in the inspector header +
13
+ * the tree row `title=` attribute, so agent-inspectability is not lost.
14
+ *
15
+ * Placement: this lives in the studio (not `component-registry`) because the
16
+ * cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,
17
+ * which the DAG bars `component-registry` from importing. The pure derivation is
18
+ * mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +
19
+ * fallback), not shared with it.
20
+ */
21
+ /**
22
+ * Prop names, in priority order, whose value best titles an instance (EC-407). The
23
+ * name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first
24
+ * (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media
25
+ * captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).
26
+ */
27
+ export const INSTANCE_TITLE_AFFINITY = [
28
+ 'title',
29
+ 'heading',
30
+ 'label',
31
+ 'text',
32
+ 'name',
33
+ 'value',
34
+ 'caption',
35
+ 'alt',
36
+ ];
37
+ /** Max characters of a derived instance title before it is truncated for a row/frame. */
38
+ export const INSTANCE_TITLE_MAX = 64;
39
+ /** A value that looks like a URL / path / protocol — never a useful title. */
40
+ const URL_LIKE = /^(https?:\/\/|\/|#|mailto:|tel:|data:)/i;
41
+ /**
42
+ * Non-empty display text a text-ish prop value carries, else `undefined`.
43
+ * - `text` props: the trimmed string (rejected when `rejectUrl` and it looks URL-ish).
44
+ * - `richText` props: the cheap first-run plain-text head (`richTextPlainText`).
45
+ * - everything else (number/boolean/date/select/relation/asset/blocks/object): not text-ish.
46
+ */
47
+ function propDisplayText(field, value, rejectUrl) {
48
+ if (field.type === 'text') {
49
+ if (typeof value !== 'string')
50
+ return undefined;
51
+ const trimmed = value.trim();
52
+ if (trimmed.length === 0)
53
+ return undefined;
54
+ if (rejectUrl && URL_LIKE.test(trimmed))
55
+ return undefined;
56
+ return trimmed;
57
+ }
58
+ if (field.type === 'richText') {
59
+ const head = richTextPlainText(value)?.trim();
60
+ return head && head.length > 0 ? head : undefined;
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Derive a placed instance's raw (untruncated) title from its effective prop values
66
+ * (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS
67
+ * `resolveDocumentTitle` precedence:
68
+ *
69
+ * 1. A declared `nodeTitle` **function** — composed from the values (throws are
70
+ * swallowed so a bad pattern never breaks the tree).
71
+ * 2. A declared `nodeTitle` **prop name** — that prop's display text.
72
+ * 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop
73
+ * that carries display text.
74
+ * 4. **Fallback** — the first declared text-ish prop (declaration order) with a
75
+ * non-URL value.
76
+ */
77
+ export function deriveInstanceTitle(manifest, node) {
78
+ if (!manifest)
79
+ return undefined;
80
+ const values = effectivePropValues(manifest, node);
81
+ const declared = manifest.nodeTitle;
82
+ if (typeof declared === 'function') {
83
+ try {
84
+ const out = declared(values);
85
+ if (typeof out === 'string' && out.trim())
86
+ return out.trim();
87
+ }
88
+ catch {
89
+ // A throwing title pattern must never break the editor — fall through.
90
+ }
91
+ }
92
+ else if (typeof declared === 'string') {
93
+ const field = manifest.props[declared];
94
+ const text = field ? propDisplayText(field, values[declared], false) : undefined;
95
+ if (text)
96
+ return text;
97
+ }
98
+ for (const name of INSTANCE_TITLE_AFFINITY) {
99
+ const field = manifest.props[name];
100
+ if (!field)
101
+ continue;
102
+ const text = propDisplayText(field, values[name], false);
103
+ if (text)
104
+ return text;
105
+ }
106
+ for (const [name, field] of Object.entries(manifest.props)) {
107
+ const text = propDisplayText(field, values[name], true);
108
+ if (text)
109
+ return text;
110
+ }
111
+ return undefined;
112
+ }
113
+ /** Truncate a title for a compact row / frame bar, appending an ellipsis. */
114
+ function truncate(value, max) {
115
+ return value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}…`;
116
+ }
117
+ /**
118
+ * The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay
119
+ * frame. `primary` is the derived instance title when one exists (truncated), else
120
+ * the localized component name; `secondary` is the localized component name, present
121
+ * only when an instance title distinguishes the row. `locale` is optional — omit it
122
+ * to resolve the component name against the active studio-UI locale.
123
+ */
124
+ export function nodeDisplayTitle(manifest, node, locale) {
125
+ const componentName = componentDisplayName(node.componentId, manifest, locale);
126
+ const raw = deriveInstanceTitle(manifest, node);
127
+ const instanceTitle = raw ? truncate(raw, INSTANCE_TITLE_MAX) : undefined;
128
+ return {
129
+ componentName,
130
+ instanceTitle,
131
+ primary: instanceTitle ?? componentName,
132
+ secondary: instanceTitle ? componentName : undefined,
133
+ };
134
+ }
135
+ //# sourceMappingURL=node-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-title.js","sourceRoot":"","sources":["../../../src/components/builder/node-title.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport { effectivePropValues } from '@elytracms/core/component-registry'\nimport { richTextPlainText } from '@elytracms/core/rich-text'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Node display titles (EC-407) — the ONE helper behind both the page-tree row and\n * the EC-404 selection-overlay frame label.\n *\n * A placed instance shows a derived **instance title** (its own text content, e.g. a\n * heading's words) as the primary label, with the localized **component name** as\n * muted secondary context; a node with no derivable instance title shows the\n * component name alone. This replaces the old tree row's redundant `shortId` (mono\n * id) + `title` pair — the raw id stays one hover away in the inspector header +\n * the tree row `title=` attribute, so agent-inspectability is not lost.\n *\n * Placement: this lives in the studio (not `component-registry`) because the\n * cheap rich-text head reuses `@elytracms/core/rich-text` `richTextPlainText`,\n * which the DAG bars `component-registry` from importing. The pure derivation is\n * mirrored from the CMS `resolveDocumentTitle` pattern (declared field/function +\n * fallback), not shared with it.\n */\n\n/**\n * Prop names, in priority order, whose value best titles an instance (EC-407). The\n * name-affinity phase of {@link deriveInstanceTitle}: identity-ish props first\n * (`title`/`heading`/`label`), then generic text (`text`/`name`/`value`), then media\n * captions (`caption`/`alt`, so an Image titles by its alt text, not its URL `src`).\n */\nexport const INSTANCE_TITLE_AFFINITY = [\n 'title',\n 'heading',\n 'label',\n 'text',\n 'name',\n 'value',\n 'caption',\n 'alt',\n] as const\n\n/** Max characters of a derived instance title before it is truncated for a row/frame. */\nexport const INSTANCE_TITLE_MAX = 64\n\n/** A value that looks like a URL / path / protocol — never a useful title. */\nconst URL_LIKE = /^(https?:\\/\\/|\\/|#|mailto:|tel:|data:)/i\n\nexport interface NodeDisplayTitle {\n /** The main label a surface shows: the instance title when derivable, else the component name. */\n primary: string\n /** The localized component name — shown as muted secondary context ONLY when an instance title distinguishes `primary`. */\n secondary?: string\n /** The localized component TYPE name (always present). */\n componentName: string\n /** The derived per-instance title (already truncated), when one could be derived. */\n instanceTitle?: string\n}\n\n/**\n * Non-empty display text a text-ish prop value carries, else `undefined`.\n * - `text` props: the trimmed string (rejected when `rejectUrl` and it looks URL-ish).\n * - `richText` props: the cheap first-run plain-text head (`richTextPlainText`).\n * - everything else (number/boolean/date/select/relation/asset/blocks/object): not text-ish.\n */\nfunction propDisplayText(field: PropField, value: unknown, rejectUrl: boolean): string | undefined {\n if (field.type === 'text') {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (trimmed.length === 0) return undefined\n if (rejectUrl && URL_LIKE.test(trimmed)) return undefined\n return trimmed\n }\n if (field.type === 'richText') {\n const head = richTextPlainText(value)?.trim()\n return head && head.length > 0 ? head : undefined\n }\n return undefined\n}\n\n/**\n * Derive a placed instance's raw (untruncated) title from its effective prop values\n * (EC-407), or `undefined` when nothing text-ish is set. Pure; mirrors the CMS\n * `resolveDocumentTitle` precedence:\n *\n * 1. A declared `nodeTitle` **function** — composed from the values (throws are\n * swallowed so a bad pattern never breaks the tree).\n * 2. A declared `nodeTitle` **prop name** — that prop's display text.\n * 3. **Name-affinity heuristic** — the first {@link INSTANCE_TITLE_AFFINITY} prop\n * that carries display text.\n * 4. **Fallback** — the first declared text-ish prop (declaration order) with a\n * non-URL value.\n */\nexport function deriveInstanceTitle(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): string | undefined {\n if (!manifest) return undefined\n const values = effectivePropValues(manifest, node)\n\n const declared = manifest.nodeTitle\n if (typeof declared === 'function') {\n try {\n const out = declared(values)\n if (typeof out === 'string' && out.trim()) return out.trim()\n } catch {\n // A throwing title pattern must never break the editor — fall through.\n }\n } else if (typeof declared === 'string') {\n const field = manifest.props[declared]\n const text = field ? propDisplayText(field, values[declared], false) : undefined\n if (text) return text\n }\n\n for (const name of INSTANCE_TITLE_AFFINITY) {\n const field = manifest.props[name]\n if (!field) continue\n const text = propDisplayText(field, values[name], false)\n if (text) return text\n }\n\n for (const [name, field] of Object.entries(manifest.props)) {\n const text = propDisplayText(field, values[name], true)\n if (text) return text\n }\n\n return undefined\n}\n\n/** Truncate a title for a compact row / frame bar, appending an ellipsis. */\nfunction truncate(value: string, max: number): string {\n return value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}…`\n}\n\n/**\n * The unified node label (EC-407) feeding the page-tree row AND the EC-404 overlay\n * frame. `primary` is the derived instance title when one exists (truncated), else\n * the localized component name; `secondary` is the localized component name, present\n * only when an instance title distinguishes the row. `locale` is optional — omit it\n * to resolve the component name against the active studio-UI locale.\n */\nexport function nodeDisplayTitle(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n locale?: string,\n): NodeDisplayTitle {\n const componentName = componentDisplayName(node.componentId, manifest, locale)\n const raw = deriveInstanceTitle(manifest, node)\n const instanceTitle = raw ? truncate(raw, INSTANCE_TITLE_MAX) : undefined\n return {\n componentName,\n instanceTitle,\n primary: instanceTitle ?? componentName,\n secondary: instanceTitle ? componentName : undefined,\n }\n}\n"]}
@@ -2,20 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Boxes, ChevronUp, ChevronDown, ClipboardCopy, ClipboardPaste, ClipboardX, Copy, EyeOff, Filter, MoreHorizontal, Plus, Trash2, Unlink, } from 'lucide-react';
3
3
  import { useEffect, useRef, useState } from 'react';
4
4
  import { getParentInfo } from '@elytracms/core/project-graph';
5
- import { canNestInto, containerFlowAxis, dropIndicatorFor, dropPositionForPointer, resolveDrop, } from '@elytracms/studio/lib/dnd/drop-rules';
5
+ import { canNestInto, containerFlowAxis, dropIndicatorFor, dropPositionForPointer, manifestRules, resolveDrop, } from '@elytracms/studio/lib/dnd/drop-rules';
6
6
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
7
7
  import { shortId } from '@elytracms/studio/lib/persona/labels';
8
- import { translate } from '@elytracms/studio/lib/i18n';
8
+ import { translate, useUiLocale } from '@elytracms/studio/lib/i18n';
9
9
  import { useData } from '@elytracms/studio/lib/data';
10
10
  import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Text, } from '@elytracms/studio/ui';
11
11
  import { cn } from '@elytracms/studio/lib/utils';
12
12
  import { useCollab } from '../collab/collab-context.js';
13
13
  import { useBuilder } from './builder-store.js';
14
14
  import { attachClipboardListeners, useStudioClipboard } from './clipboard-actions.js';
15
+ import { attachDeleteKeyListener } from './delete-key.js';
16
+ import { nodeDisplayTitle } from './node-title.js';
15
17
  import { useReuse } from './reuse-context.js';
16
18
  import { activeDrag, beginDrag, endDrag, onDragSessionEnd } from './drag-session.js';
17
19
  function flatten(node, depth, rows, meta) {
18
- rows.push({ id: node.id, componentId: node.componentId, depth, ...meta, condition: node.condition });
20
+ rows.push({ id: node.id, componentId: node.componentId, node, depth, ...meta, condition: node.condition });
19
21
  for (const children of Object.values(node.slots ?? {})) {
20
22
  children.forEach((child, i) => flatten(child, depth + 1, rows, {
21
23
  isFirst: i === 0,
@@ -40,6 +42,7 @@ export function PageTree({ onAddChild }) {
40
42
  const studio = getStudioRegistry();
41
43
  const data = useData();
42
44
  const collab = useCollab();
45
+ const uiLocale = useUiLocale();
43
46
  const rows = [];
44
47
  flatten(builder.present, 0, rows, { isFirst: true, isLast: true, isRoot: true });
45
48
  // Copy/cut/paste (EC-175): keyboard shortcuts for the whole studio window
@@ -59,6 +62,20 @@ export function PageTree({ onAddChild }) {
59
62
  getSelection: () => window.getSelection(),
60
63
  });
61
64
  }, [clipboard, readOnly]);
65
+ // Delete/Backspace removes the selected node from the tree side too (EC-404),
66
+ // beside the clipboard shortcuts. Inert in the read-only published preview and
67
+ // while focus is in a form field / inline editor (isEditingContext).
68
+ const { dispatch } = builder;
69
+ const rootId = builder.present.id;
70
+ useEffect(() => {
71
+ if (readOnly)
72
+ return;
73
+ return attachDeleteKeyListener(window, {
74
+ getSelectedId: () => selectedIdRef.current,
75
+ remove: (id) => dispatch({ type: 'remove', id }),
76
+ getRootId: () => rootId,
77
+ });
78
+ }, [readOnly, rootId, dispatch]);
62
79
  // Drag & drop (EC-160): eligibility comes from the same pure rule set the
63
80
  // canvas uses (`lib/dnd/drop-rules`); the tree only renders the resolution.
64
81
  // One eligible drop dispatches exactly one move/insert action.
@@ -71,7 +88,9 @@ export function PageTree({ onAddChild }) {
71
88
  setDrop(null);
72
89
  };
73
90
  useEffect(() => onDragSessionEnd(() => clearDrop()), []);
74
- const getManifest = (id) => studio.registry.getManifest(id);
91
+ // EC-386: adapt through `manifestRules` so blocks-prop regions (e.g. a
92
+ // Section's `body`) are drop targets with their `allow` leash, like raw slots.
93
+ const getManifest = (id) => manifestRules(studio.registry.getManifest(id));
75
94
  function handleRowDragStart(row, e) {
76
95
  if (row.isRoot) {
77
96
  e.preventDefault();
@@ -165,6 +184,7 @@ export function PageTree({ onAddChild }) {
165
184
  clearDrop();
166
185
  }, children: rows.map((row) => {
167
186
  const manifest = studio.registry.getManifest(row.componentId);
187
+ const title = nodeDisplayTitle(manifest, row.node, uiLocale);
168
188
  const selected = row.id === builder.selectedId;
169
189
  const hovered = row.id === builder.hoveredId;
170
190
  // EC-088: indicate condition-gated nodes; flag the currently-hidden ones.
@@ -179,7 +199,7 @@ export function PageTree({ onAddChild }) {
179
199
  ? 'bg-muted text-foreground'
180
200
  : 'text-muted-foreground', drop?.rowId === row.id && drop.indicator
181
201
  ? ROW_DROP_CLASSES[drop.indicator]
182
- : undefined), children: [_jsxs("button", { type: "button", onClick: () => builder.dispatch({ type: 'select', id: row.id }), className: "flex min-w-0 flex-1 items-center gap-1.5 py-1", style: { paddingLeft: `${6 + row.depth * 14}px` }, children: [_jsx("span", { className: cn('truncate font-mono', condState === 'hidden' && 'line-through opacity-70'), children: shortId(row.componentId) }), manifest?.title ? (_jsx("span", { className: "truncate text-muted-foreground/70", children: manifest.title })) : null, condState === 'hidden' ? (_jsx(EyeOff, { className: "size-3 shrink-0 text-warning", "aria-label": translate('builder.pageTree.hiddenByCondition') })) : condState === 'visible' ? (_jsx(Filter, { className: "size-3 shrink-0 text-muted-foreground/60", "aria-label": translate('builder.pageTree.conditional') })) : null] }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-0.5', selected
202
+ : undefined), children: [_jsxs("button", { type: "button", onClick: () => builder.dispatch({ type: 'select', id: row.id }), title: row.componentId, className: "flex min-w-0 flex-1 items-center gap-1.5 py-1", style: { paddingLeft: `${6 + row.depth * 14}px` }, children: [_jsx("span", { className: cn('truncate', condState === 'hidden' && 'line-through opacity-70'), children: title.primary }), title.secondary ? (_jsx("span", { className: "truncate text-muted-foreground/70", children: title.secondary })) : null, condState === 'hidden' ? (_jsx(EyeOff, { className: "size-3 shrink-0 text-warning", "aria-label": translate('builder.pageTree.hiddenByCondition') })) : condState === 'visible' ? (_jsx(Filter, { className: "size-3 shrink-0 text-muted-foreground/60", "aria-label": translate('builder.pageTree.conditional') })) : null] }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-0.5', selected
183
203
  ? 'opacity-100'
184
204
  : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100'), children: [_jsx(IconAction, { label: translate('builder.pageTree.addChild'), onClick: () => onAddChild(row.id), children: _jsx(Plus, {}) }), _jsx(RowActionsMenu, { row: row, builder: builder, clipboard: clipboard })] })] }, row.id));
185
205
  }) })] }));