@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.
- package/convex/assets.ts +11 -0
- package/convex/delivery.ts +5 -0
- package/convex/schema.ts +23 -0
- package/dist/components/account/self-account-view-source.js +9 -2
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +28 -1
- package/dist/components/app-views/default-modules.js +47 -2
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-thumb.d.ts +4 -2
- package/dist/components/assets/asset-thumb.js +26 -4
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-module.js +48 -2
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +18 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/auth-context.js +32 -1
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +30 -10
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +90 -8
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.js +4 -2
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
- package/dist/components/builder/canvas-inline-editing.js +3 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
- package/dist/components/builder/canvas-overlay-geometry.js +80 -0
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
- package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
- package/dist/components/builder/canvas-rect-tracking.js +198 -0
- package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
- package/dist/components/builder/canvas.js +61 -12
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +22 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +38 -0
- package/dist/components/builder/delete-key.js +46 -0
- package/dist/components/builder/delete-key.js.map +1 -0
- package/dist/components/builder/drag-session.d.ts +8 -0
- package/dist/components/builder/drag-session.js +15 -0
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/insert-affordance-context.d.ts +19 -0
- package/dist/components/builder/insert-affordance-context.js +7 -0
- package/dist/components/builder/insert-affordance-context.js.map +1 -0
- package/dist/components/builder/insert-context.d.ts +40 -0
- package/dist/components/builder/insert-context.js +97 -5
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-title.d.ts +60 -0
- package/dist/components/builder/node-title.js +135 -0
- package/dist/components/builder/node-title.js.map +1 -0
- package/dist/components/builder/page-tree.js +25 -5
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/prop-selection.d.ts +52 -0
- package/dist/components/builder/prop-selection.js +38 -0
- package/dist/components/builder/prop-selection.js.map +1 -0
- package/dist/components/builder/prop-sync-context.d.ts +49 -0
- package/dist/components/builder/prop-sync-context.js +23 -0
- package/dist/components/builder/prop-sync-context.js.map +1 -0
- package/dist/components/builder/property-inspector.js +24 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +1 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +79 -0
- package/dist/components/builder/selection-overlay.js +244 -0
- package/dist/components/builder/selection-overlay.js.map +1 -0
- package/dist/components/builder/spacing-handles.d.ts +57 -1
- package/dist/components/builder/spacing-handles.js +142 -26
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.js +21 -5
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker.js +11 -5
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +22 -3
- package/dist/components/cms/bulk-actions.js +27 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cms-store.js +19 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +18 -19
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +23 -16
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +21 -0
- package/dist/components/cms/publish-clean.js +30 -0
- package/dist/components/cms/publish-clean.js.map +1 -0
- package/dist/components/cms/rich-text/block-style.d.ts +48 -0
- package/dist/components/cms/rich-text/block-style.js +30 -0
- package/dist/components/cms/rich-text/block-style.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.js +4 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +2 -0
- package/dist/components/cms/rich-text/extensions.js +4 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
- package/dist/components/cms/rich-text/font-size-control.js +40 -0
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
- package/dist/components/cms/rich-text/font-size.d.ts +16 -0
- package/dist/components/cms/rich-text/font-size.js +40 -0
- package/dist/components/cms/rich-text/font-size.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
- package/dist/components/cms/rich-text/toolbar.js +17 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +24 -1
- package/dist/components/cms/use-live-url.js +26 -4
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +8 -6
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +15 -7
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/components/component-detail.js +3 -3
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +1 -1
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.js +3 -2
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
- package/dist/lib/assets/backfill-thumbs.js +120 -0
- package/dist/lib/assets/backfill-thumbs.js.map +1 -0
- package/dist/lib/assets/delete-asset.d.ts +2 -1
- package/dist/lib/assets/delete-asset.js +12 -0
- package/dist/lib/assets/delete-asset.js.map +1 -1
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/assets/thumbnail.d.ts +145 -0
- package/dist/lib/assets/thumbnail.js +220 -0
- package/dist/lib/assets/thumbnail.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +9 -0
- package/dist/lib/assets/upload.js +31 -0
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/auth/auth-adapter.d.ts +22 -0
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
- package/dist/lib/auth/convex-auth-adapter.js +69 -0
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
- package/dist/lib/auth/local-auth-adapter.js +7 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +45 -3
- package/dist/lib/auth-errors.js +46 -5
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +5 -2
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +13 -0
- package/dist/lib/dnd/drop-rules.js +15 -0
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +19 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +16 -0
- package/dist/lib/i18n/messages.en.js +24 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +4 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -3
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +9 -5
- package/dist/lib/persona/labels.js +13 -8
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +19 -6
- package/dist/lib/session-expiry.js +13 -1
- package/dist/lib/session-expiry.js.map +1 -1
- package/dist/lib/session-revalidation.d.ts +81 -0
- package/dist/lib/session-revalidation.js +94 -0
- package/dist/lib/session-revalidation.js.map +1 -0
- package/package.json +2 -2
- 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"]}
|