@elytracms/studio 0.0.63 → 0.0.65

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 (146) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/assets/asset-thumb.d.ts +4 -2
  5. package/dist/components/assets/asset-thumb.js +26 -4
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/assets/assets-module.js +48 -2
  8. package/dist/components/assets/assets-module.js.map +1 -1
  9. package/dist/components/assets/assets-store.d.ts +11 -1
  10. package/dist/components/assets/assets-store.js +18 -1
  11. package/dist/components/assets/assets-store.js.map +1 -1
  12. package/dist/components/builder/blocks-model.d.ts +11 -0
  13. package/dist/components/builder/blocks-model.js +26 -8
  14. package/dist/components/builder/blocks-model.js.map +1 -1
  15. package/dist/components/builder/canvas-chrome-css.d.ts +54 -0
  16. package/dist/components/builder/canvas-chrome-css.js +246 -0
  17. package/dist/components/builder/canvas-chrome-css.js.map +1 -0
  18. package/dist/components/builder/canvas-chrome.d.ts +122 -18
  19. package/dist/components/builder/canvas-chrome.js +148 -82
  20. package/dist/components/builder/canvas-chrome.js.map +1 -1
  21. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  22. package/dist/components/builder/canvas-inline-editing.js +3 -1
  23. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  24. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  25. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  26. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  27. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  28. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  29. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  30. package/dist/components/builder/canvas-render-context.d.ts +13 -0
  31. package/dist/components/builder/canvas-render-context.js +7 -5
  32. package/dist/components/builder/canvas-render-context.js.map +1 -1
  33. package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
  34. package/dist/components/builder/canvas-stylesheet.js +16 -9
  35. package/dist/components/builder/canvas-stylesheet.js.map +1 -1
  36. package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
  37. package/dist/components/builder/canvas-theme-notice.js +46 -0
  38. package/dist/components/builder/canvas-theme-notice.js.map +1 -0
  39. package/dist/components/builder/canvas.js +78 -22
  40. package/dist/components/builder/canvas.js.map +1 -1
  41. package/dist/components/builder/composition-field-editor.js +22 -3
  42. package/dist/components/builder/composition-field-editor.js.map +1 -1
  43. package/dist/components/builder/delete-key.d.ts +38 -0
  44. package/dist/components/builder/delete-key.js +46 -0
  45. package/dist/components/builder/delete-key.js.map +1 -0
  46. package/dist/components/builder/drag-session.d.ts +8 -0
  47. package/dist/components/builder/drag-session.js +15 -0
  48. package/dist/components/builder/drag-session.js.map +1 -1
  49. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  50. package/dist/components/builder/insert-affordance-context.js +7 -0
  51. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  52. package/dist/components/builder/insert-context.d.ts +40 -0
  53. package/dist/components/builder/insert-context.js +86 -3
  54. package/dist/components/builder/insert-context.js.map +1 -1
  55. package/dist/components/builder/node-title.d.ts +60 -0
  56. package/dist/components/builder/node-title.js +135 -0
  57. package/dist/components/builder/node-title.js.map +1 -0
  58. package/dist/components/builder/page-tree.js +21 -3
  59. package/dist/components/builder/page-tree.js.map +1 -1
  60. package/dist/components/builder/prop-selection.d.ts +52 -0
  61. package/dist/components/builder/prop-selection.js +38 -0
  62. package/dist/components/builder/prop-selection.js.map +1 -0
  63. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  64. package/dist/components/builder/prop-sync-context.js +23 -0
  65. package/dist/components/builder/prop-sync-context.js.map +1 -0
  66. package/dist/components/builder/property-inspector.js +23 -1
  67. package/dist/components/builder/property-inspector.js.map +1 -1
  68. package/dist/components/builder/reuse-context.js +1 -1
  69. package/dist/components/builder/reuse-context.js.map +1 -1
  70. package/dist/components/builder/rich-text-toolbar.d.ts +23 -0
  71. package/dist/components/builder/rich-text-toolbar.js +24 -17
  72. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  73. package/dist/components/builder/selection-overlay.d.ts +79 -0
  74. package/dist/components/builder/selection-overlay.js +244 -0
  75. package/dist/components/builder/selection-overlay.js.map +1 -0
  76. package/dist/components/builder/spacing-handles.d.ts +88 -1
  77. package/dist/components/builder/spacing-handles.js +191 -63
  78. package/dist/components/builder/spacing-handles.js.map +1 -1
  79. package/dist/components/cms/asset-picker.js +11 -5
  80. package/dist/components/cms/asset-picker.js.map +1 -1
  81. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  82. package/dist/components/cms/rich-text/block-style.js +30 -0
  83. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  84. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  85. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  86. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  87. package/dist/components/cms/rich-text/extensions.js +4 -0
  88. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  89. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  90. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  91. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  92. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  93. package/dist/components/cms/rich-text/font-size.js +40 -0
  94. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  95. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  96. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  97. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  98. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  99. package/dist/components/cms/rich-text/toolbar.js +17 -5
  100. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  101. package/dist/components/components/component-detail.js +3 -3
  102. package/dist/components/components/component-detail.js.map +1 -1
  103. package/dist/components/components/component-preview.d.ts +2 -1
  104. package/dist/components/components/component-preview.js +25 -26
  105. package/dist/components/components/component-preview.js.map +1 -1
  106. package/dist/components/components/registry-browser.js +2 -2
  107. package/dist/components/components/registry-browser.js.map +1 -1
  108. package/dist/components/components/taxonomy.js +3 -2
  109. package/dist/components/components/taxonomy.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.js +1 -1
  111. package/dist/components/components/transfer-dialog.js.map +1 -1
  112. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  113. package/dist/lib/assets/backfill-thumbs.js +120 -0
  114. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  115. package/dist/lib/assets/delete-asset.d.ts +2 -1
  116. package/dist/lib/assets/delete-asset.js +12 -0
  117. package/dist/lib/assets/delete-asset.js.map +1 -1
  118. package/dist/lib/assets/index.d.ts +2 -0
  119. package/dist/lib/assets/index.js +2 -0
  120. package/dist/lib/assets/index.js.map +1 -1
  121. package/dist/lib/assets/thumbnail.d.ts +145 -0
  122. package/dist/lib/assets/thumbnail.js +220 -0
  123. package/dist/lib/assets/thumbnail.js.map +1 -0
  124. package/dist/lib/assets/upload.d.ts +9 -0
  125. package/dist/lib/assets/upload.js +31 -0
  126. package/dist/lib/assets/upload.js.map +1 -1
  127. package/dist/lib/i18n/messages.de.js +20 -0
  128. package/dist/lib/i18n/messages.de.js.map +1 -1
  129. package/dist/lib/i18n/messages.en.d.ts +18 -0
  130. package/dist/lib/i18n/messages.en.js +21 -0
  131. package/dist/lib/i18n/messages.en.js.map +1 -1
  132. package/dist/lib/persistence/backend-mode.d.ts +7 -0
  133. package/dist/lib/persistence/backend-mode.js +9 -0
  134. package/dist/lib/persistence/backend-mode.js.map +1 -1
  135. package/dist/lib/persistence/convex-adapter.js +4 -0
  136. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  137. package/dist/lib/persistence/index.d.ts +1 -1
  138. package/dist/lib/persistence/index.js +1 -1
  139. package/dist/lib/persistence/index.js.map +1 -1
  140. package/dist/lib/persona/labels.d.ts +9 -5
  141. package/dist/lib/persona/labels.js +13 -8
  142. package/dist/lib/persona/labels.js.map +1 -1
  143. package/dist/lib/references/references-store.js +7 -1
  144. package/dist/lib/references/references-store.js.map +1 -1
  145. package/package.json +2 -2
  146. package/src/styles/studio.css +13 -0
@@ -3,17 +3,24 @@
3
3
  * The canvas renders the project graph inside an ISOLATED iframe, so this stylesheet is
4
4
  * scoped to that document alone — it is NOT the studio chrome stylesheet, and reusing the
5
5
  * chrome's stylesheet here is a mistake (it makes the consuming app's delivery CSS and the
6
- * studio UI share one file, the only way a leak between them becomes possible). The preview
7
- * stylesheet should carry: (1) the consuming app's DELIVERY CSS, since the rendered blocks
8
- * are the consuming app's own components styled by its delivery theme; and (2) the studio
9
- * CSS, since the inline editing controls (RichTextToolbar, SpacingHandles) are portaled
10
- * INTO this iframe. Both are BUILD artifacts of the consuming app (its Tailwind build picks
11
- * up the package's `@source`), so the package cannot import them — the app injects its own
12
- * compiled URL once at mount via {@link setCanvasStylesheetUrl}, and the canvas reads it
13
- * here. Unset (e.g. tests / SSR) → the canvas simply skips the stylesheet link.
6
+ * studio UI share one file, the only way a leak between them becomes possible).
7
+ *
8
+ * EC-414 (AD-A): the preview stylesheet is the consuming app's DELIVERY theme — its
9
+ * standalone full-Tailwind theme entry (`components/theme.css`, or the frontend's own
10
+ * self-contained entry), compiled by the app's own build. The studio design system does
11
+ * NOT belong in it: everything the studio portals into the iframe (RichTextToolbar,
12
+ * SpacingHandles, placeholders) is styled by the engine-injected chrome stylesheet
13
+ * (`canvas-chrome-css.ts`), so the canvas resolves exactly what delivery resolves.
14
+ * Legacy merged stylesheets (theme + `@elytracms/studio/styles.css` in one build) keep
15
+ * working via the runtime world probe in `canvas-chrome.ts` (AD-C).
16
+ *
17
+ * The URL is a BUILD artifact of the consuming app, so the package cannot import it —
18
+ * the app injects its own compiled URL once at mount via {@link setCanvasStylesheetUrl},
19
+ * and the canvas reads it here. Unset (e.g. tests / SSR) → the canvas simply skips the
20
+ * stylesheet link (and dev builds surface the explicit `no-stylesheet` state).
14
21
  */
15
22
  let stylesheetUrl = null;
16
- /** The consuming app supplies its compiled studio CSS URL at mount. */
23
+ /** The consuming app supplies its compiled canvas stylesheet URL at mount. */
17
24
  export function setCanvasStylesheetUrl(url) {
18
25
  stylesheetUrl = url;
19
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-stylesheet.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-stylesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,IAAI,aAAa,GAAkB,IAAI,CAAA;AAEvC,uEAAuE;AACvE,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,aAAa,GAAG,GAAG,CAAA;AACrB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,sBAAsB;IACpC,OAAO,aAAa,CAAA;AACtB,CAAC","sourcesContent":["/**\n * The PREVIEW stylesheet URL, injected into the canvas iframe (EC-117 / EC-223 Stage 4).\n * The canvas renders the project graph inside an ISOLATED iframe, so this stylesheet is\n * scoped to that document alone — it is NOT the studio chrome stylesheet, and reusing the\n * chrome's stylesheet here is a mistake (it makes the consuming app's delivery CSS and the\n * studio UI share one file, the only way a leak between them becomes possible). The preview\n * stylesheet should carry: (1) the consuming app's DELIVERY CSS, since the rendered blocks\n * are the consuming app's own components styled by its delivery theme; and (2) the studio\n * CSS, since the inline editing controls (RichTextToolbar, SpacingHandles) are portaled\n * INTO this iframe. Both are BUILD artifacts of the consuming app (its Tailwind build picks\n * up the package's `@source`), so the package cannot import them — the app injects its own\n * compiled URL once at mount via {@link setCanvasStylesheetUrl}, and the canvas reads it\n * here. Unset (e.g. tests / SSR) → the canvas simply skips the stylesheet link.\n */\nlet stylesheetUrl: string | null = null\n\n/** The consuming app supplies its compiled studio CSS URL at mount. */\nexport function setCanvasStylesheetUrl(url: string): void {\n stylesheetUrl = url\n}\n\n/** The injected stylesheet URL, or `null` when the app has not supplied one. */\nexport function getCanvasStylesheetUrl(): string | null {\n return stylesheetUrl\n}\n"]}
1
+ {"version":3,"file":"canvas-stylesheet.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-stylesheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,IAAI,aAAa,GAAkB,IAAI,CAAA;AAEvC,8EAA8E;AAC9E,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,aAAa,GAAG,GAAG,CAAA;AACrB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,sBAAsB;IACpC,OAAO,aAAa,CAAA;AACtB,CAAC","sourcesContent":["/**\n * The PREVIEW stylesheet URL, injected into the canvas iframe (EC-117 / EC-223 Stage 4).\n * The canvas renders the project graph inside an ISOLATED iframe, so this stylesheet is\n * scoped to that document alone — it is NOT the studio chrome stylesheet, and reusing the\n * chrome's stylesheet here is a mistake (it makes the consuming app's delivery CSS and the\n * studio UI share one file, the only way a leak between them becomes possible).\n *\n * EC-414 (AD-A): the preview stylesheet is the consuming app's DELIVERY theme — its\n * standalone full-Tailwind theme entry (`components/theme.css`, or the frontend's own\n * self-contained entry), compiled by the app's own build. The studio design system does\n * NOT belong in it: everything the studio portals into the iframe (RichTextToolbar,\n * SpacingHandles, placeholders) is styled by the engine-injected chrome stylesheet\n * (`canvas-chrome-css.ts`), so the canvas resolves exactly what delivery resolves.\n * Legacy merged stylesheets (theme + `@elytracms/studio/styles.css` in one build) keep\n * working via the runtime world probe in `canvas-chrome.ts` (AD-C).\n *\n * The URL is a BUILD artifact of the consuming app, so the package cannot import it —\n * the app injects its own compiled URL once at mount via {@link setCanvasStylesheetUrl},\n * and the canvas reads it here. Unset (e.g. tests / SSR) → the canvas simply skips the\n * stylesheet link (and dev builds surface the explicit `no-stylesheet` state).\n */\nlet stylesheetUrl: string | null = null\n\n/** The consuming app supplies its compiled canvas stylesheet URL at mount. */\nexport function setCanvasStylesheetUrl(url: string): void {\n stylesheetUrl = url\n}\n\n/** The injected stylesheet URL, or `null` when the app has not supplied one. */\nexport function getCanvasStylesheetUrl(): string | null {\n return stylesheetUrl\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { CanvasThemeStatus } from './canvas-chrome.js';
2
+ /** Test hook: reset the session-sticky dismissals. */
3
+ export declare function resetCanvasThemeNoticeForTests(): void;
4
+ export declare function CanvasThemeNotice({ status }: {
5
+ status: CanvasThemeStatus | null;
6
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { AlertTriangle, X } from 'lucide-react';
4
+ import { translate } from '@elytracms/studio/lib/i18n';
5
+ import { isDevRuntime } from '@elytracms/studio/lib/persistence';
6
+ import { Row, Text } from '@elytracms/studio/ui';
7
+ /**
8
+ * EC-414 AD-D item 2 — the canvas-theme runtime notice, in the established
9
+ * validation-surface style (the `ClipboardNoticeOverlay` shape: a dismissible,
10
+ * NEVER-blocking overlay above the canvas, in the STUDIO document — studio UI
11
+ * primitives are fine here, this is not in-frame chrome). Dev builds only:
12
+ *
13
+ * - `legacy`: the probe found the merged pre-EC-414 canvas.css — a one-time
14
+ * notice naming the migration (the canvas keeps working exactly as before).
15
+ * - `stylesheet-error` / `no-theme-tokens` / `no-stylesheet`: the canvas
16
+ * stylesheet is broken or missing — the invalid state is named explicitly
17
+ * instead of silently rendering unstyled blocks.
18
+ *
19
+ * "One-time": dismissing a kind suppresses it for the rest of the studio
20
+ * session (module state survives canvas remounts/navigation).
21
+ */
22
+ const dismissedKinds = new Set();
23
+ /** Test hook: reset the session-sticky dismissals. */
24
+ export function resetCanvasThemeNoticeForTests() {
25
+ dismissedKinds.clear();
26
+ }
27
+ const NOTICE_KEYS = {
28
+ legacy: 'builder.canvasTheme.legacyNotice',
29
+ 'stylesheet-error': 'builder.canvasTheme.stylesheetErrorNotice',
30
+ 'no-theme-tokens': 'builder.canvasTheme.noThemeTokensNotice',
31
+ 'no-stylesheet': 'builder.canvasTheme.noStylesheetNotice',
32
+ };
33
+ export function CanvasThemeNotice({ status }) {
34
+ // Local state only forces the re-render on dismiss; the truth is module-level.
35
+ const [, setDismissedAt] = useState(0);
36
+ if (!status || !isDevRuntime())
37
+ return null;
38
+ const messageKey = NOTICE_KEYS[status.kind];
39
+ if (!messageKey || dismissedKinds.has(status.kind))
40
+ return null;
41
+ return (_jsx("div", { "data-testid": "canvas-theme-notice", "data-notice-kind": status.kind, className: "absolute top-4 left-1/2 z-20 w-max max-w-[min(460px,90%)] -translate-x-1/2", children: _jsx("div", { className: "rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-lg", children: _jsxs(Row, { gap: "2", align: "start", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0 text-warning", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 flex-1", children: translate(messageKey) }), _jsx("button", { type: "button", "aria-label": translate('common.dismiss'), onClick: () => {
42
+ dismissedKinds.add(status.kind);
43
+ setDismissedAt(Date.now());
44
+ }, className: "shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(X, { className: "size-3.5" }) })] }) }) }));
45
+ }
46
+ //# sourceMappingURL=canvas-theme-notice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-theme-notice.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-theme-notice.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAChE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAGhD;;;;;;;;;;;;;;GAcG;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,EAA6B,CAAA;AAE3D,sDAAsD;AACtD,MAAM,UAAU,8BAA8B;IAC5C,cAAc,CAAC,KAAK,EAAE,CAAA;AACxB,CAAC;AAED,MAAM,WAAW,GAAuD;IACtE,MAAM,EAAE,kCAAkC;IAC1C,kBAAkB,EAAE,2CAA2C;IAC/D,iBAAiB,EAAE,yCAAyC;IAC5D,eAAe,EAAE,wCAAwC;CAC1D,CAAA;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAE,MAAM,EAAwC;IAChF,+EAA+E;IAC/E,MAAM,CAAC,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACtC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE;QAAE,OAAO,IAAI,CAAA;IAC3C,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3C,IAAI,CAAC,UAAU,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAE/D,OAAO,CACL,6BACc,qBAAqB,sBACf,MAAM,CAAC,IAAI,EAC7B,SAAS,EAAC,4EAA4E,YAEtF,cAAK,SAAS,EAAC,kFAAkF,YAC/F,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,OAAO,aACxB,KAAC,aAAa,IAAC,SAAS,EAAC,uCAAuC,wBAAe,EAC/E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,gBAAgB,YACvC,SAAS,CAAC,UAAU,CAAC,GACjB,EACP,iBACE,IAAI,EAAC,QAAQ,gBACD,SAAS,CAAC,gBAAgB,CAAC,EACvC,OAAO,EAAE,GAAG,EAAE;4BACZ,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;4BAC/B,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;wBAC5B,CAAC,EACD,SAAS,EAAC,mFAAmF,YAE7F,KAAC,CAAC,IAAC,SAAS,EAAC,UAAU,GAAG,GACnB,IACL,GACF,GACF,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport { AlertTriangle, X } from 'lucide-react'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { isDevRuntime } from '@elytracms/studio/lib/persistence'\nimport { Row, Text } from '@elytracms/studio/ui'\nimport type { CanvasThemeStatus } from './canvas-chrome'\n\n/**\n * EC-414 AD-D item 2 — the canvas-theme runtime notice, in the established\n * validation-surface style (the `ClipboardNoticeOverlay` shape: a dismissible,\n * NEVER-blocking overlay above the canvas, in the STUDIO document — studio UI\n * primitives are fine here, this is not in-frame chrome). Dev builds only:\n *\n * - `legacy`: the probe found the merged pre-EC-414 canvas.css — a one-time\n * notice naming the migration (the canvas keeps working exactly as before).\n * - `stylesheet-error` / `no-theme-tokens` / `no-stylesheet`: the canvas\n * stylesheet is broken or missing — the invalid state is named explicitly\n * instead of silently rendering unstyled blocks.\n *\n * \"One-time\": dismissing a kind suppresses it for the rest of the studio\n * session (module state survives canvas remounts/navigation).\n */\n\nconst dismissedKinds = new Set<CanvasThemeStatus['kind']>()\n\n/** Test hook: reset the session-sticky dismissals. */\nexport function resetCanvasThemeNoticeForTests(): void {\n dismissedKinds.clear()\n}\n\nconst NOTICE_KEYS: Partial<Record<CanvasThemeStatus['kind'], string>> = {\n legacy: 'builder.canvasTheme.legacyNotice',\n 'stylesheet-error': 'builder.canvasTheme.stylesheetErrorNotice',\n 'no-theme-tokens': 'builder.canvasTheme.noThemeTokensNotice',\n 'no-stylesheet': 'builder.canvasTheme.noStylesheetNotice',\n}\n\nexport function CanvasThemeNotice({ status }: { status: CanvasThemeStatus | null }) {\n // Local state only forces the re-render on dismiss; the truth is module-level.\n const [, setDismissedAt] = useState(0)\n if (!status || !isDevRuntime()) return null\n const messageKey = NOTICE_KEYS[status.kind]\n if (!messageKey || dismissedKinds.has(status.kind)) return null\n\n return (\n <div\n data-testid=\"canvas-theme-notice\"\n data-notice-kind={status.kind}\n className=\"absolute top-4 left-1/2 z-20 w-max max-w-[min(460px,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 <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0 text-warning\" aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 flex-1\">\n {translate(messageKey)}\n </Text>\n <button\n type=\"button\"\n aria-label={translate('common.dismiss')}\n onClick={() => {\n dismissedKinds.add(status.kind)\n setDismissedAt(Date.now())\n }}\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 </div>\n </div>\n )\n}\n"]}
@@ -1,23 +1,35 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { renderPageComposition } from '@elytracms/core/runtime-renderer';
4
+ import { renderPageComposition, setEditingMarkers } from '@elytracms/core/runtime-renderer';
5
+ import { getNode } from '@elytracms/core/project-graph';
5
6
  import { useParams } from '@elytracms/studio/router';
6
7
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
7
8
  import { translate } from '@elytracms/studio/lib/i18n';
8
9
  import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
9
10
  import { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace';
10
- import { applyCanvasChrome } from './canvas-chrome.js';
11
+ import { wireCanvasChrome } from './canvas-chrome.js';
12
+ import { CanvasThemeNotice } from './canvas-theme-notice.js';
11
13
  import { getCanvasStylesheetUrl } from './canvas-stylesheet.js';
12
14
  import { useBuilder } from './builder-store.js';
13
15
  import { useCanvasViewports, viewportWidth } from './canvas-breakpoints.js';
14
- import { nodeIdAt } from './canvas-dom.js';
16
+ import { nodeIdAt, wrapperAt } from './canvas-dom.js';
15
17
  import { useCanvasDnd } from './canvas-dnd.js';
16
18
  import { useCanvasInlineEditing } from './canvas-inline-editing.js';
17
19
  import { useCanvasRenderContext } from './canvas-render-context.js';
18
20
  import { ClipboardNoticeOverlay, attachClipboardListeners, useStudioClipboard, } from './clipboard-actions.js';
19
21
  import { RichTextToolbar } from './rich-text-toolbar.js';
20
22
  import { SpacingHandles, hasSpacingHandles } from './spacing-handles.js';
23
+ import { SelectionOverlay } from './selection-overlay.js';
24
+ import { attachDeleteKeyListener } from './delete-key.js';
25
+ import { usePropSync } from './prop-sync-context.js';
26
+ import { resolvePropClick } from './prop-selection.js';
27
+ // EC-408: the studio canvas is the ONE surface that turns editing markers on, so
28
+ // `ecProp` in the base primitives emits `data-ec-prop` here. Delivery
29
+ // (`@elytracms/next`) is a separate build that never imports this module, so its
30
+ // singleton stays off and shipped pages carry zero marker noise. Module-level
31
+ // (not an effect) so markers are on before the first primitive renders.
32
+ setEditingMarkers(true);
21
33
  /**
22
34
  * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders
23
35
  * into a same-origin iframe via a React portal — true style isolation + true
@@ -48,6 +60,9 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
48
60
  // (EC-117) — injected after app.css so the kit's overrides win in the cascade.
49
61
  const params = useParams({ strict: false });
50
62
  const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined;
63
+ // EC-414 AD-D: the settled canvas-theme classification (legacy merged shape /
64
+ // broken stylesheet) drives the dev-only, never-blocking notice overlay.
65
+ const [themeStatus, setThemeStatus] = useState(null);
51
66
  const wireUp = useCallback(() => {
52
67
  const d = iframeRef.current?.contentDocument;
53
68
  if (!d || !d.body)
@@ -57,18 +72,21 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
57
72
  // as a light, site-like page — never the studio chrome's current theme (a
58
73
  // black canvas is exactly what leaks through when the studio is
59
74
  // dark-themed and the iframe root inherits that class). The injected
60
- // canvas stylesheet's `:root` (light) tokens supply that backdrop for free
61
- // once `.dark` is never added here (mirrors the components-preview fix,
62
- // `component-preview.tsx`). The editing chrome (selection/hover outlines,
75
+ // canvas stylesheet (new world: the chrome's neutral backdrop; legacy
76
+ // world: the studio `:root` tokens) supplies that backdrop once `.dark` is
77
+ // never added here (mirrors the components-preview surface, which wires
78
+ // through the same chrome). The editing chrome (selection/hover outlines,
63
79
  // drop indicators, the rich-text toolbar, locale-fallback chips) uses
64
- // hardcoded colors rather than these tokens, so it stays visible against a
65
- // light page regardless. The full injection — stylesheet link, editing
66
- // chrome, kit tokens, and the consumer's `defineStudio({ canvas })` theme
67
- // (EC-389, injected LAST so it wins) — lives in `applyCanvasChrome`.
68
- applyCanvasChrome(d, {
80
+ // hardcoded colors rather than tokens, so it stays visible against a light
81
+ // page regardless. The full injection — stylesheet link, editing chrome,
82
+ // kit tokens, the consumer's `defineStudio({ canvas })` theme (EC-389,
83
+ // injected LAST so it wins), and the EC-414 world probe — lives in
84
+ // `wireCanvasChrome`/`applyCanvasChrome`.
85
+ wireCanvasChrome(d, {
69
86
  stylesheetUrl: getCanvasStylesheetUrl(),
70
87
  kitTokensCss,
71
88
  theme: resolveDeploymentCanvasTheme(),
89
+ onStatus: (status) => setThemeStatus((prev) => (prev?.kind === status.kind ? prev : status)),
72
90
  });
73
91
  setDoc(d);
74
92
  }, [kitTokensCss]);
@@ -81,7 +99,13 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
81
99
  // live session ref other listeners consult, the rich-text host (mark
82
100
  // toolbar), and the per-node remount epochs (decorate keys).
83
101
  const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio });
84
- const { editingRef, richEditHost, richEditEpochs } = inline;
102
+ const { editingRef, richEditHost, richEditEpochs, editing } = inline;
103
+ // EC-408: the field↔component sync channel (null in the component preview,
104
+ // which has no inspector). Read live through a ref — the native iframe
105
+ // listeners outlive renders.
106
+ const propSync = usePropSync();
107
+ const propSyncRef = useRef(propSync);
108
+ propSyncRef.current = propSync;
85
109
  // Native selection/hover listeners inside the iframe document.
86
110
  useEffect(() => {
87
111
  if (!doc)
@@ -90,11 +114,28 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
90
114
  // Clicks inside the active inline editor are caret moves, not selection.
91
115
  if (editingRef.current?.host.contains(e.target))
92
116
  return;
93
- const id = nodeIdAt(e.target);
94
- if (id) {
95
- e.preventDefault();
96
- dispatch({ type: 'select', id });
97
- }
117
+ const wrapper = wrapperAt(e.target);
118
+ if (!wrapper)
119
+ return;
120
+ const id = wrapper.getAttribute('data-ec-node-id');
121
+ e.preventDefault();
122
+ dispatch({ type: 'select', id });
123
+ // EC-408: resolve the clicked sub-part to its `data-ec-prop` and, unless it
124
+ // is inline-editable (inline editing stays primary), ask the inspector to
125
+ // focus that prop's control. Unannotated components resolve no marker → the
126
+ // node-level select above is the whole behavior, exactly as before.
127
+ const sync = propSyncRef.current;
128
+ if (!sync)
129
+ return;
130
+ const node = getNode(presentRef.current, id);
131
+ const manifest = node ? studio.registry.getManifest(node.componentId) : undefined;
132
+ const { propName, focusField } = resolvePropClick({
133
+ target: e.target,
134
+ wrapper,
135
+ manifest,
136
+ });
137
+ if (propName && focusField)
138
+ sync.requestFocus({ nodeId: id, propName });
98
139
  };
99
140
  const onOver = (e) => dispatch({ type: 'hover', id: nodeIdAt(e.target) });
100
141
  const onLeave = () => dispatch({ type: 'hover', id: null });
@@ -106,7 +147,7 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
106
147
  doc.removeEventListener('mouseover', onOver);
107
148
  doc.removeEventListener('mouseleave', onLeave, true);
108
149
  };
109
- }, [doc, dispatch, editingRef]);
150
+ }, [doc, dispatch, editingRef, presentRef, studio]);
110
151
  // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.
111
152
  useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef });
112
153
  // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page
@@ -126,6 +167,18 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
126
167
  getSelection: () => doc.getSelection(),
127
168
  });
128
169
  }, [doc, clipboard, editingRef]);
170
+ // Delete/Backspace removes the selected node (EC-404) — the keyboard remove
171
+ // the studio was missing, wired beside the clipboard listeners on the iframe
172
+ // document, guarded so typing in an inline editor edits text instead.
173
+ useEffect(() => {
174
+ if (!doc)
175
+ return;
176
+ return attachDeleteKeyListener(doc, {
177
+ getSelectedId: () => selectedIdRef.current,
178
+ remove: (id) => dispatch({ type: 'remove', id }),
179
+ getRootId: () => presentRef.current.id,
180
+ });
181
+ }, [doc, dispatch]);
129
182
  // The render context: the seven delivery-parity resolvers + the editable-
130
183
  // wrapper decorator (selection/hover, locale chips, remount epochs) — see
131
184
  // `canvas-render-context`.
@@ -138,19 +191,22 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
138
191
  richEditEpochs,
139
192
  });
140
193
  // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children
141
- // gets draggable gap handles between them, portaled into the iframe.
142
- const spacingTarget = doc && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)
194
+ // gets draggable gap handles between them, portaled into the iframe. Suppressed
195
+ // while an inline text/rich-text edit owns the node (EC-406 scope 4) — the
196
+ // [data-ec-editing] outline owns that state, exactly as the selection overlay
197
+ // yields; a content-drag is suppressed inside SpacingHandles via the drag session.
198
+ const spacingTarget = doc && !editing && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)
143
199
  ? builder.selectedNode
144
200
  : null;
145
201
  // EC-250: the preview frame width derives from the config-sourced breakpoints
146
202
  // (Tailwind defaults when unconfigured), the same resolved set the toolbar
147
203
  // switcher offers.
148
204
  const { viewports } = useCanvasViewports();
149
- return (_jsxs("div", { className: "canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6", children: [_jsx(ClipboardNoticeOverlay, {}), _jsxs("div", { style: { width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }, className: "flex", children: [_jsx("iframe", { ref: iframeRef, onLoad: wireUp, title: translate('builder.canvas.pagePreview'), className: "min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md", srcDoc: "<!doctype html><html><head></head><body></body></html>" }), doc
205
+ return (_jsxs("div", { className: "canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6", children: [_jsx(ClipboardNoticeOverlay, {}), _jsx(CanvasThemeNotice, { status: themeStatus }), _jsxs("div", { style: { width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }, className: "flex", children: [_jsx("iframe", { ref: iframeRef, onLoad: wireUp, title: translate('builder.canvas.pagePreview'), className: "min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md", srcDoc: "<!doctype html><html><head></head><body></body></html>" }), doc
150
206
  ? createPortal(
151
207
  // Render the composition root directly (EC-287: no layout
152
208
  // indirection — a page renders its blocks).
153
209
  renderPageComposition(activeRoot, ctx), doc.body)
154
- : null, spacingTarget && doc ? (_jsx(SpacingHandles, { doc: doc, node: spacingTarget }, spacingTarget.id)) : null, richEditHost && doc ? _jsx(RichTextToolbar, { doc: doc, host: richEditHost }) : null] })] }));
210
+ : null, doc ? _jsx(SelectionOverlay, { doc: doc, editing: editing }) : null, spacingTarget && doc ? (_jsx(SpacingHandles, { doc: doc, node: spacingTarget }, spacingTarget.id)) : null, richEditHost && doc ? _jsx(RichTextToolbar, { doc: doc, host: richEditHost }) : null] })] }));
155
211
  }
156
212
  //# sourceMappingURL=canvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/components/builder/canvas.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { renderPageComposition } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { useParams } from '@elytracms/studio/router'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace'\nimport { applyCanvasChrome } from './canvas-chrome'\nimport { getCanvasStylesheetUrl } from './canvas-stylesheet'\nimport { useBuilder } from './builder-store'\nimport { useCanvasViewports, viewportWidth } from './canvas-breakpoints'\nimport { nodeIdAt } from './canvas-dom'\nimport { useCanvasDnd } from './canvas-dnd'\nimport { useCanvasInlineEditing } from './canvas-inline-editing'\nimport { useCanvasRenderContext } from './canvas-render-context'\nimport {\n ClipboardNoticeOverlay,\n attachClipboardListeners,\n useStudioClipboard,\n} from './clipboard-actions'\nimport { RichTextToolbar } from './rich-text-toolbar'\nimport { SpacingHandles, hasSpacingHandles } from './spacing-handles'\n\n\n/**\n * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders\n * into a same-origin iframe via a React portal — true style isolation + true\n * responsive widths. Each node is wrapped with an editable marker so clicks\n * select and hover/selection draw outlines. Native listeners bridge the iframe\n * boundary (React's synthetic events don't cross frames); the two big gesture\n * state machines live in their own tested modules (EC-330): drag & drop in\n * `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and\n * the render-context assembly in `canvas-render-context`.\n */\nexport function BuilderCanvas({\n perspective = 'draft',\n}: {\n /** Resolve references against this perspective (EC-300/AD-16). The editable Draft\n * view passes `'draft'` (a just-made reusable shows immediately); the read-only\n * Published view passes `'published'` (honest preview of what delivery serves). */\n perspective?: CanvasPerspective\n} = {}) {\n const builder = useBuilder()\n const { dispatch } = builder\n const studio = getStudioRegistry()\n\n // The edited composition root, composed for preview. EC-287 retired the project\n // graph and its published-graph preview; the canvas always renders the live,\n // editable draft (content publishing is per document).\n const activeRoot: ComponentNode = builder.present\n const iframeRef = useRef<HTMLIFrameElement>(null)\n const [doc, setDoc] = useState<Document | null>(null)\n\n // Graph/selection refs let the native iframe listeners read the *current*\n // state without re-subscribing on every edit.\n const presentRef = useRef(builder.present)\n presentRef.current = builder.present\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n\n // When the project was created from a starter kit, its brand token palette drives the canvas UI\n // (EC-117) — injected after app.css so the kit's overrides win in the cascade.\n const params = useParams({ strict: false }) as { projectId?: string }\n const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined\n\n const wireUp = useCallback(() => {\n const d = iframeRef.current?.contentDocument\n if (!d || !d.body) return\n // Deliberately no `.dark` here: this iframe renders the REAL page (plus\n // studio editing chrome portaled alongside it), and its backdrop must read\n // as a light, site-like page — never the studio chrome's current theme (a\n // black canvas is exactly what leaks through when the studio is\n // dark-themed and the iframe root inherits that class). The injected\n // canvas stylesheet's `:root` (light) tokens supply that backdrop for free\n // once `.dark` is never added here (mirrors the components-preview fix,\n // `component-preview.tsx`). The editing chrome (selection/hover outlines,\n // drop indicators, the rich-text toolbar, locale-fallback chips) uses\n // hardcoded colors rather than these tokens, so it stays visible against a\n // light page regardless. The full injection — stylesheet link, editing\n // chrome, kit tokens, and the consumer's `defineStudio({ canvas })` theme\n // (EC-389, injected LAST so it wins) — lives in `applyCanvasChrome`.\n applyCanvasChrome(d, {\n stylesheetUrl: getCanvasStylesheetUrl(),\n kitTokensCss,\n theme: resolveDeploymentCanvasTheme(),\n })\n setDoc(d)\n }, [kitTokensCss])\n\n // Handle the already-loaded case (srcDoc may load before onLoad attaches).\n useEffect(() => {\n if (iframeRef.current?.contentDocument?.readyState === 'complete') wireUp()\n }, [wireUp])\n\n // Inline editing (EC-158/EC-159): the dblclick-to-edit state machine, the\n // live session ref other listeners consult, the rich-text host (mark\n // toolbar), and the per-node remount epochs (decorate keys).\n const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio })\n const { editingRef, richEditHost, richEditEpochs } = inline\n\n // Native selection/hover listeners inside the iframe document.\n useEffect(() => {\n if (!doc) return\n const onClick = (e: Event) => {\n // Clicks inside the active inline editor are caret moves, not selection.\n if (editingRef.current?.host.contains(e.target as Node)) return\n const id = nodeIdAt(e.target)\n if (id) {\n e.preventDefault()\n dispatch({ type: 'select', id })\n }\n }\n const onOver = (e: Event) => dispatch({ type: 'hover', id: nodeIdAt(e.target) })\n const onLeave = () => dispatch({ type: 'hover', id: null })\n doc.addEventListener('click', onClick, true)\n doc.addEventListener('mouseover', onOver)\n doc.addEventListener('mouseleave', onLeave, true)\n return () => {\n doc.removeEventListener('click', onClick, true)\n doc.removeEventListener('mouseover', onOver)\n doc.removeEventListener('mouseleave', onLeave, true)\n }\n }, [doc, dispatch, editingRef])\n\n // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.\n useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef })\n\n // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page\n // tree and blocks panel use, wired to the *iframe* document (events never\n // cross frames). Shortcuts stay inert while an inline editor is live and in\n // form fields; the iframe's own navigator.clipboard does the async I/O\n // (Chromium scopes clipboard access to the focused document).\n const clipboard = useStudioClipboard()\n useEffect(() => {\n if (!doc) return\n return attachClipboardListeners(doc, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n isBlocked: () => editingRef.current !== null,\n asyncClipboard: doc.defaultView?.navigator.clipboard,\n getSelection: () => doc.getSelection(),\n })\n }, [doc, clipboard, editingRef])\n\n // The render context: the seven delivery-parity resolvers + the editable-\n // wrapper decorator (selection/hover, locale chips, remount epochs) — see\n // `canvas-render-context`.\n const { ctx, pageNodeIds } = useCanvasRenderContext({\n present: builder.present,\n selectedId: builder.selectedId,\n hoveredId: builder.hoveredId,\n studio,\n perspective,\n richEditEpochs,\n })\n\n // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children\n // gets draggable gap handles between them, portaled into the iframe.\n const spacingTarget =\n doc && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)\n ? builder.selectedNode\n : null\n\n // EC-250: the preview frame width derives from the config-sourced breakpoints\n // (Tailwind defaults when unconfigured), the same resolved set the toolbar\n // switcher offers.\n const { viewports } = useCanvasViewports()\n\n return (\n <div className=\"canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6\">\n {/* Copy/cut/paste outcomes (EC-175): non-blocking, with issue deep links. */}\n <ClipboardNoticeOverlay />\n <div style={{ width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }} className=\"flex\">\n <iframe\n ref={iframeRef}\n onLoad={wireUp}\n title={translate('builder.canvas.pagePreview')}\n className=\"min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md\"\n srcDoc=\"<!doctype html><html><head></head><body></body></html>\"\n />\n {doc\n ? createPortal(\n // Render the composition root directly (EC-287: no layout\n // indirection — a page renders its blocks).\n renderPageComposition(activeRoot, ctx),\n doc.body,\n )\n : null}\n {spacingTarget && doc ? (\n <SpacingHandles key={spacingTarget.id} doc={doc} node={spacingTarget} />\n ) : null}\n {/* Floating mark toolbar for the live rich-text session (EC-159). */}\n {richEditHost && doc ? <RichTextToolbar doc={doc} host={richEditHost} /> : null}\n </div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/components/builder/canvas.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { renderPageComposition, setEditingMarkers } from '@elytracms/core/runtime-renderer'\nimport { getNode } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { useParams } from '@elytracms/studio/router'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace'\nimport { wireCanvasChrome } from './canvas-chrome'\nimport type { CanvasThemeStatus } from './canvas-chrome'\nimport { CanvasThemeNotice } from './canvas-theme-notice'\nimport { getCanvasStylesheetUrl } from './canvas-stylesheet'\nimport { useBuilder } from './builder-store'\nimport { useCanvasViewports, viewportWidth } from './canvas-breakpoints'\nimport { nodeIdAt, wrapperAt } from './canvas-dom'\nimport { useCanvasDnd } from './canvas-dnd'\nimport { useCanvasInlineEditing } from './canvas-inline-editing'\nimport { useCanvasRenderContext } from './canvas-render-context'\nimport {\n ClipboardNoticeOverlay,\n attachClipboardListeners,\n useStudioClipboard,\n} from './clipboard-actions'\nimport { RichTextToolbar } from './rich-text-toolbar'\nimport { SpacingHandles, hasSpacingHandles } from './spacing-handles'\nimport { SelectionOverlay } from './selection-overlay'\nimport { attachDeleteKeyListener } from './delete-key'\nimport { usePropSync } from './prop-sync-context'\nimport { resolvePropClick } from './prop-selection'\n\n// EC-408: the studio canvas is the ONE surface that turns editing markers on, so\n// `ecProp` in the base primitives emits `data-ec-prop` here. Delivery\n// (`@elytracms/next`) is a separate build that never imports this module, so its\n// singleton stays off and shipped pages carry zero marker noise. Module-level\n// (not an effect) so markers are on before the first primitive renders.\nsetEditingMarkers(true)\n\n\n/**\n * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders\n * into a same-origin iframe via a React portal — true style isolation + true\n * responsive widths. Each node is wrapped with an editable marker so clicks\n * select and hover/selection draw outlines. Native listeners bridge the iframe\n * boundary (React's synthetic events don't cross frames); the two big gesture\n * state machines live in their own tested modules (EC-330): drag & drop in\n * `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and\n * the render-context assembly in `canvas-render-context`.\n */\nexport function BuilderCanvas({\n perspective = 'draft',\n}: {\n /** Resolve references against this perspective (EC-300/AD-16). The editable Draft\n * view passes `'draft'` (a just-made reusable shows immediately); the read-only\n * Published view passes `'published'` (honest preview of what delivery serves). */\n perspective?: CanvasPerspective\n} = {}) {\n const builder = useBuilder()\n const { dispatch } = builder\n const studio = getStudioRegistry()\n\n // The edited composition root, composed for preview. EC-287 retired the project\n // graph and its published-graph preview; the canvas always renders the live,\n // editable draft (content publishing is per document).\n const activeRoot: ComponentNode = builder.present\n const iframeRef = useRef<HTMLIFrameElement>(null)\n const [doc, setDoc] = useState<Document | null>(null)\n\n // Graph/selection refs let the native iframe listeners read the *current*\n // state without re-subscribing on every edit.\n const presentRef = useRef(builder.present)\n presentRef.current = builder.present\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n\n // When the project was created from a starter kit, its brand token palette drives the canvas UI\n // (EC-117) — injected after app.css so the kit's overrides win in the cascade.\n const params = useParams({ strict: false }) as { projectId?: string }\n const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined\n\n // EC-414 AD-D: the settled canvas-theme classification (legacy merged shape /\n // broken stylesheet) drives the dev-only, never-blocking notice overlay.\n const [themeStatus, setThemeStatus] = useState<CanvasThemeStatus | null>(null)\n\n const wireUp = useCallback(() => {\n const d = iframeRef.current?.contentDocument\n if (!d || !d.body) return\n // Deliberately no `.dark` here: this iframe renders the REAL page (plus\n // studio editing chrome portaled alongside it), and its backdrop must read\n // as a light, site-like page — never the studio chrome's current theme (a\n // black canvas is exactly what leaks through when the studio is\n // dark-themed and the iframe root inherits that class). The injected\n // canvas stylesheet (new world: the chrome's neutral backdrop; legacy\n // world: the studio `:root` tokens) supplies that backdrop once `.dark` is\n // never added here (mirrors the components-preview surface, which wires\n // through the same chrome). The editing chrome (selection/hover outlines,\n // drop indicators, the rich-text toolbar, locale-fallback chips) uses\n // hardcoded colors rather than tokens, so it stays visible against a light\n // page regardless. The full injection — stylesheet link, editing chrome,\n // kit tokens, the consumer's `defineStudio({ canvas })` theme (EC-389,\n // injected LAST so it wins), and the EC-414 world probe — lives in\n // `wireCanvasChrome`/`applyCanvasChrome`.\n wireCanvasChrome(d, {\n stylesheetUrl: getCanvasStylesheetUrl(),\n kitTokensCss,\n theme: resolveDeploymentCanvasTheme(),\n onStatus: (status) =>\n setThemeStatus((prev) => (prev?.kind === status.kind ? prev : status)),\n })\n setDoc(d)\n }, [kitTokensCss])\n\n // Handle the already-loaded case (srcDoc may load before onLoad attaches).\n useEffect(() => {\n if (iframeRef.current?.contentDocument?.readyState === 'complete') wireUp()\n }, [wireUp])\n\n // Inline editing (EC-158/EC-159): the dblclick-to-edit state machine, the\n // live session ref other listeners consult, the rich-text host (mark\n // toolbar), and the per-node remount epochs (decorate keys).\n const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio })\n const { editingRef, richEditHost, richEditEpochs, editing } = inline\n\n // EC-408: the field↔component sync channel (null in the component preview,\n // which has no inspector). Read live through a ref — the native iframe\n // listeners outlive renders.\n const propSync = usePropSync()\n const propSyncRef = useRef(propSync)\n propSyncRef.current = propSync\n\n // Native selection/hover listeners inside the iframe document.\n useEffect(() => {\n if (!doc) return\n const onClick = (e: Event) => {\n // Clicks inside the active inline editor are caret moves, not selection.\n if (editingRef.current?.host.contains(e.target as Node)) return\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const id = wrapper.getAttribute('data-ec-node-id')!\n e.preventDefault()\n dispatch({ type: 'select', id })\n // EC-408: resolve the clicked sub-part to its `data-ec-prop` and, unless it\n // is inline-editable (inline editing stays primary), ask the inspector to\n // focus that prop's control. Unannotated components resolve no marker → the\n // node-level select above is the whole behavior, exactly as before.\n const sync = propSyncRef.current\n if (!sync) return\n const node = getNode(presentRef.current, id)\n const manifest = node ? studio.registry.getManifest(node.componentId) : undefined\n const { propName, focusField } = resolvePropClick({\n target: e.target as Element | null,\n wrapper,\n manifest,\n })\n if (propName && focusField) sync.requestFocus({ nodeId: id, propName })\n }\n const onOver = (e: Event) => dispatch({ type: 'hover', id: nodeIdAt(e.target) })\n const onLeave = () => dispatch({ type: 'hover', id: null })\n doc.addEventListener('click', onClick, true)\n doc.addEventListener('mouseover', onOver)\n doc.addEventListener('mouseleave', onLeave, true)\n return () => {\n doc.removeEventListener('click', onClick, true)\n doc.removeEventListener('mouseover', onOver)\n doc.removeEventListener('mouseleave', onLeave, true)\n }\n }, [doc, dispatch, editingRef, presentRef, studio])\n\n // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.\n useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef })\n\n // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page\n // tree and blocks panel use, wired to the *iframe* document (events never\n // cross frames). Shortcuts stay inert while an inline editor is live and in\n // form fields; the iframe's own navigator.clipboard does the async I/O\n // (Chromium scopes clipboard access to the focused document).\n const clipboard = useStudioClipboard()\n useEffect(() => {\n if (!doc) return\n return attachClipboardListeners(doc, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n isBlocked: () => editingRef.current !== null,\n asyncClipboard: doc.defaultView?.navigator.clipboard,\n getSelection: () => doc.getSelection(),\n })\n }, [doc, clipboard, editingRef])\n\n // Delete/Backspace removes the selected node (EC-404) — the keyboard remove\n // the studio was missing, wired beside the clipboard listeners on the iframe\n // document, guarded so typing in an inline editor edits text instead.\n useEffect(() => {\n if (!doc) return\n return attachDeleteKeyListener(doc, {\n getSelectedId: () => selectedIdRef.current,\n remove: (id) => dispatch({ type: 'remove', id }),\n getRootId: () => presentRef.current.id,\n })\n }, [doc, dispatch])\n\n // The render context: the seven delivery-parity resolvers + the editable-\n // wrapper decorator (selection/hover, locale chips, remount epochs) — see\n // `canvas-render-context`.\n const { ctx, pageNodeIds } = useCanvasRenderContext({\n present: builder.present,\n selectedId: builder.selectedId,\n hoveredId: builder.hoveredId,\n studio,\n perspective,\n richEditEpochs,\n })\n\n // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children\n // gets draggable gap handles between them, portaled into the iframe. Suppressed\n // while an inline text/rich-text edit owns the node (EC-406 scope 4) — the\n // [data-ec-editing] outline owns that state, exactly as the selection overlay\n // yields; a content-drag is suppressed inside SpacingHandles via the drag session.\n const spacingTarget =\n doc && !editing && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)\n ? builder.selectedNode\n : null\n\n // EC-250: the preview frame width derives from the config-sourced breakpoints\n // (Tailwind defaults when unconfigured), the same resolved set the toolbar\n // switcher offers.\n const { viewports } = useCanvasViewports()\n\n return (\n <div className=\"canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6\">\n {/* Copy/cut/paste outcomes (EC-175): non-blocking, with issue deep links. */}\n <ClipboardNoticeOverlay />\n {/* Canvas-theme classification notice (EC-414 AD-D): dev-only, never blocks. */}\n <CanvasThemeNotice status={themeStatus} />\n <div style={{ width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }} className=\"flex\">\n <iframe\n ref={iframeRef}\n onLoad={wireUp}\n title={translate('builder.canvas.pagePreview')}\n className=\"min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md\"\n srcDoc=\"<!doctype html><html><head></head><body></body></html>\"\n />\n {doc\n ? createPortal(\n // Render the composition root directly (EC-287: no layout\n // indirection — a page renders its blocks).\n renderPageComposition(activeRoot, ctx),\n doc.body,\n )\n : null}\n {/* Selection/hover frames + frame controls, topmost over the page (EC-404). */}\n {doc ? <SelectionOverlay doc={doc} editing={editing} /> : null}\n {spacingTarget && doc ? (\n <SpacingHandles key={spacingTarget.id} doc={doc} node={spacingTarget} />\n ) : null}\n {/* Floating mark toolbar for the live rich-text session (EC-159). */}\n {richEditHost && doc ? <RichTextToolbar doc={doc} host={richEditHost} /> : null}\n </div>\n </div>\n )\n}\n"]}
@@ -12,7 +12,9 @@ import { PageTree } from './page-tree.js';
12
12
  import { PropertyInspector } from './property-inspector.js';
13
13
  import { InspectorSidebar } from '../inspector/inspector-sidebar.js';
14
14
  import { InsertMenu } from './insert-menu.js';
15
- import { resolveInsertContext } from './insert-context.js';
15
+ import { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context.js';
16
+ import { InsertAffordanceProvider } from './insert-affordance-context.js';
17
+ import { PropSyncProvider } from './prop-sync-context.js';
16
18
  import { ReuseProvider } from './reuse-context.js';
17
19
  /**
18
20
  * EC-188 — the block-field surface: one canvas engine mounted on a composition
@@ -54,7 +56,7 @@ export function CompositionFieldEditor({ field, value, onChange, readOnly, trail
54
56
  // (BuilderProvider reads `composition` only in its lazy initial state, so later
55
57
  // prop changes are ignored regardless.)
56
58
  const initialRoot = useMemo(() => buildSyntheticRoot(value), []);
57
- return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) })] }));
59
+ return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) }) })] }));
58
60
  }
59
61
  /**
60
62
  * Lifts committed edits out of the scoped store into the CMS draft. The first
@@ -147,6 +149,23 @@ function CompositionEditorShell({ readOnly, fieldAllow, trailingPanes, }) {
147
149
  setInsertContext(ctx);
148
150
  setInsertOpen(true);
149
151
  }
150
- return (_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 }) }), _jsx("div", { className: cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60'), children: _jsx(BuilderCanvas, { perspective: readOnly ? 'published' : 'draft' }) }), _jsx(InspectorSidebar, { panes: panes })] }), _jsx(InsertMenu, { context: insertContext, open: insertOpen, onOpenChange: setInsertOpen })] }));
152
+ // EC-405: the in-canvas "+" affordances resolve the SAME slot rules through the
153
+ // shared position-carrying insert-context resolution, and open the panel at the
154
+ // clicked edge (an explicit position that survives to the insertion, rather than
155
+ // the panel re-deriving it from the selection). Re-memoized on every store change
156
+ // so the closures read the live graph.
157
+ const affordanceApi = useMemo(() => ({
158
+ positionsFor: (nodeId) => insertablePositions(builder, nodeId, fieldAllow),
159
+ openAt: (nodeId, position) => {
160
+ const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow);
161
+ if (!ctx)
162
+ return;
163
+ if (nodeId !== builder.selectedId)
164
+ dispatch({ type: 'select', id: nodeId });
165
+ setInsertContext(ctx);
166
+ setInsertOpen(true);
167
+ },
168
+ }), [builder, fieldAllow, dispatch]);
169
+ 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 }) }), _jsx("div", { className: cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60'), children: _jsx(BuilderCanvas, { perspective: readOnly ? 'published' : 'draft' }) }), _jsx(InspectorSidebar, { panes: panes })] }), _jsx(InsertMenu, { context: insertContext, open: insertOpen, onOpenChange: setInsertOpen })] }) }));
151
170
  }
152
171
  //# 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":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\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 { 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 { resolveInsertContext } from './insert-context'\nimport { ReuseProvider } from './reuse-context'\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()`). 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 * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\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 // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} onChange={onChange} readOnly={readOnly} />\n <ReuseProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n fieldAllow={field.allow}\n trailingPanes={trailingPanes}\n />\n </ReuseProvider>\n </BuilderProvider>\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 unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\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(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, 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 fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\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 fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\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\n className={cn(\n 'h-full min-h-0 overflow-hidden',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\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 return (\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\n className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}\n >\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div\n className={cn(\n 'flex min-h-0 flex-col bg-background',\n readOnly && 'pointer-events-none opacity-60',\n )}\n >\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 )\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, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\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 { 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'\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()`). 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 * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\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 // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} 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 readOnly={readOnly} fieldAllow={field.allow} trailingPanes={trailingPanes} />\n </PropSyncProvider>\n </ReuseProvider>\n </BuilderProvider>\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 unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\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(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, 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 fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\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 fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\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 {/* 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"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Delete/Backspace removes the selected node (EC-404, scope 3) — the gap the
3
+ * findings batch surfaced: the studio had no keyboard remove at all except cut
4
+ * (⌘X). This sits beside the clipboard listeners (`attachClipboardListeners`),
5
+ * wired on the same two surfaces (the studio window for the page tree, the
6
+ * iframe document for the canvas), and reuses the SAME editing-context guard so
7
+ * pressing Backspace while typing in an input or an inline text/rich-text editor
8
+ * edits the text — never deletes the block.
9
+ */
10
+ /**
11
+ * True when a key event's target owns its own text-editing behavior — a form
12
+ * field or any contenteditable (which includes the EC-158/EC-159 inline
13
+ * editors). The single guard for the Delete/Backspace remove, kept identical to
14
+ * the clipboard shortcuts' scope so the two can never disagree.
15
+ */
16
+ export declare function isEditingContext(target: EventTarget | null): boolean;
17
+ export interface DeleteKeyListenerOptions {
18
+ /** The current selection — the remove subject. */
19
+ getSelectedId: () => string | null;
20
+ /** Dispatch the remove (the existing `remove` tree action; one undo restores). */
21
+ remove: (id: string) => void;
22
+ /** The id that must never be removed (the composition root). */
23
+ getRootId: () => string | null;
24
+ /** Extra suppression (e.g. a read-only published preview). */
25
+ isDisabled?: () => boolean;
26
+ }
27
+ interface ListenerTarget {
28
+ addEventListener(type: string, listener: (e: Event) => void): void;
29
+ removeEventListener(type: string, listener: (e: Event) => void): void;
30
+ }
31
+ /**
32
+ * Wire Delete/Backspace → remove the selected node. Inert when the event
33
+ * targets an editing context, when nothing (or the root) is selected, or when
34
+ * disabled. Modifier combinations are ignored so it never shadows a browser
35
+ * shortcut (⌘Delete etc.).
36
+ */
37
+ export declare function attachDeleteKeyListener(target: ListenerTarget, opts: DeleteKeyListenerOptions): () => void;
38
+ export {};