@elytracms/studio 0.0.64 → 0.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/account/self-account-view-source.js +9 -2
  5. package/dist/components/account/self-account-view-source.js.map +1 -1
  6. package/dist/components/app-views/default-modules.d.ts +28 -1
  7. package/dist/components/app-views/default-modules.js +47 -2
  8. package/dist/components/app-views/default-modules.js.map +1 -1
  9. package/dist/components/assets/asset-thumb.d.ts +4 -2
  10. package/dist/components/assets/asset-thumb.js +26 -4
  11. package/dist/components/assets/asset-thumb.js.map +1 -1
  12. package/dist/components/assets/assets-module.js +48 -2
  13. package/dist/components/assets/assets-module.js.map +1 -1
  14. package/dist/components/assets/assets-store.d.ts +11 -1
  15. package/dist/components/assets/assets-store.js +18 -1
  16. package/dist/components/assets/assets-store.js.map +1 -1
  17. package/dist/components/auth-context.js +32 -1
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/blocks-model.d.ts +11 -0
  20. package/dist/components/builder/blocks-model.js +30 -10
  21. package/dist/components/builder/blocks-model.js.map +1 -1
  22. package/dist/components/builder/canvas-chrome-css.js +90 -8
  23. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  24. package/dist/components/builder/canvas-dnd.js +4 -2
  25. package/dist/components/builder/canvas-dnd.js.map +1 -1
  26. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  27. package/dist/components/builder/canvas-inline-editing.js +3 -1
  28. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  29. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  30. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  31. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  32. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  33. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  34. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  35. package/dist/components/builder/canvas.js +61 -12
  36. package/dist/components/builder/canvas.js.map +1 -1
  37. package/dist/components/builder/composition-field-editor.js +22 -3
  38. package/dist/components/builder/composition-field-editor.js.map +1 -1
  39. package/dist/components/builder/delete-key.d.ts +38 -0
  40. package/dist/components/builder/delete-key.js +46 -0
  41. package/dist/components/builder/delete-key.js.map +1 -0
  42. package/dist/components/builder/drag-session.d.ts +8 -0
  43. package/dist/components/builder/drag-session.js +15 -0
  44. package/dist/components/builder/drag-session.js.map +1 -1
  45. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  46. package/dist/components/builder/insert-affordance-context.js +7 -0
  47. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  48. package/dist/components/builder/insert-context.d.ts +40 -0
  49. package/dist/components/builder/insert-context.js +97 -5
  50. package/dist/components/builder/insert-context.js.map +1 -1
  51. package/dist/components/builder/node-title.d.ts +60 -0
  52. package/dist/components/builder/node-title.js +135 -0
  53. package/dist/components/builder/node-title.js.map +1 -0
  54. package/dist/components/builder/page-tree.js +25 -5
  55. package/dist/components/builder/page-tree.js.map +1 -1
  56. package/dist/components/builder/prop-selection.d.ts +52 -0
  57. package/dist/components/builder/prop-selection.js +38 -0
  58. package/dist/components/builder/prop-selection.js.map +1 -0
  59. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  60. package/dist/components/builder/prop-sync-context.js +23 -0
  61. package/dist/components/builder/prop-sync-context.js.map +1 -0
  62. package/dist/components/builder/property-inspector.js +24 -1
  63. package/dist/components/builder/property-inspector.js.map +1 -1
  64. package/dist/components/builder/reuse-context.js +1 -1
  65. package/dist/components/builder/reuse-context.js.map +1 -1
  66. package/dist/components/builder/selection-overlay.d.ts +79 -0
  67. package/dist/components/builder/selection-overlay.js +244 -0
  68. package/dist/components/builder/selection-overlay.js.map +1 -0
  69. package/dist/components/builder/spacing-handles.d.ts +57 -1
  70. package/dist/components/builder/spacing-handles.js +142 -26
  71. package/dist/components/builder/spacing-handles.js.map +1 -1
  72. package/dist/components/cms/asset-field.js +21 -5
  73. package/dist/components/cms/asset-field.js.map +1 -1
  74. package/dist/components/cms/asset-picker.js +11 -5
  75. package/dist/components/cms/asset-picker.js.map +1 -1
  76. package/dist/components/cms/bulk-actions.d.ts +22 -3
  77. package/dist/components/cms/bulk-actions.js +27 -3
  78. package/dist/components/cms/bulk-actions.js.map +1 -1
  79. package/dist/components/cms/cms-store.js +19 -0
  80. package/dist/components/cms/cms-store.js.map +1 -1
  81. package/dist/components/cms/detail-editor.js +18 -19
  82. package/dist/components/cms/detail-editor.js.map +1 -1
  83. package/dist/components/cms/prop-fields.js +23 -16
  84. package/dist/components/cms/prop-fields.js.map +1 -1
  85. package/dist/components/cms/publish-clean.d.ts +21 -0
  86. package/dist/components/cms/publish-clean.js +30 -0
  87. package/dist/components/cms/publish-clean.js.map +1 -0
  88. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  89. package/dist/components/cms/rich-text/block-style.js +30 -0
  90. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  91. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  92. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  93. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  94. package/dist/components/cms/rich-text/extensions.js +4 -0
  95. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  96. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  97. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  98. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  99. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  100. package/dist/components/cms/rich-text/font-size.js +40 -0
  101. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  102. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  103. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  104. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  105. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  106. package/dist/components/cms/rich-text/toolbar.js +17 -5
  107. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  108. package/dist/components/cms/use-live-url.d.ts +24 -1
  109. package/dist/components/cms/use-live-url.js +26 -4
  110. package/dist/components/cms/use-live-url.js.map +1 -1
  111. package/dist/components/cms/use-publish-controller.js +8 -6
  112. package/dist/components/cms/use-publish-controller.js.map +1 -1
  113. package/dist/components/collection-view/cms-collection-view-source.js +15 -7
  114. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  115. package/dist/components/components/component-detail.js +3 -3
  116. package/dist/components/components/component-detail.js.map +1 -1
  117. package/dist/components/components/component-preview.js +1 -1
  118. package/dist/components/components/component-preview.js.map +1 -1
  119. package/dist/components/components/registry-browser.js +2 -2
  120. package/dist/components/components/registry-browser.js.map +1 -1
  121. package/dist/components/components/taxonomy.js +3 -2
  122. package/dist/components/components/taxonomy.js.map +1 -1
  123. package/dist/components/components/transfer-dialog.js +1 -1
  124. package/dist/components/components/transfer-dialog.js.map +1 -1
  125. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  126. package/dist/lib/assets/backfill-thumbs.js +120 -0
  127. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  128. package/dist/lib/assets/delete-asset.d.ts +2 -1
  129. package/dist/lib/assets/delete-asset.js +12 -0
  130. package/dist/lib/assets/delete-asset.js.map +1 -1
  131. package/dist/lib/assets/index.d.ts +2 -0
  132. package/dist/lib/assets/index.js +2 -0
  133. package/dist/lib/assets/index.js.map +1 -1
  134. package/dist/lib/assets/thumbnail.d.ts +145 -0
  135. package/dist/lib/assets/thumbnail.js +220 -0
  136. package/dist/lib/assets/thumbnail.js.map +1 -0
  137. package/dist/lib/assets/upload.d.ts +9 -0
  138. package/dist/lib/assets/upload.js +31 -0
  139. package/dist/lib/assets/upload.js.map +1 -1
  140. package/dist/lib/auth/auth-adapter.d.ts +22 -0
  141. package/dist/lib/auth/auth-adapter.js.map +1 -1
  142. package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
  143. package/dist/lib/auth/convex-auth-adapter.js +69 -0
  144. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  145. package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
  146. package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
  147. package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
  148. package/dist/lib/auth/index.d.ts +1 -1
  149. package/dist/lib/auth/index.js.map +1 -1
  150. package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
  151. package/dist/lib/auth/local-auth-adapter.js +7 -0
  152. package/dist/lib/auth/local-auth-adapter.js.map +1 -1
  153. package/dist/lib/auth-errors.d.ts +45 -3
  154. package/dist/lib/auth-errors.js +46 -5
  155. package/dist/lib/auth-errors.js.map +1 -1
  156. package/dist/lib/clipboard/paste-target.js +5 -2
  157. package/dist/lib/clipboard/paste-target.js.map +1 -1
  158. package/dist/lib/dnd/drop-rules.d.ts +13 -0
  159. package/dist/lib/dnd/drop-rules.js +15 -0
  160. package/dist/lib/dnd/drop-rules.js.map +1 -1
  161. package/dist/lib/i18n/messages.de.js +19 -0
  162. package/dist/lib/i18n/messages.de.js.map +1 -1
  163. package/dist/lib/i18n/messages.en.d.ts +16 -0
  164. package/dist/lib/i18n/messages.en.js +24 -0
  165. package/dist/lib/i18n/messages.en.js.map +1 -1
  166. package/dist/lib/persistence/convex-adapter.js +4 -0
  167. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  168. package/dist/lib/persistence/index.js +6 -3
  169. package/dist/lib/persistence/index.js.map +1 -1
  170. package/dist/lib/persona/labels.d.ts +9 -5
  171. package/dist/lib/persona/labels.js +13 -8
  172. package/dist/lib/persona/labels.js.map +1 -1
  173. package/dist/lib/references/references-store.js +7 -1
  174. package/dist/lib/references/references-store.js.map +1 -1
  175. package/dist/lib/session-expiry.d.ts +19 -6
  176. package/dist/lib/session-expiry.js +13 -1
  177. package/dist/lib/session-expiry.js.map +1 -1
  178. package/dist/lib/session-revalidation.d.ts +81 -0
  179. package/dist/lib/session-revalidation.js +94 -0
  180. package/dist/lib/session-revalidation.js.map +1 -0
  181. package/package.json +2 -2
  182. package/src/styles/studio.css +13 -0
@@ -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;
@@ -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":"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","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"]}