@elytracms/studio 0.0.68 → 0.0.70
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/dist/components/app-views/studio-config.d.ts +22 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.d.ts +4 -2
- package/dist/components/assets/asset-detail.js +4 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +30 -19
- package/dist/components/assets/assets-collection-view-source.js +55 -23
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +91 -29
- package/dist/components/builder/blocks-model.js +83 -54
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +40 -12
- package/dist/components/builder/blocks-panel.js +88 -34
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +6 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
- package/dist/components/builder/fixture-thumbnail.js +27 -99
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +17 -4
- package/dist/components/builder/selection-overlay.js +17 -15
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +34 -6
- package/dist/components/builder/spacing-handles.js +70 -31
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +11 -1
- package/dist/components/cms/bulk-bar.js +10 -3
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/collection-table.js +19 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/field-control.js +5 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +5 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +5 -1
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +24 -5
- package/dist/components/cms/rich-text/editor.js +23 -8
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +50 -1
- package/dist/components/rail-view-source.js +56 -2
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +3 -2
- package/dist/components/redirects/redirects-collection-view-source.js +4 -3
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/session-expired-overlay.d.ts +16 -0
- package/dist/components/session-expired-overlay.js +22 -6
- package/dist/components/session-expired-overlay.js.map +1 -1
- package/dist/components/sign-in-view.d.ts +10 -1
- package/dist/components/sign-in-view.js +14 -3
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +1 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/customization.d.ts +9 -1
- package/dist/customization.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/collection-view/collection-view-source.d.ts +12 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/dnd/gap-drag.d.ts +8 -1
- package/dist/lib/dnd/gap-drag.js +4 -0
- package/dist/lib/dnd/gap-drag.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +8 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +6 -0
- package/dist/lib/i18n/messages.en.js +9 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +8 -1
- package/dist/lib/references/references-store.js +12 -2
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +10 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/ui/dialog.d.ts +9 -1
- package/dist/ui/dialog.js +8 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/surface-geometry.d.ts +8 -5
- package/dist/ui/surface-geometry.js +8 -5
- package/dist/ui/surface-geometry.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,112 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Component, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { AlertTriangle } from 'lucide-react';
|
|
4
|
-
import { renderNode } from '@elytracms/core/runtime-renderer';
|
|
5
|
-
import { getStudioRegistry, previewContext } from '@elytracms/studio/lib/registry';
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
2
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
7
|
-
import {
|
|
3
|
+
import { tileMonogram } from './blocks-model.js';
|
|
8
4
|
/**
|
|
9
|
-
*
|
|
10
|
-
* runtime
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* Blocks-panel tile previews (EC-439 AD-2): previews are ARTIFACTS, never
|
|
6
|
+
* runtime renders. A tile shows a curated `manifest.previewImage` (EC-409) or
|
|
7
|
+
* an EC-440-generated fixture image through {@link PreviewImageThumbnail}, and
|
|
8
|
+
* falls back to the deliberate icon+name placeholder
|
|
9
|
+
* ({@link PlaceholderThumbnail}) when neither exists. The in-document live
|
|
10
|
+
* fixture render (`ScaledFixtureThumbnail`, EC-164) is gone: since EC-414 the
|
|
11
|
+
* consumer theme lives only in the canvas iframe, so no in-studio-document
|
|
12
|
+
* render of consumer components can ever be styled correctly again.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* immediately (first-fold tiles, non-DOM tests).
|
|
14
|
+
* One frame per tile (AD-3): the tile's outer border is the ONLY border — the
|
|
15
|
+
* preview inset is a borderless, tinted, rounded area.
|
|
18
16
|
*/
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
21
|
-
/** Fallback scale used for the single frame before the tile is measured. */
|
|
22
|
-
const FALLBACK_SCALE = 0.45;
|
|
23
|
-
function LiveRender({ node, studio }) {
|
|
24
|
-
const ctx = useMemo(() => previewContext(studio), [studio]);
|
|
25
|
-
// Called inside a component render so the boundary catches sync throws too.
|
|
26
|
-
return _jsx(_Fragment, { children: renderNode(node, ctx) });
|
|
27
|
-
}
|
|
28
|
-
function ThumbnailFallback({ message }) {
|
|
29
|
-
return (_jsxs("div", { "data-block-thumbnail-fallback": true, className: "flex h-full w-full items-start gap-1.5 p-2 text-[0.7rem] leading-snug text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "mt-px size-3 shrink-0" }), _jsx("span", { className: "line-clamp-3", children: message })] }));
|
|
30
|
-
}
|
|
31
|
-
/** Tile-local error boundary: one broken fixture never takes down the panel. */
|
|
32
|
-
class ThumbnailErrorBoundary extends Component {
|
|
33
|
-
state = { error: null };
|
|
34
|
-
static getDerivedStateFromError(error) {
|
|
35
|
-
return { error };
|
|
36
|
-
}
|
|
37
|
-
render() {
|
|
38
|
-
if (this.state.error) {
|
|
39
|
-
return (_jsx(ThumbnailFallback, { message: translate('builder.blocksPanel.previewFailed', { error: this.state.error.message }) }));
|
|
40
|
-
}
|
|
41
|
-
return this.props.children;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
17
|
+
/** The shared borderless 16/10 inset both preview tiers and the placeholder render into. */
|
|
18
|
+
const INSET_CLASSES = 'relative w-full overflow-hidden rounded-md bg-muted/40';
|
|
44
19
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* component title (already-localized manifest data); the image is never a
|
|
50
|
-
* fetch the engine performs, just an `<img>` with the consumer's URL string.
|
|
20
|
+
* Preview-image tile (EC-409 curated / EC-440 generated): renders the resolved
|
|
21
|
+
* `previewUrl` in the shared 16/10 inset, object-fit cover. The image is never
|
|
22
|
+
* a fetch the engine performs, just an `<img>` with the consumer's URL string;
|
|
23
|
+
* `alt` is the component title (already-localized manifest data).
|
|
51
24
|
*/
|
|
52
25
|
export function PreviewImageThumbnail({ src, alt, className, }) {
|
|
53
|
-
return (_jsx("div", { "data-block-thumbnail": true, "data-block-thumbnail-image": true, className: cn(
|
|
26
|
+
return (_jsx("div", { "data-block-thumbnail": true, "data-block-thumbnail-image": true, className: cn(INSET_CLASSES, className), style: { aspectRatio: '16 / 10' }, children: _jsx("img", { src: src, alt: alt,
|
|
54
27
|
// The surrounding tile is the drag source; a natively draggable <img>
|
|
55
28
|
// would hijack the tile drag.
|
|
56
29
|
draggable: false, className: "pointer-events-none h-full w-full select-none object-cover" }) }));
|
|
57
30
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// isn't producing frames (occluded/headless automation windows), so a
|
|
67
|
-
// safety-net timer mounts the preview rather than leaving tiles blank.
|
|
68
|
-
// Frame-producing browsers report long before the timer and keep laziness.
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (inView)
|
|
71
|
-
return;
|
|
72
|
-
const el = ref.current;
|
|
73
|
-
if (!el || typeof IntersectionObserver === 'undefined') {
|
|
74
|
-
setInView(true);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
let reported = false;
|
|
78
|
-
const observer = new IntersectionObserver((entries) => {
|
|
79
|
-
reported = true;
|
|
80
|
-
if (entries.some((entry) => entry.isIntersecting))
|
|
81
|
-
setInView(true);
|
|
82
|
-
});
|
|
83
|
-
observer.observe(el);
|
|
84
|
-
const fallback = setTimeout(() => {
|
|
85
|
-
if (!reported)
|
|
86
|
-
setInView(true);
|
|
87
|
-
}, 400);
|
|
88
|
-
return () => {
|
|
89
|
-
observer.disconnect();
|
|
90
|
-
clearTimeout(fallback);
|
|
91
|
-
};
|
|
92
|
-
}, [inView]);
|
|
93
|
-
// Fit-to-tile scale, kept in sync with the tile's measured width.
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
const el = ref.current;
|
|
96
|
-
if (!el)
|
|
97
|
-
return;
|
|
98
|
-
const update = () => {
|
|
99
|
-
const width = el.clientWidth;
|
|
100
|
-
if (width > 0)
|
|
101
|
-
setScale(width / designWidth);
|
|
102
|
-
};
|
|
103
|
-
update();
|
|
104
|
-
if (typeof ResizeObserver === 'undefined')
|
|
105
|
-
return;
|
|
106
|
-
const observer = new ResizeObserver(update);
|
|
107
|
-
observer.observe(el);
|
|
108
|
-
return () => observer.disconnect();
|
|
109
|
-
}, [designWidth]);
|
|
110
|
-
return (_jsx("div", { ref: ref, "aria-hidden": true, "data-block-thumbnail": true, className: cn('relative w-full overflow-hidden rounded-md border border-border bg-background', className), style: { aspectRatio: '16 / 10' }, children: inView ? (_jsx(ThumbnailErrorBoundary, { children: _jsx("div", { className: "pointer-events-none absolute left-0 top-0 origin-top-left select-none p-3", style: { width: designWidth, transform: `scale(${scale ?? FALLBACK_SCALE})` }, children: _jsx(LiveRender, { node: node, studio: registry }) }) })) : null }));
|
|
31
|
+
/**
|
|
32
|
+
* The icon+name placeholder (EC-439 AD-2, tier 3): a deliberate glyph
|
|
33
|
+
* treatment — a centered two-letter monogram derived from the component title
|
|
34
|
+
* in a muted disc — never an empty box and never a lying render. Decorative
|
|
35
|
+
* only (`aria-hidden`): the tile itself carries the accessible name.
|
|
36
|
+
*/
|
|
37
|
+
export function PlaceholderThumbnail({ title, className }) {
|
|
38
|
+
return (_jsx("div", { "aria-hidden": true, "data-block-thumbnail": true, "data-block-thumbnail-placeholder": true, className: cn(INSET_CLASSES, 'grid place-items-center', className), style: { aspectRatio: '16 / 10' }, children: _jsx("span", { className: "grid size-10 select-none place-items-center rounded-full bg-muted text-sm font-semibold tracking-wide text-muted-foreground", children: tileMonogram(title) }) }));
|
|
111
39
|
}
|
|
112
40
|
//# sourceMappingURL=fixture-thumbnail.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-thumbnail.js","sourceRoot":"","sources":["../../../src/components/builder/fixture-thumbnail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { renderNode } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry, previewContext } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Live, scaled fixture thumbnail (EC-164). Renders a preview node through the\n * runtime renderer — the exact render path the canvas, fixture runner, and\n * export use — inside a fixed-aspect tile, scaled down with a CSS transform.\n * Never a screenshot, never interactive (pointer events are disabled), and\n * never a crash: render errors degrade to a visible fallback inside the tile.\n *\n * Off-screen thumbnails mount lazily via IntersectionObserver so a large\n * registry doesn't render its whole library up front; pass `eager` to render\n * immediately (first-fold tiles, non-DOM tests).\n */\n\n/** Width the preview is laid out at before being scaled into the tile. */\nconst DESIGN_WIDTH = 420\n/** Fallback scale used for the single frame before the tile is measured. */\nconst FALLBACK_SCALE = 0.45\n\nfunction LiveRender({ node, studio }: { node: ComponentNode; studio: StudioRegistry }) {\n const ctx = useMemo(() => previewContext(studio), [studio])\n // Called inside a component render so the boundary catches sync throws too.\n return <>{renderNode(node, ctx)}</>\n}\n\nfunction ThumbnailFallback({ message }: { message: string }) {\n return (\n <div\n data-block-thumbnail-fallback\n className=\"flex h-full w-full items-start gap-1.5 p-2 text-[0.7rem] leading-snug text-amber-700 dark:text-amber-400\"\n >\n <AlertTriangle aria-hidden className=\"mt-px size-3 shrink-0\" />\n <span className=\"line-clamp-3\">{message}</span>\n </div>\n )\n}\n\ninterface BoundaryState {\n error: Error | null\n}\n\n/** Tile-local error boundary: one broken fixture never takes down the panel. */\nclass ThumbnailErrorBoundary extends Component<{ children: ReactNode }, BoundaryState> {\n override state: BoundaryState = { error: null }\n\n static getDerivedStateFromError(error: Error): BoundaryState {\n return { error }\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n return (\n <ThumbnailFallback\n message={translate('builder.blocksPanel.previewFailed', { error: this.state.error.message })}\n />\n )\n }\n return this.props.children\n }\n}\n\n/**\n * Curated preview-image tile (EC-409): renders `manifest.previewImage` in the\n * SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.\n * The escape hatch for components whose live render needs data/context the\n * tile can't have — fixtures stay the primary curation lever. `alt` is the\n * component title (already-localized manifest data); the image is never a\n * fetch the engine performs, just an `<img>` with the consumer's URL string.\n */\nexport function PreviewImageThumbnail({\n src,\n alt,\n className,\n}: {\n /** The manifest's `previewImage` URL string, passed through verbatim. */\n src: string\n /** The component title (localized manifest data — no new catalog string). */\n alt: string\n className?: string\n}) {\n return (\n <div\n data-block-thumbnail\n data-block-thumbnail-image\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {/* Deliberately NOT loading=\"lazy\": native lazy images need rendering\n frames to trigger (occluded/headless windows never load them — the\n same hazard the live thumbnail's safety-net timer covers), and a\n curated preview is a small asset on a handful of tiles. */}\n <img\n src={src}\n alt={alt}\n // The surrounding tile is the drag source; a natively draggable <img>\n // would hijack the tile drag.\n draggable={false}\n className=\"pointer-events-none h-full w-full select-none object-cover\"\n />\n </div>\n )\n}\n\nexport function ScaledFixtureThumbnail({\n node,\n studio,\n designWidth = DESIGN_WIDTH,\n eager = false,\n className,\n}: {\n /** The preview subtree to render live (deterministic ids). */\n node: ComponentNode\n /**\n * Registry the preview renders against. Defaults to the active project's live\n * registry (`getStudioRegistry()`) — the blocks panel's tiles and their\n * thumbnails share one registry. Pass an explicit registry to render against a\n * specific component set (deterministic previews/tests).\n */\n studio?: StudioRegistry\n /** Layout width of the unscaled preview, in CSS pixels. */\n designWidth?: number\n /** Mount the live render immediately instead of waiting for visibility. */\n eager?: boolean\n className?: string\n}) {\n const registry = studio ?? getStudioRegistry()\n const ref = useRef<HTMLDivElement>(null)\n const [inView, setInView] = useState(eager)\n const [scale, setScale] = useState<number | null>(null)\n\n // Lazy mount: render the live preview only once the tile scrolls into view.\n // IntersectionObserver always delivers an initial report (intersecting or\n // not) within a rendering frame — when no report arrives at all the page\n // isn't producing frames (occluded/headless automation windows), so a\n // safety-net timer mounts the preview rather than leaving tiles blank.\n // Frame-producing browsers report long before the timer and keep laziness.\n useEffect(() => {\n if (inView) return\n const el = ref.current\n if (!el || typeof IntersectionObserver === 'undefined') {\n setInView(true)\n return\n }\n let reported = false\n const observer = new IntersectionObserver((entries) => {\n reported = true\n if (entries.some((entry) => entry.isIntersecting)) setInView(true)\n })\n observer.observe(el)\n const fallback = setTimeout(() => {\n if (!reported) setInView(true)\n }, 400)\n return () => {\n observer.disconnect()\n clearTimeout(fallback)\n }\n }, [inView])\n\n // Fit-to-tile scale, kept in sync with the tile's measured width.\n useEffect(() => {\n const el = ref.current\n if (!el) return\n const update = () => {\n const width = el.clientWidth\n if (width > 0) setScale(width / designWidth)\n }\n update()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(update)\n observer.observe(el)\n return () => observer.disconnect()\n }, [designWidth])\n\n return (\n <div\n ref={ref}\n aria-hidden\n data-block-thumbnail\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {inView ? (\n <ThumbnailErrorBoundary>\n <div\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left select-none p-3\"\n style={{ width: designWidth, transform: `scale(${scale ?? FALLBACK_SCALE})` }}\n >\n <LiveRender node={node} studio={registry} />\n </div>\n </ThumbnailErrorBoundary>\n ) : null}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fixture-thumbnail.js","sourceRoot":"","sources":["../../../src/components/builder/fixture-thumbnail.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C;;;;;;;;;;;;GAYG;AAEH,4FAA4F;AAC5F,MAAM,aAAa,GAAG,wDAAwD,CAAA;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,GAAG,EACH,GAAG,EACH,SAAS,GAOV;IACC,OAAO,CACL,gFAGE,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,KAAK,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,YAKjC,cACE,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG;YACR,sEAAsE;YACtE,8BAA8B;YAC9B,SAAS,EAAE,KAAK,EAChB,SAAS,EAAC,4DAA4D,GACtE,GACE,CACP,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAyC;IAC9F,OAAO,CACL,2GAIE,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,yBAAyB,EAAE,SAAS,CAAC,EAClE,KAAK,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,YAEjC,eAAM,SAAS,EAAC,6HAA6H,YAC1I,YAAY,CAAC,KAAK,CAAC,GACf,GACH,CACP,CAAA;AACH,CAAC","sourcesContent":["import { cn } from '@elytracms/studio/lib/utils'\nimport { tileMonogram } from './blocks-model'\n\n/**\n * Blocks-panel tile previews (EC-439 AD-2): previews are ARTIFACTS, never\n * runtime renders. A tile shows a curated `manifest.previewImage` (EC-409) or\n * an EC-440-generated fixture image through {@link PreviewImageThumbnail}, and\n * falls back to the deliberate icon+name placeholder\n * ({@link PlaceholderThumbnail}) when neither exists. The in-document live\n * fixture render (`ScaledFixtureThumbnail`, EC-164) is gone: since EC-414 the\n * consumer theme lives only in the canvas iframe, so no in-studio-document\n * render of consumer components can ever be styled correctly again.\n *\n * One frame per tile (AD-3): the tile's outer border is the ONLY border — the\n * preview inset is a borderless, tinted, rounded area.\n */\n\n/** The shared borderless 16/10 inset both preview tiers and the placeholder render into. */\nconst INSET_CLASSES = 'relative w-full overflow-hidden rounded-md bg-muted/40'\n\n/**\n * Preview-image tile (EC-409 curated / EC-440 generated): renders the resolved\n * `previewUrl` in the shared 16/10 inset, object-fit cover. The image is never\n * a fetch the engine performs, just an `<img>` with the consumer's URL string;\n * `alt` is the component title (already-localized manifest data).\n */\nexport function PreviewImageThumbnail({\n src,\n alt,\n className,\n}: {\n /** The resolved preview URL (curated `manifest.previewImage` or a generated artifact), passed through verbatim. */\n src: string\n /** The component title (localized manifest data — no new catalog string). */\n alt: string\n className?: string\n}) {\n return (\n <div\n data-block-thumbnail\n data-block-thumbnail-image\n className={cn(INSET_CLASSES, className)}\n style={{ aspectRatio: '16 / 10' }}\n >\n {/* Deliberately NOT loading=\"lazy\": native lazy images need rendering\n frames to trigger (occluded/headless windows never load them), and a\n tile preview is a small asset. */}\n <img\n src={src}\n alt={alt}\n // The surrounding tile is the drag source; a natively draggable <img>\n // would hijack the tile drag.\n draggable={false}\n className=\"pointer-events-none h-full w-full select-none object-cover\"\n />\n </div>\n )\n}\n\n/**\n * The icon+name placeholder (EC-439 AD-2, tier 3): a deliberate glyph\n * treatment — a centered two-letter monogram derived from the component title\n * in a muted disc — never an empty box and never a lying render. Decorative\n * only (`aria-hidden`): the tile itself carries the accessible name.\n */\nexport function PlaceholderThumbnail({ title, className }: { title: string; className?: string }) {\n return (\n <div\n aria-hidden\n data-block-thumbnail\n data-block-thumbnail-placeholder\n className={cn(INSET_CLASSES, 'grid place-items-center', className)}\n style={{ aspectRatio: '16 / 10' }}\n >\n <span className=\"grid size-10 select-none place-items-center rounded-full bg-muted text-sm font-semibold tracking-wide text-muted-foreground\">\n {tileMonogram(title)}\n </span>\n </div>\n )\n}\n"]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { InsertAffordanceApi } from './insert-affordance-context.js';
|
|
1
3
|
import type { InsertPosition } from './insert-context.js';
|
|
2
4
|
import type { AffordanceAnchor, FrameBarPlacement } from './canvas-overlay-geometry.js';
|
|
3
5
|
import type { Rect } from './canvas-rect-tracking.js';
|
|
@@ -39,8 +41,11 @@ export interface AffordanceItemView extends AffordanceAnchor {
|
|
|
39
41
|
position: InsertPosition;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
|
-
* The affordance buttons for one node — grouped so a single `onInsert` binds
|
|
43
|
-
* node id the inserts target
|
|
44
|
+
* The affordance buttons for one node — grouped so a single `onInsert` binds
|
|
45
|
+
* the node id the inserts target. EC-435 AD-1: only the SELECTED node ever
|
|
46
|
+
* carries affordances (they appear/disappear with the frame bar, like the rest
|
|
47
|
+
* of the chrome) — hover draws its frame and nothing else, so a selected
|
|
48
|
+
* Stack's spacing handles never share a gap with a hovered child's "+".
|
|
44
49
|
*/
|
|
45
50
|
export interface AffordanceGroupView {
|
|
46
51
|
nodeId: string;
|
|
@@ -50,8 +55,8 @@ export interface AffordanceGroupView {
|
|
|
50
55
|
export interface SelectionOverlayViewProps {
|
|
51
56
|
selected: SelectionFrameView | null;
|
|
52
57
|
hovered: Rect | null;
|
|
53
|
-
/**
|
|
54
|
-
|
|
58
|
+
/** The SELECTED node's insertion affordances (EC-405/EC-435); null when none or suppressed (drag/editing). */
|
|
59
|
+
affordance: AffordanceGroupView | null;
|
|
55
60
|
/** EC-408: the focused prop's marker rect (reverse highlight), or null. */
|
|
56
61
|
propHighlight?: Rect | null;
|
|
57
62
|
onMoveUp: () => void;
|
|
@@ -77,3 +82,11 @@ export declare function SelectionOverlay({ doc, editing }: {
|
|
|
77
82
|
doc: Document;
|
|
78
83
|
editing: boolean;
|
|
79
84
|
}): import("react").ReactPortal;
|
|
85
|
+
/**
|
|
86
|
+
* Build the affordance group for one node from its measured rect: the insertable
|
|
87
|
+
* positions the provider allows (slot rules already applied), each placed on the
|
|
88
|
+
* node's boundary along the parent's flow axis. Returns `null` when there is no
|
|
89
|
+
* rect yet or no insertable position. Exported as the pure core of the overlay's
|
|
90
|
+
* affordance derivation (EC-435 AD-1: called for the selected node only).
|
|
91
|
+
*/
|
|
92
|
+
export declare function affordanceGroup(present: ComponentNode, nodeId: string | null, rect: Rect | null, affordance: InsertAffordanceApi): AffordanceGroupView | null;
|
|
@@ -43,8 +43,10 @@ const AFFORDANCE_LABELS = {
|
|
|
43
43
|
* carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).
|
|
44
44
|
*/
|
|
45
45
|
export function SelectionOverlayView(props) {
|
|
46
|
-
const { selected, hovered,
|
|
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,
|
|
46
|
+
const { selected, hovered, affordance, 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, affordance
|
|
48
|
+
? affordance.items.map((item) => (_jsx(AddButton, { item: item, onInsert: () => affordance.onInsert(item.position) }, item.position)))
|
|
49
|
+
: null, 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
50
|
}
|
|
49
51
|
function FrameButton({ label, disabled, onClick, children, }) {
|
|
50
52
|
return (_jsx("button", { type: "button", className: "ec-overlay-btn", "aria-label": label, title: label, disabled: disabled,
|
|
@@ -210,25 +212,25 @@ export function SelectionOverlay({ doc, editing }) {
|
|
|
210
212
|
return;
|
|
211
213
|
dispatch(frameControlAction(kind, selectedId));
|
|
212
214
|
};
|
|
213
|
-
// EC-405: the "+" affordances for the
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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);
|
|
215
|
+
// EC-405/EC-435 AD-1: the "+" affordances render for the SELECTED node only,
|
|
216
|
+
// with the rest of its frame chrome — computed from the SAME measured rect the
|
|
217
|
+
// selection frame uses. Hover never carries affordances (that used to land "+"
|
|
218
|
+
// buttons inside a selected Stack's gaps, colliding with the spacing handles).
|
|
219
|
+
// A group is drawn only for a node with insertable positions, and only when no
|
|
220
|
+
// drag/inline edit is live; with no provider above (component preview), none.
|
|
221
|
+
const selectedAffordance = affordance && !dragging && !editing
|
|
222
|
+
? affordanceGroup(present, selectedId, drawSelected ? geometry.selected : null, affordance)
|
|
223
|
+
: null;
|
|
224
|
+
return createPortal(_jsx(SelectionOverlayView, { selected: selectedView, hovered: drawHover ? geometry.hovered : null, affordance: selectedAffordance, propHighlight: highlight ? geometry.propHighlight : null, onMoveUp: () => run('moveUp'), onMoveDown: () => run('moveDown'), onDuplicate: () => run('duplicate'), onDelete: () => run('delete') }), doc.body);
|
|
224
225
|
}
|
|
225
226
|
/**
|
|
226
227
|
* Build the affordance group for one node from its measured rect: the insertable
|
|
227
228
|
* positions the provider allows (slot rules already applied), each placed on the
|
|
228
229
|
* node's boundary along the parent's flow axis. Returns `null` when there is no
|
|
229
|
-
* rect yet or no insertable position.
|
|
230
|
+
* rect yet or no insertable position. Exported as the pure core of the overlay's
|
|
231
|
+
* affordance derivation (EC-435 AD-1: called for the selected node only).
|
|
230
232
|
*/
|
|
231
|
-
function affordanceGroup(present, nodeId, rect, affordance) {
|
|
233
|
+
export function affordanceGroup(present, nodeId, rect, affordance) {
|
|
232
234
|
if (!nodeId || !rect)
|
|
233
235
|
return null;
|
|
234
236
|
const positions = affordance.positionsFor(nodeId);
|
|
@@ -1 +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
|
+
{"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\n * the node id the inserts target. EC-435 AD-1: only the SELECTED node ever\n * carries affordances (they appear/disappear with the frame bar, like the rest\n * of the chrome) — hover draws its frame and nothing else, so a selected\n * Stack's spacing handles never share a gap with a hovered child's \"+\".\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 /** The SELECTED node's insertion affordances (EC-405/EC-435); null when none or suppressed (drag/editing). */\n affordance: AffordanceGroupView | null\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, affordance, 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 {affordance\n ? affordance.items.map((item) => (\n <AddButton key={item.position} item={item} onInsert={() => affordance.onInsert(item.position)} />\n ))\n : null}\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/EC-435 AD-1: the \"+\" affordances render for the SELECTED node only,\n // with the rest of its frame chrome — computed from the SAME measured rect the\n // selection frame uses. Hover never carries affordances (that used to land \"+\"\n // buttons inside a selected Stack's gaps, colliding with the spacing handles).\n // A group is drawn only for a node with insertable positions, and only when no\n // drag/inline edit is live; with no provider above (component preview), none.\n const selectedAffordance: AffordanceGroupView | null =\n affordance && !dragging && !editing\n ? affordanceGroup(present, selectedId, drawSelected ? geometry.selected : null, affordance)\n : null\n\n return createPortal(\n <SelectionOverlayView\n selected={selectedView}\n hovered={drawHover ? geometry.hovered : null}\n affordance={selectedAffordance}\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. Exported as the pure core of the overlay's\n * affordance derivation (EC-435 AD-1: called for the selected node only).\n */\nexport function 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,14 +1,15 @@
|
|
|
1
1
|
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
2
|
import type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
3
|
-
import type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag';
|
|
3
|
+
import type { GapRect, GapStep } from '@elytracms/studio/lib/dnd/gap-drag';
|
|
4
4
|
import type { FrameScheduler, RectTracker } from './canvas-rect-tracking.js';
|
|
5
5
|
/**
|
|
6
6
|
* Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid
|
|
7
7
|
* container is selected on the canvas, draggable gap handles appear between
|
|
8
8
|
* its children, axis-aware via the shared `containerFlowAxis`. Dragging snaps
|
|
9
9
|
* through the EC-161 spacing-token scale (`lib/dnd/gap-drag` — pure math, no
|
|
10
|
-
* pixel value can escape it)
|
|
11
|
-
*
|
|
10
|
+
* pixel value can escape it); the static gap tags (EC-435) stay put and read
|
|
11
|
+
* the LIVE token + re-measured px during the drag (EC-438 — no
|
|
12
|
+
* pointer-following label). The drag previews imperatively on the rendered
|
|
12
13
|
* element; releasing commits exactly ONE builder dispatch — `setProp` with the
|
|
13
14
|
* token, or `removeProp` for "no gap", the identical values the inspector's
|
|
14
15
|
* gap select writes — so one undo restores, and the graph stays the single
|
|
@@ -23,7 +24,19 @@ export interface HandleSpotView {
|
|
|
23
24
|
y: number;
|
|
24
25
|
crossStart: number;
|
|
25
26
|
crossLength: number;
|
|
27
|
+
/** Leading edge of the gap region along the flow axis (EC-435 hatch). */
|
|
28
|
+
gapStart: number;
|
|
29
|
+
/** Rendered extent of the gap along the flow axis (0 for a closed gap). */
|
|
30
|
+
gapLength: number;
|
|
26
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The hatched gap REGION for a spot (EC-435 AD-2): the rectangle between child
|
|
34
|
+
* N's trailing edge and child N+1's leading edge along the flow axis, spanning
|
|
35
|
+
* the pair's shared cross extent — the same measured child-rect basis the
|
|
36
|
+
* handle spots come from (`gapHandleSpots`), in the same document coordinates.
|
|
37
|
+
* Pure over the spot; a closed gap yields a zero-extent rect (nothing to paint).
|
|
38
|
+
*/
|
|
39
|
+
export declare function gapRegionRect(spot: HandleSpotView, axis: FlowAxis): GapRect;
|
|
27
40
|
/**
|
|
28
41
|
* The container's rendered element (the Stack/Grid div carrying the gap style).
|
|
29
42
|
* Resilient to legitimate intermediate wrappers between the editable
|
|
@@ -83,11 +96,18 @@ export declare function hasSpacingHandles(node: ComponentNode | null): node is C
|
|
|
83
96
|
export interface SpacingHandlesViewProps {
|
|
84
97
|
spots: HandleSpotView[];
|
|
85
98
|
horizontal: boolean;
|
|
86
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Token label for the container's COMMITTED gap (EC-435 AD-2) — the SAME
|
|
101
|
+
* plain-language vocabulary the layout inspector resolves (`gapStepLabel`),
|
|
102
|
+
* stamped on every hatched gap region as a static size tag while idle.
|
|
103
|
+
*/
|
|
104
|
+
gapLabel: string;
|
|
105
|
+
/**
|
|
106
|
+
* The live drag: the active handle index (tint) and the live step's token
|
|
107
|
+
* label — the static tags read it in place while dragging (EC-438).
|
|
108
|
+
*/
|
|
87
109
|
drag: {
|
|
88
110
|
index: number;
|
|
89
|
-
labelX: number;
|
|
90
|
-
labelY: number;
|
|
91
111
|
label: string;
|
|
92
112
|
} | null;
|
|
93
113
|
onPointerDown: (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => void;
|
|
@@ -100,6 +120,14 @@ export interface SpacingHandlesViewProps {
|
|
|
100
120
|
* contract test can walk it with `renderToStaticMarkup`. In-frame chrome
|
|
101
121
|
* contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root
|
|
102
122
|
* carries the `.ec-chrome` containment recipe.
|
|
123
|
+
*
|
|
124
|
+
* EC-435 AD-2: every open gap region renders a pointer-transparent hatched
|
|
125
|
+
* rectangle (diagonal accent stripes, Figma-grid style) under the handles,
|
|
126
|
+
* each with a small static token-label size tag beside its handle. During a
|
|
127
|
+
* gap drag the tags STAY PUT and read live (EC-438 — a gap drag previews ONE
|
|
128
|
+
* container-wide gap, so every tag shows the live step's token + its own
|
|
129
|
+
* re-measured px); no label follows the pointer. The gap handles remain the
|
|
130
|
+
* only interactive element.
|
|
103
131
|
*/
|
|
104
132
|
export declare function SpacingHandlesView(props: SpacingHandlesViewProps): import("react").JSX.Element;
|
|
105
133
|
export declare function SpacingHandles({ doc, node }: {
|