@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
@@ -0,0 +1,80 @@
1
+ import { getParentInfo } from '@elytracms/core/project-graph';
2
+ /**
3
+ * Pure geometry + action-mapping for the canvas selection overlay (EC-404) —
4
+ * everything the overlay decides that does NOT touch the DOM or React, extracted
5
+ * so it is unit-testable in the node env: where the frame's chrome bar sits, and
6
+ * which tree actions its controls dispatch.
7
+ */
8
+ /** Height reserved for the frame's chrome bar (the label + control cluster). */
9
+ export const FRAME_BAR_HEIGHT = 24;
10
+ /**
11
+ * Place the frame's chrome bar just above the frame's top edge, flipping to
12
+ * sit INSIDE the top when the edge is within a bar-height of the visible top
13
+ * (`docTop` = the current scroll offset, so the bar never floats above the
14
+ * viewport). Left-aligned to the frame.
15
+ */
16
+ export function placeFrameBar(rect, opts) {
17
+ const above = rect.top - opts.barHeight;
18
+ if (above >= opts.docTop)
19
+ return { top: above, left: rect.left, placement: 'above' };
20
+ return { top: rect.top, left: rect.left, placement: 'inside' };
21
+ }
22
+ /**
23
+ * Resolve a node's frame controls from the graph: the root gets none; a child's
24
+ * move affordances follow its position among its slot siblings — the identical
25
+ * enablement the page-tree row menu computes from `isFirst`/`isLast`.
26
+ */
27
+ export function frameControlsFor(root, nodeId) {
28
+ const info = getParentInfo(root, nodeId);
29
+ if (!info)
30
+ return { isRoot: true, canMoveUp: false, canMoveDown: false };
31
+ const siblings = info.parent.slots?.[info.slot] ?? [];
32
+ return {
33
+ isRoot: false,
34
+ canMoveUp: info.index > 0,
35
+ canMoveDown: info.index < siblings.length - 1,
36
+ };
37
+ }
38
+ /**
39
+ * Map a frame control to the EXISTING builder tree action it dispatches — the
40
+ * same `reorder`/`duplicate`/`remove` the page-tree "•••" menu fires (no new
41
+ * mutation path). One control, one dispatch, one undo step.
42
+ */
43
+ export function frameControlAction(kind, nodeId) {
44
+ switch (kind) {
45
+ case 'moveUp':
46
+ return { type: 'reorder', id: nodeId, delta: -1 };
47
+ case 'moveDown':
48
+ return { type: 'reorder', id: nodeId, delta: 1 };
49
+ case 'duplicate':
50
+ return { type: 'duplicate', id: nodeId };
51
+ case 'delete':
52
+ return { type: 'remove', id: nodeId };
53
+ }
54
+ }
55
+ /**
56
+ * Place a "+" insertion affordance for `position` on a node's measured rect
57
+ * (EC-405). The returned point is the button's CENTER (the chrome CSS centers the
58
+ * button on it with a translate), so it sits ON the boundary:
59
+ *
60
+ * - `before`/`after` ride the node's leading/trailing edge along the parent's
61
+ * flow axis — top/bottom mid-points for a vertical flow, left/right mid-points
62
+ * for a horizontal one (so the "+" always straddles the gap a drop `before`/
63
+ * `after` would fill).
64
+ * - `into` sits at the node's center — the affordance for dropping the first
65
+ * child into an empty container.
66
+ *
67
+ * Pure over the rect + flow; no DOM, no clamping (the 16px body gutter keeps
68
+ * edge affordances on-canvas).
69
+ */
70
+ export function placeAffordance(rect, position, flow) {
71
+ const midX = rect.left + rect.width / 2;
72
+ const midY = rect.top + rect.height / 2;
73
+ if (position === 'into')
74
+ return { left: midX, top: midY };
75
+ if (flow === 'horizontal') {
76
+ return { left: position === 'before' ? rect.left : rect.left + rect.width, top: midY };
77
+ }
78
+ return { left: midX, top: position === 'before' ? rect.top : rect.top + rect.height };
79
+ }
80
+ //# sourceMappingURL=canvas-overlay-geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-overlay-geometry.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-overlay-geometry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAM7D;;;;;GAKG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAUlC;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAU,EAAE,IAA2C;IACnF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAA;IACvC,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAA;IACpF,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAA;AAChE,CAAC;AAUD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAmB,EAAE,MAAc;IAClE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;IACxE,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACrD,OAAO;QACL,MAAM,EAAE,KAAK;QACb,SAAS,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC;QACzB,WAAW,EAAE,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC;KAC9C,CAAA;AACH,CAAC;AAKD;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAsB,EAAE,MAAc;IACvE,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAA;QACnD,KAAK,UAAU;YACb,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;QAClD,KAAK,WAAW;YACd,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,CAAA;QAC1C,KAAK,QAAQ;YACX,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAA;IACzC,CAAC;AACH,CAAC;AAYD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,IAAU,EAAE,QAAwB,EAAE,IAAc;IAClF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;IACvC,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;IACzD,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;IACxF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAA;AACvF,CAAC","sourcesContent":["import type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getParentInfo } from '@elytracms/core/project-graph'\nimport type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { BuilderAction } from './builder-store'\nimport type { Rect } from './canvas-rect-tracking'\nimport type { InsertPosition } from './insert-context'\n\n/**\n * Pure geometry + action-mapping for the canvas selection overlay (EC-404) —\n * everything the overlay decides that does NOT touch the DOM or React, extracted\n * so it is unit-testable in the node env: where the frame's chrome bar sits, and\n * which tree actions its controls dispatch.\n */\n\n/** Height reserved for the frame's chrome bar (the label + control cluster). */\nexport const FRAME_BAR_HEIGHT = 24\n\n/** Placement of the frame chrome bar in document coordinates. */\nexport interface FrameBarPlacement {\n top: number\n left: number\n /** `above` the frame's top edge, or `inside` its top when there's no room. */\n placement: 'above' | 'inside'\n}\n\n/**\n * Place the frame's chrome bar just above the frame's top edge, flipping to\n * sit INSIDE the top when the edge is within a bar-height of the visible top\n * (`docTop` = the current scroll offset, so the bar never floats above the\n * viewport). Left-aligned to the frame.\n */\nexport function placeFrameBar(rect: Rect, opts: { barHeight: number; docTop: number }): FrameBarPlacement {\n const above = rect.top - opts.barHeight\n if (above >= opts.docTop) return { top: above, left: rect.left, placement: 'above' }\n return { top: rect.top, left: rect.left, placement: 'inside' }\n}\n\n/** Which frame controls are available for a node (root has none; ends can't move further). */\nexport interface FrameControls {\n /** The composition root — no move/duplicate/delete affordances (mirrors the tree). */\n isRoot: boolean\n canMoveUp: boolean\n canMoveDown: boolean\n}\n\n/**\n * Resolve a node's frame controls from the graph: the root gets none; a child's\n * move affordances follow its position among its slot siblings — the identical\n * enablement the page-tree row menu computes from `isFirst`/`isLast`.\n */\nexport function frameControlsFor(root: ComponentNode, nodeId: string): FrameControls {\n const info = getParentInfo(root, nodeId)\n if (!info) return { isRoot: true, canMoveUp: false, canMoveDown: false }\n const siblings = info.parent.slots?.[info.slot] ?? []\n return {\n isRoot: false,\n canMoveUp: info.index > 0,\n canMoveDown: info.index < siblings.length - 1,\n }\n}\n\n/** The four frame controls, in display order. */\nexport type FrameControlKind = 'moveUp' | 'moveDown' | 'duplicate' | 'delete'\n\n/**\n * Map a frame control to the EXISTING builder tree action it dispatches — the\n * same `reorder`/`duplicate`/`remove` the page-tree \"•••\" menu fires (no new\n * mutation path). One control, one dispatch, one undo step.\n */\nexport function frameControlAction(kind: FrameControlKind, nodeId: string): BuilderAction {\n switch (kind) {\n case 'moveUp':\n return { type: 'reorder', id: nodeId, delta: -1 }\n case 'moveDown':\n return { type: 'reorder', id: nodeId, delta: 1 }\n case 'duplicate':\n return { type: 'duplicate', id: nodeId }\n case 'delete':\n return { type: 'remove', id: nodeId }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Insertion affordances (EC-405)\n// ---------------------------------------------------------------------------\n\n/** The anchor point (document coords) a \"+\" affordance button centers on. */\nexport interface AffordanceAnchor {\n left: number\n top: number\n}\n\n/**\n * Place a \"+\" insertion affordance for `position` on a node's measured rect\n * (EC-405). The returned point is the button's CENTER (the chrome CSS centers the\n * button on it with a translate), so it sits ON the boundary:\n *\n * - `before`/`after` ride the node's leading/trailing edge along the parent's\n * flow axis — top/bottom mid-points for a vertical flow, left/right mid-points\n * for a horizontal one (so the \"+\" always straddles the gap a drop `before`/\n * `after` would fill).\n * - `into` sits at the node's center — the affordance for dropping the first\n * child into an empty container.\n *\n * Pure over the rect + flow; no DOM, no clamping (the 16px body gutter keeps\n * edge affordances on-canvas).\n */\nexport function placeAffordance(rect: Rect, position: InsertPosition, flow: FlowAxis): AffordanceAnchor {\n const midX = rect.left + rect.width / 2\n const midY = rect.top + rect.height / 2\n if (position === 'into') return { left: midX, top: midY }\n if (flow === 'horizontal') {\n return { left: position === 'before' ? rect.left : rect.left + rect.width, top: midY }\n }\n return { left: midX, top: position === 'before' ? rect.top : rect.top + rect.height }\n}\n"]}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The canvas overlay's rect-tracking core (EC-404) — the PURE, reusable half of
3
+ * the selection/hover overlay, kept free of React and of the DOM so the
4
+ * subscription + coalescing + change-detection bookkeeping is unit-testable with
5
+ * injected fakes (the `canvas-dnd`/`canvas-inline-editing` house pattern).
6
+ *
7
+ * The overlay draws its frames from measured node rects and must keep them glued
8
+ * to the page as async layout settles (images decode, fonts load, data resolves,
9
+ * the viewport resizes, the user scrolls). This module separates the two
10
+ * concerns EC-405 (insertion affordances) and EC-406 (spacing handles) will
11
+ * reuse:
12
+ *
13
+ * - {@link createRectTracker} — generic bookkeeping: subscribe once, coalesce
14
+ * invalidations into a single scheduled flush, re-measure, and emit ONLY when
15
+ * the measured value actually changed (an injected `equals`). Pure over its
16
+ * injected environment; no DOM, no React.
17
+ * - {@link subscribeCanvasLayout} / {@link measureElementRect} — the thin DOM
18
+ * glue that wires the real invalidation sources (scroll/resize/observers/
19
+ * fonts) and reads a document-coordinate rect. Exercised live, not in jsdom.
20
+ */
21
+ /** A document-coordinate rectangle (viewport rect + scroll offset). */
22
+ export interface Rect {
23
+ left: number;
24
+ top: number;
25
+ width: number;
26
+ height: number;
27
+ }
28
+ /** Structural exact-equality of two rects (null-aware). */
29
+ export declare function rectsEqual(a: Rect | null, b: Rect | null): boolean;
30
+ /**
31
+ * The generic tracker: emits the CURRENT measured value on creation, then a new
32
+ * value whenever an invalidation resolves to a changed measurement. Coalescing
33
+ * is the injected `schedule`, which returns its OWN canceller (a compound
34
+ * rAF+timeout scheduler in production — see {@link scheduleFrame}); repeated
35
+ * invalidations before a flush collapse to one measure. `dispose` is idempotent
36
+ * and stops all further work.
37
+ */
38
+ export interface RectTracker {
39
+ /** Request a coalesced re-measure (safe to call at any rate). */
40
+ invalidate(): void;
41
+ /** Unsubscribe + drop any pending flush; idempotent. */
42
+ dispose(): void;
43
+ }
44
+ export interface RectTrackerConfig<T> {
45
+ /** Read the current value (a rect, a geometry bundle, a set of slots …). */
46
+ measure(): T;
47
+ /** True when two measurements are equivalent — suppresses redundant emits. */
48
+ equals(a: T, b: T): boolean;
49
+ /** Receives the initial value and every subsequent CHANGED value. */
50
+ onChange(value: T): void;
51
+ /** Schedule a coalesced flush; return a canceller that unschedules it. */
52
+ schedule(flush: () => void): () => void;
53
+ /**
54
+ * Wire every invalidation source once, calling `invalidate` on each; return a
55
+ * teardown. Called exactly once during creation.
56
+ */
57
+ subscribe(invalidate: () => void): () => void;
58
+ }
59
+ export declare function createRectTracker<T>(config: RectTrackerConfig<T>): RectTracker;
60
+ /** The window surface {@link scheduleFrame} needs (a real `Window` satisfies it). */
61
+ export interface FrameScheduler {
62
+ requestAnimationFrame(cb: FrameRequestCallback): number;
63
+ cancelAnimationFrame(handle: number): void;
64
+ setTimeout(cb: () => void, ms: number): number;
65
+ clearTimeout(handle: number): void;
66
+ }
67
+ /**
68
+ * Schedule a coalesced flush on the next animation frame WITH a timeout
69
+ * backstop: rAF keeps the overlay paint-aligned while the canvas is on screen,
70
+ * and the timer guarantees the flush still runs when rAF is paused (a
71
+ * backgrounded/offscreen preview, or a non-painting automation environment) —
72
+ * otherwise a stranded rAF would freeze the tracker at its `cancelPending` gate.
73
+ * Whichever fires first cancels the other and runs the flush once. Returns a
74
+ * canceller that unschedules both.
75
+ */
76
+ export declare function scheduleFrame(host: FrameScheduler, flush: () => void): () => void;
77
+ /** The window surface the DOM glue reads (a real `Window` satisfies it). */
78
+ interface LayoutWindow {
79
+ scrollX: number;
80
+ scrollY: number;
81
+ requestAnimationFrame(cb: FrameRequestCallback): number;
82
+ cancelAnimationFrame(handle: number): void;
83
+ addEventListener(type: string, listener: () => void, options?: boolean | AddEventListenerOptions): void;
84
+ removeEventListener(type: string, listener: () => void, options?: boolean | EventListenerOptions): void;
85
+ ResizeObserver?: typeof ResizeObserver;
86
+ MutationObserver?: typeof MutationObserver;
87
+ }
88
+ /**
89
+ * Measure an element's document-coordinate rect: viewport rect + scroll offset,
90
+ * so an absolutely-positioned overlay child in the iframe `body` stays glued to
91
+ * the element through scroll WITHOUT re-measuring (only layout changes move it in
92
+ * document space). Mirrors `SpacingHandles`' measurement convention exactly.
93
+ */
94
+ export declare function measureElementRect(el: Element, win: Pick<LayoutWindow, 'scrollX' | 'scrollY'>): Rect;
95
+ /** Locate a rendered node's editable wrapper and measure its document rect. */
96
+ export declare function measureNodeRect(doc: Document, nodeId: string | null): Rect | null;
97
+ /**
98
+ * Measure the `[data-ec-prop]` marker element for `propName` that belongs to
99
+ * node `nodeId` (EC-408 reverse highlight): the first matching marker inside the
100
+ * node's wrapper whose NEAREST node wrapper is that node — so a nested child
101
+ * node's same-named marker is never mistaken for the parent's. `null` when the
102
+ * node isn't rendered or carries no such marker (→ no highlight, silently).
103
+ */
104
+ export declare function measurePropRect(doc: Document, nodeId: string | null, propName: string | null): Rect | null;
105
+ /**
106
+ * Wire every invalidation source the canvas overlay needs and return a teardown:
107
+ *
108
+ * - `scroll` (capture — inner scroll containers bubble nothing) + `resize` on
109
+ * the iframe window;
110
+ * - a `ResizeObserver` on the tracked target elements AND the body (a sibling
111
+ * node growing pushes the target in document space — an image decoding, a
112
+ * data list filling in);
113
+ * - a `MutationObserver` on the body subtree (`childList` — nodes inserted or
114
+ * removed by editing or async data), deliberately NOT observing attributes
115
+ * so the overlay's own inline-style writes can't feed back a measure storm;
116
+ * - `document.fonts.ready` — a one-shot re-measure once web fonts settle
117
+ * (their metrics reflow text after first paint).
118
+ *
119
+ * Every source funnels through `invalidate`, which the tracker coalesces to one
120
+ * measure per frame. `targets` is read lazily so the caller can re-point the
121
+ * ResizeObserver on selection/hover changes by rebuilding the subscription.
122
+ */
123
+ export declare function subscribeCanvasLayout(doc: Document, targets: () => Element[], invalidate: () => void): () => void;
124
+ export {};
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The canvas overlay's rect-tracking core (EC-404) — the PURE, reusable half of
3
+ * the selection/hover overlay, kept free of React and of the DOM so the
4
+ * subscription + coalescing + change-detection bookkeeping is unit-testable with
5
+ * injected fakes (the `canvas-dnd`/`canvas-inline-editing` house pattern).
6
+ *
7
+ * The overlay draws its frames from measured node rects and must keep them glued
8
+ * to the page as async layout settles (images decode, fonts load, data resolves,
9
+ * the viewport resizes, the user scrolls). This module separates the two
10
+ * concerns EC-405 (insertion affordances) and EC-406 (spacing handles) will
11
+ * reuse:
12
+ *
13
+ * - {@link createRectTracker} — generic bookkeeping: subscribe once, coalesce
14
+ * invalidations into a single scheduled flush, re-measure, and emit ONLY when
15
+ * the measured value actually changed (an injected `equals`). Pure over its
16
+ * injected environment; no DOM, no React.
17
+ * - {@link subscribeCanvasLayout} / {@link measureElementRect} — the thin DOM
18
+ * glue that wires the real invalidation sources (scroll/resize/observers/
19
+ * fonts) and reads a document-coordinate rect. Exercised live, not in jsdom.
20
+ */
21
+ /** Structural exact-equality of two rects (null-aware). */
22
+ export function rectsEqual(a, b) {
23
+ if (a === null || b === null)
24
+ return a === b;
25
+ return a.left === b.left && a.top === b.top && a.width === b.width && a.height === b.height;
26
+ }
27
+ export function createRectTracker(config) {
28
+ let last = config.measure();
29
+ let cancelPending = null;
30
+ let disposed = false;
31
+ const flush = () => {
32
+ cancelPending = null;
33
+ if (disposed)
34
+ return;
35
+ const next = config.measure();
36
+ if (!config.equals(last, next)) {
37
+ last = next;
38
+ config.onChange(next);
39
+ }
40
+ };
41
+ const invalidate = () => {
42
+ if (disposed || cancelPending !== null)
43
+ return;
44
+ cancelPending = config.schedule(flush);
45
+ };
46
+ // Emit the initial measurement, then subscribe. (Emitting first means a
47
+ // synchronous subscription that invalidates immediately still coalesces
48
+ // against a stable baseline.)
49
+ config.onChange(last);
50
+ const unsubscribe = config.subscribe(invalidate);
51
+ return {
52
+ invalidate,
53
+ dispose() {
54
+ if (disposed)
55
+ return;
56
+ disposed = true;
57
+ if (cancelPending !== null) {
58
+ cancelPending();
59
+ cancelPending = null;
60
+ }
61
+ unsubscribe();
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * Schedule a coalesced flush on the next animation frame WITH a timeout
67
+ * backstop: rAF keeps the overlay paint-aligned while the canvas is on screen,
68
+ * and the timer guarantees the flush still runs when rAF is paused (a
69
+ * backgrounded/offscreen preview, or a non-painting automation environment) —
70
+ * otherwise a stranded rAF would freeze the tracker at its `cancelPending` gate.
71
+ * Whichever fires first cancels the other and runs the flush once. Returns a
72
+ * canceller that unschedules both.
73
+ */
74
+ export function scheduleFrame(host, flush) {
75
+ let raf = 0;
76
+ let timer = 0;
77
+ let done = false;
78
+ const fire = () => {
79
+ if (done)
80
+ return;
81
+ done = true;
82
+ host.cancelAnimationFrame(raf);
83
+ host.clearTimeout(timer);
84
+ flush();
85
+ };
86
+ raf = host.requestAnimationFrame(fire);
87
+ timer = host.setTimeout(fire, 100);
88
+ return () => {
89
+ if (done)
90
+ return;
91
+ done = true;
92
+ host.cancelAnimationFrame(raf);
93
+ host.clearTimeout(timer);
94
+ };
95
+ }
96
+ /**
97
+ * Measure an element's document-coordinate rect: viewport rect + scroll offset,
98
+ * so an absolutely-positioned overlay child in the iframe `body` stays glued to
99
+ * the element through scroll WITHOUT re-measuring (only layout changes move it in
100
+ * document space). Mirrors `SpacingHandles`' measurement convention exactly.
101
+ */
102
+ export function measureElementRect(el, win) {
103
+ const r = el.getBoundingClientRect();
104
+ return { left: r.left + win.scrollX, top: r.top + win.scrollY, width: r.width, height: r.height };
105
+ }
106
+ /** Locate a rendered node's editable wrapper and measure its document rect. */
107
+ export function measureNodeRect(doc, nodeId) {
108
+ if (!nodeId)
109
+ return null;
110
+ const win = doc.defaultView;
111
+ if (!win)
112
+ return null;
113
+ const el = doc.querySelector(`[data-ec-node-id="${nodeId}"]`);
114
+ return el ? measureElementRect(el, win) : null;
115
+ }
116
+ /**
117
+ * Measure the `[data-ec-prop]` marker element for `propName` that belongs to
118
+ * node `nodeId` (EC-408 reverse highlight): the first matching marker inside the
119
+ * node's wrapper whose NEAREST node wrapper is that node — so a nested child
120
+ * node's same-named marker is never mistaken for the parent's. `null` when the
121
+ * node isn't rendered or carries no such marker (→ no highlight, silently).
122
+ */
123
+ export function measurePropRect(doc, nodeId, propName) {
124
+ if (!nodeId || !propName)
125
+ return null;
126
+ const win = doc.defaultView;
127
+ if (!win)
128
+ return null;
129
+ const wrapper = doc.querySelector(`[data-ec-node-id="${nodeId}"]`);
130
+ if (!wrapper)
131
+ return null;
132
+ // Prop names are plain manifest keys; escape defensively where CSS.escape exists.
133
+ const escaped = win.CSS?.escape?.(propName) ?? propName;
134
+ const markers = wrapper.querySelectorAll(`[data-ec-prop="${escaped}"]`);
135
+ for (const marker of markers) {
136
+ if (marker.closest('[data-ec-node-id]') === wrapper)
137
+ return measureElementRect(marker, win);
138
+ }
139
+ return null;
140
+ }
141
+ /**
142
+ * Wire every invalidation source the canvas overlay needs and return a teardown:
143
+ *
144
+ * - `scroll` (capture — inner scroll containers bubble nothing) + `resize` on
145
+ * the iframe window;
146
+ * - a `ResizeObserver` on the tracked target elements AND the body (a sibling
147
+ * node growing pushes the target in document space — an image decoding, a
148
+ * data list filling in);
149
+ * - a `MutationObserver` on the body subtree (`childList` — nodes inserted or
150
+ * removed by editing or async data), deliberately NOT observing attributes
151
+ * so the overlay's own inline-style writes can't feed back a measure storm;
152
+ * - `document.fonts.ready` — a one-shot re-measure once web fonts settle
153
+ * (their metrics reflow text after first paint).
154
+ *
155
+ * Every source funnels through `invalidate`, which the tracker coalesces to one
156
+ * measure per frame. `targets` is read lazily so the caller can re-point the
157
+ * ResizeObserver on selection/hover changes by rebuilding the subscription.
158
+ */
159
+ export function subscribeCanvasLayout(doc, targets, invalidate) {
160
+ const win = doc.defaultView;
161
+ if (!win)
162
+ return () => { };
163
+ const teardowns = [];
164
+ win.addEventListener('scroll', invalidate, true);
165
+ win.addEventListener('resize', invalidate);
166
+ teardowns.push(() => {
167
+ win.removeEventListener('scroll', invalidate, true);
168
+ win.removeEventListener('resize', invalidate);
169
+ });
170
+ const RO = win.ResizeObserver;
171
+ if (RO) {
172
+ const ro = new RO(() => invalidate());
173
+ for (const el of targets())
174
+ ro.observe(el);
175
+ if (doc.body)
176
+ ro.observe(doc.body);
177
+ teardowns.push(() => ro.disconnect());
178
+ }
179
+ const MO = win.MutationObserver;
180
+ if (MO && doc.body) {
181
+ const mo = new MO(() => invalidate());
182
+ mo.observe(doc.body, { childList: true, subtree: true });
183
+ teardowns.push(() => mo.disconnect());
184
+ }
185
+ const fonts = doc.fonts;
186
+ if (fonts?.ready) {
187
+ let cancelled = false;
188
+ fonts.ready.then(() => !cancelled && invalidate()).catch(() => { });
189
+ teardowns.push(() => {
190
+ cancelled = true;
191
+ });
192
+ }
193
+ return () => {
194
+ for (const t of teardowns)
195
+ t();
196
+ };
197
+ }
198
+ //# sourceMappingURL=canvas-rect-tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-rect-tracking.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-rect-tracking.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The canvas overlay's rect-tracking core (EC-404) — the PURE, reusable half of\n * the selection/hover overlay, kept free of React and of the DOM so the\n * subscription + coalescing + change-detection bookkeeping is unit-testable with\n * injected fakes (the `canvas-dnd`/`canvas-inline-editing` house pattern).\n *\n * The overlay draws its frames from measured node rects and must keep them glued\n * to the page as async layout settles (images decode, fonts load, data resolves,\n * the viewport resizes, the user scrolls). This module separates the two\n * concerns EC-405 (insertion affordances) and EC-406 (spacing handles) will\n * reuse:\n *\n * - {@link createRectTracker} — generic bookkeeping: subscribe once, coalesce\n * invalidations into a single scheduled flush, re-measure, and emit ONLY when\n * the measured value actually changed (an injected `equals`). Pure over its\n * injected environment; no DOM, no React.\n * - {@link subscribeCanvasLayout} / {@link measureElementRect} — the thin DOM\n * glue that wires the real invalidation sources (scroll/resize/observers/\n * fonts) and reads a document-coordinate rect. Exercised live, not in jsdom.\n */\n\n/** A document-coordinate rectangle (viewport rect + scroll offset). */\nexport interface Rect {\n left: number\n top: number\n width: number\n height: number\n}\n\n/** Structural exact-equality of two rects (null-aware). */\nexport function rectsEqual(a: Rect | null, b: Rect | null): boolean {\n if (a === null || b === null) return a === b\n return a.left === b.left && a.top === b.top && a.width === b.width && a.height === b.height\n}\n\n/**\n * The generic tracker: emits the CURRENT measured value on creation, then a new\n * value whenever an invalidation resolves to a changed measurement. Coalescing\n * is the injected `schedule`, which returns its OWN canceller (a compound\n * rAF+timeout scheduler in production — see {@link scheduleFrame}); repeated\n * invalidations before a flush collapse to one measure. `dispose` is idempotent\n * and stops all further work.\n */\nexport interface RectTracker {\n /** Request a coalesced re-measure (safe to call at any rate). */\n invalidate(): void\n /** Unsubscribe + drop any pending flush; idempotent. */\n dispose(): void\n}\n\nexport interface RectTrackerConfig<T> {\n /** Read the current value (a rect, a geometry bundle, a set of slots …). */\n measure(): T\n /** True when two measurements are equivalent — suppresses redundant emits. */\n equals(a: T, b: T): boolean\n /** Receives the initial value and every subsequent CHANGED value. */\n onChange(value: T): void\n /** Schedule a coalesced flush; return a canceller that unschedules it. */\n schedule(flush: () => void): () => void\n /**\n * Wire every invalidation source once, calling `invalidate` on each; return a\n * teardown. Called exactly once during creation.\n */\n subscribe(invalidate: () => void): () => void\n}\n\nexport function createRectTracker<T>(config: RectTrackerConfig<T>): RectTracker {\n let last = config.measure()\n let cancelPending: (() => void) | null = null\n let disposed = false\n\n const flush = (): void => {\n cancelPending = null\n if (disposed) return\n const next = config.measure()\n if (!config.equals(last, next)) {\n last = next\n config.onChange(next)\n }\n }\n\n const invalidate = (): void => {\n if (disposed || cancelPending !== null) return\n cancelPending = config.schedule(flush)\n }\n\n // Emit the initial measurement, then subscribe. (Emitting first means a\n // synchronous subscription that invalidates immediately still coalesces\n // against a stable baseline.)\n config.onChange(last)\n const unsubscribe = config.subscribe(invalidate)\n\n return {\n invalidate,\n dispose(): void {\n if (disposed) return\n disposed = true\n if (cancelPending !== null) {\n cancelPending()\n cancelPending = null\n }\n unsubscribe()\n },\n }\n}\n\n/** The window surface {@link scheduleFrame} needs (a real `Window` satisfies it). */\nexport interface FrameScheduler {\n requestAnimationFrame(cb: FrameRequestCallback): number\n cancelAnimationFrame(handle: number): void\n // Browser `Window.setTimeout` returns a number (not the Node `Timeout` handle).\n setTimeout(cb: () => void, ms: number): number\n clearTimeout(handle: number): void\n}\n\n/**\n * Schedule a coalesced flush on the next animation frame WITH a timeout\n * backstop: rAF keeps the overlay paint-aligned while the canvas is on screen,\n * and the timer guarantees the flush still runs when rAF is paused (a\n * backgrounded/offscreen preview, or a non-painting automation environment) —\n * otherwise a stranded rAF would freeze the tracker at its `cancelPending` gate.\n * Whichever fires first cancels the other and runs the flush once. Returns a\n * canceller that unschedules both.\n */\nexport function scheduleFrame(host: FrameScheduler, flush: () => void): () => void {\n let raf = 0\n let timer = 0\n let done = false\n const fire = (): void => {\n if (done) return\n done = true\n host.cancelAnimationFrame(raf)\n host.clearTimeout(timer)\n flush()\n }\n raf = host.requestAnimationFrame(fire)\n timer = host.setTimeout(fire, 100)\n return () => {\n if (done) return\n done = true\n host.cancelAnimationFrame(raf)\n host.clearTimeout(timer)\n }\n}\n\n/** The window surface the DOM glue reads (a real `Window` satisfies it). */\ninterface LayoutWindow {\n scrollX: number\n scrollY: number\n requestAnimationFrame(cb: FrameRequestCallback): number\n cancelAnimationFrame(handle: number): void\n addEventListener(type: string, listener: () => void, options?: boolean | AddEventListenerOptions): void\n removeEventListener(type: string, listener: () => void, options?: boolean | EventListenerOptions): void\n ResizeObserver?: typeof ResizeObserver\n MutationObserver?: typeof MutationObserver\n}\n\n/**\n * Measure an element's document-coordinate rect: viewport rect + scroll offset,\n * so an absolutely-positioned overlay child in the iframe `body` stays glued to\n * the element through scroll WITHOUT re-measuring (only layout changes move it in\n * document space). Mirrors `SpacingHandles`' measurement convention exactly.\n */\nexport function measureElementRect(el: Element, win: Pick<LayoutWindow, 'scrollX' | 'scrollY'>): Rect {\n const r = el.getBoundingClientRect()\n return { left: r.left + win.scrollX, top: r.top + win.scrollY, width: r.width, height: r.height }\n}\n\n/** Locate a rendered node's editable wrapper and measure its document rect. */\nexport function measureNodeRect(doc: Document, nodeId: string | null): Rect | null {\n if (!nodeId) return null\n const win = doc.defaultView\n if (!win) return null\n const el = doc.querySelector(`[data-ec-node-id=\"${nodeId}\"]`)\n return el ? measureElementRect(el, win) : null\n}\n\n/**\n * Measure the `[data-ec-prop]` marker element for `propName` that belongs to\n * node `nodeId` (EC-408 reverse highlight): the first matching marker inside the\n * node's wrapper whose NEAREST node wrapper is that node — so a nested child\n * node's same-named marker is never mistaken for the parent's. `null` when the\n * node isn't rendered or carries no such marker (→ no highlight, silently).\n */\nexport function measurePropRect(\n doc: Document,\n nodeId: string | null,\n propName: string | null,\n): Rect | null {\n if (!nodeId || !propName) return null\n const win = doc.defaultView\n if (!win) return null\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${nodeId}\"]`)\n if (!wrapper) return null\n // Prop names are plain manifest keys; escape defensively where CSS.escape exists.\n const escaped = (win as Window & typeof globalThis).CSS?.escape?.(propName) ?? propName\n const markers = wrapper.querySelectorAll(`[data-ec-prop=\"${escaped}\"]`)\n for (const marker of markers) {\n if (marker.closest('[data-ec-node-id]') === wrapper) return measureElementRect(marker, win)\n }\n return null\n}\n\n/**\n * Wire every invalidation source the canvas overlay needs and return a teardown:\n *\n * - `scroll` (capture — inner scroll containers bubble nothing) + `resize` on\n * the iframe window;\n * - a `ResizeObserver` on the tracked target elements AND the body (a sibling\n * node growing pushes the target in document space — an image decoding, a\n * data list filling in);\n * - a `MutationObserver` on the body subtree (`childList` — nodes inserted or\n * removed by editing or async data), deliberately NOT observing attributes\n * so the overlay's own inline-style writes can't feed back a measure storm;\n * - `document.fonts.ready` — a one-shot re-measure once web fonts settle\n * (their metrics reflow text after first paint).\n *\n * Every source funnels through `invalidate`, which the tracker coalesces to one\n * measure per frame. `targets` is read lazily so the caller can re-point the\n * ResizeObserver on selection/hover changes by rebuilding the subscription.\n */\nexport function subscribeCanvasLayout(\n doc: Document,\n targets: () => Element[],\n invalidate: () => void,\n): () => void {\n const win = doc.defaultView as LayoutWindow | null\n if (!win) return () => {}\n const teardowns: Array<() => void> = []\n\n win.addEventListener('scroll', invalidate, true)\n win.addEventListener('resize', invalidate)\n teardowns.push(() => {\n win.removeEventListener('scroll', invalidate, true)\n win.removeEventListener('resize', invalidate)\n })\n\n const RO = win.ResizeObserver\n if (RO) {\n const ro = new RO(() => invalidate())\n for (const el of targets()) ro.observe(el)\n if (doc.body) ro.observe(doc.body)\n teardowns.push(() => ro.disconnect())\n }\n\n const MO = win.MutationObserver\n if (MO && doc.body) {\n const mo = new MO(() => invalidate())\n mo.observe(doc.body, { childList: true, subtree: true })\n teardowns.push(() => mo.disconnect())\n }\n\n const fonts = (doc as Document & { fonts?: { ready?: Promise<unknown> } }).fonts\n if (fonts?.ready) {\n let cancelled = false\n fonts.ready.then(() => !cancelled && invalidate()).catch(() => {})\n teardowns.push(() => {\n cancelled = true\n })\n }\n\n return () => {\n for (const t of teardowns) t()\n }\n}\n"]}
@@ -81,3 +81,16 @@ export declare function useCanvasRenderContext(args: {
81
81
  ctx: RenderContext;
82
82
  pageNodeIds: ReadonlySet<string>;
83
83
  };
84
+ /**
85
+ * In-canvas placeholder for a transcluding embed whose target doesn't resolve
86
+ * (EC-300/AD-16) — shown in place of the empty section so a hard reference never
87
+ * silently vanishes. Rendered INTO the iframe (portal), which carries only the
88
+ * project's stylesheet, so it uses inline styles rather than studio UI classes
89
+ * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe
90
+ * on its root. Exported for the chrome contract test. The node stays
91
+ * selectable; the inspector's `element` relation chip (EC-296) deep-links to
92
+ * the reusable to edit or publish it.
93
+ */
94
+ export declare function UnresolvedReusablePlaceholder({ status }: {
95
+ status: 'unpublished' | 'missing';
96
+ }): import("react").JSX.Element;
@@ -170,18 +170,20 @@ export function useCanvasRenderContext(args) {
170
170
  * In-canvas placeholder for a transcluding embed whose target doesn't resolve
171
171
  * (EC-300/AD-16) — shown in place of the empty section so a hard reference never
172
172
  * silently vanishes. Rendered INTO the iframe (portal), which carries only the
173
- * project's stylesheet, so it uses inline styles rather than studio UI classes.
174
- * The node stays selectable; the inspector's `element` relation chip (EC-296)
175
- * deep-links to the reusable to edit or publish it.
173
+ * project's stylesheet, so it uses inline styles rather than studio UI classes
174
+ * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe
175
+ * on its root. Exported for the chrome contract test. The node stays
176
+ * selectable; the inspector's `element` relation chip (EC-296) deep-links to
177
+ * the reusable to edit or publish it.
176
178
  */
177
- function UnresolvedReusablePlaceholder({ status }) {
179
+ export function UnresolvedReusablePlaceholder({ status }) {
178
180
  const title = status === 'unpublished'
179
181
  ? translate('builder.canvas.reusableUnpublishedTitle')
180
182
  : translate('builder.canvas.reusableMissingTitle');
181
183
  const hint = status === 'unpublished'
182
184
  ? translate('builder.canvas.reusableUnpublishedHint')
183
185
  : translate('builder.canvas.reusableMissingHint');
184
- return (_jsxs("div", { style: {
186
+ return (_jsxs("div", { className: "ec-chrome", style: {
185
187
  boxSizing: 'border-box',
186
188
  border: '1px dashed rgba(120,120,120,0.5)',
187
189
  borderRadius: 8,
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type {\n RenderContext,\n ResolveAsset,\n ResolveBinding,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { BindingReference, ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n useData,\n} from '@elytracms/studio/lib/data'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { cmsBindingFallback, useStudioLocale } from '@elytracms/studio/lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * seven injected resolvers that make the iframe preview match delivery —\n * bindings, assets (EC-195), relations (EC-254), listings (EC-255), rich-text\n * links (EC-291) — plus the per-node decorator that stamps the editable\n * wrappers (selection/hover state, locale-fallback chips EC-176, rich-text\n * remount epochs, and the EC-300 unresolved-reusable placeholder).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over pure decision helpers ({@link unresolvedReuseStatus},\n * {@link firstBindingFallbackLocale}), so the decoration and resolver wiring\n * are unit-testable without providers; {@link useCanvasRenderContext} binds\n * them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: CanvasPerspective) => unknown,\n perspective: CanvasPerspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, 'draft') ? 'unpublished' : 'missing'\n}\n\n/**\n * Locale-fallback chips (EC-176): the locale the node's first fallback-served\n * bound prop actually came from, or undefined when every bound value is a real\n * translation (or nothing is bound). Pure over an injected per-binding decision\n * (the hook wires `cmsBindingFallback` — the same decision function as the\n * inspector's Content tab, so chip and label can never disagree).\n */\nexport function firstBindingFallbackLocale(\n node: ComponentNode,\n fallbackOf: (binding: BindingReference) => { sourceLocale: string } | null,\n): string | undefined {\n for (const value of Object.values(node.props ?? {})) {\n if (value.kind !== 'binding') continue\n const fallback = fallbackOf(value.binding)\n if (fallback) return fallback.sourceLocale\n }\n return undefined\n}\n\n/** Everything the pure context factory needs — resolvers plus decoration state. */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveBinding: ResolveBinding\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n localeFallbackOf(node: ComponentNode): string | undefined\n}\n\n/**\n * Assemble the canvas `RenderContext`. Pure (no hooks): the same deps produce\n * the same context, so the decorator's wrapper markup is pinnable in tests.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return {\n registry: deps.registry,\n implementations: deps.implementations,\n resolveBinding: deps.resolveBinding,\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n data-ec-locale-fallback={deps.localeFallbackOf(node)}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n }\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (data sources, asset library, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: CanvasPerspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n const data = useData()\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // Bridge the renderer's untyped ItemContext (item: unknown) to the binding\n // layer's RepeaterItemContext (item: JsonValue). The renderer feeds back values\n // it resolved from JSON payloads, so the item is a JsonValue.\n const resolveBinding: ResolveBinding = useCallback(\n (ref, item) =>\n data.resolveValue(ref, item ? { item: item.item as JsonValue, index: item.index } : undefined),\n [data],\n )\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) to its url +\n // intrinsic dimensions from the live asset library, so the canvas previews\n // exactly what the host renders. A record without a resolved url (e.g. a\n // never-served seed) → null → the implementation's visible empty state.\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => {\n const record = assetsStore?.assets.find((a) => a.id === assetId)\n if (!record?.url) return null\n return {\n url: record.url,\n width: record.width ?? null,\n height: record.height ?? null,\n alt: record.alt ?? null,\n focalPoint: record.focalPoint ?? null,\n }\n },\n [assetsStore],\n )\n\n // Locale-fallback chips (EC-176): a node whose bound content currently\n // previews via EC-018 locale fallback wears a corner chip naming the locale\n // the value actually came from. Same decision function as the inspector's\n // Content tab (`cmsBindingFallback`), so chip and label can never disagree.\n const { locale: activeLocale } = useStudioLocale()\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective],\n )\n\n const localeFallbackOf = useCallback(\n (node: ComponentNode): string | undefined =>\n firstBindingFallbackLocale(node, (binding) =>\n cmsBindingFallback(binding, data.getSource(binding.sourceId), activeLocale),\n ),\n [data, activeLocale],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveBinding,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n localeFallbackOf,\n }),\n [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, localeFallbackOf, richEditEpochs],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes.\n * The node stays selectable; the inspector's `element` relation chip (EC-296)\n * deep-links to the reusable to edit or publish it.\n */\nfunction UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type {\n RenderContext,\n ResolveAsset,\n ResolveBinding,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { BindingReference, ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n useData,\n} from '@elytracms/studio/lib/data'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { cmsBindingFallback, useStudioLocale } from '@elytracms/studio/lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * seven injected resolvers that make the iframe preview match delivery —\n * bindings, assets (EC-195), relations (EC-254), listings (EC-255), rich-text\n * links (EC-291) — plus the per-node decorator that stamps the editable\n * wrappers (selection/hover state, locale-fallback chips EC-176, rich-text\n * remount epochs, and the EC-300 unresolved-reusable placeholder).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over pure decision helpers ({@link unresolvedReuseStatus},\n * {@link firstBindingFallbackLocale}), so the decoration and resolver wiring\n * are unit-testable without providers; {@link useCanvasRenderContext} binds\n * them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: CanvasPerspective) => unknown,\n perspective: CanvasPerspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, 'draft') ? 'unpublished' : 'missing'\n}\n\n/**\n * Locale-fallback chips (EC-176): the locale the node's first fallback-served\n * bound prop actually came from, or undefined when every bound value is a real\n * translation (or nothing is bound). Pure over an injected per-binding decision\n * (the hook wires `cmsBindingFallback` — the same decision function as the\n * inspector's Content tab, so chip and label can never disagree).\n */\nexport function firstBindingFallbackLocale(\n node: ComponentNode,\n fallbackOf: (binding: BindingReference) => { sourceLocale: string } | null,\n): string | undefined {\n for (const value of Object.values(node.props ?? {})) {\n if (value.kind !== 'binding') continue\n const fallback = fallbackOf(value.binding)\n if (fallback) return fallback.sourceLocale\n }\n return undefined\n}\n\n/** Everything the pure context factory needs — resolvers plus decoration state. */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveBinding: ResolveBinding\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n localeFallbackOf(node: ComponentNode): string | undefined\n}\n\n/**\n * Assemble the canvas `RenderContext`. Pure (no hooks): the same deps produce\n * the same context, so the decorator's wrapper markup is pinnable in tests.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return {\n registry: deps.registry,\n implementations: deps.implementations,\n resolveBinding: deps.resolveBinding,\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n data-ec-locale-fallback={deps.localeFallbackOf(node)}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n }\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (data sources, asset library, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: CanvasPerspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n const data = useData()\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // Bridge the renderer's untyped ItemContext (item: unknown) to the binding\n // layer's RepeaterItemContext (item: JsonValue). The renderer feeds back values\n // it resolved from JSON payloads, so the item is a JsonValue.\n const resolveBinding: ResolveBinding = useCallback(\n (ref, item) =>\n data.resolveValue(ref, item ? { item: item.item as JsonValue, index: item.index } : undefined),\n [data],\n )\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) to its url +\n // intrinsic dimensions from the live asset library, so the canvas previews\n // exactly what the host renders. A record without a resolved url (e.g. a\n // never-served seed) → null → the implementation's visible empty state.\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => {\n const record = assetsStore?.assets.find((a) => a.id === assetId)\n if (!record?.url) return null\n return {\n url: record.url,\n width: record.width ?? null,\n height: record.height ?? null,\n alt: record.alt ?? null,\n focalPoint: record.focalPoint ?? null,\n }\n },\n [assetsStore],\n )\n\n // Locale-fallback chips (EC-176): a node whose bound content currently\n // previews via EC-018 locale fallback wears a corner chip naming the locale\n // the value actually came from. Same decision function as the inspector's\n // Content tab (`cmsBindingFallback`), so chip and label can never disagree.\n const { locale: activeLocale } = useStudioLocale()\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective],\n )\n\n const localeFallbackOf = useCallback(\n (node: ComponentNode): string | undefined =>\n firstBindingFallbackLocale(node, (binding) =>\n cmsBindingFallback(binding, data.getSource(binding.sourceId), activeLocale),\n ),\n [data, activeLocale],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveBinding,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n localeFallbackOf,\n }),\n [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, localeFallbackOf, richEditEpochs],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes\n * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe\n * on its root. Exported for the chrome contract test. The node stays\n * selectable; the inspector's `element` relation chip (EC-296) deep-links to\n * the reusable to edit or publish it.\n */\nexport function UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n className=\"ec-chrome\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
@@ -1,4 +1,4 @@
1
- /** The consuming app supplies its compiled studio CSS URL at mount. */
1
+ /** The consuming app supplies its compiled canvas stylesheet URL at mount. */
2
2
  export declare function setCanvasStylesheetUrl(url: string): void;
3
3
  /** The injected stylesheet URL, or `null` when the app has not supplied one. */
4
4
  export declare function getCanvasStylesheetUrl(): string | null;