@elytracms/studio 0.0.88 → 0.0.91

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 (246) hide show
  1. package/convex/asset-revalidate.test.ts +49 -43
  2. package/convex/cms.ts +3 -4
  3. package/convex/delivery.ts +8 -11
  4. package/convex/document-revalidate.test.ts +188 -0
  5. package/convex/redirect-revalidate.test.ts +50 -37
  6. package/convex/revalidate.ts +18 -30
  7. package/convex/triggers.ts +47 -38
  8. package/convex/webhooks.ts +77 -62
  9. package/dist/components/account/self-account-view-source.js +2 -1
  10. package/dist/components/account/self-account-view-source.js.map +1 -1
  11. package/dist/components/ai/ai-chat-panel.js +1 -1
  12. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  13. package/dist/components/app-views/default-modules.js +3 -3
  14. package/dist/components/app-views/default-modules.js.map +1 -1
  15. package/dist/components/assets/asset-grid.js +1 -1
  16. package/dist/components/assets/asset-grid.js.map +1 -1
  17. package/dist/components/assets/asset-selection.d.ts +8 -9
  18. package/dist/components/assets/asset-selection.js +26 -16
  19. package/dist/components/assets/asset-selection.js.map +1 -1
  20. package/dist/components/assets/assets-collection-view-source.js +3 -2
  21. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  22. package/dist/components/builder/blocks-model.d.ts +7 -5
  23. package/dist/components/builder/blocks-model.js +11 -9
  24. package/dist/components/builder/blocks-model.js.map +1 -1
  25. package/dist/components/builder/blocks-panel.js +1 -1
  26. package/dist/components/builder/blocks-panel.js.map +1 -1
  27. package/dist/components/builder/builder-store.d.ts +11 -3
  28. package/dist/components/builder/builder-store.js +12 -5
  29. package/dist/components/builder/builder-store.js.map +1 -1
  30. package/dist/components/builder/canvas-dnd.d.ts +7 -1
  31. package/dist/components/builder/canvas-dnd.js +6 -3
  32. package/dist/components/builder/canvas-dnd.js.map +1 -1
  33. package/dist/components/builder/canvas-render-context.d.ts +13 -1
  34. package/dist/components/builder/canvas-render-context.js +28 -18
  35. package/dist/components/builder/canvas-render-context.js.map +1 -1
  36. package/dist/components/builder/canvas.js +1 -1
  37. package/dist/components/builder/canvas.js.map +1 -1
  38. package/dist/components/builder/clipboard-actions.d.ts +0 -6
  39. package/dist/components/builder/clipboard-actions.js +9 -4
  40. package/dist/components/builder/clipboard-actions.js.map +1 -1
  41. package/dist/components/builder/composition-field-editor.d.ts +22 -0
  42. package/dist/components/builder/composition-field-editor.js +47 -8
  43. package/dist/components/builder/composition-field-editor.js.map +1 -1
  44. package/dist/components/builder/inline-edit.d.ts +7 -4
  45. package/dist/components/builder/inline-edit.js +11 -8
  46. package/dist/components/builder/inline-edit.js.map +1 -1
  47. package/dist/components/builder/insert-context.js +9 -5
  48. package/dist/components/builder/insert-context.js.map +1 -1
  49. package/dist/components/builder/layout-inspector.d.ts +2 -2
  50. package/dist/components/builder/layout-inspector.js +8 -6
  51. package/dist/components/builder/layout-inspector.js.map +1 -1
  52. package/dist/components/builder/node-factory.d.ts +31 -14
  53. package/dist/components/builder/node-factory.js +73 -93
  54. package/dist/components/builder/node-factory.js.map +1 -1
  55. package/dist/components/builder/page-tree.d.ts +3 -1
  56. package/dist/components/builder/page-tree.js +6 -4
  57. package/dist/components/builder/page-tree.js.map +1 -1
  58. package/dist/components/builder/property-inspector.js +2 -1
  59. package/dist/components/builder/property-inspector.js.map +1 -1
  60. package/dist/components/builder/reuse-context.js +2 -2
  61. package/dist/components/builder/reuse-context.js.map +1 -1
  62. package/dist/components/builder/reuse.d.ts +7 -3
  63. package/dist/components/builder/reuse.js +19 -11
  64. package/dist/components/builder/reuse.js.map +1 -1
  65. package/dist/components/builder/selection-overlay.d.ts +2 -2
  66. package/dist/components/builder/selection-overlay.js +3 -3
  67. package/dist/components/builder/selection-overlay.js.map +1 -1
  68. package/dist/components/builder/spacing-handles.d.ts +2 -2
  69. package/dist/components/builder/spacing-handles.js +3 -3
  70. package/dist/components/builder/spacing-handles.js.map +1 -1
  71. package/dist/components/cms/asset-field.d.ts +1 -1
  72. package/dist/components/cms/asset-field.js +15 -13
  73. package/dist/components/cms/asset-field.js.map +1 -1
  74. package/dist/components/cms/asset-picker-core.js +3 -4
  75. package/dist/components/cms/asset-picker-core.js.map +1 -1
  76. package/dist/components/cms/cell-edit.d.ts +0 -6
  77. package/dist/components/cms/cell-edit.js +5 -23
  78. package/dist/components/cms/cell-edit.js.map +1 -1
  79. package/dist/components/cms/cms-store.d.ts +3 -2
  80. package/dist/components/cms/cms-store.js +23 -24
  81. package/dist/components/cms/cms-store.js.map +1 -1
  82. package/dist/components/cms/collection-table.js +14 -4
  83. package/dist/components/cms/collection-table.js.map +1 -1
  84. package/dist/components/cms/detail-editor.js +28 -24
  85. package/dist/components/cms/detail-editor.js.map +1 -1
  86. package/dist/components/cms/discard-draft.d.ts +1 -2
  87. package/dist/components/cms/discard-draft.js +1 -1
  88. package/dist/components/cms/discard-draft.js.map +1 -1
  89. package/dist/components/cms/duplicate-document.d.ts +3 -2
  90. package/dist/components/cms/duplicate-document.js +5 -5
  91. package/dist/components/cms/duplicate-document.js.map +1 -1
  92. package/dist/components/cms/editable-cell.js +11 -7
  93. package/dist/components/cms/editable-cell.js.map +1 -1
  94. package/dist/components/cms/field-control-lib.d.ts +10 -0
  95. package/dist/components/cms/field-control-lib.js +20 -0
  96. package/dist/components/cms/field-control-lib.js.map +1 -1
  97. package/dist/components/cms/field-control.js +10 -9
  98. package/dist/components/cms/field-control.js.map +1 -1
  99. package/dist/components/cms/object-array-field.js +3 -5
  100. package/dist/components/cms/object-array-field.js.map +1 -1
  101. package/dist/components/cms/prop-fields.d.ts +16 -3
  102. package/dist/components/cms/prop-fields.js +57 -12
  103. package/dist/components/cms/prop-fields.js.map +1 -1
  104. package/dist/components/cms/publish-clean.d.ts +13 -6
  105. package/dist/components/cms/publish-clean.js +14 -6
  106. package/dist/components/cms/publish-clean.js.map +1 -1
  107. package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
  108. package/dist/components/cms/rich-text/embed-props.js +7 -12
  109. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  110. package/dist/components/cms/rich-text/insert.d.ts +13 -6
  111. package/dist/components/cms/rich-text/insert.js +16 -17
  112. package/dist/components/cms/rich-text/insert.js.map +1 -1
  113. package/dist/components/cms/use-cell-edit.js +7 -12
  114. package/dist/components/cms/use-cell-edit.js.map +1 -1
  115. package/dist/components/cms/use-discard-controller.d.ts +1 -2
  116. package/dist/components/cms/use-discard-controller.js +1 -2
  117. package/dist/components/cms/use-discard-controller.js.map +1 -1
  118. package/dist/components/cms/use-publish-controller.js +3 -2
  119. package/dist/components/cms/use-publish-controller.js.map +1 -1
  120. package/dist/components/cms/versions-pane.js +2 -2
  121. package/dist/components/cms/versions-pane.js.map +1 -1
  122. package/dist/components/components/component-detail.d.ts +1 -1
  123. package/dist/components/components/component-detail.js +2 -4
  124. package/dist/components/components/component-detail.js.map +1 -1
  125. package/dist/components/components/component-preview.js +9 -4
  126. package/dist/components/components/component-preview.js.map +1 -1
  127. package/dist/components/components/components-index.d.ts +2 -2
  128. package/dist/components/components/components-index.js +3 -6
  129. package/dist/components/components/components-index.js.map +1 -1
  130. package/dist/components/components/field-inspector.js +5 -2
  131. package/dist/components/components/field-inspector.js.map +1 -1
  132. package/dist/components/components/registry-browser.d.ts +6 -5
  133. package/dist/components/components/registry-browser.js +11 -37
  134. package/dist/components/components/registry-browser.js.map +1 -1
  135. package/dist/components/components/taxonomy.d.ts +14 -21
  136. package/dist/components/components/taxonomy.js +5 -29
  137. package/dist/components/components/taxonomy.js.map +1 -1
  138. package/dist/components/components/transfer-dialog.js +1 -0
  139. package/dist/components/components/transfer-dialog.js.map +1 -1
  140. package/dist/components/components/transfer.d.ts +20 -12
  141. package/dist/components/components/transfer.js +27 -21
  142. package/dist/components/components/transfer.js.map +1 -1
  143. package/dist/components/delivery-health-banner.d.ts +3 -2
  144. package/dist/components/delivery-health-banner.js +3 -2
  145. package/dist/components/delivery-health-banner.js.map +1 -1
  146. package/dist/components/redirects/redirects-collection-view-source.js +3 -2
  147. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  148. package/dist/components/users/users-collection-view-source.js +2 -1
  149. package/dist/components/users/users-collection-view-source.js.map +1 -1
  150. package/dist/lib/ai/apply-content.js +32 -28
  151. package/dist/lib/ai/apply-content.js.map +1 -1
  152. package/dist/lib/ai/constraints.d.ts +16 -14
  153. package/dist/lib/ai/constraints.js +64 -48
  154. package/dist/lib/ai/constraints.js.map +1 -1
  155. package/dist/lib/ai/translate-document.js +11 -7
  156. package/dist/lib/ai/translate-document.js.map +1 -1
  157. package/dist/lib/ai/types.d.ts +9 -5
  158. package/dist/lib/ai/types.js.map +1 -1
  159. package/dist/lib/ai/update-fields.js +15 -32
  160. package/dist/lib/ai/update-fields.js.map +1 -1
  161. package/dist/lib/ai/vite.d.ts +22 -2
  162. package/dist/lib/ai/vite.js +12 -3
  163. package/dist/lib/ai/vite.js.map +1 -1
  164. package/dist/lib/assets/asset-placements.d.ts +3 -10
  165. package/dist/lib/assets/asset-placements.js +17 -8
  166. package/dist/lib/assets/asset-placements.js.map +1 -1
  167. package/dist/lib/clipboard/paste-target.js +3 -3
  168. package/dist/lib/clipboard/paste-target.js.map +1 -1
  169. package/dist/lib/cms/published-documents.d.ts +14 -3
  170. package/dist/lib/cms/published-documents.js +12 -3
  171. package/dist/lib/cms/published-documents.js.map +1 -1
  172. package/dist/lib/cms-views/view-model.d.ts +4 -4
  173. package/dist/lib/cms-views/view-model.js +26 -20
  174. package/dist/lib/cms-views/view-model.js.map +1 -1
  175. package/dist/lib/data/collection-content.d.ts +10 -7
  176. package/dist/lib/data/collection-content.js +70 -29
  177. package/dist/lib/data/collection-content.js.map +1 -1
  178. package/dist/lib/delivery/instant-publish.d.ts +0 -3
  179. package/dist/lib/delivery/instant-publish.js +0 -1
  180. package/dist/lib/delivery/instant-publish.js.map +1 -1
  181. package/dist/lib/dnd/drop-rules.d.ts +22 -7
  182. package/dist/lib/dnd/drop-rules.js +21 -14
  183. package/dist/lib/dnd/drop-rules.js.map +1 -1
  184. package/dist/lib/engine-version.d.ts +1 -1
  185. package/dist/lib/engine-version.js +1 -1
  186. package/dist/lib/engine-version.js.map +1 -1
  187. package/dist/lib/i18n/messages.de.js +25 -9
  188. package/dist/lib/i18n/messages.de.js.map +1 -1
  189. package/dist/lib/i18n/messages.en.d.ts +21 -7
  190. package/dist/lib/i18n/messages.en.js +28 -9
  191. package/dist/lib/i18n/messages.en.js.map +1 -1
  192. package/dist/lib/locale/fallback.d.ts +5 -19
  193. package/dist/lib/locale/fallback.js +18 -39
  194. package/dist/lib/locale/fallback.js.map +1 -1
  195. package/dist/lib/locale/locale-store.js +2 -1
  196. package/dist/lib/locale/locale-store.js.map +1 -1
  197. package/dist/lib/operations/cms-persistence.js +7 -4
  198. package/dist/lib/operations/cms-persistence.js.map +1 -1
  199. package/dist/lib/operations/studio-operations.js +4 -6
  200. package/dist/lib/operations/studio-operations.js.map +1 -1
  201. package/dist/lib/persistence/backend-mode.d.ts +5 -25
  202. package/dist/lib/persistence/backend-mode.js +13 -30
  203. package/dist/lib/persistence/backend-mode.js.map +1 -1
  204. package/dist/lib/persona/labels.d.ts +4 -9
  205. package/dist/lib/persona/labels.js +5 -12
  206. package/dist/lib/persona/labels.js.map +1 -1
  207. package/dist/lib/references/reference-model.d.ts +17 -5
  208. package/dist/lib/references/reference-model.js +20 -28
  209. package/dist/lib/references/reference-model.js.map +1 -1
  210. package/dist/lib/references/references-store.js +7 -0
  211. package/dist/lib/references/references-store.js.map +1 -1
  212. package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
  213. package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
  214. package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
  215. package/dist/lib/registry/index.d.ts +9 -13
  216. package/dist/lib/registry/index.js +34 -61
  217. package/dist/lib/registry/index.js.map +1 -1
  218. package/dist/lib/registry/preview.d.ts +22 -7
  219. package/dist/lib/registry/preview.js +26 -89
  220. package/dist/lib/registry/preview.js.map +1 -1
  221. package/dist/lib/workspace/index.d.ts +0 -7
  222. package/dist/lib/workspace/index.js +0 -9
  223. package/dist/lib/workspace/index.js.map +1 -1
  224. package/dist/previews/enumerate.d.ts +23 -27
  225. package/dist/previews/enumerate.js +27 -48
  226. package/dist/previews/enumerate.js.map +1 -1
  227. package/dist/previews/harness.js +1 -1
  228. package/dist/previews/harness.js.map +1 -1
  229. package/package.json +4 -7
  230. package/dist/lib/registry/project-components.d.ts +0 -16
  231. package/dist/lib/registry/project-components.js.map +0 -1
  232. package/dist/lib/webhooks/config.d.ts +0 -15
  233. package/dist/lib/webhooks/config.js +0 -65
  234. package/dist/lib/webhooks/config.js.map +0 -1
  235. package/dist/lib/webhooks/dispatcher.d.ts +0 -70
  236. package/dist/lib/webhooks/dispatcher.js +0 -61
  237. package/dist/lib/webhooks/dispatcher.js.map +0 -1
  238. package/dist/lib/webhooks/index.d.ts +0 -11
  239. package/dist/lib/webhooks/index.js +0 -10
  240. package/dist/lib/webhooks/index.js.map +0 -1
  241. package/dist/lib/webhooks/signing.d.ts +0 -15
  242. package/dist/lib/webhooks/signing.js +0 -24
  243. package/dist/lib/webhooks/signing.js.map +0 -1
  244. package/dist/lib/webhooks/tags.d.ts +0 -25
  245. package/dist/lib/webhooks/tags.js +0 -289
  246. package/dist/lib/webhooks/tags.js.map +0 -1
@@ -70,12 +70,6 @@ export interface StudioClipboard {
70
70
  pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>;
71
71
  }
72
72
  export declare function useStudioClipboard(): StudioClipboard;
73
- /**
74
- * The visible reason a refused paste shows (EC-545) — pure, so the notice
75
- * wording per refusal is pinned without mounting the hook. EC-554: a field-leash
76
- * refusal names the FIELD (`fieldLabel`), never the invisible synthetic root's
77
- * slot — the rejection carries no destination label to misuse.
78
- */
79
73
  export declare function pasteRejectionMessage(rejection: PasteRejection, fieldLabel: string): string;
80
74
  export interface ClipboardListenerOptions {
81
75
  clipboard: StudioClipboard;
@@ -5,7 +5,7 @@ import { cloneWithNewIds } from '@elytracms/core/project-graph';
5
5
  import { buildFragmentFromComposition } from '@elytracms/core/clipboard';
6
6
  import { readFragmentFromClipboard, readFragmentFromDataTransfer, resolvePasteTarget, diffPastedSubtree, pasteSummaryMessage, writeFragmentToClipboard, writeFragmentToDataTransfer, } from '@elytracms/studio/lib/clipboard';
7
7
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
8
- import { clipboardErrorMessage, shortId } from '@elytracms/studio/lib/persona/labels';
8
+ import { clipboardErrorMessage, componentDisplayName } from '@elytracms/studio/lib/persona/labels';
9
9
  import { translate } from '@elytracms/studio/lib/i18n';
10
10
  import { useProjectSession } from '../project-session.js';
11
11
  import { Badge, Row, Text } from '@elytracms/studio/ui';
@@ -63,7 +63,8 @@ export function useStudioClipboard() {
63
63
  publishClipboardNotice({ kind: 'error', message: clipboardErrorMessage(built.error) });
64
64
  return null;
65
65
  }
66
- return { fragment: built.fragment, label: shortId(built.fragment.payload.root.componentId) };
66
+ const rootId = built.fragment.payload.root.componentId;
67
+ return { fragment: built.fragment, label: componentDisplayName(rootId, getStudioRegistry().registry.getManifest(rootId)) };
67
68
  };
68
69
  const write = (fragment, label, kind, dt, asyncClipboard) => {
69
70
  if (dt) {
@@ -161,15 +162,19 @@ export function useStudioClipboard() {
161
162
  * refusal names the FIELD (`fieldLabel`), never the invisible synthetic root's
162
163
  * slot — the rejection carries no destination label to misuse.
163
164
  */
165
+ /** A component's label in a notice — its manifest title (EC-591: never a stripped id prefix). */
166
+ function componentLabel(componentId) {
167
+ return componentDisplayName(componentId, getStudioRegistry().registry.getManifest(componentId));
168
+ }
164
169
  export function pasteRejectionMessage(rejection, fieldLabel) {
165
170
  if (rejection.code === 'field-not-allowed') {
166
171
  return translate('builder.clipboard.notAllowedByField', {
167
- component: shortId(rejection.componentId),
172
+ component: componentLabel(rejection.componentId),
168
173
  field: fieldLabel,
169
174
  });
170
175
  }
171
176
  return translate('builder.clipboard.notAllowed', {
172
- component: shortId(rejection.componentId),
177
+ component: componentLabel(rejection.componentId),
173
178
  destination: rejection.destinationLabel,
174
179
  });
175
180
  }
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard-actions.js","sourceRoot":"","sources":["../../../src/components/builder/clipboard-actions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CircleCheck, Crosshair, X } from 'lucide-react'\nimport { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { buildFragmentFromComposition } from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\nimport {\n readFragmentFromClipboard,\n readFragmentFromDataTransfer,\n resolvePasteTarget,\n diffPastedSubtree,\n pasteSummaryMessage,\n writeFragmentToClipboard,\n writeFragmentToDataTransfer,\n} from '@elytracms/studio/lib/clipboard'\nimport type { AsyncClipboard, PasteIssue, PasteRejection } from '@elytracms/studio/lib/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { clipboardErrorMessage, shortId } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useProjectSession } from '../project-session'\nimport { Badge, Row, Text } from '@elytracms/studio/ui'\nimport { makeNodeIdCounter, useBuilder } from './builder-store'\n\n/**\n * Copy/cut/paste everywhere (EC-175, vision 3.3/3.7).\n *\n * One hook ({@link useStudioClipboard}) backs every affordance — canvas\n * keyboard shortcuts (wired inside the iframe by the canvas), page-tree\n * shortcuts and row buttons, and the blocks panel's \"copy exact\n * configuration\" — so they all produce/consume the same EC-174 fragment via\n * the OS clipboard (`lib/clipboard/os-clipboard`), never an app-internal\n * buffer. That is what makes paste work across documents, pages, projects,\n * and browser tabs.\n *\n * Dispatch contract: a paste is exactly ONE `insert` action (the fragment\n * root is a single subtree) → one history entry → one mirrored\n * `graph.insertNode` command — one undo step removes everything the paste\n * inserted. A cut is copy + exactly one `remove`; one undo restores it.\n *\n * Paste is PERMISSIVE: missing components and unresolvable bindings insert\n * anyway (the renderer's fallbacks keep them visible) and surface in a\n * notice — \"Pasted — 2 bindings unresolved, 1 component missing\" — whose\n * issues deep-link to the inserted nodes. Only structurally invalid clipboard\n * content is reported as a non-blocking error, never a crash or silent no-op.\n *\n * The one thing paste REFUSES is the leash: a pasted root the resolved slot's\n * `allow` excludes (EC-545), or — landing as a direct child of the composition\n * root — the composition field's top-level `allow` excludes (EC-554), is\n * rejected with a visible notice and no dispatch — the same verdict\n * drag-and-drop gives. Permissiveness is about what the target project can\n * *resolve*; the leash is what the component and field authors *declared*.\n */\n\n// ---------------------------------------------------------------------------\n// Notice store — module-level pub/sub (the drag-session pattern) so any\n// surface can publish and the canvas overlay renders it.\n// ---------------------------------------------------------------------------\n\nexport type ClipboardNotice =\n | { kind: 'copied'; label: string }\n | { kind: 'cut'; label: string }\n | { kind: 'pasted'; summary: string; destinationLabel: string; issues: PasteIssue[] }\n | { kind: 'error'; message: string }\n\nlet currentNotice: ClipboardNotice | null = null\nconst noticeListeners = new Set<() => void>()\n\nexport function publishClipboardNotice(notice: ClipboardNotice | null): void {\n currentNotice = notice\n for (const listener of [...noticeListeners]) listener()\n}\n\nfunction subscribeNotice(listener: () => void): () => void {\n noticeListeners.add(listener)\n return () => noticeListeners.delete(listener)\n}\n\nfunction getNotice(): ClipboardNotice | null {\n return currentNotice\n}\n\nexport function useClipboardNotice(): ClipboardNotice | null {\n return useSyncExternalStore(subscribeNotice, getNotice, getNotice)\n}\n\n// ---------------------------------------------------------------------------\n// Shortcut scoping\n// ---------------------------------------------------------------------------\n\n/**\n * Whether a clipboard shortcut/event must stay inert for this target: form\n * fields and any contenteditable (which includes the EC-158/EC-159 inline\n * editors) own their own native clipboard behaviour.\n */\nexport function clipboardEventBlocked(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (typeof el.closest === 'function' && el.closest('[contenteditable]')) return true\n return el.isContentEditable === true\n}\n\n// ---------------------------------------------------------------------------\n// The clipboard surface\n// ---------------------------------------------------------------------------\n\nexport interface StudioClipboard {\n /**\n * Copy a canvas/tree node's subtree. Writes synchronously when a\n * ClipboardEvent's DataTransfer is given, else via the async Clipboard API\n * (`asyncClipboard` lets the canvas pass the iframe's own navigator —\n * Chromium scopes clipboard access to the *focused* document).\n */\n copyNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy, then remove the source in exactly one dispatch (one undo restores). */\n cutNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy a prebuilt fragment (blocks-panel \"copy exact configuration\"). */\n copyFragment(fragment: ClipboardFragment, label: string): void\n /** Paste from a paste event's DataTransfer (no permissions needed). */\n pasteFromDataTransfer(dt: DataTransfer, anchorId?: string | null): void\n /** Paste via the async Clipboard API (explicit affordances + shortcuts). */\n pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>\n}\n\nexport function useStudioClipboard(): StudioClipboard {\n const builder = useBuilder()\n const { projectId } = useProjectSession()\n const studio = getStudioRegistry()\n\n // Event listeners outlive renders; refs keep them on the current state.\n const stateRef = useRef({ builder, projectId })\n stateRef.current = { builder, projectId }\n\n return useMemo<StudioClipboard>(() => {\n const fragmentFor = (nodeId: string): { fragment: ClipboardFragment; label: string } | null => {\n const { builder: b, projectId } = stateRef.current\n const built = buildFragmentFromComposition(b.present, nodeId, { source: { projectId } })\n if (!built.ok) {\n // EC-494: the engine's clipboard errors carry a catalog ref, so the notice\n // reads in the active studio locale instead of English inside German chrome.\n publishClipboardNotice({ kind: 'error', message: clipboardErrorMessage(built.error) })\n return null\n }\n return { fragment: built.fragment, label: shortId(built.fragment.payload.root.componentId) }\n }\n\n const write = (\n fragment: ClipboardFragment,\n label: string,\n kind: 'copied' | 'cut',\n dt?: DataTransfer | null,\n asyncClipboard?: AsyncClipboard,\n ): void => {\n if (dt) {\n writeFragmentToDataTransfer(dt, fragment)\n publishClipboardNotice({ kind, label })\n return\n }\n writeFragmentToClipboard(asyncClipboard ?? navigator.clipboard, fragment)\n .then(() => publishClipboardNotice({ kind, label }))\n .catch((cause: unknown) => {\n const detail = cause instanceof Error ? cause.message : String(cause)\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.writeError', { detail }) })\n })\n }\n\n const currentRoot = (): ComponentNode | undefined => {\n const { builder: b } = stateRef.current\n return b.present\n }\n\n const applyPaste = (result: ParseFragmentResult, anchorId?: string | null): void => {\n const { builder: b } = stateRef.current\n if (!result.ok) {\n publishClipboardNotice({ kind: 'error', message: clipboardErrorMessage(result.error) })\n return\n }\n const root = currentRoot()\n if (!root) return\n const resolution = resolvePasteTarget({\n root,\n anchorId: anchorId !== undefined ? anchorId : b.selectedId,\n componentId: result.fragment.payload.root.componentId,\n getManifest: (id) => studio.registry.getManifest(id),\n // EC-554: the composition field's top-level vocabulary, from the store.\n fieldAllow: b.field.allow,\n })\n if (!resolution.ok) {\n // EC-545 / EC-554: the slot leash or the field leash refuses the pasted\n // root — the same verdict a drop gets, made visible. Nothing is dispatched.\n publishClipboardNotice({ kind: 'error', message: pasteRejectionMessage(resolution, b.field.label) })\n return\n }\n const { target, destinationLabel } = resolution\n // Mirror the reducer's deterministic re-id so the issue deep links point\n // at exactly the node ids the inserted subtree will carry — the SAME counter\n // the reducer uses (`makeNodeIdCounter`), so the mirror can never diverge.\n const inserted = cloneWithNewIds(result.fragment.payload.root, makeNodeIdCounter(b.nodeSeq).genId)\n // ONE dispatch: one history entry, one graph.insertNode command, one undo.\n b.dispatch({ type: 'insert', target, node: result.fragment.payload.root })\n const diff = diffPastedSubtree({\n node: inserted,\n hasComponent: (id) => studio.registry.getManifest(id) !== undefined,\n })\n publishClipboardNotice({\n kind: 'pasted',\n summary: pasteSummaryMessage(diff),\n destinationLabel,\n issues: diff.issues,\n })\n }\n\n return {\n copyNode(nodeId, dt, asyncClipboard) {\n const built = fragmentFor(nodeId)\n if (built) write(built.fragment, built.label, 'copied', dt, asyncClipboard)\n },\n cutNode(nodeId, dt, asyncClipboard) {\n const { builder: b } = stateRef.current\n const root = currentRoot()\n if (root && nodeId === root.id) {\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.rootCannotBeCut') })\n return\n }\n const built = fragmentFor(nodeId)\n if (!built) return\n write(built.fragment, built.label, 'cut', dt, asyncClipboard)\n // Exactly one structural dispatch: a single undo restores the cut node.\n b.dispatch({ type: 'remove', id: nodeId })\n },\n copyFragment(fragment, label) {\n write(fragment, label, 'copied')\n },\n pasteFromDataTransfer(dt, anchorId) {\n applyPaste(readFragmentFromDataTransfer(dt), anchorId)\n },\n async pasteFromClipboard(anchorId, asyncClipboard) {\n applyPaste(await readFragmentFromClipboard(asyncClipboard ?? navigator.clipboard), anchorId)\n },\n }\n }, [studio])\n}\n\n/**\n * The visible reason a refused paste shows (EC-545) — pure, so the notice\n * wording per refusal is pinned without mounting the hook. EC-554: a field-leash\n * refusal names the FIELD (`fieldLabel`), never the invisible synthetic root's\n * slot — the rejection carries no destination label to misuse.\n */\nexport function pasteRejectionMessage(rejection: PasteRejection, fieldLabel: string): string {\n if (rejection.code === 'field-not-allowed') {\n return translate('builder.clipboard.notAllowedByField', {\n component: shortId(rejection.componentId),\n field: fieldLabel,\n })\n }\n return translate('builder.clipboard.notAllowed', {\n component: shortId(rejection.componentId),\n destination: rejection.destinationLabel,\n })\n}\n\n// ---------------------------------------------------------------------------\n// Listener wiring — one implementation for the page-tree window and the\n// canvas iframe document (events never cross the frame boundary).\n// ---------------------------------------------------------------------------\n\nexport interface ClipboardListenerOptions {\n clipboard: StudioClipboard\n /** Current selection: the copy/cut subject and the paste anchor. */\n getSelectedId: () => string | null\n /** Extra guard (e.g. the canvas's live inline-edit session). */\n isBlocked?: (target: EventTarget | null) => boolean\n /** The async Clipboard API of the listening document's own window. */\n asyncClipboard?: AsyncClipboard\n /** The listening document's selection — real text selections stay native. */\n getSelection?: () => Selection | null\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Cmd/Ctrl+C/X/V plus native copy/cut/paste ClipboardEvents.\n *\n * Shortcuts are handled on `keydown` (a selected node has no text selection,\n * so the browser would not fire native clipboard events for it); the\n * `preventDefault` there suppresses the native event, so the ClipboardEvent\n * listeners only ever see *other* triggers — context-menu copy/paste and\n * synthetic events — and the two paths can never double-handle one gesture.\n * Everything stays inert in form fields and inside the inline editors.\n */\nexport function attachClipboardListeners(target: ListenerTarget, opts: ClipboardListenerOptions): () => void {\n const blocked = (t: EventTarget | null): boolean =>\n clipboardEventBlocked(t) || (opts.isBlocked?.(t) ?? false)\n const hasTextSelection = (): boolean => {\n const selection = opts.getSelection?.()\n return selection !== null && selection !== undefined && !selection.isCollapsed\n }\n\n const onKeyDown = (e: Event) => {\n const event = e as KeyboardEvent\n if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return\n const key = event.key.toLowerCase()\n if (key !== 'c' && key !== 'x' && key !== 'v') return\n if (blocked(event.target)) return\n if ((key === 'c' || key === 'x') && hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (key === 'v') {\n event.preventDefault()\n void opts.clipboard.pasteFromClipboard(selectedId, opts.asyncClipboard)\n return\n }\n if (!selectedId) return\n event.preventDefault()\n if (key === 'c') opts.clipboard.copyNode(selectedId, null, opts.asyncClipboard)\n else opts.clipboard.cutNode(selectedId, null, opts.asyncClipboard)\n }\n\n const onCopyOrCut = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || !event.clipboardData) return\n event.preventDefault()\n if (event.type === 'copy') opts.clipboard.copyNode(selectedId, event.clipboardData)\n else opts.clipboard.cutNode(selectedId, event.clipboardData)\n }\n\n const onPaste = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || !event.clipboardData) return\n event.preventDefault()\n opts.clipboard.pasteFromDataTransfer(event.clipboardData, opts.getSelectedId())\n }\n\n target.addEventListener('keydown', onKeyDown)\n target.addEventListener('copy', onCopyOrCut)\n target.addEventListener('cut', onCopyOrCut)\n target.addEventListener('paste', onPaste)\n return () => {\n target.removeEventListener('keydown', onKeyDown)\n target.removeEventListener('copy', onCopyOrCut)\n target.removeEventListener('cut', onCopyOrCut)\n target.removeEventListener('paste', onPaste)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Notice overlay — rendered by the canvas; issues deep-link to their nodes.\n// ---------------------------------------------------------------------------\n\nexport function ClipboardNoticeOverlay() {\n const notice = useClipboardNotice()\n const builder = useBuilder()\n if (!notice) return null\n\n const tone =\n notice.kind === 'error' ? 'error' : notice.kind === 'pasted' && notice.issues.length > 0 ? 'warning' : 'success'\n\n return (\n <div\n data-testid=\"clipboard-notice\"\n data-notice-kind={notice.kind}\n className=\"absolute bottom-4 left-1/2 z-20 w-max max-w-[min(420px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n {tone === 'error' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-destructive\" aria-hidden />\n ) : tone === 'warning' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n ) : (\n <CircleCheck className=\"mt-0.5 size-3.5 shrink-0 text-success\" aria-hidden />\n )}\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {notice.kind === 'copied'\n ? translate('builder.clipboard.copied', { label: notice.label })\n : notice.kind === 'cut'\n ? translate('builder.clipboard.cutNotice', { label: notice.label })\n : notice.kind === 'pasted'\n ? translate('builder.clipboard.pastedInto', {\n summary: notice.summary,\n destination: notice.destinationLabel,\n })\n : notice.message}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => publishClipboardNotice(null)}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n {notice.kind === 'pasted' && notice.issues.length > 0 ? (\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {notice.issues.map((issue, i) => (\n <button\n key={`${issue.nodeId}-${i}`}\n type=\"button\"\n data-testid=\"clipboard-notice-issue\"\n onClick={() => builder.dispatch({ type: 'select', id: issue.nodeId })}\n className=\"flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-muted\"\n >\n <Badge variant=\"warning\" className=\"shrink-0\">\n {translate('builder.clipboard.issueComponent')}\n </Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1 truncate\">\n {issue.message}\n </Text>\n <Crosshair className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"clipboard-actions.js","sourceRoot":"","sources":["../../../src/components/builder/clipboard-actions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport { AlertTriangle, CircleCheck, Crosshair, X } from 'lucide-react'\nimport { cloneWithNewIds } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { buildFragmentFromComposition } from '@elytracms/core/clipboard'\nimport type { ClipboardFragment, ParseFragmentResult } from '@elytracms/core/clipboard'\nimport {\n readFragmentFromClipboard,\n readFragmentFromDataTransfer,\n resolvePasteTarget,\n diffPastedSubtree,\n pasteSummaryMessage,\n writeFragmentToClipboard,\n writeFragmentToDataTransfer,\n} from '@elytracms/studio/lib/clipboard'\nimport type { AsyncClipboard, PasteIssue, PasteRejection } from '@elytracms/studio/lib/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { clipboardErrorMessage, componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useProjectSession } from '../project-session'\nimport { Badge, Row, Text } from '@elytracms/studio/ui'\nimport { makeNodeIdCounter, useBuilder } from './builder-store'\n\n/**\n * Copy/cut/paste everywhere (EC-175, vision 3.3/3.7).\n *\n * One hook ({@link useStudioClipboard}) backs every affordance — canvas\n * keyboard shortcuts (wired inside the iframe by the canvas), page-tree\n * shortcuts and row buttons, and the blocks panel's \"copy exact\n * configuration\" — so they all produce/consume the same EC-174 fragment via\n * the OS clipboard (`lib/clipboard/os-clipboard`), never an app-internal\n * buffer. That is what makes paste work across documents, pages, projects,\n * and browser tabs.\n *\n * Dispatch contract: a paste is exactly ONE `insert` action (the fragment\n * root is a single subtree) → one history entry → one mirrored\n * `graph.insertNode` command — one undo step removes everything the paste\n * inserted. A cut is copy + exactly one `remove`; one undo restores it.\n *\n * Paste is PERMISSIVE: missing components and unresolvable bindings insert\n * anyway (the renderer's fallbacks keep them visible) and surface in a\n * notice — \"Pasted — 2 bindings unresolved, 1 component missing\" — whose\n * issues deep-link to the inserted nodes. Only structurally invalid clipboard\n * content is reported as a non-blocking error, never a crash or silent no-op.\n *\n * The one thing paste REFUSES is the leash: a pasted root the resolved slot's\n * `allow` excludes (EC-545), or — landing as a direct child of the composition\n * root — the composition field's top-level `allow` excludes (EC-554), is\n * rejected with a visible notice and no dispatch — the same verdict\n * drag-and-drop gives. Permissiveness is about what the target project can\n * *resolve*; the leash is what the component and field authors *declared*.\n */\n\n// ---------------------------------------------------------------------------\n// Notice store — module-level pub/sub (the drag-session pattern) so any\n// surface can publish and the canvas overlay renders it.\n// ---------------------------------------------------------------------------\n\nexport type ClipboardNotice =\n | { kind: 'copied'; label: string }\n | { kind: 'cut'; label: string }\n | { kind: 'pasted'; summary: string; destinationLabel: string; issues: PasteIssue[] }\n | { kind: 'error'; message: string }\n\nlet currentNotice: ClipboardNotice | null = null\nconst noticeListeners = new Set<() => void>()\n\nexport function publishClipboardNotice(notice: ClipboardNotice | null): void {\n currentNotice = notice\n for (const listener of [...noticeListeners]) listener()\n}\n\nfunction subscribeNotice(listener: () => void): () => void {\n noticeListeners.add(listener)\n return () => noticeListeners.delete(listener)\n}\n\nfunction getNotice(): ClipboardNotice | null {\n return currentNotice\n}\n\nexport function useClipboardNotice(): ClipboardNotice | null {\n return useSyncExternalStore(subscribeNotice, getNotice, getNotice)\n}\n\n// ---------------------------------------------------------------------------\n// Shortcut scoping\n// ---------------------------------------------------------------------------\n\n/**\n * Whether a clipboard shortcut/event must stay inert for this target: form\n * fields and any contenteditable (which includes the EC-158/EC-159 inline\n * editors) own their own native clipboard behaviour.\n */\nexport function clipboardEventBlocked(target: EventTarget | null): boolean {\n const el = target as HTMLElement | null\n if (!el) return false\n const tag = el.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (typeof el.closest === 'function' && el.closest('[contenteditable]')) return true\n return el.isContentEditable === true\n}\n\n// ---------------------------------------------------------------------------\n// The clipboard surface\n// ---------------------------------------------------------------------------\n\nexport interface StudioClipboard {\n /**\n * Copy a canvas/tree node's subtree. Writes synchronously when a\n * ClipboardEvent's DataTransfer is given, else via the async Clipboard API\n * (`asyncClipboard` lets the canvas pass the iframe's own navigator —\n * Chromium scopes clipboard access to the *focused* document).\n */\n copyNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy, then remove the source in exactly one dispatch (one undo restores). */\n cutNode(nodeId: string, dt?: DataTransfer | null, asyncClipboard?: AsyncClipboard): void\n /** Copy a prebuilt fragment (blocks-panel \"copy exact configuration\"). */\n copyFragment(fragment: ClipboardFragment, label: string): void\n /** Paste from a paste event's DataTransfer (no permissions needed). */\n pasteFromDataTransfer(dt: DataTransfer, anchorId?: string | null): void\n /** Paste via the async Clipboard API (explicit affordances + shortcuts). */\n pasteFromClipboard(anchorId?: string | null, asyncClipboard?: AsyncClipboard): Promise<void>\n}\n\nexport function useStudioClipboard(): StudioClipboard {\n const builder = useBuilder()\n const { projectId } = useProjectSession()\n const studio = getStudioRegistry()\n\n // Event listeners outlive renders; refs keep them on the current state.\n const stateRef = useRef({ builder, projectId })\n stateRef.current = { builder, projectId }\n\n return useMemo<StudioClipboard>(() => {\n const fragmentFor = (nodeId: string): { fragment: ClipboardFragment; label: string } | null => {\n const { builder: b, projectId } = stateRef.current\n const built = buildFragmentFromComposition(b.present, nodeId, { source: { projectId } })\n if (!built.ok) {\n // EC-494: the engine's clipboard errors carry a catalog ref, so the notice\n // reads in the active studio locale instead of English inside German chrome.\n publishClipboardNotice({ kind: 'error', message: clipboardErrorMessage(built.error) })\n return null\n }\n const rootId = built.fragment.payload.root.componentId\n return { fragment: built.fragment, label: componentDisplayName(rootId, getStudioRegistry().registry.getManifest(rootId)) }\n }\n\n const write = (\n fragment: ClipboardFragment,\n label: string,\n kind: 'copied' | 'cut',\n dt?: DataTransfer | null,\n asyncClipboard?: AsyncClipboard,\n ): void => {\n if (dt) {\n writeFragmentToDataTransfer(dt, fragment)\n publishClipboardNotice({ kind, label })\n return\n }\n writeFragmentToClipboard(asyncClipboard ?? navigator.clipboard, fragment)\n .then(() => publishClipboardNotice({ kind, label }))\n .catch((cause: unknown) => {\n const detail = cause instanceof Error ? cause.message : String(cause)\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.writeError', { detail }) })\n })\n }\n\n const currentRoot = (): ComponentNode | undefined => {\n const { builder: b } = stateRef.current\n return b.present\n }\n\n const applyPaste = (result: ParseFragmentResult, anchorId?: string | null): void => {\n const { builder: b } = stateRef.current\n if (!result.ok) {\n publishClipboardNotice({ kind: 'error', message: clipboardErrorMessage(result.error) })\n return\n }\n const root = currentRoot()\n if (!root) return\n const resolution = resolvePasteTarget({\n root,\n anchorId: anchorId !== undefined ? anchorId : b.selectedId,\n componentId: result.fragment.payload.root.componentId,\n getManifest: (id) => studio.registry.getManifest(id),\n // EC-554: the composition field's top-level vocabulary, from the store.\n fieldAllow: b.field.allow,\n })\n if (!resolution.ok) {\n // EC-545 / EC-554: the slot leash or the field leash refuses the pasted\n // root — the same verdict a drop gets, made visible. Nothing is dispatched.\n publishClipboardNotice({ kind: 'error', message: pasteRejectionMessage(resolution, b.field.label) })\n return\n }\n const { target, destinationLabel } = resolution\n // Mirror the reducer's deterministic re-id so the issue deep links point\n // at exactly the node ids the inserted subtree will carry — the SAME counter\n // the reducer uses (`makeNodeIdCounter`), so the mirror can never diverge.\n const inserted = cloneWithNewIds(result.fragment.payload.root, makeNodeIdCounter(b.nodeSeq).genId)\n // ONE dispatch: one history entry, one graph.insertNode command, one undo.\n b.dispatch({ type: 'insert', target, node: result.fragment.payload.root })\n const diff = diffPastedSubtree({\n node: inserted,\n hasComponent: (id) => studio.registry.getManifest(id) !== undefined,\n })\n publishClipboardNotice({\n kind: 'pasted',\n summary: pasteSummaryMessage(diff),\n destinationLabel,\n issues: diff.issues,\n })\n }\n\n return {\n copyNode(nodeId, dt, asyncClipboard) {\n const built = fragmentFor(nodeId)\n if (built) write(built.fragment, built.label, 'copied', dt, asyncClipboard)\n },\n cutNode(nodeId, dt, asyncClipboard) {\n const { builder: b } = stateRef.current\n const root = currentRoot()\n if (root && nodeId === root.id) {\n publishClipboardNotice({ kind: 'error', message: translate('builder.clipboard.rootCannotBeCut') })\n return\n }\n const built = fragmentFor(nodeId)\n if (!built) return\n write(built.fragment, built.label, 'cut', dt, asyncClipboard)\n // Exactly one structural dispatch: a single undo restores the cut node.\n b.dispatch({ type: 'remove', id: nodeId })\n },\n copyFragment(fragment, label) {\n write(fragment, label, 'copied')\n },\n pasteFromDataTransfer(dt, anchorId) {\n applyPaste(readFragmentFromDataTransfer(dt), anchorId)\n },\n async pasteFromClipboard(anchorId, asyncClipboard) {\n applyPaste(await readFragmentFromClipboard(asyncClipboard ?? navigator.clipboard), anchorId)\n },\n }\n }, [studio])\n}\n\n/**\n * The visible reason a refused paste shows (EC-545) — pure, so the notice\n * wording per refusal is pinned without mounting the hook. EC-554: a field-leash\n * refusal names the FIELD (`fieldLabel`), never the invisible synthetic root's\n * slot — the rejection carries no destination label to misuse.\n */\n/** A component's label in a notice — its manifest title (EC-591: never a stripped id prefix). */\nfunction componentLabel(componentId: string): string {\n return componentDisplayName(componentId, getStudioRegistry().registry.getManifest(componentId))\n}\n\nexport function pasteRejectionMessage(rejection: PasteRejection, fieldLabel: string): string {\n if (rejection.code === 'field-not-allowed') {\n return translate('builder.clipboard.notAllowedByField', {\n component: componentLabel(rejection.componentId),\n field: fieldLabel,\n })\n }\n return translate('builder.clipboard.notAllowed', {\n component: componentLabel(rejection.componentId),\n destination: rejection.destinationLabel,\n })\n}\n\n// ---------------------------------------------------------------------------\n// Listener wiring — one implementation for the page-tree window and the\n// canvas iframe document (events never cross the frame boundary).\n// ---------------------------------------------------------------------------\n\nexport interface ClipboardListenerOptions {\n clipboard: StudioClipboard\n /** Current selection: the copy/cut subject and the paste anchor. */\n getSelectedId: () => string | null\n /** Extra guard (e.g. the canvas's live inline-edit session). */\n isBlocked?: (target: EventTarget | null) => boolean\n /** The async Clipboard API of the listening document's own window. */\n asyncClipboard?: AsyncClipboard\n /** The listening document's selection — real text selections stay native. */\n getSelection?: () => Selection | null\n}\n\ninterface ListenerTarget {\n addEventListener(type: string, listener: (e: Event) => void): void\n removeEventListener(type: string, listener: (e: Event) => void): void\n}\n\n/**\n * Wire Cmd/Ctrl+C/X/V plus native copy/cut/paste ClipboardEvents.\n *\n * Shortcuts are handled on `keydown` (a selected node has no text selection,\n * so the browser would not fire native clipboard events for it); the\n * `preventDefault` there suppresses the native event, so the ClipboardEvent\n * listeners only ever see *other* triggers — context-menu copy/paste and\n * synthetic events — and the two paths can never double-handle one gesture.\n * Everything stays inert in form fields and inside the inline editors.\n */\nexport function attachClipboardListeners(target: ListenerTarget, opts: ClipboardListenerOptions): () => void {\n const blocked = (t: EventTarget | null): boolean =>\n clipboardEventBlocked(t) || (opts.isBlocked?.(t) ?? false)\n const hasTextSelection = (): boolean => {\n const selection = opts.getSelection?.()\n return selection !== null && selection !== undefined && !selection.isCollapsed\n }\n\n const onKeyDown = (e: Event) => {\n const event = e as KeyboardEvent\n if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return\n const key = event.key.toLowerCase()\n if (key !== 'c' && key !== 'x' && key !== 'v') return\n if (blocked(event.target)) return\n if ((key === 'c' || key === 'x') && hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (key === 'v') {\n event.preventDefault()\n void opts.clipboard.pasteFromClipboard(selectedId, opts.asyncClipboard)\n return\n }\n if (!selectedId) return\n event.preventDefault()\n if (key === 'c') opts.clipboard.copyNode(selectedId, null, opts.asyncClipboard)\n else opts.clipboard.cutNode(selectedId, null, opts.asyncClipboard)\n }\n\n const onCopyOrCut = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || hasTextSelection()) return\n const selectedId = opts.getSelectedId()\n if (!selectedId || !event.clipboardData) return\n event.preventDefault()\n if (event.type === 'copy') opts.clipboard.copyNode(selectedId, event.clipboardData)\n else opts.clipboard.cutNode(selectedId, event.clipboardData)\n }\n\n const onPaste = (e: Event) => {\n const event = e as ClipboardEvent\n if (blocked(event.target) || !event.clipboardData) return\n event.preventDefault()\n opts.clipboard.pasteFromDataTransfer(event.clipboardData, opts.getSelectedId())\n }\n\n target.addEventListener('keydown', onKeyDown)\n target.addEventListener('copy', onCopyOrCut)\n target.addEventListener('cut', onCopyOrCut)\n target.addEventListener('paste', onPaste)\n return () => {\n target.removeEventListener('keydown', onKeyDown)\n target.removeEventListener('copy', onCopyOrCut)\n target.removeEventListener('cut', onCopyOrCut)\n target.removeEventListener('paste', onPaste)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Notice overlay — rendered by the canvas; issues deep-link to their nodes.\n// ---------------------------------------------------------------------------\n\nexport function ClipboardNoticeOverlay() {\n const notice = useClipboardNotice()\n const builder = useBuilder()\n if (!notice) return null\n\n const tone =\n notice.kind === 'error' ? 'error' : notice.kind === 'pasted' && notice.issues.length > 0 ? 'warning' : 'success'\n\n return (\n <div\n data-testid=\"clipboard-notice\"\n data-notice-kind={notice.kind}\n className=\"absolute bottom-4 left-1/2 z-20 w-max max-w-[min(420px,90%)] -translate-x-1/2\"\n >\n <div className=\"rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg\">\n <Row gap=\"2\" align=\"start\">\n {tone === 'error' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-destructive\" aria-hidden />\n ) : tone === 'warning' ? (\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n ) : (\n <CircleCheck className=\"mt-0.5 size-3.5 shrink-0 text-success\" aria-hidden />\n )}\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {notice.kind === 'copied'\n ? translate('builder.clipboard.copied', { label: notice.label })\n : notice.kind === 'cut'\n ? translate('builder.clipboard.cutNotice', { label: notice.label })\n : notice.kind === 'pasted'\n ? translate('builder.clipboard.pastedInto', {\n summary: notice.summary,\n destination: notice.destinationLabel,\n })\n : notice.message}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => publishClipboardNotice(null)}\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <X className=\"size-3.5\" />\n </button>\n </Row>\n {notice.kind === 'pasted' && notice.issues.length > 0 ? (\n <div className=\"mt-1.5 flex flex-col gap-1\">\n {notice.issues.map((issue, i) => (\n <button\n key={`${issue.nodeId}-${i}`}\n type=\"button\"\n data-testid=\"clipboard-notice-issue\"\n onClick={() => builder.dispatch({ type: 'select', id: issue.nodeId })}\n className=\"flex w-full items-center gap-2 rounded-md p-1 text-left transition-colors hover:bg-muted\"\n >\n <Badge variant=\"warning\" className=\"shrink-0\">\n {translate('builder.clipboard.issueComponent')}\n </Badge>\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 flex-1 truncate\">\n {issue.message}\n </Text>\n <Crosshair className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  import type { ComponentNode, PreservedSequenceItem, SplitBlockRoots } from '@elytracms/core/project-graph';
2
2
  import type { BlocksFieldDef } from '@elytracms/core/cms-core';
3
+ import type { ComponentRegistry } from '@elytracms/core/component-registry';
3
4
  import type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text';
5
+ import type { MessageKey } from '@elytracms/studio/lib/i18n';
4
6
  import type { CompositionFieldScope } from './builder-store.js';
5
7
  import type { InspectorPane } from '../inspector/inspector-sidebar.js';
6
8
  /**
@@ -41,6 +43,26 @@ export declare function mergeCompositionValue(roots: readonly ComponentNode[], p
41
43
  * omitted `allow` stays `undefined` (EC-544: any registered component).
42
44
  */
43
45
  export declare function compositionFieldScope(field: BlocksFieldDef): CompositionFieldScope;
46
+ /**
47
+ * The canvas notices for roles nobody in the component list fills (EC-591) — each
48
+ * names the gesture that is OFF because of it, so a missing role is visible, never
49
+ * a silent dead affordance. Only gestures the canvas actually has are listed: the
50
+ * `layout.stack` role is the canvas itself (see {@link CompositionFieldEditor}),
51
+ * `layout.grid` the wrap-beside gesture, `text` the seeded layout presets.
52
+ * (`richText` — the typography projection, EC-549 — and `image` — asset drop —
53
+ * have no canvas gesture yet, so there is nothing to switch off.) Pure.
54
+ */
55
+ export declare function canvasRoleNotices(registry: ComponentRegistry): MessageKey[];
56
+ /** The role notices above the canvas (EC-591) — a standing truth about the component list. */
57
+ export declare function CanvasRoleNotices({ notices }: {
58
+ notices: readonly MessageKey[];
59
+ }): import("react").JSX.Element | null;
60
+ /**
61
+ * The canvas in place of the editor when nobody fills `layout.stack` (EC-591): the
62
+ * blocks of a composition field compose inside that component, so there is no
63
+ * canvas without it. The value is not touched — nothing mounts that could write.
64
+ */
65
+ export declare function NoStackCanvasNotice(): import("react").JSX.Element;
44
66
  export declare function CompositionFieldEditor({ field, value, onChange, readOnly, trailingPanes, }: {
45
67
  field: BlocksFieldDef;
46
68
  value: unknown;
@@ -5,6 +5,7 @@ import { DEFAULT_SLOT, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/
5
5
  import { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui';
6
6
  import { cn } from '@elytracms/studio/lib/utils';
7
7
  import { translate } from '@elytracms/studio/lib/i18n';
8
+ import { getStudioRegistry } from '@elytracms/studio/lib/registry';
8
9
  import { BuilderProvider, useBuilder } from './builder-store.js';
9
10
  import { useCanvasViewports } from './canvas-breakpoints.js';
10
11
  import { BuilderCanvas } from './canvas.js';
@@ -40,8 +41,6 @@ import { fieldLabel } from '../cms/cms-labels.js';
40
41
  * than being crammed into the field list.
41
42
  */
42
43
  const SYNTH_ROOT_ID = 'composition.root';
43
- /** The implicit root container the field's blocks compose inside (like a page root). */
44
- const SYNTH_ROOT_COMPONENT = 'base.Stack';
45
44
  /**
46
45
  * Split a stored blocks-field value into the component nodes the canvas edits
47
46
  * and the sequence items it must preserve (EC-547, the EC-500 round-trip law:
@@ -79,14 +78,47 @@ export function compositionFieldScope(field) {
79
78
  * Wrap the field value's component roots in a synthetic root container (EC-287).
80
79
  * The builder store edits this single root-bearing composition directly; its
81
80
  * children are the field's component blocks, lifted back out via `onChange`.
81
+ * EC-591: the root is the component that fills the `layout.stack` role — the
82
+ * project's own, from its component list — never a hardcoded engine id.
82
83
  */
83
- function buildSyntheticRoot(roots) {
84
+ function buildSyntheticRoot(roots, stackComponentId) {
84
85
  return {
85
86
  id: SYNTH_ROOT_ID,
86
- componentId: SYNTH_ROOT_COMPONENT,
87
+ componentId: stackComponentId,
87
88
  slots: { [DEFAULT_SLOT]: [...roots] },
88
89
  };
89
90
  }
91
+ /**
92
+ * The canvas notices for roles nobody in the component list fills (EC-591) — each
93
+ * names the gesture that is OFF because of it, so a missing role is visible, never
94
+ * a silent dead affordance. Only gestures the canvas actually has are listed: the
95
+ * `layout.stack` role is the canvas itself (see {@link CompositionFieldEditor}),
96
+ * `layout.grid` the wrap-beside gesture, `text` the seeded layout presets.
97
+ * (`richText` — the typography projection, EC-549 — and `image` — asset drop —
98
+ * have no canvas gesture yet, so there is nothing to switch off.) Pure.
99
+ */
100
+ export function canvasRoleNotices(registry) {
101
+ const notices = [];
102
+ if (!registry.roleComponent('layout.grid'))
103
+ notices.push('builder.roles.noGrid');
104
+ if (!registry.roleComponent('text'))
105
+ notices.push('builder.roles.noText');
106
+ return notices;
107
+ }
108
+ /** The role notices above the canvas (EC-591) — a standing truth about the component list. */
109
+ export function CanvasRoleNotices({ notices }) {
110
+ if (notices.length === 0)
111
+ return null;
112
+ return (_jsx("div", { "data-testid": "canvas-role-notices", className: "shrink-0 border-b border-border bg-muted/40", children: notices.map((key) => (_jsxs(Row, { gap: "2", align: "center", className: "px-3 py-1.5", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-3.5 shrink-0 text-muted-foreground" }), _jsx(Text, { size: "xs", tone: "muted", children: translate(key) })] }, key))) }));
113
+ }
114
+ /**
115
+ * The canvas in place of the editor when nobody fills `layout.stack` (EC-591): the
116
+ * blocks of a composition field compose inside that component, so there is no
117
+ * canvas without it. The value is not touched — nothing mounts that could write.
118
+ */
119
+ export function NoStackCanvasNotice() {
120
+ return (_jsx(Panel, { "data-testid": "canvas-no-stack", className: "flex h-full min-h-0 items-center justify-center p-6", children: _jsxs(Row, { gap: "2", align: "center", className: "max-w-prose", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" }), _jsx(Text, { size: "sm", tone: "muted", children: translate('builder.roles.noStack') })] }) }));
121
+ }
90
122
  export function CompositionFieldEditor({ field, value, onChange, readOnly, trailingPanes, }) {
91
123
  // Split the value ONCE at mount (EC-547): component nodes become the canvas
92
124
  // roots; every other sequence item is preserved aside for write-back. The
@@ -95,11 +127,18 @@ export function CompositionFieldEditor({ field, value, onChange, readOnly, trail
95
127
  // draft. (BuilderProvider reads `composition` only in its lazy initial state,
96
128
  // so later prop changes are ignored regardless.)
97
129
  const initial = useMemo(() => splitCompositionValue(value), []);
98
- const initialRoot = useMemo(() => buildSyntheticRoot(initial.roots), [initial]);
130
+ // EC-591: the roles come from the project's component list (the registry).
131
+ const registry = getStudioRegistry().registry;
132
+ const stack = registry.roleComponent('layout.stack');
133
+ const layout = registry.layout;
134
+ const notices = useMemo(() => canvasRoleNotices(registry), [registry]);
135
+ const initialRoot = useMemo(() => (stack ? buildSyntheticRoot(initial.roots, stack.id) : null), [initial, stack]);
99
136
  // EC-554: the field's `allow` travels WITH the store, so every placement
100
137
  // surface (canvas + tree DnD, paste, the insert offer) judges the same value.
101
138
  const fieldScope = useMemo(() => compositionFieldScope(field), [field]);
102
- return (_jsxs(BuilderProvider, { composition: initialRoot, field: fieldScope, children: [_jsx(CompositionLift, { preserved: initial.preserved, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, trailingPanes: trailingPanes, preservedCount: initial.preserved.length }) }) })] }));
139
+ if (!initialRoot)
140
+ return _jsx(NoStackCanvasNotice, {});
141
+ return (_jsxs(BuilderProvider, { composition: initialRoot, field: fieldScope, layout: layout, children: [_jsx(CompositionLift, { preserved: initial.preserved, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, trailingPanes: trailingPanes, preservedCount: initial.preserved.length, roleNotices: notices }) }) })] }));
103
142
  }
104
143
  /**
105
144
  * EC-549 interim (scope item 3, the honesty floor): a mixed sequence's typography
@@ -177,7 +216,7 @@ function BreakpointSwitcher() {
177
216
  * field. It keeps spatial/preview controls — including the breakpoint
178
217
  * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.
179
218
  */
180
- function CompositionEditorShell({ readOnly, trailingPanes, preservedCount = 0, }) {
219
+ function CompositionEditorShell({ readOnly, trailingPanes, preservedCount = 0, roleNotices, }) {
181
220
  const builder = useBuilder();
182
221
  const dispatch = builder.dispatch;
183
222
  // The field's top-level component vocabulary (EC-188) — the leash applied to
@@ -231,6 +270,6 @@ function CompositionEditorShell({ readOnly, trailingPanes, preservedCount = 0, }
231
270
  setInsertOpen(true);
232
271
  },
233
272
  }), [builder, fieldAllow, dispatch]);
234
- return (_jsx(InsertAffordanceProvider, { value: affordanceApi, children: _jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border", children: _jsxs(Toolbar, { children: [_jsxs(ToolbarGroup, { children: [_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('builder.canvas.undo'), title: translate('builder.canvas.undo'), disabled: readOnly || !builder.canUndo, onClick: () => dispatch({ type: 'undo' }), children: _jsx(Undo2, {}) }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('builder.canvas.redo'), title: translate('builder.canvas.redo'), disabled: readOnly || !builder.canRedo, onClick: () => dispatch({ type: 'redo' }), children: _jsx(Redo2, {}) })] }), _jsx(ToolbarSeparator, {}), _jsxs(Button, { size: "sm", variant: "ghost", disabled: readOnly, onClick: () => openInsert(builder.selectedId ?? rootId), children: [_jsx(Plus, {}), " ", translate('builder.canvas.insert')] }), _jsx(Spacer, {}), _jsx(BreakpointSwitcher, {}), _jsx(Spacer, {})] }) }), _jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border", children: [_jsx(Panel, { className: cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60'), children: _jsx(PageTree, { onAddChild: openInsert }) }), _jsxs("div", { className: cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60'), children: [_jsx(PreservedItemsNotice, { count: preservedCount }), _jsx(BuilderCanvas, { perspective: readOnly ? 'published' : 'draft' })] }), _jsx(InspectorSidebar, { panes: panes })] }), _jsx(InsertMenu, { context: insertContext, open: insertOpen, onOpenChange: setInsertOpen })] }) }));
273
+ return (_jsx(InsertAffordanceProvider, { value: affordanceApi, children: _jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border", children: _jsxs(Toolbar, { children: [_jsxs(ToolbarGroup, { children: [_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('builder.canvas.undo'), title: translate('builder.canvas.undo'), disabled: readOnly || !builder.canUndo, onClick: () => dispatch({ type: 'undo' }), children: _jsx(Undo2, {}) }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('builder.canvas.redo'), title: translate('builder.canvas.redo'), disabled: readOnly || !builder.canRedo, onClick: () => dispatch({ type: 'redo' }), children: _jsx(Redo2, {}) })] }), _jsx(ToolbarSeparator, {}), _jsxs(Button, { size: "sm", variant: "ghost", disabled: readOnly, onClick: () => openInsert(builder.selectedId ?? rootId), children: [_jsx(Plus, {}), " ", translate('builder.canvas.insert')] }), _jsx(Spacer, {}), _jsx(BreakpointSwitcher, {}), _jsx(Spacer, {})] }) }), _jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border", children: [_jsx(Panel, { className: cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60'), children: _jsx(PageTree, { onAddChild: openInsert }) }), _jsxs("div", { className: cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60'), children: [_jsx(PreservedItemsNotice, { count: preservedCount }), _jsx(CanvasRoleNotices, { notices: roleNotices ?? [] }), _jsx(BuilderCanvas, { perspective: readOnly ? 'published' : 'draft' })] }), _jsx(InspectorSidebar, { panes: panes })] }), _jsx(InsertMenu, { context: insertContext, open: insertOpen, onOpenChange: setInsertOpen })] }) }));
235
274
  }
236
275
  //# sourceMappingURL=composition-field-editor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAC5F,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAI9F,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAChH,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEjE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE1C,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAEpG,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAA;AAEtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,aAAa,GAAG,kBAAkB,CAAA;AACxC,wFAAwF;AACxF,MAAM,oBAAoB,GAAG,YAAY,CAAA;AAezC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,OAAO,eAAe,CAAC,KAAK,CAA0B,CAAA;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAA+B,EAC/B,SAAwC;IAExC,OAAO,eAAe,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAqB;IACzD,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAA;AACzD,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,KAA+B;IACzD,OAAO;QACL,EAAE,EAAE,aAAa;QACjB,WAAW,EAAE,oBAAoB;QACjC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE;KACtC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,EACrC,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,aAAa,GASd;IACC,4EAA4E;IAC5E,0EAA0E;IAC1E,+EAA+E;IAC/E,+EAA+E;IAC/E,8EAA8E;IAC9E,iDAAiD;IACjD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;IAC/D,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAC/E,yEAAyE;IACzE,8EAA8E;IAC9E,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEvE,OAAO,CACL,MAAC,eAAe,IAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,aAC1D,KAAC,eAAe,IAAC,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAI,EACzF,KAAC,aAAa,cAGZ,KAAC,gBAAgB,cACf,KAAC,sBAAsB,IACrB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,aAAa,EAC5B,cAAc,EAAE,OAAO,CAAC,SAAS,CAAC,MAAM,GACxC,GACe,GACL,IACA,CACnB,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,KAAK,EAAqB;IAC/D,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC5B,OAAO,CACL,MAAC,GAAG,IACF,GAAG,EAAC,GAAG,EACP,KAAK,EAAC,QAAQ,iBACF,8BAA8B,EAC1C,SAAS,EAAC,yDAAyD,aAEnE,KAAC,OAAO,yBAAa,SAAS,EAAC,yCAAyC,GAAG,EAC3E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,qCAAqC,EAAE,EAAE,KAAK,EAAE,CAAC,GACvD,IACH,CACP,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,EACvB,SAAS,EACT,QAAQ,EACR,QAAQ,GAKT;IACC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,IAAI,YAAY,CAAA;IACpE,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IACpC,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAA;IAC9B,MAAM,WAAW,GAAG,MAAM,CAAkC,IAAI,CAAC,CAAA;IAEjE,SAAS,CAAC,GAAG,EAAE;QACb,2EAA2E;QAC3E,8EAA8E;QAC9E,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,uBAAuB;QACvB,IAAI,QAAQ;YAAE,OAAM;QACpB,IAAI,WAAW,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACjC,WAAW,CAAC,OAAO,GAAG,MAAM,CAAA;YAC5B,OAAM;QACR,CAAC;QACD,IAAI,MAAM,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;YACnC,WAAW,CAAC,OAAO,GAAG,MAAM,CAAA;YAC5B,WAAW,CAAC,OAAO,CAAC,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;QAC/D,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEjC,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,YAAY,GAA6B,EAAE,CAAA;AAEjD;;;;kCAIkC;AAClC,SAAS,cAAc,CAAC,EAAE,MAAM,EAA8B;IAC5D,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,MAAM,CAAC,GAAG,CAAA;IAC9C,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,oCAAoC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,kBACtE,MAAM,EACpB,KAAK,EACH,MAAM,CAAC,EAAE,KAAK,SAAS;YACrB,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,MAAM,MAAM,CAAC,EAAE,IAAI;YACpC,CAAC,CAAC,SAAS,CAAC,mCAAmC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,EAE7E,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,EAC3E,SAAS,EAAE,EAAE,CAAC,cAAc,EAAE,MAAM,IAAI,kCAAkC,CAAC,YAE1E,MAAM,CAAC,KAAK,GACN,CACV,CAAA;AACH,CAAC;AAED;uFACuF;AACvF,SAAS,kBAAkB;IACzB,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,kBAAkB,EAAE,CAAA;IACjD,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,YAAY,cACV,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACpB,KAAC,cAAc,IAAa,MAAM,EAAE,CAAC,IAAhB,CAAC,CAAC,GAAG,CAAe,CAC1C,CAAC,GACW,EACd,KAAK,CAAC,CAAC,CAAC,CACP,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,oCAAoC,EAAC,KAAK,EAAE,KAAK,aACrF,KAAC,aAAa,yBAAa,SAAS,EAAC,UAAU,GAAG,EAClD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,YAAE,SAAS,CAAC,sCAAsC,CAAC,GAAQ,IACtE,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,sBAAsB,CAAC,EAC9B,QAAQ,EACR,aAAa,EACb,cAAc,GAAG,CAAC,GAOnB;IACC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,6EAA6E;IAC7E,+EAA+E;IAC/E,kFAAkF;IAClF,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAA;IACtC,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAuB,IAAI,CAAC,CAAA;IAC9E,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,CAAA;IAEjC,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,cAAc,GAAG,OAAO,CAC5B,GAAG,EAAE,CAAC,CAAC;QACL,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,SAAS,CAAC,kCAAkC,CAAC;QACpD,IAAI,EAAE,KAAC,iBAAiB,IAAC,SAAS,EAAC,UAAU,GAAG;QAChD,MAAM,EAAE,GAAG,EAAE,CAAC,CACZ,cAAK,SAAS,EAAE,EAAE,CAAC,gCAAgC,EAAE,QAAQ,IAAI,gCAAgC,CAAC,YAChG,KAAC,iBAAiB,KAAG,GACjB,CACP;KACF,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAA;IACD,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,cAAc,EAAE,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,EAChD,CAAC,cAAc,EAAE,aAAa,CAAC,CAChC,CAAA;IAED,yEAAyE;IACzE,4EAA4E;IAC5E,6EAA6E;IAC7E,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,2DAA2D;IAC3D,SAAS,UAAU,CAAC,MAAc;QAChC,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAA;QAC7D,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,IAAI,MAAM,KAAK,OAAO,CAAC,UAAU;YAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QAC3E,gBAAgB,CAAC,GAAG,CAAC,CAAA;QACrB,aAAa,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,gFAAgF;IAChF,gFAAgF;IAChF,iFAAiF;IACjF,kFAAkF;IAClF,uCAAuC;IACvC,MAAM,aAAa,GAAG,OAAO,CAC3B,GAAG,EAAE,CAAC,CAAC;QACL,YAAY,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC;QAClF,MAAM,EAAE,CAAC,MAAc,EAAE,QAAwB,EAAE,EAAE;YACnD,MAAM,GAAG,GAAG,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAA;YACzE,IAAI,CAAC,GAAG;gBAAE,OAAM;YAChB,IAAI,MAAM,KAAK,OAAO,CAAC,UAAU;gBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;YAC3E,gBAAgB,CAAC,GAAG,CAAC,CAAA;YACrB,aAAa,CAAC,IAAI,CAAC,CAAA;QACrB,CAAC;KACF,CAAC,EACF,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAChC,CAAA;IAED,OAAO,CACL,KAAC,wBAAwB,IAAC,KAAK,EAAE,aAAa,YAC5C,eAAK,SAAS,EAAC,8BAA8B,aAC3C,cAAK,SAAS,EAAC,iCAAiC,YAC9C,MAAC,OAAO,eACN,MAAC,YAAY,eACX,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,qBAAqB,CAAC,EAC5C,KAAK,EAAE,SAAS,CAAC,qBAAqB,CAAC,EACvC,QAAQ,EAAE,QAAQ,IAAI,CAAC,OAAO,CAAC,OAAO,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,YAEzC,KAAC,KAAK,KAAG,GACF,EACT,KAAC,MAAM,IACL,IAAI,EAAC,SAAS,EACd,OAAO,EAAC,OAAO,gBACH,SAAS,CAAC,qBAAqB,CAAC,EAC5C,KAAK,EAAE,SAAS,CAAC,qBAAqB,CAAC,EACvC,QAAQ,EAAE,QAAQ,IAAI,CAAC,OAAO,CAAC,OAAO,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,YAEzC,KAAC,KAAK,KAAG,GACF,IACI,EACf,KAAC,gBAAgB,KAAG,EACpB,MAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,OAAO,EACf,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,aAEvD,KAAC,IAAI,KAAG,OAAE,SAAS,CAAC,uBAAuB,CAAC,IACrC,EAIT,KAAC,MAAM,KAAG,EACV,KAAC,kBAAkB,KAAG,EACtB,KAAC,MAAM,KAAG,IACF,GACN,EAEN,eAAK,SAAS,EAAC,8FAA8F,aAC3G,KAAC,KAAK,IAAC,SAAS,EAAE,EAAE,CAAC,uBAAuB,EAAE,QAAQ,IAAI,gCAAgC,CAAC,YACzF,KAAC,QAAQ,IAAC,UAAU,EAAE,UAAU,GAAI,GAC9B,EACR,eAAK,SAAS,EAAE,EAAE,CAAC,qCAAqC,EAAE,QAAQ,IAAI,gCAAgC,CAAC,aACrG,KAAC,oBAAoB,IAAC,KAAK,EAAE,cAAc,GAAI,EAI/C,KAAC,aAAa,IAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,GAAI,IAC5D,EACN,KAAC,gBAAgB,IAAC,KAAK,EAAE,KAAK,GAAI,IAC9B,EAEN,KAAC,UAAU,IAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,GAAI,IACjF,GACmB,CAC5B,CAAA;AACH,CAAC","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Pilcrow, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode, PreservedSequenceItem, SplitBlockRoots } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport type { CompositionFieldScope } from './builder-store'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context'\nimport type { InsertPosition } from './insert-context'\nimport { InsertAffordanceProvider } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport { PropSyncProvider } from './prop-sync-context'\nimport { ReuseProvider } from './reuse-context'\nimport { fieldLabel } from '../cms/cms-labels'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`).\n *\n * EC-547 (EC-500 D1–D3): the stored value is the converged v2 block sequence\n * `{ version: 2, content: BlockNode[] }`. The canvas edits the sequence's\n * `elytra/component` nodes; typography nodes survive the session byte-for-byte\n * and keep their order relative to components on write-back (see\n * {@link splitCompositionValue} / {@link mergeCompositionValue}). Legacy bare\n * node/array values are still ACCEPTED on read during the staging window, but\n * every write emits v2. This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\n\n/**\n * A sequence item the canvas does not natively edit (EC-547 / EC-500 D3):\n * typography nodes, unknown/future node kinds, even entries the v2 schema would\n * reject — the editor is not a validator, so they are carried BYTE-FOR-BYTE and\n * re-merged on write-back rather than parsed, rewritten, or dropped.\n * `componentPrefix` records how many component nodes preceded the item in the\n * original sequence — the anchor {@link mergeCompositionValue} splices by.\n */\nexport type PreservedBlockNode = PreservedSequenceItem<BlockNode>\n\n/** A field value split for canvas editing: component roots + everything else. */\nexport type SplitCompositionValue = SplitBlockRoots<BlockNode>\n\n/**\n * Split a stored blocks-field value into the component nodes the canvas edits\n * and the sequence items it must preserve (EC-547, the EC-500 round-trip law:\n * a projection may only rewrite nodes it natively edits). Typography items are\n * deliberately NOT surfaced as editable canvas blocks in v1 (EC-500 D3) — they\n * survive the editing session and keep their order on write-back.\n *\n * EC-576: the logic is core's {@link splitBlockRoots} — the ONE reader every\n * blocks writer shares. This is the canvas's typed view of it: project-graph\n * sits below rich-text and cannot name {@link BlockNode}, so the carried items\n * arrive `unknown`; they are typed here, not checked (preservation over parsing).\n */\nexport function splitCompositionValue(value: unknown): SplitCompositionValue {\n return splitBlockRoots(value) as SplitCompositionValue\n}\n\n/**\n * Merge canvas-edited component roots back into the stored sequence (EC-547).\n * The merge rule — preserved items anchored to their ordinal position between\n * components, every root tagged, the write always v2 — is core's\n * {@link mergeBlockRoots} (EC-576: the same merge every blocks writer goes\n * through, via `editBlockRoots`). Typed here as the rich-text value.\n */\nexport function mergeCompositionValue(\n roots: readonly ComponentNode[],\n preserved: readonly PreservedBlockNode[],\n): BlockSequenceValue {\n return mergeBlockRoots(roots, preserved)\n}\n\n/**\n * The field scope the editor's store carries (EC-554): the field's label (named\n * by refusal notices) and its top-level `allow`, passed through verbatim — an\n * omitted `allow` stays `undefined` (EC-544: any registered component).\n */\nexport function compositionFieldScope(field: BlocksFieldDef): CompositionFieldScope {\n return { label: fieldLabel(field), allow: field.allow }\n}\n\n/**\n * Wrap the field value's component roots in a synthetic root container (EC-287).\n * The builder store edits this single root-bearing composition directly; its\n * children are the field's component blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(roots: readonly ComponentNode[]): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: [...roots] },\n }\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Split the value ONCE at mount (EC-547): component nodes become the canvas\n // roots; every other sequence item is preserved aside for write-back. The\n // editor is uncontrolled thereafter — it is the source of truth while open and\n // lifts changes out via onChange; re-opening the tab remounts from the current\n // draft. (BuilderProvider reads `composition` only in its lazy initial state,\n // so later prop changes are ignored regardless.)\n const initial = useMemo(() => splitCompositionValue(value), [])\n const initialRoot = useMemo(() => buildSyntheticRoot(initial.roots), [initial])\n // EC-554: the field's `allow` travels WITH the store, so every placement\n // surface (canvas + tree DnD, paste, the insert offer) judges the same value.\n const fieldScope = useMemo(() => compositionFieldScope(field), [field])\n\n return (\n <BuilderProvider composition={initialRoot} field={fieldScope}>\n <CompositionLift preserved={initial.preserved} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n {/* EC-408: the field↔component selection-sync channel wraps BOTH the canvas\n and the inspector (they reach each other through nothing else). */}\n <PropSyncProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n trailingPanes={trailingPanes}\n preservedCount={initial.preserved.length}\n />\n </PropSyncProvider>\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * EC-549 interim (scope item 3, the honesty floor): a mixed sequence's typography\n * items are preserved for write-back but NOT rendered on the canvas — and a canvas\n * that silently shows half the sequence lets an editor rearrange around content\n * they cannot see. Until typography items are first-class, path-addressed canvas\n * items (EC-549's main scope), the surface must SAY what it is not showing: a\n * persistent, non-dismissible count above the canvas. Deliberately NOT the\n * dismissible overlay shape (`CanvasThemeNotice`) — this states a standing truth\n * about the VALUE, not a one-time migration hint, and it must not cover blocks.\n */\nexport function PreservedItemsNotice({ count }: { count: number }) {\n if (count === 0) return null\n return (\n <Row\n gap=\"2\"\n align=\"center\"\n data-testid=\"composition-preserved-notice\"\n className=\"shrink-0 border-b border-border bg-muted/40 px-3 py-1.5\"\n >\n <Pilcrow aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"muted\">\n {translate('builder.composition.preservedNotice', { count })}\n </Text>\n </Row>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the merged v2 sequence value (EC-547) — the canvas roots spliced back\n * between the preserved typography items. Cardinality (`multiple`) no longer\n * shapes the stored value: it is a validation constraint over the one envelope\n * (EC-500 D2), so the bare-object special case for `multiple: false` is gone.\n */\nfunction CompositionLift({\n preserved,\n onChange,\n readOnly,\n}: {\n preserved: readonly PreservedBlockNode[]\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(mergeCompositionValue(blocks, preserved))\n }\n }, [blocks, preserved, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n trailingPanes,\n preservedCount = 0,\n}: {\n readOnly: boolean\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n /** Preserved (non-component) sequence items the canvas cannot show (EC-549 interim). */\n preservedCount?: number\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n // The field's top-level component vocabulary (EC-188) — the leash applied to\n // inserts at the composition root so forbidden components aren't even offered.\n // Read from the store's field scope (EC-554), the same value DnD and paste judge.\n const fieldAllow = builder.field.allow\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div className={cn('h-full min-h-0 overflow-hidden', readOnly && 'pointer-events-none opacity-60')}>\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n // The single insert entry point for BOTH the toolbar and the page tree's\n // \"Add child\" (EC-383). It selects its target first so `builder.selectedId`\n // is the one source of truth for where an insert lands: the toolbar resolves\n // `builder.selectedId ?? rootId` while Add-child passes an explicit row, and\n // without this sync clicking Add-child (which otherwise leaves selection put)\n // could target a different node than the toolbar — surfacing as a mismatched\n // (even empty \"No allowed components match\") insert dialog. Both now resolve\n // the identical target + allow set for the same selection.\n function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n\n // EC-405: the in-canvas \"+\" affordances resolve the SAME slot rules through the\n // shared position-carrying insert-context resolution, and open the panel at the\n // clicked edge (an explicit position that survives to the insertion, rather than\n // the panel re-deriving it from the selection). Re-memoized on every store change\n // so the closures read the live graph.\n const affordanceApi = useMemo<InsertAffordanceApi>(\n () => ({\n positionsFor: (nodeId: string) => insertablePositions(builder, nodeId, fieldAllow),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, fieldAllow, dispatch],\n )\n\n return (\n <InsertAffordanceProvider value={affordanceApi}>\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}>\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div className={cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60')}>\n <PreservedItemsNotice count={preservedCount} />\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : 'draft'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n </InsertAffordanceProvider>\n )\n}\n"]}
1
+ {"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Pilcrow, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode, PreservedSequenceItem, SplitBlockRoots } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport type { ComponentRegistry } from '@elytracms/core/component-registry'\nimport type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { MessageKey } from '@elytracms/studio/lib/i18n'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport type { CompositionFieldScope } from './builder-store'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context'\nimport type { InsertPosition } from './insert-context'\nimport { InsertAffordanceProvider } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport { PropSyncProvider } from './prop-sync-context'\nimport { ReuseProvider } from './reuse-context'\nimport { fieldLabel } from '../cms/cms-labels'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`).\n *\n * EC-547 (EC-500 D1–D3): the stored value is the converged v2 block sequence\n * `{ version: 2, content: BlockNode[] }`. The canvas edits the sequence's\n * `elytra/component` nodes; typography nodes survive the session byte-for-byte\n * and keep their order relative to components on write-back (see\n * {@link splitCompositionValue} / {@link mergeCompositionValue}). Legacy bare\n * node/array values are still ACCEPTED on read during the staging window, but\n * every write emits v2. This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n\n/**\n * A sequence item the canvas does not natively edit (EC-547 / EC-500 D3):\n * typography nodes, unknown/future node kinds, even entries the v2 schema would\n * reject — the editor is not a validator, so they are carried BYTE-FOR-BYTE and\n * re-merged on write-back rather than parsed, rewritten, or dropped.\n * `componentPrefix` records how many component nodes preceded the item in the\n * original sequence — the anchor {@link mergeCompositionValue} splices by.\n */\nexport type PreservedBlockNode = PreservedSequenceItem<BlockNode>\n\n/** A field value split for canvas editing: component roots + everything else. */\nexport type SplitCompositionValue = SplitBlockRoots<BlockNode>\n\n/**\n * Split a stored blocks-field value into the component nodes the canvas edits\n * and the sequence items it must preserve (EC-547, the EC-500 round-trip law:\n * a projection may only rewrite nodes it natively edits). Typography items are\n * deliberately NOT surfaced as editable canvas blocks in v1 (EC-500 D3) — they\n * survive the editing session and keep their order on write-back.\n *\n * EC-576: the logic is core's {@link splitBlockRoots} — the ONE reader every\n * blocks writer shares. This is the canvas's typed view of it: project-graph\n * sits below rich-text and cannot name {@link BlockNode}, so the carried items\n * arrive `unknown`; they are typed here, not checked (preservation over parsing).\n */\nexport function splitCompositionValue(value: unknown): SplitCompositionValue {\n return splitBlockRoots(value) as SplitCompositionValue\n}\n\n/**\n * Merge canvas-edited component roots back into the stored sequence (EC-547).\n * The merge rule — preserved items anchored to their ordinal position between\n * components, every root tagged, the write always v2 — is core's\n * {@link mergeBlockRoots} (EC-576: the same merge every blocks writer goes\n * through, via `editBlockRoots`). Typed here as the rich-text value.\n */\nexport function mergeCompositionValue(\n roots: readonly ComponentNode[],\n preserved: readonly PreservedBlockNode[],\n): BlockSequenceValue {\n return mergeBlockRoots(roots, preserved)\n}\n\n/**\n * The field scope the editor's store carries (EC-554): the field's label (named\n * by refusal notices) and its top-level `allow`, passed through verbatim — an\n * omitted `allow` stays `undefined` (EC-544: any registered component).\n */\nexport function compositionFieldScope(field: BlocksFieldDef): CompositionFieldScope {\n return { label: fieldLabel(field), allow: field.allow }\n}\n\n/**\n * Wrap the field value's component roots in a synthetic root container (EC-287).\n * The builder store edits this single root-bearing composition directly; its\n * children are the field's component blocks, lifted back out via `onChange`.\n * EC-591: the root is the component that fills the `layout.stack` role — the\n * project's own, from its component list — never a hardcoded engine id.\n */\nfunction buildSyntheticRoot(roots: readonly ComponentNode[], stackComponentId: string): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: stackComponentId,\n slots: { [DEFAULT_SLOT]: [...roots] },\n }\n}\n\n/**\n * The canvas notices for roles nobody in the component list fills (EC-591) — each\n * names the gesture that is OFF because of it, so a missing role is visible, never\n * a silent dead affordance. Only gestures the canvas actually has are listed: the\n * `layout.stack` role is the canvas itself (see {@link CompositionFieldEditor}),\n * `layout.grid` the wrap-beside gesture, `text` the seeded layout presets.\n * (`richText` — the typography projection, EC-549 — and `image` — asset drop —\n * have no canvas gesture yet, so there is nothing to switch off.) Pure.\n */\nexport function canvasRoleNotices(registry: ComponentRegistry): MessageKey[] {\n const notices: MessageKey[] = []\n if (!registry.roleComponent('layout.grid')) notices.push('builder.roles.noGrid')\n if (!registry.roleComponent('text')) notices.push('builder.roles.noText')\n return notices\n}\n\n/** The role notices above the canvas (EC-591) — a standing truth about the component list. */\nexport function CanvasRoleNotices({ notices }: { notices: readonly MessageKey[] }) {\n if (notices.length === 0) return null\n return (\n <div data-testid=\"canvas-role-notices\" className=\"shrink-0 border-b border-border bg-muted/40\">\n {notices.map((key) => (\n <Row key={key} gap=\"2\" align=\"center\" className=\"px-3 py-1.5\">\n <AlertTriangle aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"muted\">\n {translate(key)}\n </Text>\n </Row>\n ))}\n </div>\n )\n}\n\n/**\n * The canvas in place of the editor when nobody fills `layout.stack` (EC-591): the\n * blocks of a composition field compose inside that component, so there is no\n * canvas without it. The value is not touched — nothing mounts that could write.\n */\nexport function NoStackCanvasNotice() {\n return (\n <Panel data-testid=\"canvas-no-stack\" className=\"flex h-full min-h-0 items-center justify-center p-6\">\n <Row gap=\"2\" align=\"center\" className=\"max-w-prose\">\n <AlertTriangle aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.roles.noStack')}\n </Text>\n </Row>\n </Panel>\n )\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Split the value ONCE at mount (EC-547): component nodes become the canvas\n // roots; every other sequence item is preserved aside for write-back. The\n // editor is uncontrolled thereafter — it is the source of truth while open and\n // lifts changes out via onChange; re-opening the tab remounts from the current\n // draft. (BuilderProvider reads `composition` only in its lazy initial state,\n // so later prop changes are ignored regardless.)\n const initial = useMemo(() => splitCompositionValue(value), [])\n // EC-591: the roles come from the project's component list (the registry).\n const registry = getStudioRegistry().registry\n const stack = registry.roleComponent('layout.stack')\n const layout = registry.layout\n const notices = useMemo(() => canvasRoleNotices(registry), [registry])\n const initialRoot = useMemo(\n () => (stack ? buildSyntheticRoot(initial.roots, stack.id) : null),\n [initial, stack],\n )\n // EC-554: the field's `allow` travels WITH the store, so every placement\n // surface (canvas + tree DnD, paste, the insert offer) judges the same value.\n const fieldScope = useMemo(() => compositionFieldScope(field), [field])\n\n if (!initialRoot) return <NoStackCanvasNotice />\n\n return (\n <BuilderProvider composition={initialRoot} field={fieldScope} layout={layout}>\n <CompositionLift preserved={initial.preserved} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n {/* EC-408: the field↔component selection-sync channel wraps BOTH the canvas\n and the inspector (they reach each other through nothing else). */}\n <PropSyncProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n trailingPanes={trailingPanes}\n preservedCount={initial.preserved.length}\n roleNotices={notices}\n />\n </PropSyncProvider>\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * EC-549 interim (scope item 3, the honesty floor): a mixed sequence's typography\n * items are preserved for write-back but NOT rendered on the canvas — and a canvas\n * that silently shows half the sequence lets an editor rearrange around content\n * they cannot see. Until typography items are first-class, path-addressed canvas\n * items (EC-549's main scope), the surface must SAY what it is not showing: a\n * persistent, non-dismissible count above the canvas. Deliberately NOT the\n * dismissible overlay shape (`CanvasThemeNotice`) — this states a standing truth\n * about the VALUE, not a one-time migration hint, and it must not cover blocks.\n */\nexport function PreservedItemsNotice({ count }: { count: number }) {\n if (count === 0) return null\n return (\n <Row\n gap=\"2\"\n align=\"center\"\n data-testid=\"composition-preserved-notice\"\n className=\"shrink-0 border-b border-border bg-muted/40 px-3 py-1.5\"\n >\n <Pilcrow aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"muted\">\n {translate('builder.composition.preservedNotice', { count })}\n </Text>\n </Row>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the merged v2 sequence value (EC-547) — the canvas roots spliced back\n * between the preserved typography items. Cardinality (`multiple`) no longer\n * shapes the stored value: it is a validation constraint over the one envelope\n * (EC-500 D2), so the bare-object special case for `multiple: false` is gone.\n */\nfunction CompositionLift({\n preserved,\n onChange,\n readOnly,\n}: {\n preserved: readonly PreservedBlockNode[]\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(mergeCompositionValue(blocks, preserved))\n }\n }, [blocks, preserved, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n trailingPanes,\n preservedCount = 0,\n roleNotices,\n}: {\n readOnly: boolean\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n /** Preserved (non-component) sequence items the canvas cannot show (EC-549 interim). */\n preservedCount?: number\n /** Notices for the roles nobody fills (EC-591) — see {@link canvasRoleNotices}. */\n roleNotices?: readonly MessageKey[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n // The field's top-level component vocabulary (EC-188) — the leash applied to\n // inserts at the composition root so forbidden components aren't even offered.\n // Read from the store's field scope (EC-554), the same value DnD and paste judge.\n const fieldAllow = builder.field.allow\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div className={cn('h-full min-h-0 overflow-hidden', readOnly && 'pointer-events-none opacity-60')}>\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n // The single insert entry point for BOTH the toolbar and the page tree's\n // \"Add child\" (EC-383). It selects its target first so `builder.selectedId`\n // is the one source of truth for where an insert lands: the toolbar resolves\n // `builder.selectedId ?? rootId` while Add-child passes an explicit row, and\n // without this sync clicking Add-child (which otherwise leaves selection put)\n // could target a different node than the toolbar — surfacing as a mismatched\n // (even empty \"No allowed components match\") insert dialog. Both now resolve\n // the identical target + allow set for the same selection.\n function openInsert(nodeId: string) {\n const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n\n // EC-405: the in-canvas \"+\" affordances resolve the SAME slot rules through the\n // shared position-carrying insert-context resolution, and open the panel at the\n // clicked edge (an explicit position that survives to the insertion, rather than\n // the panel re-deriving it from the selection). Re-memoized on every store change\n // so the closures read the live graph.\n const affordanceApi = useMemo<InsertAffordanceApi>(\n () => ({\n positionsFor: (nodeId: string) => insertablePositions(builder, nodeId, fieldAllow),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, fieldAllow, dispatch],\n )\n\n return (\n <InsertAffordanceProvider value={affordanceApi}>\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}>\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div className={cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60')}>\n <PreservedItemsNotice count={preservedCount} />\n <CanvasRoleNotices notices={roleNotices ?? []} />\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : 'draft'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n </InsertAffordanceProvider>\n )\n}\n"]}
@@ -63,21 +63,24 @@ export interface InlineEditableProp {
63
63
  /**
64
64
  * Resolve which prop of `node` an inline-edit gesture should target: the
65
65
  * first manifest prop flagged `inlineEditable` whose *current* value is a
66
- * plain static string (or unset with a string default). Bound props and
67
- * non-string values are never inline-editable — they stay inspector-only.
66
+ * plain static string, or that is unset and `text`-typed — an unset prop starts
67
+ * EMPTY (EC-591/2f: absent is `undefined`; the manifest `default` is a hint, never
68
+ * the value being edited). Bound props and non-string values are never
69
+ * inline-editable — they stay inspector-only.
68
70
  */
69
71
  export declare function resolveInlineEditableProp(manifest: ComponentManifest | undefined, node: ComponentNode): InlineEditableProp | null;
70
72
  /** A node prop carrying rich-text content that can be edited in place now. */
71
73
  export interface InlineRichTextProp {
72
74
  propName: string;
73
- /** The current stored value (or the manifest default when unset). */
75
+ /** The current stored value (an empty document when unset — EC-591/2f). */
74
76
  value: RichTextValue;
75
77
  }
76
78
  /**
77
79
  * Resolve which rich-text prop of `node` an inline-edit gesture should
78
80
  * target: the first manifest prop flagged `inlineEditable` whose inspector
79
81
  * control is `richtext` and whose *current* value is a static, schema-valid
80
- * rich-text value (or unset with a valid rich-text default). Bound props are
82
+ * rich-text value, or that is unset — then the session starts from an EMPTY
83
+ * document (EC-591/2f: never the manifest `default`, a hint only). Bound props are
81
84
  * never inline-editable — translating/bound content stays inspector- and
82
85
  * CMS-owned, exactly like EC-158's plain-text rule.
83
86
  */
@@ -29,8 +29,10 @@ export function createInlineEditSession(args) {
29
29
  /**
30
30
  * Resolve which prop of `node` an inline-edit gesture should target: the
31
31
  * first manifest prop flagged `inlineEditable` whose *current* value is a
32
- * plain static string (or unset with a string default). Bound props and
33
- * non-string values are never inline-editable — they stay inspector-only.
32
+ * plain static string, or that is unset and `text`-typed — an unset prop starts
33
+ * EMPTY (EC-591/2f: absent is `undefined`; the manifest `default` is a hint, never
34
+ * the value being edited). Bound props and non-string values are never
35
+ * inline-editable — they stay inspector-only.
34
36
  */
35
37
  export function resolveInlineEditableProp(manifest, node) {
36
38
  if (!manifest)
@@ -38,9 +40,8 @@ export function resolveInlineEditableProp(manifest, node) {
38
40
  for (const propName of inlineEditablePropNames(manifest)) {
39
41
  const current = node.props?.[propName];
40
42
  if (current === undefined) {
41
- const fallback = manifest.props[propName]?.default;
42
- if (typeof fallback === 'string')
43
- return { propName, value: fallback };
43
+ if (manifest.props[propName]?.type === 'text')
44
+ return { propName, value: '' };
44
45
  continue;
45
46
  }
46
47
  if (isStaticProp(current) && typeof current.value === 'string') {
@@ -53,7 +54,8 @@ export function resolveInlineEditableProp(manifest, node) {
53
54
  * Resolve which rich-text prop of `node` an inline-edit gesture should
54
55
  * target: the first manifest prop flagged `inlineEditable` whose inspector
55
56
  * control is `richtext` and whose *current* value is a static, schema-valid
56
- * rich-text value (or unset with a valid rich-text default). Bound props are
57
+ * rich-text value, or that is unset — then the session starts from an EMPTY
58
+ * document (EC-591/2f: never the manifest `default`, a hint only). Bound props are
57
59
  * never inline-editable — translating/bound content stays inspector- and
58
60
  * CMS-owned, exactly like EC-158's plain-text rule.
59
61
  */
@@ -65,15 +67,16 @@ export function resolveInlineRichTextProp(manifest, node) {
65
67
  if (manifest.props[propName]?.type !== 'richText')
66
68
  continue;
67
69
  const current = node.props?.[propName];
68
- const raw = current === undefined ? manifest.props[propName]?.default : undefined;
69
70
  if (current !== undefined && !isStaticProp(current))
70
71
  continue;
71
- const parsed = parseRichTextValue(isStaticProp(current) ? current.value : raw);
72
+ const parsed = parseRichTextValue(isStaticProp(current) ? current.value : EMPTY_RICH_TEXT);
72
73
  if (parsed.ok)
73
74
  return { propName, value: parsed.value };
74
75
  }
75
76
  return null;
76
77
  }
78
+ /** The document an unset rich-text prop starts its inline session from (EC-591/2f). */
79
+ const EMPTY_RICH_TEXT = { version: 2, content: [{ type: 'paragraph' }] };
77
80
  /**
78
81
  * One rich-text editing session (EC-159), with EC-158's one-finish guarantee:
79
82
  * the first `commit()`/`cancel()` wins and every later call is inert, so