@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
@@ -1,96 +1,113 @@
1
- import { PRESERVE_NODE_ATTR } from '@elytracms/core/rich-text';
1
+ import { CANVAS_BACKDROP_LAYER_STATEMENT, canvasChromeCss } from './canvas-chrome-css.js';
2
+ export { canvasChromeCss, CANVAS_BACKDROP_LAYER_STATEMENT } from './canvas-chrome-css.js';
2
3
  /**
3
- * The canvas iframe's document chrome (EC-389) — the framework-free half of
4
- * `BuilderCanvas`'s `wireUp` (house pattern, EC-330): everything injected into
5
- * the preview document lives here, in ONE pinned cascade order, unit-testable
6
- * without jsdom.
4
+ * The canvas iframe's document chrome (EC-389, reshaped by EC-414) — the
5
+ * framework-free half of `BuilderCanvas`'s `wireUp` (house pattern, EC-330):
6
+ * everything injected into the preview document lives here, in ONE pinned
7
+ * cascade order, unit-testable without jsdom.
8
+ *
9
+ * EC-414 (AD-A/AD-C): the studio design system is leaving the canvas document.
10
+ * Which world a project lives in is decided by a RUNTIME PROBE of the loaded
11
+ * canvas stylesheet — no flag, no config, zero consumer breakage:
12
+ *
13
+ * - `legacy` (marker tokens present): the merged pre-EC-414 canvas.css that
14
+ * bundles `@elytracms/studio/styles.css` with the consumer theme. The
15
+ * EC-389 body-class default and today's behavior apply EXACTLY.
16
+ * - `new` (markers absent): the consumer's standalone full-Tailwind theme
17
+ * entry. No studio utilities exist in the document, so the body-class
18
+ * default retires and the neutral hand-written backdrop applies instead.
19
+ */
20
+ /**
21
+ * The studio design system's marker tokens (AD-C probe). These are the plain
22
+ * `:root` custom properties from the studio's generated `tokens.css`, which any
23
+ * merged legacy build carries VERBATIM — unlike `@theme inline` variables
24
+ * (e.g. `--color-panel`), which Tailwind v4 inlines into utilities and never
25
+ * emits as custom properties (verified against the repo's Tailwind 4.3.2; the
26
+ * task file's `--color-panel` example probe would classify every legacy build
27
+ * as new). Two tokens ANDed so a consumer theme that happens to define one
28
+ * generic name (`--panel`) cannot be mistaken for the studio design system.
29
+ */
30
+ export const CANVAS_LEGACY_MARKER_TOKENS = ['--panel', '--grid-line'];
31
+ /**
32
+ * A Tailwind v4 build unconditionally emits `--default-font-family` on
33
+ * `:root, :host` (preflight consumes it) — its absence after a successful
34
+ * stylesheet load means the canvas stylesheet is not a Tailwind theme entry at
35
+ * all (AD-D item 2's "defines no @theme tokens" invalid state).
7
36
  */
8
- /** Selection/hover outlines, scoped to the per-node editable wrappers. */
9
- export const OUTLINE_CSS = `
10
- /* The canvas backdrop is deliberately always light/site-like (see the
11
- wireUp note in canvas.tsx) — force the native form-control/scrollbar theme
12
- to match, so a page with real inputs never shows dark native chrome
13
- floating on a light page. */
14
- html { color-scheme: light; }
15
- body { margin: 0; padding: 16px; }
16
- .ec-editable { outline-offset: -1px; cursor: default; }
17
- .ec-editable[data-hovered]:not([data-selected]) { outline: 1px dashed rgba(130,150,190,0.75); }
18
- .ec-editable[data-selected] { outline: 2px solid #5b8cff; }
19
- /* Inline editing (EC-158 text, EC-159 rich text): the rendered element
20
- itself becomes the editor — no overlay, no replacement input — so
21
- typography, alignment, and layout are pixel-identical between edit and
22
- render states. The wrapper keeps the selection outline; the host only
23
- gets a caret. pre-wrap applies to PLAIN text hosts only — rich-text
24
- markup must keep its rendered whitespace handling (no layout shift). */
25
- .ec-editable[data-ec-editing] { outline: 2px solid #5b8cff; }
26
- .ec-editable [data-ec-edit] { outline: none; cursor: text; }
27
- .ec-editable [data-ec-edit='text'] { white-space: pre-wrap; }
28
- /* Preserved atoms during a rich-text session (custom blocks, unknown nodes,
29
- images, embeds): visible and rendered, but not text-editable. */
30
- .ec-editable [data-ec-edit='rich'] [${PRESERVE_NODE_ATTR}] { user-select: none; }
31
- /* Floating mark toolbar (EC-159), portaled into the iframe. */
32
- .ec-rt-toolbar { color-scheme: dark; }
33
- .ec-rt-toolbar-row { display: flex; align-items: center; gap: 2px; padding: 3px;
34
- border: 1px solid rgba(130,150,190,0.4); border-radius: 8px;
35
- background: rgb(24,26,32); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }
36
- .ec-rt-toolbar-btn { min-width: 26px; height: 24px; padding: 0 7px; border: 0; border-radius: 5px;
37
- background: transparent; color: rgb(225,228,236); font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;
38
- cursor: pointer; }
39
- .ec-rt-toolbar-btn:hover { background: rgba(130,150,190,0.25); }
40
- .ec-rt-toolbar-btn[data-active] { background: #5b8cff; color: white; }
41
- .ec-rt-toolbar-input { width: 200px; height: 24px; padding: 0 7px; border-radius: 5px;
42
- border: 1px solid rgba(130,150,190,0.4); background: rgb(16,18,22); color: rgb(225,228,236);
43
- font: 12px/1 ui-sans-serif, system-ui, sans-serif; outline: none; }
44
- /* Locale-fallback chips (EC-176): a node whose bound content is currently
45
- served via locale fallback wears a small corner chip naming the locale it
46
- actually came from — the canvas never silently mixes locales. */
47
- .ec-editable[data-ec-locale-fallback] { position: relative; }
48
- .ec-editable[data-ec-locale-fallback]::after {
49
- content: attr(data-ec-locale-fallback);
50
- position: absolute; top: 2px; right: 2px; z-index: 5; pointer-events: none;
51
- padding: 0 4px; border-radius: 4px;
52
- font: 600 9px/1.5 ui-sans-serif, system-ui, sans-serif;
53
- letter-spacing: 0.05em; text-transform: uppercase;
54
- color: #fbbf24; background: rgba(180,83,9,0.25); border: 1px solid rgba(245,158,11,0.45);
55
- }
56
- /* Drag & drop states (EC-160/EC-162): eligibility comes from the shared pure
57
- rules (lib/dnd/drop-rules); these classes only *show* the resolution.
58
- Rejection is explicit and visible during the drag — never a silent failed
59
- drop. Before/after bars follow the parent's flow axis (data-ec-drop-axis);
60
- left/right are the wrap-into-columns gesture (violet, paired highlight). */
61
- .ec-editable[data-ec-drop='into'] { outline: 2px solid #34d399; outline-offset: -2px; background-color: rgba(52, 211, 153, 0.08); }
62
- .ec-editable[data-ec-drop='before'] { box-shadow: 0 -3px 0 0 #38bdf8; }
63
- .ec-editable[data-ec-drop='after'] { box-shadow: 0 3px 0 0 #38bdf8; }
64
- .ec-editable[data-ec-drop='before'][data-ec-drop-axis='x'] { box-shadow: -3px 0 0 0 #38bdf8; }
65
- .ec-editable[data-ec-drop='after'][data-ec-drop-axis='x'] { box-shadow: 3px 0 0 0 #38bdf8; }
66
- .ec-editable[data-ec-drop='left'] { box-shadow: -3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }
67
- .ec-editable[data-ec-drop='right'] { box-shadow: 3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }
68
- .ec-editable[data-ec-drop='blocked'] { outline: 2px solid #ef4444; outline-offset: -2px; background-color: rgba(239, 68, 68, 0.08); cursor: not-allowed; }
69
- `;
37
+ export const CANVAS_TAILWIND_MARKER_TOKEN = '--default-font-family';
38
+ const present = (value) => typeof value === 'string' && value.trim() !== '';
39
+ /**
40
+ * AD-C pure core: classify the canvas stylesheet world from computed-token
41
+ * presence. Legacy needs EVERY legacy marker; a loaded stylesheet with no
42
+ * Tailwind marker is the explicit `no-theme-tokens` invalid state.
43
+ */
44
+ export function classifyCanvasTheme(sample) {
45
+ if (sample.loadFailed)
46
+ return { kind: 'stylesheet-error' };
47
+ if (sample.legacyMarkers.length > 0 && sample.legacyMarkers.every(present)) {
48
+ return { kind: 'legacy' };
49
+ }
50
+ if (present(sample.tailwindMarker))
51
+ return { kind: 'new' };
52
+ return { kind: 'no-theme-tokens' };
53
+ }
54
+ /** The world a status renders as: only the legacy merged stylesheet keeps the
55
+ * EC-389 body-class default; every other state gets the neutral backdrop. */
56
+ export function canvasWorldOf(status) {
57
+ return status.kind === 'legacy' ? 'legacy' : 'new';
58
+ }
59
+ /** Sample the live document's computed root styles for {@link classifyCanvasTheme}. */
60
+ export function probeCanvasTheme(doc, loadFailed = false) {
61
+ const root = doc.documentElement;
62
+ const win = doc.defaultView;
63
+ if (!root || !win)
64
+ return { kind: 'new' };
65
+ const computed = win.getComputedStyle(root);
66
+ return classifyCanvasTheme({
67
+ loadFailed,
68
+ legacyMarkers: CANVAS_LEGACY_MARKER_TOKENS.map((token) => computed.getPropertyValue(token)),
69
+ tailwindMarker: computed.getPropertyValue(CANVAS_TAILWIND_MARKER_TOKEN),
70
+ });
71
+ }
70
72
  /**
71
- * The studio's DEFAULT canvas body classes — a replaceable default (EC-389
72
- * decision): a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES
73
- * it wholesale (including `''` to drop it), because these utility classes
73
+ * The studio's DEFAULT canvas body classes — LEGACY world only (EC-389 → EC-414):
74
+ * a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES it
75
+ * wholesale (including `''` to drop it), because these utility classes
74
76
  * out-cascade a site theme's `@layer base body` rules — keeping them additive
75
- * would make the site's own page background unreachable (the NK-004 bug).
77
+ * would make the site's own page background unreachable (the NK-004 bug). In
78
+ * the NEW world the default retires entirely (the utilities don't exist there);
79
+ * the chrome stylesheet's neutral backdrop takes over.
76
80
  */
77
81
  export const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans';
78
82
  /**
79
- * Apply the canvas document chrome in the PINNED cascade order (EC-389):
83
+ * Apply the canvas document chrome in the PINNED cascade order (EC-389/EC-414):
80
84
  *
81
- * 1. `ec-app-css` — the canvas stylesheet `<link>` (site delivery CSS + studio CSS);
82
- * 2. `ec-outline-css` — the studio's editing chrome (selection/DnD/toolbar styles);
83
- * 3. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);
84
- * 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).
85
+ * 1. `ec-layer-order` — the backdrop layer STATEMENT, pinning the chrome's
86
+ * `ec-canvas-backdrop` layer to the bottom of the layer order (it must
87
+ * precede the consumer stylesheet in document order to lose to it);
88
+ * 2. `ec-app-css` — the canvas stylesheet `<link>` (the consumer theme; in
89
+ * the legacy world also the studio design system);
90
+ * 3. `ec-outline-css` — the studio's editing chrome (world-composed:
91
+ * backdrop + selection/DnD/toolbar/containment styles);
92
+ * 4. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);
93
+ * 5. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).
85
94
  *
86
95
  * Later elements win the cascade at equal specificity, so studio defaults <
87
96
  * kit tokens < consumer config — the customization-seam ordering. The body
88
97
  * class is the consumer's `bodyClassName` when given (even `''`), else the
89
- * studio default. Safe to run repeatedly (iframe onLoad + the already-loaded
90
- * fast path): links/styles are deduped by id, and the two token styles are
91
- * re-appended IN ORDER so a re-wire never flips who wins.
98
+ * legacy default in the legacy world and empty in the new world (AD-C). Safe
99
+ * to run repeatedly (iframe onLoad + the already-loaded fast path + the probe
100
+ * settling): links/styles are deduped by id, the chrome CSS is re-composed in
101
+ * place when the world flips, and the two token styles are re-appended IN
102
+ * ORDER so a re-wire never flips who wins.
92
103
  */
93
104
  export function applyCanvasChrome(d, inputs) {
105
+ if (!d.getElementById('ec-layer-order')) {
106
+ const style = d.createElement('style');
107
+ style.id = 'ec-layer-order';
108
+ style.textContent = CANVAS_BACKDROP_LAYER_STATEMENT;
109
+ d.head.appendChild(style);
110
+ }
94
111
  if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {
95
112
  const link = d.createElement('link');
96
113
  link.id = 'ec-app-css';
@@ -98,17 +115,25 @@ export function applyCanvasChrome(d, inputs) {
98
115
  link.href = inputs.stylesheetUrl;
99
116
  d.head.appendChild(link);
100
117
  }
101
- if (!d.getElementById('ec-outline-css')) {
118
+ const chromeCss = canvasChromeCss(inputs.world);
119
+ const outline = d.getElementById('ec-outline-css');
120
+ if (outline) {
121
+ // Probe settled on the other world: re-compose in place (position stable).
122
+ if (outline.textContent !== chromeCss)
123
+ outline.textContent = chromeCss;
124
+ }
125
+ else {
102
126
  const style = d.createElement('style');
103
127
  style.id = 'ec-outline-css';
104
- style.textContent = OUTLINE_CSS;
128
+ style.textContent = chromeCss;
105
129
  d.head.appendChild(style);
106
130
  }
107
131
  // Both token layers re-append (remove + add) on every wire, in sequence, so
108
132
  // the consumer element always lands after the kit element.
109
133
  appendCssLayer(d, 'ec-kit-tokens', inputs.kitTokensCss);
110
134
  appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss);
111
- d.body.className = inputs.theme?.bodyClassName ?? DEFAULT_CANVAS_BODY_CLASS;
135
+ d.body.className =
136
+ inputs.theme?.bodyClassName ?? (inputs.world === 'legacy' ? DEFAULT_CANVAS_BODY_CLASS : '');
112
137
  }
113
138
  /** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */
114
139
  function appendCssLayer(d, id, css) {
@@ -122,4 +147,45 @@ function appendCssLayer(d, id, css) {
122
147
  style.textContent = css;
123
148
  d.head.appendChild(style);
124
149
  }
150
+ /** Links whose settle listeners are already attached (re-wires must not stack them). */
151
+ const wiredLinks = new WeakSet();
152
+ /**
153
+ * The DOM-level wiring around {@link applyCanvasChrome}: applies the chrome
154
+ * immediately with the CURRENT classification, then re-applies once the
155
+ * `ec-app-css` link settles (the AD-C probe reads computed custom properties,
156
+ * which exist only after the stylesheet loads). Before the stylesheet arrives
157
+ * neither world's styling is resolvable anyway (no consumer CSS yet), so the
158
+ * pre-settle pass is visually neutral in both worlds.
159
+ */
160
+ export function wireCanvasChrome(doc, options) {
161
+ const apply = (settled, loadFailed) => {
162
+ const status = options.stylesheetUrl
163
+ ? probeCanvasTheme(doc, loadFailed)
164
+ : { kind: 'no-stylesheet' };
165
+ applyCanvasChrome(doc, {
166
+ stylesheetUrl: options.stylesheetUrl,
167
+ world: canvasWorldOf(status),
168
+ ...(options.kitTokensCss !== undefined ? { kitTokensCss: options.kitTokensCss } : {}),
169
+ ...(options.theme !== undefined ? { theme: options.theme } : {}),
170
+ });
171
+ if (settled)
172
+ options.onStatus?.(status);
173
+ };
174
+ if (!options.stylesheetUrl) {
175
+ apply(true, false);
176
+ return;
177
+ }
178
+ // First pass injects the link (and everything else) with the current sample.
179
+ const already = doc.getElementById('ec-app-css');
180
+ const loaded = already?.sheet != null;
181
+ apply(loaded, false);
182
+ if (loaded)
183
+ return;
184
+ const link = doc.getElementById('ec-app-css');
185
+ if (!link || wiredLinks.has(link))
186
+ return;
187
+ wiredLinks.add(link);
188
+ link.addEventListener('load', () => apply(true, false), { once: true });
189
+ link.addEventListener('error', () => apply(true, true), { once: true });
190
+ }
125
191
  //# sourceMappingURL=canvas-chrome.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-chrome.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-chrome.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAG9D;;;;;GAKG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;wCAqBa,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCzD,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,yCAAyC,CAAA;AAmClF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAuB,EAAE,MAA0B;IACnF,IAAI,MAAM,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,CAAC,EAAE,GAAG,YAAY,CAAA;QACtB,IAAI,CAAC,GAAG,GAAG,YAAY,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,aAAa,CAAA;QAChC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IACD,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;QACtC,KAAK,CAAC,EAAE,GAAG,gBAAgB,CAAA;QAC3B,KAAK,CAAC,WAAW,GAAG,WAAW,CAAA;QAC/B,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;IAC3B,CAAC;IACD,4EAA4E;IAC5E,2DAA2D;IAC3D,cAAc,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;IACvD,cAAc,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAC7D,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,aAAa,IAAI,yBAAyB,CAAA;AAC7E,CAAC;AAED,yFAAyF;AACzF,SAAS,cAAc,CAAC,CAAuB,EAAE,EAAU,EAAE,GAAuB;IAClF,MAAM,QAAQ,GAAG,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,CAAA;IACrC,IAAI,QAAQ;QAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;IAC/B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAM;IAC3C,MAAM,KAAK,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;IACtC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAA;IACb,KAAK,CAAC,WAAW,GAAG,GAAG,CAAA;IACvB,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC","sourcesContent":["import { PRESERVE_NODE_ATTR } from '@elytracms/core/rich-text'\nimport type { StudioCanvasTheme } from '../app-views/studio-config'\n\n/**\n * The canvas iframe's document chrome (EC-389) — the framework-free half of\n * `BuilderCanvas`'s `wireUp` (house pattern, EC-330): everything injected into\n * the preview document lives here, in ONE pinned cascade order, unit-testable\n * without jsdom.\n */\n\n/** Selection/hover outlines, scoped to the per-node editable wrappers. */\nexport const OUTLINE_CSS = `\n /* The canvas backdrop is deliberately always light/site-like (see the\n wireUp note in canvas.tsx) — force the native form-control/scrollbar theme\n to match, so a page with real inputs never shows dark native chrome\n floating on a light page. */\n html { color-scheme: light; }\n body { margin: 0; padding: 16px; }\n .ec-editable { outline-offset: -1px; cursor: default; }\n .ec-editable[data-hovered]:not([data-selected]) { outline: 1px dashed rgba(130,150,190,0.75); }\n .ec-editable[data-selected] { outline: 2px solid #5b8cff; }\n /* Inline editing (EC-158 text, EC-159 rich text): the rendered element\n itself becomes the editor — no overlay, no replacement input — so\n typography, alignment, and layout are pixel-identical between edit and\n render states. The wrapper keeps the selection outline; the host only\n gets a caret. pre-wrap applies to PLAIN text hosts only — rich-text\n markup must keep its rendered whitespace handling (no layout shift). */\n .ec-editable[data-ec-editing] { outline: 2px solid #5b8cff; }\n .ec-editable [data-ec-edit] { outline: none; cursor: text; }\n .ec-editable [data-ec-edit='text'] { white-space: pre-wrap; }\n /* Preserved atoms during a rich-text session (custom blocks, unknown nodes,\n images, embeds): visible and rendered, but not text-editable. */\n .ec-editable [data-ec-edit='rich'] [${PRESERVE_NODE_ATTR}] { user-select: none; }\n /* Floating mark toolbar (EC-159), portaled into the iframe. */\n .ec-rt-toolbar { color-scheme: dark; }\n .ec-rt-toolbar-row { display: flex; align-items: center; gap: 2px; padding: 3px;\n border: 1px solid rgba(130,150,190,0.4); border-radius: 8px;\n background: rgb(24,26,32); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }\n .ec-rt-toolbar-btn { min-width: 26px; height: 24px; padding: 0 7px; border: 0; border-radius: 5px;\n background: transparent; color: rgb(225,228,236); font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;\n cursor: pointer; }\n .ec-rt-toolbar-btn:hover { background: rgba(130,150,190,0.25); }\n .ec-rt-toolbar-btn[data-active] { background: #5b8cff; color: white; }\n .ec-rt-toolbar-input { width: 200px; height: 24px; padding: 0 7px; border-radius: 5px;\n border: 1px solid rgba(130,150,190,0.4); background: rgb(16,18,22); color: rgb(225,228,236);\n font: 12px/1 ui-sans-serif, system-ui, sans-serif; outline: none; }\n /* Locale-fallback chips (EC-176): a node whose bound content is currently\n served via locale fallback wears a small corner chip naming the locale it\n actually came from — the canvas never silently mixes locales. */\n .ec-editable[data-ec-locale-fallback] { position: relative; }\n .ec-editable[data-ec-locale-fallback]::after {\n content: attr(data-ec-locale-fallback);\n position: absolute; top: 2px; right: 2px; z-index: 5; pointer-events: none;\n padding: 0 4px; border-radius: 4px;\n font: 600 9px/1.5 ui-sans-serif, system-ui, sans-serif;\n letter-spacing: 0.05em; text-transform: uppercase;\n color: #fbbf24; background: rgba(180,83,9,0.25); border: 1px solid rgba(245,158,11,0.45);\n }\n /* Drag & drop states (EC-160/EC-162): eligibility comes from the shared pure\n rules (lib/dnd/drop-rules); these classes only *show* the resolution.\n Rejection is explicit and visible during the drag — never a silent failed\n drop. Before/after bars follow the parent's flow axis (data-ec-drop-axis);\n left/right are the wrap-into-columns gesture (violet, paired highlight). */\n .ec-editable[data-ec-drop='into'] { outline: 2px solid #34d399; outline-offset: -2px; background-color: rgba(52, 211, 153, 0.08); }\n .ec-editable[data-ec-drop='before'] { box-shadow: 0 -3px 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='after'] { box-shadow: 0 3px 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='before'][data-ec-drop-axis='x'] { box-shadow: -3px 0 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='after'][data-ec-drop-axis='x'] { box-shadow: 3px 0 0 0 #38bdf8; }\n .ec-editable[data-ec-drop='left'] { box-shadow: -3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }\n .ec-editable[data-ec-drop='right'] { box-shadow: 3px 0 0 0 #a78bfa; background-color: rgba(167, 139, 250, 0.1); }\n .ec-editable[data-ec-drop='blocked'] { outline: 2px solid #ef4444; outline-offset: -2px; background-color: rgba(239, 68, 68, 0.08); cursor: not-allowed; }\n`\n\n/**\n * The studio's DEFAULT canvas body classes — a replaceable default (EC-389\n * decision): a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES\n * it wholesale (including `''` to drop it), because these utility classes\n * out-cascade a site theme's `@layer base body` rules — keeping them additive\n * would make the site's own page background unreachable (the NK-004 bug).\n */\nexport const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans'\n\n/** Everything injected into the canvas preview document. */\nexport interface CanvasChromeInputs {\n /** The consuming app's compiled canvas stylesheet URL; `null` → no link (tests/SSR). */\n stylesheetUrl: string | null\n /** An instantiated starter kit's brand token palette (EC-117), when the project has one. */\n kitTokensCss?: string\n /** The consumer's canvas theme from `defineStudio({ canvas })` (EC-389). */\n theme?: StudioCanvasTheme\n}\n\n/** The head element surface the chrome creates (link/style). */\nexport interface CanvasChromeElement {\n id: string\n rel?: string\n href?: string\n textContent: string | null\n remove(): void\n}\n\n/**\n * The document surface `applyCanvasChrome` needs (the `canvas-dnd` house\n * pattern: a narrow structural type so the core is unit-testable with a\n * hand-rolled fake — no jsdom). A real iframe `Document` satisfies it.\n */\nexport interface CanvasChromeDocument {\n getElementById(id: string): CanvasChromeElement | null\n createElement(tag: string): CanvasChromeElement\n // `object` (not CanvasChromeElement): the real DOM `appendChild<T extends Node>`\n // is only bivariance-compatible with a parameter every Node satisfies.\n head: { appendChild(node: object): void }\n body: { className: string }\n}\n\n/**\n * Apply the canvas document chrome in the PINNED cascade order (EC-389):\n *\n * 1. `ec-app-css` — the canvas stylesheet `<link>` (site delivery CSS + studio CSS);\n * 2. `ec-outline-css` — the studio's editing chrome (selection/DnD/toolbar styles);\n * 3. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);\n * 4. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).\n *\n * Later elements win the cascade at equal specificity, so studio defaults <\n * kit tokens < consumer config — the customization-seam ordering. The body\n * class is the consumer's `bodyClassName` when given (even `''`), else the\n * studio default. Safe to run repeatedly (iframe onLoad + the already-loaded\n * fast path): links/styles are deduped by id, and the two token styles are\n * re-appended IN ORDER so a re-wire never flips who wins.\n */\nexport function applyCanvasChrome(d: CanvasChromeDocument, inputs: CanvasChromeInputs): void {\n if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {\n const link = d.createElement('link')\n link.id = 'ec-app-css'\n link.rel = 'stylesheet'\n link.href = inputs.stylesheetUrl\n d.head.appendChild(link)\n }\n if (!d.getElementById('ec-outline-css')) {\n const style = d.createElement('style')\n style.id = 'ec-outline-css'\n style.textContent = OUTLINE_CSS\n d.head.appendChild(style)\n }\n // Both token layers re-append (remove + add) on every wire, in sequence, so\n // the consumer element always lands after the kit element.\n appendCssLayer(d, 'ec-kit-tokens', inputs.kitTokensCss)\n appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss)\n d.body.className = inputs.theme?.bodyClassName ?? DEFAULT_CANVAS_BODY_CLASS\n}\n\n/** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */\nfunction appendCssLayer(d: CanvasChromeDocument, id: string, css: string | undefined): void {\n const existing = d.getElementById(id)\n if (existing) existing.remove()\n if (css === undefined || css === '') return\n const style = d.createElement('style')\n style.id = id\n style.textContent = css\n d.head.appendChild(style)\n}\n"]}
1
+ {"version":3,"file":"canvas-chrome.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-chrome.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StudioCanvasTheme } from '../app-views/studio-config'\nimport { CANVAS_BACKDROP_LAYER_STATEMENT, canvasChromeCss } from './canvas-chrome-css'\nimport type { CanvasWorld } from './canvas-chrome-css'\n\nexport { canvasChromeCss, CANVAS_BACKDROP_LAYER_STATEMENT } from './canvas-chrome-css'\nexport type { CanvasWorld } from './canvas-chrome-css'\n\n/**\n * The canvas iframe's document chrome (EC-389, reshaped by EC-414) — the\n * framework-free half of `BuilderCanvas`'s `wireUp` (house pattern, EC-330):\n * everything injected into the preview document lives here, in ONE pinned\n * cascade order, unit-testable without jsdom.\n *\n * EC-414 (AD-A/AD-C): the studio design system is leaving the canvas document.\n * Which world a project lives in is decided by a RUNTIME PROBE of the loaded\n * canvas stylesheet — no flag, no config, zero consumer breakage:\n *\n * - `legacy` (marker tokens present): the merged pre-EC-414 canvas.css that\n * bundles `@elytracms/studio/styles.css` with the consumer theme. The\n * EC-389 body-class default and today's behavior apply EXACTLY.\n * - `new` (markers absent): the consumer's standalone full-Tailwind theme\n * entry. No studio utilities exist in the document, so the body-class\n * default retires and the neutral hand-written backdrop applies instead.\n */\n\n/**\n * The studio design system's marker tokens (AD-C probe). These are the plain\n * `:root` custom properties from the studio's generated `tokens.css`, which any\n * merged legacy build carries VERBATIM — unlike `@theme inline` variables\n * (e.g. `--color-panel`), which Tailwind v4 inlines into utilities and never\n * emits as custom properties (verified against the repo's Tailwind 4.3.2; the\n * task file's `--color-panel` example probe would classify every legacy build\n * as new). Two tokens ANDed so a consumer theme that happens to define one\n * generic name (`--panel`) cannot be mistaken for the studio design system.\n */\nexport const CANVAS_LEGACY_MARKER_TOKENS = ['--panel', '--grid-line'] as const\n\n/**\n * A Tailwind v4 build unconditionally emits `--default-font-family` on\n * `:root, :host` (preflight consumes it) — its absence after a successful\n * stylesheet load means the canvas stylesheet is not a Tailwind theme entry at\n * all (AD-D item 2's \"defines no @theme tokens\" invalid state).\n */\nexport const CANVAS_TAILWIND_MARKER_TOKEN = '--default-font-family'\n\n/** The settled classification of the canvas stylesheet (AD-C + AD-D item 2). */\nexport type CanvasThemeStatus =\n /** Pre-EC-414 merged canvas.css — studio design system + consumer theme. */\n | { kind: 'legacy' }\n /** The EC-414 shape: a standalone consumer Tailwind theme entry. */\n | { kind: 'new' }\n /** The app never provided a canvas stylesheet (`setCanvasStylesheetUrl`). */\n | { kind: 'no-stylesheet' }\n /** The stylesheet `<link>` failed to load. */\n | { kind: 'stylesheet-error' }\n /** Loaded, but defines no Tailwind theme tokens at all. */\n | { kind: 'no-theme-tokens' }\n\n/** The raw computed-style sample the pure classifier decides from. */\nexport interface CanvasThemeProbeSample {\n /** The `ec-app-css` link fired `error` (or never resolved). */\n loadFailed: boolean\n /** Computed values of {@link CANVAS_LEGACY_MARKER_TOKENS}, in order. */\n legacyMarkers: readonly string[]\n /** Computed value of {@link CANVAS_TAILWIND_MARKER_TOKEN}. */\n tailwindMarker: string\n}\n\nconst present = (value: string | null | undefined): boolean =>\n typeof value === 'string' && value.trim() !== ''\n\n/**\n * AD-C pure core: classify the canvas stylesheet world from computed-token\n * presence. Legacy needs EVERY legacy marker; a loaded stylesheet with no\n * Tailwind marker is the explicit `no-theme-tokens` invalid state.\n */\nexport function classifyCanvasTheme(sample: CanvasThemeProbeSample): CanvasThemeStatus {\n if (sample.loadFailed) return { kind: 'stylesheet-error' }\n if (sample.legacyMarkers.length > 0 && sample.legacyMarkers.every(present)) {\n return { kind: 'legacy' }\n }\n if (present(sample.tailwindMarker)) return { kind: 'new' }\n return { kind: 'no-theme-tokens' }\n}\n\n/** The world a status renders as: only the legacy merged stylesheet keeps the\n * EC-389 body-class default; every other state gets the neutral backdrop. */\nexport function canvasWorldOf(status: CanvasThemeStatus): CanvasWorld {\n return status.kind === 'legacy' ? 'legacy' : 'new'\n}\n\n/** Sample the live document's computed root styles for {@link classifyCanvasTheme}. */\nexport function probeCanvasTheme(doc: Document, loadFailed = false): CanvasThemeStatus {\n const root = doc.documentElement\n const win = doc.defaultView\n if (!root || !win) return { kind: 'new' }\n const computed = win.getComputedStyle(root)\n return classifyCanvasTheme({\n loadFailed,\n legacyMarkers: CANVAS_LEGACY_MARKER_TOKENS.map((token) => computed.getPropertyValue(token)),\n tailwindMarker: computed.getPropertyValue(CANVAS_TAILWIND_MARKER_TOKEN),\n })\n}\n\n/**\n * The studio's DEFAULT canvas body classes — LEGACY world only (EC-389 → EC-414):\n * a consumer's `defineStudio({ canvas: { bodyClassName } })` REPLACES it\n * wholesale (including `''` to drop it), because these utility classes\n * out-cascade a site theme's `@layer base body` rules — keeping them additive\n * would make the site's own page background unreachable (the NK-004 bug). In\n * the NEW world the default retires entirely (the utilities don't exist there);\n * the chrome stylesheet's neutral backdrop takes over.\n */\nexport const DEFAULT_CANVAS_BODY_CLASS = 'bg-background text-foreground font-sans'\n\n/** Everything injected into the canvas preview document. */\nexport interface CanvasChromeInputs {\n /** The consuming app's compiled canvas stylesheet URL; `null` → no link (tests/SSR). */\n stylesheetUrl: string | null\n /** Which stylesheet world the document is in (AD-C probe; see `wireCanvasChrome`). */\n world: CanvasWorld\n /** An instantiated starter kit's brand token palette (EC-117), when the project has one. */\n kitTokensCss?: string\n /** The consumer's canvas theme from `defineStudio({ canvas })` (EC-389). */\n theme?: StudioCanvasTheme\n}\n\n/** The head element surface the chrome creates (link/style). */\nexport interface CanvasChromeElement {\n id: string\n rel?: string\n href?: string\n textContent: string | null\n remove(): void\n}\n\n/**\n * The document surface `applyCanvasChrome` needs (the `canvas-dnd` house\n * pattern: a narrow structural type so the core is unit-testable with a\n * hand-rolled fake — no jsdom). A real iframe `Document` satisfies it.\n */\nexport interface CanvasChromeDocument {\n getElementById(id: string): CanvasChromeElement | null\n createElement(tag: string): CanvasChromeElement\n // `object` (not CanvasChromeElement): the real DOM `appendChild<T extends Node>`\n // is only bivariance-compatible with a parameter every Node satisfies.\n head: { appendChild(node: object): void }\n body: { className: string }\n}\n\n/**\n * Apply the canvas document chrome in the PINNED cascade order (EC-389/EC-414):\n *\n * 1. `ec-layer-order` — the backdrop layer STATEMENT, pinning the chrome's\n * `ec-canvas-backdrop` layer to the bottom of the layer order (it must\n * precede the consumer stylesheet in document order to lose to it);\n * 2. `ec-app-css` — the canvas stylesheet `<link>` (the consumer theme; in\n * the legacy world also the studio design system);\n * 3. `ec-outline-css` — the studio's editing chrome (world-composed:\n * backdrop + selection/DnD/toolbar/containment styles);\n * 4. `ec-kit-tokens` — an instantiated starter kit's `tokensCss` (kit provides…);\n * 5. `ec-canvas-theme` — the consumer's `canvas.tokensCss` (…consumer overrides).\n *\n * Later elements win the cascade at equal specificity, so studio defaults <\n * kit tokens < consumer config — the customization-seam ordering. The body\n * class is the consumer's `bodyClassName` when given (even `''`), else the\n * legacy default in the legacy world and empty in the new world (AD-C). Safe\n * to run repeatedly (iframe onLoad + the already-loaded fast path + the probe\n * settling): links/styles are deduped by id, the chrome CSS is re-composed in\n * place when the world flips, and the two token styles are re-appended IN\n * ORDER so a re-wire never flips who wins.\n */\nexport function applyCanvasChrome(d: CanvasChromeDocument, inputs: CanvasChromeInputs): void {\n if (!d.getElementById('ec-layer-order')) {\n const style = d.createElement('style')\n style.id = 'ec-layer-order'\n style.textContent = CANVAS_BACKDROP_LAYER_STATEMENT\n d.head.appendChild(style)\n }\n if (inputs.stylesheetUrl && !d.getElementById('ec-app-css')) {\n const link = d.createElement('link')\n link.id = 'ec-app-css'\n link.rel = 'stylesheet'\n link.href = inputs.stylesheetUrl\n d.head.appendChild(link)\n }\n const chromeCss = canvasChromeCss(inputs.world)\n const outline = d.getElementById('ec-outline-css')\n if (outline) {\n // Probe settled on the other world: re-compose in place (position stable).\n if (outline.textContent !== chromeCss) outline.textContent = chromeCss\n } else {\n const style = d.createElement('style')\n style.id = 'ec-outline-css'\n style.textContent = chromeCss\n d.head.appendChild(style)\n }\n // Both token layers re-append (remove + add) on every wire, in sequence, so\n // the consumer element always lands after the kit element.\n appendCssLayer(d, 'ec-kit-tokens', inputs.kitTokensCss)\n appendCssLayer(d, 'ec-canvas-theme', inputs.theme?.tokensCss)\n d.body.className =\n inputs.theme?.bodyClassName ?? (inputs.world === 'legacy' ? DEFAULT_CANVAS_BODY_CLASS : '')\n}\n\n/** Replace-append one identified `<style>` layer; absent CSS removes a stale element. */\nfunction appendCssLayer(d: CanvasChromeDocument, id: string, css: string | undefined): void {\n const existing = d.getElementById(id)\n if (existing) existing.remove()\n if (css === undefined || css === '') return\n const style = d.createElement('style')\n style.id = id\n style.textContent = css\n d.head.appendChild(style)\n}\n\n/** `applyCanvasChrome` inputs minus the probe-owned `world`, plus the settle callback. */\nexport interface WireCanvasChromeOptions {\n stylesheetUrl: string | null\n kitTokensCss?: string\n theme?: StudioCanvasTheme\n /**\n * Fired when the classification SETTLES — the stylesheet finished loading\n * (or failing), or there is no stylesheet to wait for. Feeds the AD-D dev\n * notice; may fire again on re-wires (callers dedupe by kind).\n */\n onStatus?: (status: CanvasThemeStatus) => void\n}\n\n/** Links whose settle listeners are already attached (re-wires must not stack them). */\nconst wiredLinks = new WeakSet<object>()\n\n/**\n * The DOM-level wiring around {@link applyCanvasChrome}: applies the chrome\n * immediately with the CURRENT classification, then re-applies once the\n * `ec-app-css` link settles (the AD-C probe reads computed custom properties,\n * which exist only after the stylesheet loads). Before the stylesheet arrives\n * neither world's styling is resolvable anyway (no consumer CSS yet), so the\n * pre-settle pass is visually neutral in both worlds.\n */\nexport function wireCanvasChrome(doc: Document, options: WireCanvasChromeOptions): void {\n const apply = (settled: boolean, loadFailed: boolean) => {\n const status: CanvasThemeStatus = options.stylesheetUrl\n ? probeCanvasTheme(doc, loadFailed)\n : { kind: 'no-stylesheet' }\n applyCanvasChrome(doc, {\n stylesheetUrl: options.stylesheetUrl,\n world: canvasWorldOf(status),\n ...(options.kitTokensCss !== undefined ? { kitTokensCss: options.kitTokensCss } : {}),\n ...(options.theme !== undefined ? { theme: options.theme } : {}),\n })\n if (settled) options.onStatus?.(status)\n }\n\n if (!options.stylesheetUrl) {\n apply(true, false)\n return\n }\n // First pass injects the link (and everything else) with the current sample.\n const already = doc.getElementById('ec-app-css') as HTMLLinkElement | null\n const loaded = already?.sheet != null\n apply(loaded, false)\n if (loaded) return\n const link = doc.getElementById('ec-app-css') as HTMLLinkElement | null\n if (!link || wiredLinks.has(link)) return\n wiredLinks.add(link)\n link.addEventListener('load', () => apply(true, false), { once: true })\n link.addEventListener('error', () => apply(true, true), { once: true })\n}\n"]}
@@ -76,6 +76,13 @@ export interface CanvasInlineEditing {
76
76
  richEditHost: HTMLElement | null;
77
77
  /** Per-node remount epochs — decorate keys change when a rich session ends. */
78
78
  richEditEpochs: Record<string, number>;
79
+ /**
80
+ * Reactive mirror of `editingRef` for renderers that must react to the
81
+ * edit state (EC-404: the selection overlay suppresses its frame + chrome
82
+ * while a text/rich-text session owns the node, so the `[data-ec-editing]`
83
+ * outline alone marks it).
84
+ */
85
+ editing: boolean;
79
86
  }
80
87
  /**
81
88
  * The React face of the canvas inline editing (EC-158/EC-159): owns the
@@ -219,6 +219,7 @@ export function attachCanvasInlineEditingListeners(doc, deps) {
219
219
  */
220
220
  export function useCanvasInlineEditing(doc, deps) {
221
221
  const editingRef = useRef(null);
222
+ const [editing, setEditing] = useState(false);
222
223
  const [richEditHost, setRichEditHost] = useState(null);
223
224
  const [richEditEpochs, setRichEditEpochs] = useState({});
224
225
  const { presentRef, dispatch, studio } = deps;
@@ -232,11 +233,12 @@ export function useCanvasInlineEditing(doc, deps) {
232
233
  getEditing: () => editingRef.current,
233
234
  setEditing: (edit) => {
234
235
  editingRef.current = edit;
236
+ setEditing(edit !== null);
235
237
  },
236
238
  setRichEditHost,
237
239
  bumpRichEditEpoch: (nodeId) => setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),
238
240
  });
239
241
  }, [doc, dispatch, studio, presentRef]);
240
- return { editingRef, richEditHost, richEditEpochs };
242
+ return { editingRef, richEditHost, richEditEpochs, editing };
241
243
  }
242
244
  //# sourceMappingURL=canvas-inline-editing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-inline-editing.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-inline-editing.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport {\n PRESERVE_NODE_ATTR,\n collectPreservedPairs,\n richTextDocFromDom,\n} from '@elytracms/core/rich-text'\nimport type { RtDomElement } from '@elytracms/core/rich-text'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { BuilderAction } from './builder-store'\nimport {\n createInlineEditSession,\n createInlineRichTextSession,\n findInlineTextHost,\n resolveInlineEditableProp,\n resolveInlineRichTextProp,\n} from './inline-edit'\nimport type { InlineEditSession, InlineRichTextProp } from './inline-edit'\nimport { wrapperAt } from './canvas-dom'\n\n/**\n * Inline editing on the canvas (EC-158 plain text, EC-159 rich text),\n * extracted from `BuilderCanvas` (EC-330): double-click on a node whose\n * manifest flags an inline-editable prop turns the *rendered element itself*\n * contenteditable — no overlay, no input swap, so typography and layout are\n * identical in edit and render states. A session finishes exactly once, and a\n * commit dispatches exactly one `setProp` action — one undo step. The graph\n * stays the single source of truth.\n *\n * Commit/cancel gestures: plain text commits on Enter or blur; rich text is\n * multiline, so plain Enter inserts a paragraph and the commit gesture is\n * Cmd/Ctrl+Enter or blur (Enter-to-commit would make multi-paragraph content\n * unauthorable in place). Esc always cancels, dispatching nothing.\n *\n * The listener wiring is framework-free\n * ({@link attachCanvasInlineEditingListeners}, the `clipboard-actions`\n * pattern) so the begin/commit/cancel state machine is unit-testable without\n * a DOM environment; {@link useCanvasInlineEditing} owns the React state the\n * canvas renders from (the live rich-text host, the remount epochs) and the\n * session ref other canvas listeners consult.\n */\n\n/** A live inline-editing session bound to its DOM host (EC-158/EC-159). */\nexport interface ActiveInlineEdit {\n nodeId: string\n host: HTMLElement\n /** Finish the session exactly once (later calls are no-ops). */\n finish: (mode: 'commit' | 'cancel') => void\n}\n\n/** The structural slice of the iframe document the editing wiring needs. */\nexport interface CanvasInlineEditingDocument {\n addEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n removeEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n getSelection(): Selection | null\n createRange(): Range\n}\n\nexport interface CanvasInlineEditingDeps {\n /** The current composition root (read live — listeners outlive renders). */\n currentRoot(): ComponentNode\n getManifest(componentId: string): ComponentManifest | undefined\n dispatch(action: BuilderAction): void\n /** The live session — imperative DOM state, mirrored for other listeners. */\n getEditing(): ActiveInlineEdit | null\n setEditing(edit: ActiveInlineEdit | null): void\n /** Mirror the rich-text host into render state (mounts the mark toolbar). */\n setRichEditHost(host: HTMLElement | null): void\n /**\n * A rich-text session mutates the rendered DOM behind React's back\n * (contenteditable + mark commands), so finishing one must REMOUNT the\n * node's subtree — bumping its epoch changes the decorate key, React\n * discards the edited DOM wholesale and re-renders from the graph (the\n * committed value, or the untouched original on cancel). Patching the old\n * fiber tree against editor-mutated DOM would leave stale artifacts.\n */\n bumpRichEditEpoch(nodeId: string): void\n}\n\n/**\n * The rendered `.rich-text` element inside a node's wrapper — the rich-text\n * editing host (EC-159). Never crosses into a nested node's wrapper.\n */\nfunction findRichTextHost(wrapper: HTMLElement): HTMLElement | null {\n const host = wrapper.querySelector('.rich-text') as HTMLElement | null\n return host && host.closest('[data-ec-node-id]') === wrapper ? host : null\n}\n\n/**\n * Wire the inline-editing dblclick listener onto the iframe document (native —\n * React's synthetic events don't cross frames). Returns the detach function;\n * detaching mid-session cancels it (never dispatch from cleanup).\n */\nexport function attachCanvasInlineEditingListeners(\n doc: CanvasInlineEditingDocument,\n deps: CanvasInlineEditingDeps,\n): () => void {\n const { dispatch } = deps\n\n // ---- plain text (EC-158) ------------------------------------------------\n const beginText = (wrapper: HTMLElement, host: HTMLElement, session: InlineEditSession) => {\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n const text = host.textContent ?? ''\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n if (mode === 'commit') {\n const outcome = session.commit(text)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value),\n })\n }\n } else {\n const outcome = session.cancel()\n if (outcome) host.textContent = outcome.value\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n } else if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n const onBlur = () => finish('commit')\n // `plaintext-only` keeps paste/typing plain; fall back where unsupported.\n host.setAttribute('contenteditable', 'plaintext-only')\n if (!host.isContentEditable) host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'text')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId: session.nodeId, host, finish })\n host.focus({ preventScroll: true })\n // Keep the gesture's native caret/word selection when it landed inside\n // the host; otherwise select the whole value.\n const sel = doc.getSelection()\n if (!sel || sel.rangeCount === 0 || !host.contains(sel.anchorNode)) {\n const range = doc.createRange()\n range.selectNodeContents(host)\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // ---- rich text (EC-159) -------------------------------------------------\n const beginRichText = (wrapper: HTMLElement, nodeId: string, target: InlineRichTextProp) => {\n const host = findRichTextHost(wrapper)\n if (!host) return\n // Pair the storage tree against the rendered DOM and stamp every\n // non-invertible node (custom blocks, unknown nodes, images, embeds)\n // with its exact JSON: they stay rendered *inside* the live editor as\n // non-editable atoms and survive the session byte-exact. A pairing\n // mismatch means the DOM diverged from the deterministic render —\n // refuse to start rather than risk lossy parsing (\"polished or absent\").\n const pairs = collectPreservedPairs(target.value.doc, host as unknown as RtDomElement)\n if (!pairs) return\n const session = createInlineRichTextSession({\n nodeId,\n propName: target.propName,\n original: target.value,\n })\n for (const pair of pairs) {\n const el = pair.element as unknown as HTMLElement\n el.setAttribute(PRESERVE_NODE_ATTR, JSON.stringify(pair.node))\n el.setAttribute('contenteditable', 'false')\n }\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n deps.setRichEditHost(null)\n // Parse the edited DOM, detach the session, then bump the node's\n // remount epoch: React discards the editor-mutated DOM and renders\n // fresh from the graph — committed content or, on cancel, the\n // untouched original. Never patch a fiber tree against DOM the\n // editing commands rewrote.\n const parsed = mode === 'commit' ? richTextDocFromDom(host as unknown as RtDomElement) : null\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n deps.bumpRichEditEpoch(nodeId)\n if (parsed) {\n // An unchanged document commits nothing: the stored value is never\n // rewritten, so untouched content round-trips byte-identical.\n const outcome = session.commit(parsed.doc)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value as unknown as JsonValue),\n })\n }\n } else {\n session.cancel()\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n }\n // Plain Enter falls through: native paragraph insertion (multiline).\n }\n const onBlur = (e: FocusEvent) => {\n // Interacting with the floating toolbar is part of the session.\n const next = e.relatedTarget as HTMLElement | null\n if (next?.closest?.('[data-ec-rt-toolbar]')) return\n finish('commit')\n }\n host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'rich')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId, host, finish })\n deps.setRichEditHost(host)\n host.focus({ preventScroll: true })\n }\n\n const onDblClick = (e: Event) => {\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const nodeId = wrapper.getAttribute('data-ec-node-id')!\n const editing = deps.getEditing()\n if (editing) {\n // Double-click inside the live editor is native word selection.\n if (editing.nodeId === nodeId) return\n editing.finish('commit')\n }\n const node = getNode(deps.currentRoot(), nodeId)\n if (!node) return\n const manifest = deps.getManifest(node.componentId)\n const target = resolveInlineEditableProp(manifest, node)\n if (target) {\n const host = findInlineTextHost(wrapper, target.value) as HTMLElement | null\n if (!host || typeof host.focus !== 'function') return\n dispatch({ type: 'select', id: nodeId })\n beginText(\n wrapper,\n host,\n createInlineEditSession({ nodeId, propName: target.propName, original: target.value }),\n )\n return\n }\n const richTarget = resolveInlineRichTextProp(manifest, node)\n if (!richTarget) return\n dispatch({ type: 'select', id: nodeId })\n beginRichText(wrapper, nodeId, richTarget)\n }\n\n doc.addEventListener('dblclick', onDblClick, true)\n return () => {\n doc.removeEventListener('dblclick', onDblClick, true)\n // Teardown mid-session (doc swap/unmount): cancel — never dispatch from cleanup.\n deps.getEditing()?.finish('cancel')\n }\n}\n\n/** What `useCanvasInlineEditing` hands back to the canvas. */\nexport interface CanvasInlineEditing {\n /**\n * The active session — imperative DOM state inside the iframe, not render\n * state. Other canvas listeners (selection clicks, DnD arming, clipboard\n * blocking) consult it live through this ref.\n */\n editingRef: RefObject<ActiveInlineEdit | null>\n /** The live rich-text host, mirrored into render state (mark toolbar mount). */\n richEditHost: HTMLElement | null\n /** Per-node remount epochs — decorate keys change when a rich session ends. */\n richEditEpochs: Record<string, number>\n}\n\n/**\n * The React face of the canvas inline editing (EC-158/EC-159): owns the\n * session ref and the render state (`richEditHost`, `richEditEpochs`), wires\n * the dblclick listener when the iframe document exists, and cancels a live\n * session on teardown.\n */\nexport function useCanvasInlineEditing(\n doc: Document | null,\n deps: {\n /** The live composition root (`builder.present`, mirrored into a ref). */\n presentRef: RefObject<ComponentNode>\n dispatch: (action: BuilderAction) => void\n studio: StudioRegistry\n },\n): CanvasInlineEditing {\n const editingRef = useRef<ActiveInlineEdit | null>(null)\n const [richEditHost, setRichEditHost] = useState<HTMLElement | null>(null)\n const [richEditEpochs, setRichEditEpochs] = useState<Record<string, number>>({})\n const { presentRef, dispatch, studio } = deps\n\n useEffect(() => {\n if (!doc) return\n return attachCanvasInlineEditingListeners(doc, {\n currentRoot: () => presentRef.current,\n getManifest: (id) => studio.registry.getManifest(id),\n dispatch,\n getEditing: () => editingRef.current,\n setEditing: (edit) => {\n editingRef.current = edit\n },\n setRichEditHost,\n bumpRichEditEpoch: (nodeId) =>\n setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),\n })\n }, [doc, dispatch, studio, presentRef])\n\n return { editingRef, richEditHost, richEditEpochs }\n}\n"]}
1
+ {"version":3,"file":"canvas-inline-editing.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-inline-editing.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport type { RefObject } from 'react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport {\n PRESERVE_NODE_ATTR,\n collectPreservedPairs,\n richTextDocFromDom,\n} from '@elytracms/core/rich-text'\nimport type { RtDomElement } from '@elytracms/core/rich-text'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { BuilderAction } from './builder-store'\nimport {\n createInlineEditSession,\n createInlineRichTextSession,\n findInlineTextHost,\n resolveInlineEditableProp,\n resolveInlineRichTextProp,\n} from './inline-edit'\nimport type { InlineEditSession, InlineRichTextProp } from './inline-edit'\nimport { wrapperAt } from './canvas-dom'\n\n/**\n * Inline editing on the canvas (EC-158 plain text, EC-159 rich text),\n * extracted from `BuilderCanvas` (EC-330): double-click on a node whose\n * manifest flags an inline-editable prop turns the *rendered element itself*\n * contenteditable — no overlay, no input swap, so typography and layout are\n * identical in edit and render states. A session finishes exactly once, and a\n * commit dispatches exactly one `setProp` action — one undo step. The graph\n * stays the single source of truth.\n *\n * Commit/cancel gestures: plain text commits on Enter or blur; rich text is\n * multiline, so plain Enter inserts a paragraph and the commit gesture is\n * Cmd/Ctrl+Enter or blur (Enter-to-commit would make multi-paragraph content\n * unauthorable in place). Esc always cancels, dispatching nothing.\n *\n * The listener wiring is framework-free\n * ({@link attachCanvasInlineEditingListeners}, the `clipboard-actions`\n * pattern) so the begin/commit/cancel state machine is unit-testable without\n * a DOM environment; {@link useCanvasInlineEditing} owns the React state the\n * canvas renders from (the live rich-text host, the remount epochs) and the\n * session ref other canvas listeners consult.\n */\n\n/** A live inline-editing session bound to its DOM host (EC-158/EC-159). */\nexport interface ActiveInlineEdit {\n nodeId: string\n host: HTMLElement\n /** Finish the session exactly once (later calls are no-ops). */\n finish: (mode: 'commit' | 'cancel') => void\n}\n\n/** The structural slice of the iframe document the editing wiring needs. */\nexport interface CanvasInlineEditingDocument {\n addEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n removeEventListener(type: string, listener: (e: Event) => void, capture?: boolean): void\n getSelection(): Selection | null\n createRange(): Range\n}\n\nexport interface CanvasInlineEditingDeps {\n /** The current composition root (read live — listeners outlive renders). */\n currentRoot(): ComponentNode\n getManifest(componentId: string): ComponentManifest | undefined\n dispatch(action: BuilderAction): void\n /** The live session — imperative DOM state, mirrored for other listeners. */\n getEditing(): ActiveInlineEdit | null\n setEditing(edit: ActiveInlineEdit | null): void\n /** Mirror the rich-text host into render state (mounts the mark toolbar). */\n setRichEditHost(host: HTMLElement | null): void\n /**\n * A rich-text session mutates the rendered DOM behind React's back\n * (contenteditable + mark commands), so finishing one must REMOUNT the\n * node's subtree — bumping its epoch changes the decorate key, React\n * discards the edited DOM wholesale and re-renders from the graph (the\n * committed value, or the untouched original on cancel). Patching the old\n * fiber tree against editor-mutated DOM would leave stale artifacts.\n */\n bumpRichEditEpoch(nodeId: string): void\n}\n\n/**\n * The rendered `.rich-text` element inside a node's wrapper — the rich-text\n * editing host (EC-159). Never crosses into a nested node's wrapper.\n */\nfunction findRichTextHost(wrapper: HTMLElement): HTMLElement | null {\n const host = wrapper.querySelector('.rich-text') as HTMLElement | null\n return host && host.closest('[data-ec-node-id]') === wrapper ? host : null\n}\n\n/**\n * Wire the inline-editing dblclick listener onto the iframe document (native —\n * React's synthetic events don't cross frames). Returns the detach function;\n * detaching mid-session cancels it (never dispatch from cleanup).\n */\nexport function attachCanvasInlineEditingListeners(\n doc: CanvasInlineEditingDocument,\n deps: CanvasInlineEditingDeps,\n): () => void {\n const { dispatch } = deps\n\n // ---- plain text (EC-158) ------------------------------------------------\n const beginText = (wrapper: HTMLElement, host: HTMLElement, session: InlineEditSession) => {\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n const text = host.textContent ?? ''\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n if (mode === 'commit') {\n const outcome = session.commit(text)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value),\n })\n }\n } else {\n const outcome = session.cancel()\n if (outcome) host.textContent = outcome.value\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n } else if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n const onBlur = () => finish('commit')\n // `plaintext-only` keeps paste/typing plain; fall back where unsupported.\n host.setAttribute('contenteditable', 'plaintext-only')\n if (!host.isContentEditable) host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'text')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId: session.nodeId, host, finish })\n host.focus({ preventScroll: true })\n // Keep the gesture's native caret/word selection when it landed inside\n // the host; otherwise select the whole value.\n const sel = doc.getSelection()\n if (!sel || sel.rangeCount === 0 || !host.contains(sel.anchorNode)) {\n const range = doc.createRange()\n range.selectNodeContents(host)\n sel?.removeAllRanges()\n sel?.addRange(range)\n }\n }\n\n // ---- rich text (EC-159) -------------------------------------------------\n const beginRichText = (wrapper: HTMLElement, nodeId: string, target: InlineRichTextProp) => {\n const host = findRichTextHost(wrapper)\n if (!host) return\n // Pair the storage tree against the rendered DOM and stamp every\n // non-invertible node (custom blocks, unknown nodes, images, embeds)\n // with its exact JSON: they stay rendered *inside* the live editor as\n // non-editable atoms and survive the session byte-exact. A pairing\n // mismatch means the DOM diverged from the deterministic render —\n // refuse to start rather than risk lossy parsing (\"polished or absent\").\n const pairs = collectPreservedPairs(target.value.doc, host as unknown as RtDomElement)\n if (!pairs) return\n const session = createInlineRichTextSession({\n nodeId,\n propName: target.propName,\n original: target.value,\n })\n for (const pair of pairs) {\n const el = pair.element as unknown as HTMLElement\n el.setAttribute(PRESERVE_NODE_ATTR, JSON.stringify(pair.node))\n el.setAttribute('contenteditable', 'false')\n }\n let done = false\n const finish = (mode: 'commit' | 'cancel') => {\n if (done) return\n done = true\n deps.setEditing(null)\n deps.setRichEditHost(null)\n // Parse the edited DOM, detach the session, then bump the node's\n // remount epoch: React discards the editor-mutated DOM and renders\n // fresh from the graph — committed content or, on cancel, the\n // untouched original. Never patch a fiber tree against DOM the\n // editing commands rewrote.\n const parsed = mode === 'commit' ? richTextDocFromDom(host as unknown as RtDomElement) : null\n host.removeEventListener('keydown', onKeyDown)\n host.removeEventListener('blur', onBlur)\n host.removeAttribute('contenteditable')\n host.removeAttribute('data-ec-edit')\n wrapper.removeAttribute('data-ec-editing')\n deps.bumpRichEditEpoch(nodeId)\n if (parsed) {\n // An unchanged document commits nothing: the stored value is never\n // rewritten, so untouched content round-trips byte-identical.\n const outcome = session.commit(parsed.doc)\n if (outcome) {\n dispatch({\n type: 'setProp',\n id: outcome.nodeId,\n name: outcome.propName,\n value: staticProp(outcome.value as unknown as JsonValue),\n })\n }\n } else {\n session.cancel()\n }\n doc.getSelection()?.removeAllRanges()\n }\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault()\n e.stopPropagation()\n finish('commit')\n }\n // Plain Enter falls through: native paragraph insertion (multiline).\n }\n const onBlur = (e: FocusEvent) => {\n // Interacting with the floating toolbar is part of the session.\n const next = e.relatedTarget as HTMLElement | null\n if (next?.closest?.('[data-ec-rt-toolbar]')) return\n finish('commit')\n }\n host.setAttribute('contenteditable', 'true')\n host.setAttribute('data-ec-edit', 'rich')\n wrapper.setAttribute('data-ec-editing', '')\n host.addEventListener('keydown', onKeyDown)\n host.addEventListener('blur', onBlur)\n deps.setEditing({ nodeId, host, finish })\n deps.setRichEditHost(host)\n host.focus({ preventScroll: true })\n }\n\n const onDblClick = (e: Event) => {\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const nodeId = wrapper.getAttribute('data-ec-node-id')!\n const editing = deps.getEditing()\n if (editing) {\n // Double-click inside the live editor is native word selection.\n if (editing.nodeId === nodeId) return\n editing.finish('commit')\n }\n const node = getNode(deps.currentRoot(), nodeId)\n if (!node) return\n const manifest = deps.getManifest(node.componentId)\n const target = resolveInlineEditableProp(manifest, node)\n if (target) {\n const host = findInlineTextHost(wrapper, target.value) as HTMLElement | null\n if (!host || typeof host.focus !== 'function') return\n dispatch({ type: 'select', id: nodeId })\n beginText(\n wrapper,\n host,\n createInlineEditSession({ nodeId, propName: target.propName, original: target.value }),\n )\n return\n }\n const richTarget = resolveInlineRichTextProp(manifest, node)\n if (!richTarget) return\n dispatch({ type: 'select', id: nodeId })\n beginRichText(wrapper, nodeId, richTarget)\n }\n\n doc.addEventListener('dblclick', onDblClick, true)\n return () => {\n doc.removeEventListener('dblclick', onDblClick, true)\n // Teardown mid-session (doc swap/unmount): cancel — never dispatch from cleanup.\n deps.getEditing()?.finish('cancel')\n }\n}\n\n/** What `useCanvasInlineEditing` hands back to the canvas. */\nexport interface CanvasInlineEditing {\n /**\n * The active session — imperative DOM state inside the iframe, not render\n * state. Other canvas listeners (selection clicks, DnD arming, clipboard\n * blocking) consult it live through this ref.\n */\n editingRef: RefObject<ActiveInlineEdit | null>\n /** The live rich-text host, mirrored into render state (mark toolbar mount). */\n richEditHost: HTMLElement | null\n /** Per-node remount epochs — decorate keys change when a rich session ends. */\n richEditEpochs: Record<string, number>\n /**\n * Reactive mirror of `editingRef` for renderers that must react to the\n * edit state (EC-404: the selection overlay suppresses its frame + chrome\n * while a text/rich-text session owns the node, so the `[data-ec-editing]`\n * outline alone marks it).\n */\n editing: boolean\n}\n\n/**\n * The React face of the canvas inline editing (EC-158/EC-159): owns the\n * session ref and the render state (`richEditHost`, `richEditEpochs`), wires\n * the dblclick listener when the iframe document exists, and cancels a live\n * session on teardown.\n */\nexport function useCanvasInlineEditing(\n doc: Document | null,\n deps: {\n /** The live composition root (`builder.present`, mirrored into a ref). */\n presentRef: RefObject<ComponentNode>\n dispatch: (action: BuilderAction) => void\n studio: StudioRegistry\n },\n): CanvasInlineEditing {\n const editingRef = useRef<ActiveInlineEdit | null>(null)\n const [editing, setEditing] = useState(false)\n const [richEditHost, setRichEditHost] = useState<HTMLElement | null>(null)\n const [richEditEpochs, setRichEditEpochs] = useState<Record<string, number>>({})\n const { presentRef, dispatch, studio } = deps\n\n useEffect(() => {\n if (!doc) return\n return attachCanvasInlineEditingListeners(doc, {\n currentRoot: () => presentRef.current,\n getManifest: (id) => studio.registry.getManifest(id),\n dispatch,\n getEditing: () => editingRef.current,\n setEditing: (edit) => {\n editingRef.current = edit\n setEditing(edit !== null)\n },\n setRichEditHost,\n bumpRichEditEpoch: (nodeId) =>\n setRichEditEpochs((prev) => ({ ...prev, [nodeId]: (prev[nodeId] ?? 0) + 1 })),\n })\n }, [doc, dispatch, studio, presentRef])\n\n return { editingRef, richEditHost, richEditEpochs, editing }\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import type { ComponentNode } from '@elytracms/core/project-graph';
2
+ import type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
3
+ import type { BuilderAction } from './builder-store.js';
4
+ import type { Rect } from './canvas-rect-tracking.js';
5
+ import type { InsertPosition } from './insert-context.js';
6
+ /**
7
+ * Pure geometry + action-mapping for the canvas selection overlay (EC-404) —
8
+ * everything the overlay decides that does NOT touch the DOM or React, extracted
9
+ * so it is unit-testable in the node env: where the frame's chrome bar sits, and
10
+ * which tree actions its controls dispatch.
11
+ */
12
+ /** Height reserved for the frame's chrome bar (the label + control cluster). */
13
+ export declare const FRAME_BAR_HEIGHT = 24;
14
+ /** Placement of the frame chrome bar in document coordinates. */
15
+ export interface FrameBarPlacement {
16
+ top: number;
17
+ left: number;
18
+ /** `above` the frame's top edge, or `inside` its top when there's no room. */
19
+ placement: 'above' | 'inside';
20
+ }
21
+ /**
22
+ * Place the frame's chrome bar just above the frame's top edge, flipping to
23
+ * sit INSIDE the top when the edge is within a bar-height of the visible top
24
+ * (`docTop` = the current scroll offset, so the bar never floats above the
25
+ * viewport). Left-aligned to the frame.
26
+ */
27
+ export declare function placeFrameBar(rect: Rect, opts: {
28
+ barHeight: number;
29
+ docTop: number;
30
+ }): FrameBarPlacement;
31
+ /** Which frame controls are available for a node (root has none; ends can't move further). */
32
+ export interface FrameControls {
33
+ /** The composition root — no move/duplicate/delete affordances (mirrors the tree). */
34
+ isRoot: boolean;
35
+ canMoveUp: boolean;
36
+ canMoveDown: boolean;
37
+ }
38
+ /**
39
+ * Resolve a node's frame controls from the graph: the root gets none; a child's
40
+ * move affordances follow its position among its slot siblings — the identical
41
+ * enablement the page-tree row menu computes from `isFirst`/`isLast`.
42
+ */
43
+ export declare function frameControlsFor(root: ComponentNode, nodeId: string): FrameControls;
44
+ /** The four frame controls, in display order. */
45
+ export type FrameControlKind = 'moveUp' | 'moveDown' | 'duplicate' | 'delete';
46
+ /**
47
+ * Map a frame control to the EXISTING builder tree action it dispatches — the
48
+ * same `reorder`/`duplicate`/`remove` the page-tree "•••" menu fires (no new
49
+ * mutation path). One control, one dispatch, one undo step.
50
+ */
51
+ export declare function frameControlAction(kind: FrameControlKind, nodeId: string): BuilderAction;
52
+ /** The anchor point (document coords) a "+" affordance button centers on. */
53
+ export interface AffordanceAnchor {
54
+ left: number;
55
+ top: number;
56
+ }
57
+ /**
58
+ * Place a "+" insertion affordance for `position` on a node's measured rect
59
+ * (EC-405). The returned point is the button's CENTER (the chrome CSS centers the
60
+ * button on it with a translate), so it sits ON the boundary:
61
+ *
62
+ * - `before`/`after` ride the node's leading/trailing edge along the parent's
63
+ * flow axis — top/bottom mid-points for a vertical flow, left/right mid-points
64
+ * for a horizontal one (so the "+" always straddles the gap a drop `before`/
65
+ * `after` would fill).
66
+ * - `into` sits at the node's center — the affordance for dropping the first
67
+ * child into an empty container.
68
+ *
69
+ * Pure over the rect + flow; no DOM, no clamping (the 16px body gutter keeps
70
+ * edge affordances on-canvas).
71
+ */
72
+ export declare function placeAffordance(rect: Rect, position: InsertPosition, flow: FlowAxis): AffordanceAnchor;