@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,79 @@
|
|
|
1
|
+
import type { InsertPosition } from './insert-context.js';
|
|
2
|
+
import type { AffordanceAnchor, FrameBarPlacement } from './canvas-overlay-geometry.js';
|
|
3
|
+
import type { Rect } from './canvas-rect-tracking.js';
|
|
4
|
+
/**
|
|
5
|
+
* Canvas selection/hover overlay (EC-404) — a topmost layer portaled into the
|
|
6
|
+
* iframe `body` that draws the selection and hover frames ABOVE the page from
|
|
7
|
+
* measured node rects, so a full-bleed background or a higher-`z-index` sibling
|
|
8
|
+
* can never paint over them (the structural fix for the in-document inset
|
|
9
|
+
* outline the findings batch flagged). The selected frame carries a compact tag:
|
|
10
|
+
* the component's display name plus move-up / move-down / duplicate / delete
|
|
11
|
+
* controls that dispatch the EXISTING tree actions.
|
|
12
|
+
*
|
|
13
|
+
* EC-414 AD-B chrome contract: everything here is `ec-*` classes / `data-ec-*`
|
|
14
|
+
* attributes / inline geometry only, styled by `canvas-chrome-css.ts`, with the
|
|
15
|
+
* `.ec-chrome` containment recipe on the root; no studio Tailwind utilities and
|
|
16
|
+
* no studio React UI primitives cross into the iframe. The rect-tracking core
|
|
17
|
+
* ({@link createRectTracker}) is a pure, reusable module — EC-405 rides this
|
|
18
|
+
* overlay layer next, and EC-406 can share the tracking.
|
|
19
|
+
*/
|
|
20
|
+
/** The overlay's full measured geometry (both frames + the bar placement). */
|
|
21
|
+
export interface OverlayGeometry {
|
|
22
|
+
selected: Rect | null;
|
|
23
|
+
/** Chrome-bar placement for the selected frame (null when nothing to draw). */
|
|
24
|
+
selectedBar: FrameBarPlacement | null;
|
|
25
|
+
hovered: Rect | null;
|
|
26
|
+
/** EC-408: the focused prop's marker rect (reverse highlight), or null. */
|
|
27
|
+
propHighlight: Rect | null;
|
|
28
|
+
}
|
|
29
|
+
/** A drawn selection frame plus its control enablement. */
|
|
30
|
+
export interface SelectionFrameView {
|
|
31
|
+
rect: Rect;
|
|
32
|
+
bar: FrameBarPlacement;
|
|
33
|
+
label: string;
|
|
34
|
+
canMoveUp: boolean;
|
|
35
|
+
canMoveDown: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** One "+" affordance button: its position and its placed anchor point. */
|
|
38
|
+
export interface AffordanceItemView extends AffordanceAnchor {
|
|
39
|
+
position: InsertPosition;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The affordance buttons for one node — grouped so a single `onInsert` binds the
|
|
43
|
+
* node id the inserts target (the selected node and, separately, a hovered one).
|
|
44
|
+
*/
|
|
45
|
+
export interface AffordanceGroupView {
|
|
46
|
+
nodeId: string;
|
|
47
|
+
items: AffordanceItemView[];
|
|
48
|
+
onInsert: (position: InsertPosition) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface SelectionOverlayViewProps {
|
|
51
|
+
selected: SelectionFrameView | null;
|
|
52
|
+
hovered: Rect | null;
|
|
53
|
+
/** In-canvas insertion affordances (EC-405); empty when suppressed (drag/editing). */
|
|
54
|
+
affordances: AffordanceGroupView[];
|
|
55
|
+
/** EC-408: the focused prop's marker rect (reverse highlight), or null. */
|
|
56
|
+
propHighlight?: Rect | null;
|
|
57
|
+
onMoveUp: () => void;
|
|
58
|
+
onMoveDown: () => void;
|
|
59
|
+
onDuplicate: () => void;
|
|
60
|
+
onDelete: () => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The overlay's rendered markup, portal-free so the AD-D chrome contract test
|
|
64
|
+
* can walk it with `renderToStaticMarkup`: every class is `ec-*`, the root
|
|
65
|
+
* carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).
|
|
66
|
+
*/
|
|
67
|
+
export declare function SelectionOverlayView(props: SelectionOverlayViewProps): import("react").JSX.Element;
|
|
68
|
+
/**
|
|
69
|
+
* The live overlay: tracks the selected + hovered node rects (glued through
|
|
70
|
+
* scroll/resize/async layout via {@link subscribeCanvasLayout} + a coalescing
|
|
71
|
+
* {@link createRectTracker}) and portals the frames into the iframe `body`.
|
|
72
|
+
* Suppressed for the composition root (no frame around the whole page — mirrors
|
|
73
|
+
* the tree, which offers the root no actions) and while an inline text/rich-text
|
|
74
|
+
* edit is live (the `[data-ec-editing]` outline owns that state).
|
|
75
|
+
*/
|
|
76
|
+
export declare function SelectionOverlay({ doc, editing }: {
|
|
77
|
+
doc: Document;
|
|
78
|
+
editing: boolean;
|
|
79
|
+
}): import("react").ReactPortal;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
5
|
+
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
6
|
+
import { translate, useUiLocale } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
8
|
+
import { useBuilder } from './builder-store.js';
|
|
9
|
+
import { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session.js';
|
|
10
|
+
import { useInsertAffordance } from './insert-affordance-context.js';
|
|
11
|
+
import { FRAME_BAR_HEIGHT, frameControlAction, frameControlsFor, placeAffordance, placeFrameBar, } from './canvas-overlay-geometry.js';
|
|
12
|
+
import { createRectTracker, measureNodeRect, measurePropRect, rectsEqual, scheduleFrame, subscribeCanvasLayout, } from './canvas-rect-tracking.js';
|
|
13
|
+
import { usePropSync } from './prop-sync-context.js';
|
|
14
|
+
import { nodeDisplayTitle } from './node-title.js';
|
|
15
|
+
const EMPTY_GEOMETRY = {
|
|
16
|
+
selected: null,
|
|
17
|
+
selectedBar: null,
|
|
18
|
+
hovered: null,
|
|
19
|
+
propHighlight: null,
|
|
20
|
+
};
|
|
21
|
+
function barsEqual(a, b) {
|
|
22
|
+
if (a === null || b === null)
|
|
23
|
+
return a === b;
|
|
24
|
+
return a.top === b.top && a.left === b.left && a.placement === b.placement;
|
|
25
|
+
}
|
|
26
|
+
function geometryEquals(a, b) {
|
|
27
|
+
return (rectsEqual(a.selected, b.selected) &&
|
|
28
|
+
rectsEqual(a.hovered, b.hovered) &&
|
|
29
|
+
rectsEqual(a.propHighlight, b.propHighlight) &&
|
|
30
|
+
barsEqual(a.selectedBar, b.selectedBar));
|
|
31
|
+
}
|
|
32
|
+
function frameStyle(rect) {
|
|
33
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
34
|
+
}
|
|
35
|
+
const AFFORDANCE_LABELS = {
|
|
36
|
+
before: 'builder.canvas.insertBefore',
|
|
37
|
+
after: 'builder.canvas.insertAfter',
|
|
38
|
+
into: 'builder.canvas.insertInto',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The overlay's rendered markup, portal-free so the AD-D chrome contract test
|
|
42
|
+
* can walk it with `renderToStaticMarkup`: every class is `ec-*`, the root
|
|
43
|
+
* carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).
|
|
44
|
+
*/
|
|
45
|
+
export function SelectionOverlayView(props) {
|
|
46
|
+
const { selected, hovered, affordances, propHighlight } = props;
|
|
47
|
+
return (_jsxs("div", { className: "ec-chrome ec-overlay", "data-ec-overlay": "", children: [propHighlight ? (_jsx("div", { className: "ec-overlay-prop", "data-ec-prop-highlight": "", style: frameStyle(propHighlight) })) : null, hovered ? _jsx("div", { className: "ec-overlay-frame", "data-ec-hover": "", style: frameStyle(hovered) }) : null, affordances.map((group) => group.items.map((item) => (_jsx(AddButton, { item: item, onInsert: () => group.onInsert(item.position) }, `${group.nodeId}:${item.position}`)))), selected ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "ec-overlay-frame", "data-ec-selected": "", style: frameStyle(selected.rect) }), _jsxs("div", { className: "ec-overlay-bar", "data-ec-frame-bar": "", "data-ec-placement": selected.bar.placement, style: { left: selected.bar.left, top: selected.bar.top }, children: [_jsx("span", { className: "ec-overlay-label", title: selected.label, children: selected.label }), _jsx("span", { className: "ec-overlay-sep", "aria-hidden": "true" }), _jsx(FrameButton, { label: translate('builder.pageTree.moveUp'), disabled: !selected.canMoveUp, onClick: props.onMoveUp, children: _jsx(OverlayIcon, { kind: "moveUp" }) }), _jsx(FrameButton, { label: translate('builder.pageTree.moveDown'), disabled: !selected.canMoveDown, onClick: props.onMoveDown, children: _jsx(OverlayIcon, { kind: "moveDown" }) }), _jsx(FrameButton, { label: translate('common.duplicate'), onClick: props.onDuplicate, children: _jsx(OverlayIcon, { kind: "duplicate" }) }), _jsx(FrameButton, { label: translate('common.delete'), onClick: props.onDelete, children: _jsx(OverlayIcon, { kind: "delete" }) })] })] })) : null] }));
|
|
48
|
+
}
|
|
49
|
+
function FrameButton({ label, disabled, onClick, children, }) {
|
|
50
|
+
return (_jsx("button", { type: "button", className: "ec-overlay-btn", "aria-label": label, title: label, disabled: disabled,
|
|
51
|
+
// Keep the click on the control: it must not fall through to the canvas
|
|
52
|
+
// selection listener (the bar sits over the page) nor start a text drag.
|
|
53
|
+
onMouseDown: (e) => e.preventDefault(), onClick: (e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
onClick();
|
|
57
|
+
}, children: children }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A "+" insertion affordance (EC-405): a small round button centered on a block
|
|
61
|
+
* boundary (before/after edge, or the center of an empty container). Centered on
|
|
62
|
+
* its anchor by the chrome CSS `translate(-50%,-50%)`; `pointer-events` re-enabled
|
|
63
|
+
* on the button while the overlay root stays click-through. The gesture guards
|
|
64
|
+
* mirror `FrameButton` — the press must not fall through to the canvas selection
|
|
65
|
+
* listener nor start a native drag.
|
|
66
|
+
*/
|
|
67
|
+
function AddButton({ item, onInsert }) {
|
|
68
|
+
const label = translate(AFFORDANCE_LABELS[item.position]);
|
|
69
|
+
return (_jsx("button", { type: "button", className: "ec-overlay-add", "data-ec-affordance": item.position, "aria-label": label, title: label, style: { left: item.left, top: item.top }, onMouseDown: (e) => e.preventDefault(), onClick: (e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
onInsert();
|
|
73
|
+
}, children: _jsx("svg", { width: 14, height: 14, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M8 3.5v9M3.5 8h9" }) }) }));
|
|
74
|
+
}
|
|
75
|
+
/** Hand-rolled inline icons — no `class` attribute (chrome contract). */
|
|
76
|
+
function OverlayIcon({ kind }) {
|
|
77
|
+
const common = {
|
|
78
|
+
width: 13,
|
|
79
|
+
height: 13,
|
|
80
|
+
viewBox: '0 0 16 16',
|
|
81
|
+
fill: 'none',
|
|
82
|
+
stroke: 'currentColor',
|
|
83
|
+
strokeWidth: 1.6,
|
|
84
|
+
strokeLinecap: 'round',
|
|
85
|
+
strokeLinejoin: 'round',
|
|
86
|
+
'aria-hidden': true,
|
|
87
|
+
};
|
|
88
|
+
switch (kind) {
|
|
89
|
+
case 'moveUp':
|
|
90
|
+
return (_jsx("svg", { ...common, children: _jsx("path", { d: "M4 10l4-4 4 4" }) }));
|
|
91
|
+
case 'moveDown':
|
|
92
|
+
return (_jsx("svg", { ...common, children: _jsx("path", { d: "M4 6l4 4 4-4" }) }));
|
|
93
|
+
case 'duplicate':
|
|
94
|
+
return (_jsxs("svg", { ...common, children: [_jsx("rect", { x: "6", y: "6", width: "8", height: "8", rx: "1.5" }), _jsx("path", { d: "M10 6V4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2" })] }));
|
|
95
|
+
case 'delete':
|
|
96
|
+
return (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M3 4.5h10" }), _jsx("path", { d: "M6.5 4.5V3a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1.5" }), _jsx("path", { d: "M5 4.5l.5 8a1 1 0 0 0 1 .9h3a1 1 0 0 0 1-.9l.5-8" })] }));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The live overlay: tracks the selected + hovered node rects (glued through
|
|
101
|
+
* scroll/resize/async layout via {@link subscribeCanvasLayout} + a coalescing
|
|
102
|
+
* {@link createRectTracker}) and portals the frames into the iframe `body`.
|
|
103
|
+
* Suppressed for the composition root (no frame around the whole page — mirrors
|
|
104
|
+
* the tree, which offers the root no actions) and while an inline text/rich-text
|
|
105
|
+
* edit is live (the `[data-ec-editing]` outline owns that state).
|
|
106
|
+
*/
|
|
107
|
+
export function SelectionOverlay({ doc, editing }) {
|
|
108
|
+
const builder = useBuilder();
|
|
109
|
+
const { present, selectedId, hoveredId, dispatch } = builder;
|
|
110
|
+
const studio = getStudioRegistry();
|
|
111
|
+
const affordance = useInsertAffordance();
|
|
112
|
+
const propSync = usePropSync();
|
|
113
|
+
const uiLocale = useUiLocale();
|
|
114
|
+
const [geometry, setGeometry] = useState(EMPTY_GEOMETRY);
|
|
115
|
+
// EC-408 reverse highlight: the focused prop's marker, but only for the
|
|
116
|
+
// selected node (the inspector only shows that node's props) and never while
|
|
117
|
+
// inline-editing (the [data-ec-editing] outline owns the node then).
|
|
118
|
+
const highlight = propSync?.highlighted && propSync.highlighted.nodeId === selectedId && !editing
|
|
119
|
+
? propSync.highlighted
|
|
120
|
+
: null;
|
|
121
|
+
const highlightNodeId = highlight?.nodeId ?? null;
|
|
122
|
+
const highlightPropName = highlight?.propName ?? null;
|
|
123
|
+
// EC-405 scope 4: insertion affordances hide while ANY drag is live (the
|
|
124
|
+
// DnD before/after/into indicators own the "where it lands" story then).
|
|
125
|
+
const [dragging, setDragging] = useState(() => activeDrag() !== null);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const offStart = onDragSessionStart(() => setDragging(true));
|
|
128
|
+
const offEnd = onDragSessionEnd(() => setDragging(false));
|
|
129
|
+
return () => {
|
|
130
|
+
offStart();
|
|
131
|
+
offEnd();
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
// The selected node's controls/label come from the graph; the root and a
|
|
135
|
+
// never-found selection draw nothing. Hover never frames the currently
|
|
136
|
+
// selected node (one frame at a time) nor the root.
|
|
137
|
+
const selectedNode = selectedId && !editing ? getNode(present, selectedId) : null;
|
|
138
|
+
const controls = selectedNode ? frameControlsFor(present, selectedId) : null;
|
|
139
|
+
const drawSelected = selectedNode !== null && controls !== null && !controls.isRoot;
|
|
140
|
+
const hoveredNode = hoveredId && hoveredId !== selectedId ? getNode(present, hoveredId) : null;
|
|
141
|
+
const drawHover = hoveredNode !== null && !frameControlsFor(present, hoveredId).isRoot;
|
|
142
|
+
useLayoutEffect(() => {
|
|
143
|
+
const win = doc.defaultView;
|
|
144
|
+
if (!win)
|
|
145
|
+
return;
|
|
146
|
+
const selId = drawSelected ? selectedId : null;
|
|
147
|
+
const hovId = drawHover ? hoveredId : null;
|
|
148
|
+
const measure = () => {
|
|
149
|
+
const selected = measureNodeRect(doc, selId);
|
|
150
|
+
const hovered = measureNodeRect(doc, hovId);
|
|
151
|
+
return {
|
|
152
|
+
selected,
|
|
153
|
+
selectedBar: selected ? placeFrameBar(selected, { barHeight: FRAME_BAR_HEIGHT, docTop: win.scrollY }) : null,
|
|
154
|
+
hovered,
|
|
155
|
+
propHighlight: measurePropRect(doc, highlightNodeId, highlightPropName),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
const targets = () => {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const id of [selId, hovId]) {
|
|
161
|
+
if (!id)
|
|
162
|
+
continue;
|
|
163
|
+
const el = doc.querySelector(`[data-ec-node-id="${id}"]`);
|
|
164
|
+
if (el)
|
|
165
|
+
out.push(el);
|
|
166
|
+
}
|
|
167
|
+
// Track the highlighted marker element too, so it re-glues on async layout.
|
|
168
|
+
if (highlightNodeId) {
|
|
169
|
+
const wrapper = doc.querySelector(`[data-ec-node-id="${highlightNodeId}"]`);
|
|
170
|
+
if (wrapper)
|
|
171
|
+
out.push(wrapper);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
};
|
|
175
|
+
// Coalesce on the PARENT window's frame loop (not the iframe's): this
|
|
176
|
+
// component + its state live in the parent React tree, and the iframe's rAF
|
|
177
|
+
// is throttled/paused whenever the preview is offscreen or backgrounded. The
|
|
178
|
+
// compound rAF+timeout scheduler (scheduleFrame) keeps the flush paint-
|
|
179
|
+
// aligned on screen while guaranteeing it still runs when rAF is paused.
|
|
180
|
+
const host = win.parent ?? window;
|
|
181
|
+
const tracker = createRectTracker({
|
|
182
|
+
measure,
|
|
183
|
+
equals: geometryEquals,
|
|
184
|
+
onChange: setGeometry,
|
|
185
|
+
schedule: (flush) => scheduleFrame(host, flush),
|
|
186
|
+
subscribe: (invalidate) => subscribeCanvasLayout(doc, targets, invalidate),
|
|
187
|
+
});
|
|
188
|
+
return () => tracker.dispose();
|
|
189
|
+
// Re-subscribe when the tracked ids / graph / highlighted prop change so the
|
|
190
|
+
// ResizeObserver re-points at the current (possibly remounted) wrappers and
|
|
191
|
+
// the measure closure reads the current highlight target.
|
|
192
|
+
}, [doc, drawSelected, drawHover, selectedId, hoveredId, present, highlightNodeId, highlightPropName]);
|
|
193
|
+
// EC-407: the frame label shares the tree row's resolution — the SAME
|
|
194
|
+
// nodeDisplayTitle.primary the selected row shows (instance title when
|
|
195
|
+
// derivable, else the localized component name).
|
|
196
|
+
const label = selectedNode
|
|
197
|
+
? nodeDisplayTitle(studio.registry.getManifest(selectedNode.componentId), selectedNode, uiLocale).primary
|
|
198
|
+
: '';
|
|
199
|
+
const selectedView = drawSelected && geometry.selected && geometry.selectedBar && controls
|
|
200
|
+
? {
|
|
201
|
+
rect: geometry.selected,
|
|
202
|
+
bar: geometry.selectedBar,
|
|
203
|
+
label,
|
|
204
|
+
canMoveUp: controls.canMoveUp,
|
|
205
|
+
canMoveDown: controls.canMoveDown,
|
|
206
|
+
}
|
|
207
|
+
: null;
|
|
208
|
+
const run = (kind) => {
|
|
209
|
+
if (!selectedId)
|
|
210
|
+
return;
|
|
211
|
+
dispatch(frameControlAction(kind, selectedId));
|
|
212
|
+
};
|
|
213
|
+
// EC-405: the "+" affordances for the selected node and (separately) a hovered
|
|
214
|
+
// one — computed from the SAME measured rects the frames use. A group is drawn
|
|
215
|
+
// only for a node with insertable positions, and only when no drag/inline edit
|
|
216
|
+
// is live; a node with no provider above it (component preview) shows none.
|
|
217
|
+
const affordanceGroups = affordance && !dragging && !editing
|
|
218
|
+
? [
|
|
219
|
+
affordanceGroup(present, selectedId, drawSelected ? geometry.selected : null, affordance),
|
|
220
|
+
affordanceGroup(present, hoveredId, drawHover ? geometry.hovered : null, affordance),
|
|
221
|
+
].filter((g) => g !== null)
|
|
222
|
+
: [];
|
|
223
|
+
return createPortal(_jsx(SelectionOverlayView, { selected: selectedView, hovered: drawHover ? geometry.hovered : null, affordances: affordanceGroups, propHighlight: highlight ? geometry.propHighlight : null, onMoveUp: () => run('moveUp'), onMoveDown: () => run('moveDown'), onDuplicate: () => run('duplicate'), onDelete: () => run('delete') }), doc.body);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Build the affordance group for one node from its measured rect: the insertable
|
|
227
|
+
* positions the provider allows (slot rules already applied), each placed on the
|
|
228
|
+
* node's boundary along the parent's flow axis. Returns `null` when there is no
|
|
229
|
+
* rect yet or no insertable position.
|
|
230
|
+
*/
|
|
231
|
+
function affordanceGroup(present, nodeId, rect, affordance) {
|
|
232
|
+
if (!nodeId || !rect)
|
|
233
|
+
return null;
|
|
234
|
+
const positions = affordance.positionsFor(nodeId);
|
|
235
|
+
if (positions.length === 0)
|
|
236
|
+
return null;
|
|
237
|
+
const flow = containerFlowAxis(getParentInfo(present, nodeId)?.parent);
|
|
238
|
+
return {
|
|
239
|
+
nodeId,
|
|
240
|
+
items: positions.map((position) => ({ position, ...placeAffordance(rect, position, flow) })),
|
|
241
|
+
onInsert: (position) => affordance.openAt(nodeId, position),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=selection-overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection-overlay.js","sourceRoot":"","sources":["../../../src/components/builder/selection-overlay.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect, useState } from 'react'\nimport type { CSSProperties, ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate, useUiLocale } from '@elytracms/studio/lib/i18n'\nimport { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { useBuilder } from './builder-store'\nimport { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session'\nimport { useInsertAffordance } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport type { InsertPosition } from './insert-context'\nimport type { AffordanceAnchor, FrameBarPlacement, FrameControlKind } from './canvas-overlay-geometry'\nimport {\n FRAME_BAR_HEIGHT,\n frameControlAction,\n frameControlsFor,\n placeAffordance,\n placeFrameBar,\n} from './canvas-overlay-geometry'\nimport {\n createRectTracker,\n measureNodeRect,\n measurePropRect,\n rectsEqual,\n scheduleFrame,\n subscribeCanvasLayout,\n} from './canvas-rect-tracking'\nimport type { Rect } from './canvas-rect-tracking'\nimport { usePropSync } from './prop-sync-context'\nimport { nodeDisplayTitle } from './node-title'\n\n/**\n * Canvas selection/hover overlay (EC-404) — a topmost layer portaled into the\n * iframe `body` that draws the selection and hover frames ABOVE the page from\n * measured node rects, so a full-bleed background or a higher-`z-index` sibling\n * can never paint over them (the structural fix for the in-document inset\n * outline the findings batch flagged). The selected frame carries a compact tag:\n * the component's display name plus move-up / move-down / duplicate / delete\n * controls that dispatch the EXISTING tree actions.\n *\n * EC-414 AD-B chrome contract: everything here is `ec-*` classes / `data-ec-*`\n * attributes / inline geometry only, styled by `canvas-chrome-css.ts`, with the\n * `.ec-chrome` containment recipe on the root; no studio Tailwind utilities and\n * no studio React UI primitives cross into the iframe. The rect-tracking core\n * ({@link createRectTracker}) is a pure, reusable module — EC-405 rides this\n * overlay layer next, and EC-406 can share the tracking.\n */\n\n/** The overlay's full measured geometry (both frames + the bar placement). */\nexport interface OverlayGeometry {\n selected: Rect | null\n /** Chrome-bar placement for the selected frame (null when nothing to draw). */\n selectedBar: FrameBarPlacement | null\n hovered: Rect | null\n /** EC-408: the focused prop's marker rect (reverse highlight), or null. */\n propHighlight: Rect | null\n}\n\nconst EMPTY_GEOMETRY: OverlayGeometry = {\n selected: null,\n selectedBar: null,\n hovered: null,\n propHighlight: null,\n}\n\nfunction barsEqual(a: FrameBarPlacement | null, b: FrameBarPlacement | null): boolean {\n if (a === null || b === null) return a === b\n return a.top === b.top && a.left === b.left && a.placement === b.placement\n}\n\nfunction geometryEquals(a: OverlayGeometry, b: OverlayGeometry): boolean {\n return (\n rectsEqual(a.selected, b.selected) &&\n rectsEqual(a.hovered, b.hovered) &&\n rectsEqual(a.propHighlight, b.propHighlight) &&\n barsEqual(a.selectedBar, b.selectedBar)\n )\n}\n\n/** A drawn selection frame plus its control enablement. */\nexport interface SelectionFrameView {\n rect: Rect\n bar: FrameBarPlacement\n label: string\n canMoveUp: boolean\n canMoveDown: boolean\n}\n\n/** One \"+\" affordance button: its position and its placed anchor point. */\nexport interface AffordanceItemView extends AffordanceAnchor {\n position: InsertPosition\n}\n\n/**\n * The affordance buttons for one node — grouped so a single `onInsert` binds the\n * node id the inserts target (the selected node and, separately, a hovered one).\n */\nexport interface AffordanceGroupView {\n nodeId: string\n items: AffordanceItemView[]\n onInsert: (position: InsertPosition) => void\n}\n\nexport interface SelectionOverlayViewProps {\n selected: SelectionFrameView | null\n hovered: Rect | null\n /** In-canvas insertion affordances (EC-405); empty when suppressed (drag/editing). */\n affordances: AffordanceGroupView[]\n /** EC-408: the focused prop's marker rect (reverse highlight), or null. */\n propHighlight?: Rect | null\n onMoveUp: () => void\n onMoveDown: () => void\n onDuplicate: () => void\n onDelete: () => void\n}\n\nfunction frameStyle(rect: Rect): CSSProperties {\n return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }\n}\n\nconst AFFORDANCE_LABELS: Record<InsertPosition, string> = {\n before: 'builder.canvas.insertBefore',\n after: 'builder.canvas.insertAfter',\n into: 'builder.canvas.insertInto',\n}\n\n/**\n * The overlay's rendered markup, portal-free so the AD-D chrome contract test\n * can walk it with `renderToStaticMarkup`: every class is `ec-*`, the root\n * carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).\n */\nexport function SelectionOverlayView(props: SelectionOverlayViewProps) {\n const { selected, hovered, affordances, propHighlight } = props\n return (\n <div className=\"ec-chrome ec-overlay\" data-ec-overlay=\"\">\n {/* EC-408 reverse highlight: a quiet filled tint on the focused prop's\n element, drawn UNDER the selection frame so its border stays crisp. */}\n {propHighlight ? (\n <div className=\"ec-overlay-prop\" data-ec-prop-highlight=\"\" style={frameStyle(propHighlight)} />\n ) : null}\n {hovered ? <div className=\"ec-overlay-frame\" data-ec-hover=\"\" style={frameStyle(hovered)} /> : null}\n {affordances.map((group) =>\n group.items.map((item) => (\n <AddButton\n key={`${group.nodeId}:${item.position}`}\n item={item}\n onInsert={() => group.onInsert(item.position)}\n />\n )),\n )}\n {selected ? (\n <>\n <div className=\"ec-overlay-frame\" data-ec-selected=\"\" style={frameStyle(selected.rect)} />\n <div\n className=\"ec-overlay-bar\"\n data-ec-frame-bar=\"\"\n data-ec-placement={selected.bar.placement}\n style={{ left: selected.bar.left, top: selected.bar.top }}\n >\n <span className=\"ec-overlay-label\" title={selected.label}>\n {selected.label}\n </span>\n <span className=\"ec-overlay-sep\" aria-hidden=\"true\" />\n <FrameButton\n label={translate('builder.pageTree.moveUp')}\n disabled={!selected.canMoveUp}\n onClick={props.onMoveUp}\n >\n <OverlayIcon kind=\"moveUp\" />\n </FrameButton>\n <FrameButton\n label={translate('builder.pageTree.moveDown')}\n disabled={!selected.canMoveDown}\n onClick={props.onMoveDown}\n >\n <OverlayIcon kind=\"moveDown\" />\n </FrameButton>\n <FrameButton label={translate('common.duplicate')} onClick={props.onDuplicate}>\n <OverlayIcon kind=\"duplicate\" />\n </FrameButton>\n <FrameButton label={translate('common.delete')} onClick={props.onDelete}>\n <OverlayIcon kind=\"delete\" />\n </FrameButton>\n </div>\n </>\n ) : null}\n </div>\n )\n}\n\nfunction FrameButton({\n label,\n disabled,\n onClick,\n children,\n}: {\n label: string\n disabled?: boolean\n onClick: () => void\n children: ReactNode\n}) {\n return (\n <button\n type=\"button\"\n className=\"ec-overlay-btn\"\n aria-label={label}\n title={label}\n disabled={disabled}\n // Keep the click on the control: it must not fall through to the canvas\n // selection listener (the bar sits over the page) nor start a text drag.\n onMouseDown={(e) => e.preventDefault()}\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n onClick()\n }}\n >\n {children}\n </button>\n )\n}\n\n/**\n * A \"+\" insertion affordance (EC-405): a small round button centered on a block\n * boundary (before/after edge, or the center of an empty container). Centered on\n * its anchor by the chrome CSS `translate(-50%,-50%)`; `pointer-events` re-enabled\n * on the button while the overlay root stays click-through. The gesture guards\n * mirror `FrameButton` — the press must not fall through to the canvas selection\n * listener nor start a native drag.\n */\nfunction AddButton({ item, onInsert }: { item: AffordanceItemView; onInsert: () => void }) {\n const label = translate(AFFORDANCE_LABELS[item.position])\n return (\n <button\n type=\"button\"\n className=\"ec-overlay-add\"\n data-ec-affordance={item.position}\n aria-label={label}\n title={label}\n style={{ left: item.left, top: item.top }}\n onMouseDown={(e) => e.preventDefault()}\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n onInsert()\n }}\n >\n <svg\n width={14}\n height={14}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.8}\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M8 3.5v9M3.5 8h9\" />\n </svg>\n </button>\n )\n}\n\n/** Hand-rolled inline icons — no `class` attribute (chrome contract). */\nfunction OverlayIcon({ kind }: { kind: FrameControlKind }) {\n const common = {\n width: 13,\n height: 13,\n viewBox: '0 0 16 16',\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 1.6,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n 'aria-hidden': true,\n }\n switch (kind) {\n case 'moveUp':\n return (\n <svg {...common}>\n <path d=\"M4 10l4-4 4 4\" />\n </svg>\n )\n case 'moveDown':\n return (\n <svg {...common}>\n <path d=\"M4 6l4 4 4-4\" />\n </svg>\n )\n case 'duplicate':\n return (\n <svg {...common}>\n <rect x=\"6\" y=\"6\" width=\"8\" height=\"8\" rx=\"1.5\" />\n <path d=\"M10 6V4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2\" />\n </svg>\n )\n case 'delete':\n return (\n <svg {...common}>\n <path d=\"M3 4.5h10\" />\n <path d=\"M6.5 4.5V3a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1.5\" />\n <path d=\"M5 4.5l.5 8a1 1 0 0 0 1 .9h3a1 1 0 0 0 1-.9l.5-8\" />\n </svg>\n )\n }\n}\n\n/**\n * The live overlay: tracks the selected + hovered node rects (glued through\n * scroll/resize/async layout via {@link subscribeCanvasLayout} + a coalescing\n * {@link createRectTracker}) and portals the frames into the iframe `body`.\n * Suppressed for the composition root (no frame around the whole page — mirrors\n * the tree, which offers the root no actions) and while an inline text/rich-text\n * edit is live (the `[data-ec-editing]` outline owns that state).\n */\nexport function SelectionOverlay({ doc, editing }: { doc: Document; editing: boolean }) {\n const builder = useBuilder()\n const { present, selectedId, hoveredId, dispatch } = builder\n const studio = getStudioRegistry()\n const affordance = useInsertAffordance()\n const propSync = usePropSync()\n const uiLocale = useUiLocale()\n const [geometry, setGeometry] = useState<OverlayGeometry>(EMPTY_GEOMETRY)\n\n // EC-408 reverse highlight: the focused prop's marker, but only for the\n // selected node (the inspector only shows that node's props) and never while\n // inline-editing (the [data-ec-editing] outline owns the node then).\n const highlight =\n propSync?.highlighted && propSync.highlighted.nodeId === selectedId && !editing\n ? propSync.highlighted\n : null\n const highlightNodeId = highlight?.nodeId ?? null\n const highlightPropName = highlight?.propName ?? null\n\n // EC-405 scope 4: insertion affordances hide while ANY drag is live (the\n // DnD before/after/into indicators own the \"where it lands\" story then).\n const [dragging, setDragging] = useState(() => activeDrag() !== null)\n useEffect(() => {\n const offStart = onDragSessionStart(() => setDragging(true))\n const offEnd = onDragSessionEnd(() => setDragging(false))\n return () => {\n offStart()\n offEnd()\n }\n }, [])\n\n // The selected node's controls/label come from the graph; the root and a\n // never-found selection draw nothing. Hover never frames the currently\n // selected node (one frame at a time) nor the root.\n const selectedNode = selectedId && !editing ? getNode(present, selectedId) : null\n const controls = selectedNode ? frameControlsFor(present, selectedId!) : null\n const drawSelected = selectedNode !== null && controls !== null && !controls.isRoot\n const hoveredNode = hoveredId && hoveredId !== selectedId ? getNode(present, hoveredId) : null\n const drawHover = hoveredNode !== null && !frameControlsFor(present, hoveredId!).isRoot\n\n useLayoutEffect(() => {\n const win = doc.defaultView\n if (!win) return\n const selId = drawSelected ? selectedId : null\n const hovId = drawHover ? hoveredId : null\n const measure = (): OverlayGeometry => {\n const selected = measureNodeRect(doc, selId)\n const hovered = measureNodeRect(doc, hovId)\n return {\n selected,\n selectedBar: selected ? placeFrameBar(selected, { barHeight: FRAME_BAR_HEIGHT, docTop: win.scrollY }) : null,\n hovered,\n propHighlight: measurePropRect(doc, highlightNodeId, highlightPropName),\n }\n }\n const targets = (): Element[] => {\n const out: Element[] = []\n for (const id of [selId, hovId]) {\n if (!id) continue\n const el = doc.querySelector(`[data-ec-node-id=\"${id}\"]`)\n if (el) out.push(el)\n }\n // Track the highlighted marker element too, so it re-glues on async layout.\n if (highlightNodeId) {\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${highlightNodeId}\"]`)\n if (wrapper) out.push(wrapper)\n }\n return out\n }\n // Coalesce on the PARENT window's frame loop (not the iframe's): this\n // component + its state live in the parent React tree, and the iframe's rAF\n // is throttled/paused whenever the preview is offscreen or backgrounded. The\n // compound rAF+timeout scheduler (scheduleFrame) keeps the flush paint-\n // aligned on screen while guaranteeing it still runs when rAF is paused.\n const host = win.parent ?? window\n const tracker = createRectTracker<OverlayGeometry>({\n measure,\n equals: geometryEquals,\n onChange: setGeometry,\n schedule: (flush) => scheduleFrame(host, flush),\n subscribe: (invalidate) => subscribeCanvasLayout(doc, targets, invalidate),\n })\n return () => tracker.dispose()\n // Re-subscribe when the tracked ids / graph / highlighted prop change so the\n // ResizeObserver re-points at the current (possibly remounted) wrappers and\n // the measure closure reads the current highlight target.\n }, [doc, drawSelected, drawHover, selectedId, hoveredId, present, highlightNodeId, highlightPropName])\n\n // EC-407: the frame label shares the tree row's resolution — the SAME\n // nodeDisplayTitle.primary the selected row shows (instance title when\n // derivable, else the localized component name).\n const label = selectedNode\n ? nodeDisplayTitle(studio.registry.getManifest(selectedNode.componentId), selectedNode, uiLocale).primary\n : ''\n\n const selectedView: SelectionFrameView | null =\n drawSelected && geometry.selected && geometry.selectedBar && controls\n ? {\n rect: geometry.selected,\n bar: geometry.selectedBar,\n label,\n canMoveUp: controls.canMoveUp,\n canMoveDown: controls.canMoveDown,\n }\n : null\n\n const run = (kind: FrameControlKind): void => {\n if (!selectedId) return\n dispatch(frameControlAction(kind, selectedId))\n }\n\n // EC-405: the \"+\" affordances for the selected node and (separately) a hovered\n // one — computed from the SAME measured rects the frames use. A group is drawn\n // only for a node with insertable positions, and only when no drag/inline edit\n // is live; a node with no provider above it (component preview) shows none.\n const affordanceGroups: AffordanceGroupView[] =\n affordance && !dragging && !editing\n ? [\n affordanceGroup(present, selectedId, drawSelected ? geometry.selected : null, affordance),\n affordanceGroup(present, hoveredId, drawHover ? geometry.hovered : null, affordance),\n ].filter((g): g is AffordanceGroupView => g !== null)\n : []\n\n return createPortal(\n <SelectionOverlayView\n selected={selectedView}\n hovered={drawHover ? geometry.hovered : null}\n affordances={affordanceGroups}\n propHighlight={highlight ? geometry.propHighlight : null}\n onMoveUp={() => run('moveUp')}\n onMoveDown={() => run('moveDown')}\n onDuplicate={() => run('duplicate')}\n onDelete={() => run('delete')}\n />,\n doc.body,\n )\n}\n\n/**\n * Build the affordance group for one node from its measured rect: the insertable\n * positions the provider allows (slot rules already applied), each placed on the\n * node's boundary along the parent's flow axis. Returns `null` when there is no\n * rect yet or no insertable position.\n */\nfunction affordanceGroup(\n present: ComponentNode,\n nodeId: string | null,\n rect: Rect | null,\n affordance: InsertAffordanceApi,\n): AffordanceGroupView | null {\n if (!nodeId || !rect) return null\n const positions = affordance.positionsFor(nodeId)\n if (positions.length === 0) return null\n const flow = containerFlowAxis(getParentInfo(present, nodeId)?.parent)\n return {\n nodeId,\n items: positions.map((position) => ({ position, ...placeAffordance(rect, position, flow) })),\n onInsert: (position) => affordance.openAt(nodeId, position),\n }\n}\n"]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
2
3
|
import type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag';
|
|
4
|
+
import type { FrameScheduler, RectTracker } from './canvas-rect-tracking.js';
|
|
3
5
|
/**
|
|
4
6
|
* Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid
|
|
5
7
|
* container is selected on the canvas, draggable gap handles appear between
|
|
@@ -22,7 +24,60 @@ export interface HandleSpotView {
|
|
|
22
24
|
crossStart: number;
|
|
23
25
|
crossLength: number;
|
|
24
26
|
}
|
|
25
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The container's rendered element (the Stack/Grid div carrying the gap style).
|
|
29
|
+
* Resilient to legitimate intermediate wrappers between the editable
|
|
30
|
+
* `[data-ec-node-id]` wrapper and the primitive (an error-boundary/provider DOM
|
|
31
|
+
* node, a future decorator layer): the FIRST `[data-ec-primitive]` inside the
|
|
32
|
+
* wrapper whose NEAREST node wrapper is THIS node — so it tolerates a wrapper
|
|
33
|
+
* above the primitive but never descends into a CHILD node's subtree and grabs
|
|
34
|
+
* that nested container's own `[data-ec-primitive]`. Mirrors `measurePropRect`'s
|
|
35
|
+
* nearest-node scoping (EC-408). Today no such wrapper exists — `decorate` wraps
|
|
36
|
+
* `<RenderErrorBoundary>` which renders its children with no DOM element, so the
|
|
37
|
+
* primitive is a direct child — but the scoping makes 2(b) unregressable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function containerElementFor(doc: Document, nodeId: string): HTMLElement | null;
|
|
40
|
+
interface Measured {
|
|
41
|
+
spots: HandleSpotView[];
|
|
42
|
+
/** The axis the children are *rendered* along (drag direction follows it). */
|
|
43
|
+
axis: FlowAxis;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Measure the handle spots between the container's rendered children — the
|
|
47
|
+
* SINGLE source of truth for whether handles show (spots) and where. Eligibility
|
|
48
|
+
* and measurement agree on the SAME (DOM) child basis by construction: a
|
|
49
|
+
* null-rendering graph child (condition-gated → no `[data-ec-node-id]` wrapper)
|
|
50
|
+
* simply isn't counted, so it can neither block handles when ≥2 children render
|
|
51
|
+
* nor phantom them when <2 do; kept fresh by the live tracker (EC-404) so a child
|
|
52
|
+
* add/remove or resize re-runs this. The graph-level flow axis
|
|
53
|
+
* (`containerFlowAxis`) is tried first; when the rendered layout disagrees — a
|
|
54
|
+
* stacking Grid below the mobile breakpoint lays its "columns" out vertically —
|
|
55
|
+
* the cross axis is measured instead, so handles always sit in the gaps the
|
|
56
|
+
* editor actually sees.
|
|
57
|
+
*/
|
|
58
|
+
export declare function measureSpots(doc: Document, nodeId: string, flowAxis: FlowAxis): Measured;
|
|
59
|
+
/** True when two measurements are equivalent (same axis, same spots). */
|
|
60
|
+
export declare function measuredEqual(a: Measured, b: Measured): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Wire a live rect tracker for the spacing handles onto the shared EC-404 core:
|
|
63
|
+
* `subscribeCanvasLayout` re-measures under child resize (image/font decode),
|
|
64
|
+
* child add/remove (async population, conditional render — `MutationObserver`
|
|
65
|
+
* `childList`), container resize, and scroll/viewport change; `scheduleFrame`
|
|
66
|
+
* coalesces on the PARENT window's frame loop with the mandatory 100 ms timeout
|
|
67
|
+
* backstop (the iframe's rAF is paused offscreen / never fires under automation).
|
|
68
|
+
* `dispose` tears down every observer + listener — no leaks on unmount. The
|
|
69
|
+
* once-only `useLayoutEffect` measure this replaces went stale the instant any of
|
|
70
|
+
* those settled after first paint (EC-406 evidence).
|
|
71
|
+
*/
|
|
72
|
+
export declare function createSpacingTracker(doc: Document, nodeId: string, flowAxis: FlowAxis, host: FrameScheduler, onChange: (measured: Measured) => void): RectTracker;
|
|
73
|
+
/**
|
|
74
|
+
* True when the node is a layout container the handles can attach to — a cheap,
|
|
75
|
+
* NECESSARY graph precondition for the mount (the authoritative "are there ≥2
|
|
76
|
+
* gaps to space" answer is the live DOM {@link measureSpots}). Because each graph
|
|
77
|
+
* child renders at most one `[data-ec-node-id]` wrapper, rendered DOM children ⊆
|
|
78
|
+
* graph children, so requiring ≥2 graph children can never wrongly hide handles
|
|
79
|
+
* that a fresh DOM measure would show.
|
|
80
|
+
*/
|
|
26
81
|
export declare function hasSpacingHandles(node: ComponentNode | null): node is ComponentNode;
|
|
27
82
|
/** Everything the portal-free handle markup needs (chrome contract surface). */
|
|
28
83
|
export interface SpacingHandlesViewProps {
|
|
@@ -51,3 +106,4 @@ export declare function SpacingHandles({ doc, node }: {
|
|
|
51
106
|
doc: Document;
|
|
52
107
|
node: ComponentNode;
|
|
53
108
|
}): import("react").ReactPortal;
|
|
109
|
+
export {};
|