@elytracms/studio 0.0.64 → 0.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/assets.ts +11 -0
- package/convex/delivery.ts +5 -0
- package/convex/schema.ts +23 -0
- 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/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +26 -8
- 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-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 +86 -3
- 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 +21 -3
- 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 +23 -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-picker.js +11 -5
- package/dist/components/cms/asset-picker.js.map +1 -1
- 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/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/i18n/messages.de.js +16 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +14 -0
- package/dist/lib/i18n/messages.en.js +16 -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/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/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -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"]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
|
-
import { renderPageComposition } from '@elytracms/core/runtime-renderer';
|
|
4
|
+
import { renderPageComposition, setEditingMarkers } from '@elytracms/core/runtime-renderer';
|
|
5
|
+
import { getNode } from '@elytracms/core/project-graph';
|
|
5
6
|
import { useParams } from '@elytracms/studio/router';
|
|
6
7
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
7
8
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
@@ -12,13 +13,23 @@ import { CanvasThemeNotice } from './canvas-theme-notice.js';
|
|
|
12
13
|
import { getCanvasStylesheetUrl } from './canvas-stylesheet.js';
|
|
13
14
|
import { useBuilder } from './builder-store.js';
|
|
14
15
|
import { useCanvasViewports, viewportWidth } from './canvas-breakpoints.js';
|
|
15
|
-
import { nodeIdAt } from './canvas-dom.js';
|
|
16
|
+
import { nodeIdAt, wrapperAt } from './canvas-dom.js';
|
|
16
17
|
import { useCanvasDnd } from './canvas-dnd.js';
|
|
17
18
|
import { useCanvasInlineEditing } from './canvas-inline-editing.js';
|
|
18
19
|
import { useCanvasRenderContext } from './canvas-render-context.js';
|
|
19
20
|
import { ClipboardNoticeOverlay, attachClipboardListeners, useStudioClipboard, } from './clipboard-actions.js';
|
|
20
21
|
import { RichTextToolbar } from './rich-text-toolbar.js';
|
|
21
22
|
import { SpacingHandles, hasSpacingHandles } from './spacing-handles.js';
|
|
23
|
+
import { SelectionOverlay } from './selection-overlay.js';
|
|
24
|
+
import { attachDeleteKeyListener } from './delete-key.js';
|
|
25
|
+
import { usePropSync } from './prop-sync-context.js';
|
|
26
|
+
import { resolvePropClick } from './prop-selection.js';
|
|
27
|
+
// EC-408: the studio canvas is the ONE surface that turns editing markers on, so
|
|
28
|
+
// `ecProp` in the base primitives emits `data-ec-prop` here. Delivery
|
|
29
|
+
// (`@elytracms/next`) is a separate build that never imports this module, so its
|
|
30
|
+
// singleton stays off and shipped pages carry zero marker noise. Module-level
|
|
31
|
+
// (not an effect) so markers are on before the first primitive renders.
|
|
32
|
+
setEditingMarkers(true);
|
|
22
33
|
/**
|
|
23
34
|
* Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders
|
|
24
35
|
* into a same-origin iframe via a React portal — true style isolation + true
|
|
@@ -88,7 +99,13 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
88
99
|
// live session ref other listeners consult, the rich-text host (mark
|
|
89
100
|
// toolbar), and the per-node remount epochs (decorate keys).
|
|
90
101
|
const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio });
|
|
91
|
-
const { editingRef, richEditHost, richEditEpochs } = inline;
|
|
102
|
+
const { editingRef, richEditHost, richEditEpochs, editing } = inline;
|
|
103
|
+
// EC-408: the field↔component sync channel (null in the component preview,
|
|
104
|
+
// which has no inspector). Read live through a ref — the native iframe
|
|
105
|
+
// listeners outlive renders.
|
|
106
|
+
const propSync = usePropSync();
|
|
107
|
+
const propSyncRef = useRef(propSync);
|
|
108
|
+
propSyncRef.current = propSync;
|
|
92
109
|
// Native selection/hover listeners inside the iframe document.
|
|
93
110
|
useEffect(() => {
|
|
94
111
|
if (!doc)
|
|
@@ -97,11 +114,28 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
97
114
|
// Clicks inside the active inline editor are caret moves, not selection.
|
|
98
115
|
if (editingRef.current?.host.contains(e.target))
|
|
99
116
|
return;
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
const wrapper = wrapperAt(e.target);
|
|
118
|
+
if (!wrapper)
|
|
119
|
+
return;
|
|
120
|
+
const id = wrapper.getAttribute('data-ec-node-id');
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
dispatch({ type: 'select', id });
|
|
123
|
+
// EC-408: resolve the clicked sub-part to its `data-ec-prop` and, unless it
|
|
124
|
+
// is inline-editable (inline editing stays primary), ask the inspector to
|
|
125
|
+
// focus that prop's control. Unannotated components resolve no marker → the
|
|
126
|
+
// node-level select above is the whole behavior, exactly as before.
|
|
127
|
+
const sync = propSyncRef.current;
|
|
128
|
+
if (!sync)
|
|
129
|
+
return;
|
|
130
|
+
const node = getNode(presentRef.current, id);
|
|
131
|
+
const manifest = node ? studio.registry.getManifest(node.componentId) : undefined;
|
|
132
|
+
const { propName, focusField } = resolvePropClick({
|
|
133
|
+
target: e.target,
|
|
134
|
+
wrapper,
|
|
135
|
+
manifest,
|
|
136
|
+
});
|
|
137
|
+
if (propName && focusField)
|
|
138
|
+
sync.requestFocus({ nodeId: id, propName });
|
|
105
139
|
};
|
|
106
140
|
const onOver = (e) => dispatch({ type: 'hover', id: nodeIdAt(e.target) });
|
|
107
141
|
const onLeave = () => dispatch({ type: 'hover', id: null });
|
|
@@ -113,7 +147,7 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
113
147
|
doc.removeEventListener('mouseover', onOver);
|
|
114
148
|
doc.removeEventListener('mouseleave', onLeave, true);
|
|
115
149
|
};
|
|
116
|
-
}, [doc, dispatch, editingRef]);
|
|
150
|
+
}, [doc, dispatch, editingRef, presentRef, studio]);
|
|
117
151
|
// Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.
|
|
118
152
|
useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef });
|
|
119
153
|
// Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page
|
|
@@ -133,6 +167,18 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
133
167
|
getSelection: () => doc.getSelection(),
|
|
134
168
|
});
|
|
135
169
|
}, [doc, clipboard, editingRef]);
|
|
170
|
+
// Delete/Backspace removes the selected node (EC-404) — the keyboard remove
|
|
171
|
+
// the studio was missing, wired beside the clipboard listeners on the iframe
|
|
172
|
+
// document, guarded so typing in an inline editor edits text instead.
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!doc)
|
|
175
|
+
return;
|
|
176
|
+
return attachDeleteKeyListener(doc, {
|
|
177
|
+
getSelectedId: () => selectedIdRef.current,
|
|
178
|
+
remove: (id) => dispatch({ type: 'remove', id }),
|
|
179
|
+
getRootId: () => presentRef.current.id,
|
|
180
|
+
});
|
|
181
|
+
}, [doc, dispatch]);
|
|
136
182
|
// The render context: the seven delivery-parity resolvers + the editable-
|
|
137
183
|
// wrapper decorator (selection/hover, locale chips, remount epochs) — see
|
|
138
184
|
// `canvas-render-context`.
|
|
@@ -145,8 +191,11 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
145
191
|
richEditEpochs,
|
|
146
192
|
});
|
|
147
193
|
// Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children
|
|
148
|
-
// gets draggable gap handles between them, portaled into the iframe.
|
|
149
|
-
|
|
194
|
+
// gets draggable gap handles between them, portaled into the iframe. Suppressed
|
|
195
|
+
// while an inline text/rich-text edit owns the node (EC-406 scope 4) — the
|
|
196
|
+
// [data-ec-editing] outline owns that state, exactly as the selection overlay
|
|
197
|
+
// yields; a content-drag is suppressed inside SpacingHandles via the drag session.
|
|
198
|
+
const spacingTarget = doc && !editing && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)
|
|
150
199
|
? builder.selectedNode
|
|
151
200
|
: null;
|
|
152
201
|
// EC-250: the preview frame width derives from the config-sourced breakpoints
|
|
@@ -158,6 +207,6 @@ export function BuilderCanvas({ perspective = 'draft', } = {}) {
|
|
|
158
207
|
// Render the composition root directly (EC-287: no layout
|
|
159
208
|
// indirection — a page renders its blocks).
|
|
160
209
|
renderPageComposition(activeRoot, ctx), doc.body)
|
|
161
|
-
: null, spacingTarget && doc ? (_jsx(SpacingHandles, { doc: doc, node: spacingTarget }, spacingTarget.id)) : null, richEditHost && doc ? _jsx(RichTextToolbar, { doc: doc, host: richEditHost }) : null] })] }));
|
|
210
|
+
: null, doc ? _jsx(SelectionOverlay, { doc: doc, editing: editing }) : null, spacingTarget && doc ? (_jsx(SpacingHandles, { doc: doc, node: spacingTarget }, spacingTarget.id)) : null, richEditHost && doc ? _jsx(RichTextToolbar, { doc: doc, host: richEditHost }) : null] })] }));
|
|
162
211
|
}
|
|
163
212
|
//# sourceMappingURL=canvas.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/components/builder/canvas.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { renderPageComposition } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { useParams } from '@elytracms/studio/router'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace'\nimport { wireCanvasChrome } from './canvas-chrome'\nimport type { CanvasThemeStatus } from './canvas-chrome'\nimport { CanvasThemeNotice } from './canvas-theme-notice'\nimport { getCanvasStylesheetUrl } from './canvas-stylesheet'\nimport { useBuilder } from './builder-store'\nimport { useCanvasViewports, viewportWidth } from './canvas-breakpoints'\nimport { nodeIdAt } from './canvas-dom'\nimport { useCanvasDnd } from './canvas-dnd'\nimport { useCanvasInlineEditing } from './canvas-inline-editing'\nimport { useCanvasRenderContext } from './canvas-render-context'\nimport {\n ClipboardNoticeOverlay,\n attachClipboardListeners,\n useStudioClipboard,\n} from './clipboard-actions'\nimport { RichTextToolbar } from './rich-text-toolbar'\nimport { SpacingHandles, hasSpacingHandles } from './spacing-handles'\n\n\n/**\n * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders\n * into a same-origin iframe via a React portal — true style isolation + true\n * responsive widths. Each node is wrapped with an editable marker so clicks\n * select and hover/selection draw outlines. Native listeners bridge the iframe\n * boundary (React's synthetic events don't cross frames); the two big gesture\n * state machines live in their own tested modules (EC-330): drag & drop in\n * `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and\n * the render-context assembly in `canvas-render-context`.\n */\nexport function BuilderCanvas({\n perspective = 'draft',\n}: {\n /** Resolve references against this perspective (EC-300/AD-16). The editable Draft\n * view passes `'draft'` (a just-made reusable shows immediately); the read-only\n * Published view passes `'published'` (honest preview of what delivery serves). */\n perspective?: CanvasPerspective\n} = {}) {\n const builder = useBuilder()\n const { dispatch } = builder\n const studio = getStudioRegistry()\n\n // The edited composition root, composed for preview. EC-287 retired the project\n // graph and its published-graph preview; the canvas always renders the live,\n // editable draft (content publishing is per document).\n const activeRoot: ComponentNode = builder.present\n const iframeRef = useRef<HTMLIFrameElement>(null)\n const [doc, setDoc] = useState<Document | null>(null)\n\n // Graph/selection refs let the native iframe listeners read the *current*\n // state without re-subscribing on every edit.\n const presentRef = useRef(builder.present)\n presentRef.current = builder.present\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n\n // When the project was created from a starter kit, its brand token palette drives the canvas UI\n // (EC-117) — injected after app.css so the kit's overrides win in the cascade.\n const params = useParams({ strict: false }) as { projectId?: string }\n const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined\n\n // EC-414 AD-D: the settled canvas-theme classification (legacy merged shape /\n // broken stylesheet) drives the dev-only, never-blocking notice overlay.\n const [themeStatus, setThemeStatus] = useState<CanvasThemeStatus | null>(null)\n\n const wireUp = useCallback(() => {\n const d = iframeRef.current?.contentDocument\n if (!d || !d.body) return\n // Deliberately no `.dark` here: this iframe renders the REAL page (plus\n // studio editing chrome portaled alongside it), and its backdrop must read\n // as a light, site-like page — never the studio chrome's current theme (a\n // black canvas is exactly what leaks through when the studio is\n // dark-themed and the iframe root inherits that class). The injected\n // canvas stylesheet (new world: the chrome's neutral backdrop; legacy\n // world: the studio `:root` tokens) supplies that backdrop once `.dark` is\n // never added here (mirrors the components-preview surface, which wires\n // through the same chrome). The editing chrome (selection/hover outlines,\n // drop indicators, the rich-text toolbar, locale-fallback chips) uses\n // hardcoded colors rather than tokens, so it stays visible against a light\n // page regardless. The full injection — stylesheet link, editing chrome,\n // kit tokens, the consumer's `defineStudio({ canvas })` theme (EC-389,\n // injected LAST so it wins), and the EC-414 world probe — lives in\n // `wireCanvasChrome`/`applyCanvasChrome`.\n wireCanvasChrome(d, {\n stylesheetUrl: getCanvasStylesheetUrl(),\n kitTokensCss,\n theme: resolveDeploymentCanvasTheme(),\n onStatus: (status) =>\n setThemeStatus((prev) => (prev?.kind === status.kind ? prev : status)),\n })\n setDoc(d)\n }, [kitTokensCss])\n\n // Handle the already-loaded case (srcDoc may load before onLoad attaches).\n useEffect(() => {\n if (iframeRef.current?.contentDocument?.readyState === 'complete') wireUp()\n }, [wireUp])\n\n // Inline editing (EC-158/EC-159): the dblclick-to-edit state machine, the\n // live session ref other listeners consult, the rich-text host (mark\n // toolbar), and the per-node remount epochs (decorate keys).\n const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio })\n const { editingRef, richEditHost, richEditEpochs } = inline\n\n // Native selection/hover listeners inside the iframe document.\n useEffect(() => {\n if (!doc) return\n const onClick = (e: Event) => {\n // Clicks inside the active inline editor are caret moves, not selection.\n if (editingRef.current?.host.contains(e.target as Node)) return\n const id = nodeIdAt(e.target)\n if (id) {\n e.preventDefault()\n dispatch({ type: 'select', id })\n }\n }\n const onOver = (e: Event) => dispatch({ type: 'hover', id: nodeIdAt(e.target) })\n const onLeave = () => dispatch({ type: 'hover', id: null })\n doc.addEventListener('click', onClick, true)\n doc.addEventListener('mouseover', onOver)\n doc.addEventListener('mouseleave', onLeave, true)\n return () => {\n doc.removeEventListener('click', onClick, true)\n doc.removeEventListener('mouseover', onOver)\n doc.removeEventListener('mouseleave', onLeave, true)\n }\n }, [doc, dispatch, editingRef])\n\n // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.\n useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef })\n\n // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page\n // tree and blocks panel use, wired to the *iframe* document (events never\n // cross frames). Shortcuts stay inert while an inline editor is live and in\n // form fields; the iframe's own navigator.clipboard does the async I/O\n // (Chromium scopes clipboard access to the focused document).\n const clipboard = useStudioClipboard()\n useEffect(() => {\n if (!doc) return\n return attachClipboardListeners(doc, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n isBlocked: () => editingRef.current !== null,\n asyncClipboard: doc.defaultView?.navigator.clipboard,\n getSelection: () => doc.getSelection(),\n })\n }, [doc, clipboard, editingRef])\n\n // The render context: the seven delivery-parity resolvers + the editable-\n // wrapper decorator (selection/hover, locale chips, remount epochs) — see\n // `canvas-render-context`.\n const { ctx, pageNodeIds } = useCanvasRenderContext({\n present: builder.present,\n selectedId: builder.selectedId,\n hoveredId: builder.hoveredId,\n studio,\n perspective,\n richEditEpochs,\n })\n\n // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children\n // gets draggable gap handles between them, portaled into the iframe.\n const spacingTarget =\n doc && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)\n ? builder.selectedNode\n : null\n\n // EC-250: the preview frame width derives from the config-sourced breakpoints\n // (Tailwind defaults when unconfigured), the same resolved set the toolbar\n // switcher offers.\n const { viewports } = useCanvasViewports()\n\n return (\n <div className=\"canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6\">\n {/* Copy/cut/paste outcomes (EC-175): non-blocking, with issue deep links. */}\n <ClipboardNoticeOverlay />\n {/* Canvas-theme classification notice (EC-414 AD-D): dev-only, never blocks. */}\n <CanvasThemeNotice status={themeStatus} />\n <div style={{ width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }} className=\"flex\">\n <iframe\n ref={iframeRef}\n onLoad={wireUp}\n title={translate('builder.canvas.pagePreview')}\n className=\"min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md\"\n srcDoc=\"<!doctype html><html><head></head><body></body></html>\"\n />\n {doc\n ? createPortal(\n // Render the composition root directly (EC-287: no layout\n // indirection — a page renders its blocks).\n renderPageComposition(activeRoot, ctx),\n doc.body,\n )\n : null}\n {spacingTarget && doc ? (\n <SpacingHandles key={spacingTarget.id} doc={doc} node={spacingTarget} />\n ) : null}\n {/* Floating mark toolbar for the live rich-text session (EC-159). */}\n {richEditHost && doc ? <RichTextToolbar doc={doc} host={richEditHost} /> : null}\n </div>\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/components/builder/canvas.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { renderPageComposition, setEditingMarkers } from '@elytracms/core/runtime-renderer'\nimport { getNode } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { useParams } from '@elytracms/studio/router'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { CanvasPerspective } from '@elytracms/studio/lib/data'\nimport { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session'\nimport { resolveDeploymentCanvasTheme } from '@elytracms/studio/lib/workspace'\nimport { wireCanvasChrome } from './canvas-chrome'\nimport type { CanvasThemeStatus } from './canvas-chrome'\nimport { CanvasThemeNotice } from './canvas-theme-notice'\nimport { getCanvasStylesheetUrl } from './canvas-stylesheet'\nimport { useBuilder } from './builder-store'\nimport { useCanvasViewports, viewportWidth } from './canvas-breakpoints'\nimport { nodeIdAt, wrapperAt } from './canvas-dom'\nimport { useCanvasDnd } from './canvas-dnd'\nimport { useCanvasInlineEditing } from './canvas-inline-editing'\nimport { useCanvasRenderContext } from './canvas-render-context'\nimport {\n ClipboardNoticeOverlay,\n attachClipboardListeners,\n useStudioClipboard,\n} from './clipboard-actions'\nimport { RichTextToolbar } from './rich-text-toolbar'\nimport { SpacingHandles, hasSpacingHandles } from './spacing-handles'\nimport { SelectionOverlay } from './selection-overlay'\nimport { attachDeleteKeyListener } from './delete-key'\nimport { usePropSync } from './prop-sync-context'\nimport { resolvePropClick } from './prop-selection'\n\n// EC-408: the studio canvas is the ONE surface that turns editing markers on, so\n// `ecProp` in the base primitives emits `data-ec-prop` here. Delivery\n// (`@elytracms/next`) is a separate build that never imports this module, so its\n// singleton stays off and shipped pages carry zero marker noise. Module-level\n// (not an effect) so markers are on before the first primitive renders.\nsetEditingMarkers(true)\n\n\n/**\n * Iframe-isolated page preview (brief §8.2, EC-061, EC-064). The graph renders\n * into a same-origin iframe via a React portal — true style isolation + true\n * responsive widths. Each node is wrapped with an editable marker so clicks\n * select and hover/selection draw outlines. Native listeners bridge the iframe\n * boundary (React's synthetic events don't cross frames); the two big gesture\n * state machines live in their own tested modules (EC-330): drag & drop in\n * `canvas-dnd`, inline text/rich-text editing in `canvas-inline-editing`, and\n * the render-context assembly in `canvas-render-context`.\n */\nexport function BuilderCanvas({\n perspective = 'draft',\n}: {\n /** Resolve references against this perspective (EC-300/AD-16). The editable Draft\n * view passes `'draft'` (a just-made reusable shows immediately); the read-only\n * Published view passes `'published'` (honest preview of what delivery serves). */\n perspective?: CanvasPerspective\n} = {}) {\n const builder = useBuilder()\n const { dispatch } = builder\n const studio = getStudioRegistry()\n\n // The edited composition root, composed for preview. EC-287 retired the project\n // graph and its published-graph preview; the canvas always renders the live,\n // editable draft (content publishing is per document).\n const activeRoot: ComponentNode = builder.present\n const iframeRef = useRef<HTMLIFrameElement>(null)\n const [doc, setDoc] = useState<Document | null>(null)\n\n // Graph/selection refs let the native iframe listeners read the *current*\n // state without re-subscribing on every edit.\n const presentRef = useRef(builder.present)\n presentRef.current = builder.present\n const selectedIdRef = useRef(builder.selectedId)\n selectedIdRef.current = builder.selectedId\n\n // When the project was created from a starter kit, its brand token palette drives the canvas UI\n // (EC-117) — injected after app.css so the kit's overrides win in the cascade.\n const params = useParams({ strict: false }) as { projectId?: string }\n const kitTokensCss = params.projectId ? getInstantiatedKit(params.projectId)?.tokensCss : undefined\n\n // EC-414 AD-D: the settled canvas-theme classification (legacy merged shape /\n // broken stylesheet) drives the dev-only, never-blocking notice overlay.\n const [themeStatus, setThemeStatus] = useState<CanvasThemeStatus | null>(null)\n\n const wireUp = useCallback(() => {\n const d = iframeRef.current?.contentDocument\n if (!d || !d.body) return\n // Deliberately no `.dark` here: this iframe renders the REAL page (plus\n // studio editing chrome portaled alongside it), and its backdrop must read\n // as a light, site-like page — never the studio chrome's current theme (a\n // black canvas is exactly what leaks through when the studio is\n // dark-themed and the iframe root inherits that class). The injected\n // canvas stylesheet (new world: the chrome's neutral backdrop; legacy\n // world: the studio `:root` tokens) supplies that backdrop once `.dark` is\n // never added here (mirrors the components-preview surface, which wires\n // through the same chrome). The editing chrome (selection/hover outlines,\n // drop indicators, the rich-text toolbar, locale-fallback chips) uses\n // hardcoded colors rather than tokens, so it stays visible against a light\n // page regardless. The full injection — stylesheet link, editing chrome,\n // kit tokens, the consumer's `defineStudio({ canvas })` theme (EC-389,\n // injected LAST so it wins), and the EC-414 world probe — lives in\n // `wireCanvasChrome`/`applyCanvasChrome`.\n wireCanvasChrome(d, {\n stylesheetUrl: getCanvasStylesheetUrl(),\n kitTokensCss,\n theme: resolveDeploymentCanvasTheme(),\n onStatus: (status) =>\n setThemeStatus((prev) => (prev?.kind === status.kind ? prev : status)),\n })\n setDoc(d)\n }, [kitTokensCss])\n\n // Handle the already-loaded case (srcDoc may load before onLoad attaches).\n useEffect(() => {\n if (iframeRef.current?.contentDocument?.readyState === 'complete') wireUp()\n }, [wireUp])\n\n // Inline editing (EC-158/EC-159): the dblclick-to-edit state machine, the\n // live session ref other listeners consult, the rich-text host (mark\n // toolbar), and the per-node remount epochs (decorate keys).\n const inline = useCanvasInlineEditing(doc, { presentRef, dispatch, studio })\n const { editingRef, richEditHost, richEditEpochs, editing } = inline\n\n // EC-408: the field↔component sync channel (null in the component preview,\n // which has no inspector). Read live through a ref — the native iframe\n // listeners outlive renders.\n const propSync = usePropSync()\n const propSyncRef = useRef(propSync)\n propSyncRef.current = propSync\n\n // Native selection/hover listeners inside the iframe document.\n useEffect(() => {\n if (!doc) return\n const onClick = (e: Event) => {\n // Clicks inside the active inline editor are caret moves, not selection.\n if (editingRef.current?.host.contains(e.target as Node)) return\n const wrapper = wrapperAt(e.target)\n if (!wrapper) return\n const id = wrapper.getAttribute('data-ec-node-id')!\n e.preventDefault()\n dispatch({ type: 'select', id })\n // EC-408: resolve the clicked sub-part to its `data-ec-prop` and, unless it\n // is inline-editable (inline editing stays primary), ask the inspector to\n // focus that prop's control. Unannotated components resolve no marker → the\n // node-level select above is the whole behavior, exactly as before.\n const sync = propSyncRef.current\n if (!sync) return\n const node = getNode(presentRef.current, id)\n const manifest = node ? studio.registry.getManifest(node.componentId) : undefined\n const { propName, focusField } = resolvePropClick({\n target: e.target as Element | null,\n wrapper,\n manifest,\n })\n if (propName && focusField) sync.requestFocus({ nodeId: id, propName })\n }\n const onOver = (e: Event) => dispatch({ type: 'hover', id: nodeIdAt(e.target) })\n const onLeave = () => dispatch({ type: 'hover', id: null })\n doc.addEventListener('click', onClick, true)\n doc.addEventListener('mouseover', onOver)\n doc.addEventListener('mouseleave', onLeave, true)\n return () => {\n doc.removeEventListener('click', onClick, true)\n doc.removeEventListener('mouseover', onOver)\n doc.removeEventListener('mouseleave', onLeave, true)\n }\n }, [doc, dispatch, editingRef, presentRef, studio])\n\n // Drag & drop on the canvas (EC-160/EC-162) — see `canvas-dnd`.\n useCanvasDnd(doc, { presentRef, dispatch, studio, editingRef })\n\n // Copy/cut/paste on the canvas (EC-175): the same studio clipboard the page\n // tree and blocks panel use, wired to the *iframe* document (events never\n // cross frames). Shortcuts stay inert while an inline editor is live and in\n // form fields; the iframe's own navigator.clipboard does the async I/O\n // (Chromium scopes clipboard access to the focused document).\n const clipboard = useStudioClipboard()\n useEffect(() => {\n if (!doc) return\n return attachClipboardListeners(doc, {\n clipboard,\n getSelectedId: () => selectedIdRef.current,\n isBlocked: () => editingRef.current !== null,\n asyncClipboard: doc.defaultView?.navigator.clipboard,\n getSelection: () => doc.getSelection(),\n })\n }, [doc, clipboard, editingRef])\n\n // Delete/Backspace removes the selected node (EC-404) — the keyboard remove\n // the studio was missing, wired beside the clipboard listeners on the iframe\n // document, guarded so typing in an inline editor edits text instead.\n useEffect(() => {\n if (!doc) return\n return attachDeleteKeyListener(doc, {\n getSelectedId: () => selectedIdRef.current,\n remove: (id) => dispatch({ type: 'remove', id }),\n getRootId: () => presentRef.current.id,\n })\n }, [doc, dispatch])\n\n // The render context: the seven delivery-parity resolvers + the editable-\n // wrapper decorator (selection/hover, locale chips, remount epochs) — see\n // `canvas-render-context`.\n const { ctx, pageNodeIds } = useCanvasRenderContext({\n present: builder.present,\n selectedId: builder.selectedId,\n hoveredId: builder.hoveredId,\n studio,\n perspective,\n richEditEpochs,\n })\n\n // Tokenized spacing handles (EC-163): a selected Stack/Grid with 2+ children\n // gets draggable gap handles between them, portaled into the iframe. Suppressed\n // while an inline text/rich-text edit owns the node (EC-406 scope 4) — the\n // [data-ec-editing] outline owns that state, exactly as the selection overlay\n // yields; a content-drag is suppressed inside SpacingHandles via the drag session.\n const spacingTarget =\n doc && !editing && hasSpacingHandles(builder.selectedNode) && pageNodeIds.has(builder.selectedNode.id)\n ? builder.selectedNode\n : null\n\n // EC-250: the preview frame width derives from the config-sourced breakpoints\n // (Tailwind defaults when unconfigured), the same resolved set the toolbar\n // switcher offers.\n const { viewports } = useCanvasViewports()\n\n return (\n <div className=\"canvas-grid relative flex min-h-0 flex-1 items-stretch justify-center overflow-auto p-6\">\n {/* Copy/cut/paste outcomes (EC-175): non-blocking, with issue deep links. */}\n <ClipboardNoticeOverlay />\n {/* Canvas-theme classification notice (EC-414 AD-D): dev-only, never blocks. */}\n <CanvasThemeNotice status={themeStatus} />\n <div style={{ width: viewportWidth(viewports, builder.viewport), maxWidth: '100%' }} className=\"flex\">\n <iframe\n ref={iframeRef}\n onLoad={wireUp}\n title={translate('builder.canvas.pagePreview')}\n className=\"min-h-[480px] w-full rounded-lg border border-border bg-background shadow-md\"\n srcDoc=\"<!doctype html><html><head></head><body></body></html>\"\n />\n {doc\n ? createPortal(\n // Render the composition root directly (EC-287: no layout\n // indirection — a page renders its blocks).\n renderPageComposition(activeRoot, ctx),\n doc.body,\n )\n : null}\n {/* Selection/hover frames + frame controls, topmost over the page (EC-404). */}\n {doc ? <SelectionOverlay doc={doc} editing={editing} /> : null}\n {spacingTarget && doc ? (\n <SpacingHandles key={spacingTarget.id} doc={doc} node={spacingTarget} />\n ) : null}\n {/* Floating mark toolbar for the live rich-text session (EC-159). */}\n {richEditHost && doc ? <RichTextToolbar doc={doc} host={richEditHost} /> : null}\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -12,7 +12,9 @@ import { PageTree } from './page-tree.js';
|
|
|
12
12
|
import { PropertyInspector } from './property-inspector.js';
|
|
13
13
|
import { InspectorSidebar } from '../inspector/inspector-sidebar.js';
|
|
14
14
|
import { InsertMenu } from './insert-menu.js';
|
|
15
|
-
import { resolveInsertContext } from './insert-context.js';
|
|
15
|
+
import { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context.js';
|
|
16
|
+
import { InsertAffordanceProvider } from './insert-affordance-context.js';
|
|
17
|
+
import { PropSyncProvider } from './prop-sync-context.js';
|
|
16
18
|
import { ReuseProvider } from './reuse-context.js';
|
|
17
19
|
/**
|
|
18
20
|
* EC-188 — the block-field surface: one canvas engine mounted on a composition
|
|
@@ -54,7 +56,7 @@ export function CompositionFieldEditor({ field, value, onChange, readOnly, trail
|
|
|
54
56
|
// (BuilderProvider reads `composition` only in its lazy initial state, so later
|
|
55
57
|
// prop changes are ignored regardless.)
|
|
56
58
|
const initialRoot = useMemo(() => buildSyntheticRoot(value), []);
|
|
57
|
-
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) })] }));
|
|
59
|
+
return (_jsxs(BuilderProvider, { composition: initialRoot, children: [_jsx(CompositionLift, { multiple: field.multiple, onChange: onChange, readOnly: readOnly }), _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, fieldAllow: field.allow, trailingPanes: trailingPanes }) }) })] }));
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
60
62
|
* Lifts committed edits out of the scoped store into the CMS draft. The first
|
|
@@ -147,6 +149,23 @@ function CompositionEditorShell({ readOnly, fieldAllow, trailingPanes, }) {
|
|
|
147
149
|
setInsertContext(ctx);
|
|
148
150
|
setInsertOpen(true);
|
|
149
151
|
}
|
|
150
|
-
|
|
152
|
+
// EC-405: the in-canvas "+" affordances resolve the SAME slot rules through the
|
|
153
|
+
// shared position-carrying insert-context resolution, and open the panel at the
|
|
154
|
+
// clicked edge (an explicit position that survives to the insertion, rather than
|
|
155
|
+
// the panel re-deriving it from the selection). Re-memoized on every store change
|
|
156
|
+
// so the closures read the live graph.
|
|
157
|
+
const affordanceApi = useMemo(() => ({
|
|
158
|
+
positionsFor: (nodeId) => insertablePositions(builder, nodeId, fieldAllow),
|
|
159
|
+
openAt: (nodeId, position) => {
|
|
160
|
+
const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow);
|
|
161
|
+
if (!ctx)
|
|
162
|
+
return;
|
|
163
|
+
if (nodeId !== builder.selectedId)
|
|
164
|
+
dispatch({ type: 'select', id: nodeId });
|
|
165
|
+
setInsertContext(ctx);
|
|
166
|
+
setInsertOpen(true);
|
|
167
|
+
},
|
|
168
|
+
}), [builder, fieldAllow, dispatch]);
|
|
169
|
+
return (_jsx(InsertAffordanceProvider, { value: affordanceApi, children: _jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border", children: _jsxs(Toolbar, { children: [_jsxs(ToolbarGroup, { children: [_jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('builder.canvas.undo'), title: translate('builder.canvas.undo'), disabled: readOnly || !builder.canUndo, onClick: () => dispatch({ type: 'undo' }), children: _jsx(Undo2, {}) }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": translate('builder.canvas.redo'), title: translate('builder.canvas.redo'), disabled: readOnly || !builder.canRedo, onClick: () => dispatch({ type: 'redo' }), children: _jsx(Redo2, {}) })] }), _jsx(ToolbarSeparator, {}), _jsxs(Button, { size: "sm", variant: "ghost", disabled: readOnly, onClick: () => openInsert(builder.selectedId ?? rootId), children: [_jsx(Plus, {}), " ", translate('builder.canvas.insert')] }), _jsx(Spacer, {}), _jsx(BreakpointSwitcher, {}), _jsx(Spacer, {})] }) }), _jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border", children: [_jsx(Panel, { className: cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60'), children: _jsx(PageTree, { onAddChild: openInsert }) }), _jsx("div", { className: cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60'), children: _jsx(BuilderCanvas, { perspective: readOnly ? 'published' : 'draft' }) }), _jsx(InspectorSidebar, { panes: panes })] }), _jsx(InsertMenu, { context: insertContext, open: insertOpen, onOpenChange: setInsertOpen })] }) }));
|
|
151
170
|
}
|
|
152
171
|
//# sourceMappingURL=composition-field-editor.js.map
|