@elytracms/studio 0.0.63 → 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.d.ts +54 -0
- package/dist/components/builder/canvas-chrome-css.js +246 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -0
- package/dist/components/builder/canvas-chrome.d.ts +122 -18
- package/dist/components/builder/canvas-chrome.js +148 -82
- package/dist/components/builder/canvas-chrome.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-render-context.d.ts +13 -0
- package/dist/components/builder/canvas-render-context.js +7 -5
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
- package/dist/components/builder/canvas-stylesheet.js +16 -9
- package/dist/components/builder/canvas-stylesheet.js.map +1 -1
- package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
- package/dist/components/builder/canvas-theme-notice.js +46 -0
- package/dist/components/builder/canvas-theme-notice.js.map +1 -0
- package/dist/components/builder/canvas.js +78 -22
- 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/rich-text-toolbar.d.ts +23 -0
- package/dist/components/builder/rich-text-toolbar.js +24 -17
- package/dist/components/builder/rich-text-toolbar.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 +88 -1
- package/dist/components/builder/spacing-handles.js +191 -63
- 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.d.ts +2 -1
- package/dist/components/components/component-preview.js +25 -26
- 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 +20 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +18 -0
- package/dist/lib/i18n/messages.en.js +21 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +7 -0
- package/dist/lib/persistence/backend-mode.js +9 -0
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +4 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.d.ts +1 -1
- package/dist/lib/persistence/index.js +1 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +9 -5
- package/dist/lib/persona/labels.js +13 -8
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection-overlay.js","sourceRoot":"","sources":["../../../src/components/builder/selection-overlay.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect, useState } from 'react'\nimport type { CSSProperties, ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { getNode, getParentInfo } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate, useUiLocale } from '@elytracms/studio/lib/i18n'\nimport { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport { useBuilder } from './builder-store'\nimport { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session'\nimport { useInsertAffordance } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport type { InsertPosition } from './insert-context'\nimport type { AffordanceAnchor, FrameBarPlacement, FrameControlKind } from './canvas-overlay-geometry'\nimport {\n FRAME_BAR_HEIGHT,\n frameControlAction,\n frameControlsFor,\n placeAffordance,\n placeFrameBar,\n} from './canvas-overlay-geometry'\nimport {\n createRectTracker,\n measureNodeRect,\n measurePropRect,\n rectsEqual,\n scheduleFrame,\n subscribeCanvasLayout,\n} from './canvas-rect-tracking'\nimport type { Rect } from './canvas-rect-tracking'\nimport { usePropSync } from './prop-sync-context'\nimport { nodeDisplayTitle } from './node-title'\n\n/**\n * Canvas selection/hover overlay (EC-404) — a topmost layer portaled into the\n * iframe `body` that draws the selection and hover frames ABOVE the page from\n * measured node rects, so a full-bleed background or a higher-`z-index` sibling\n * can never paint over them (the structural fix for the in-document inset\n * outline the findings batch flagged). The selected frame carries a compact tag:\n * the component's display name plus move-up / move-down / duplicate / delete\n * controls that dispatch the EXISTING tree actions.\n *\n * EC-414 AD-B chrome contract: everything here is `ec-*` classes / `data-ec-*`\n * attributes / inline geometry only, styled by `canvas-chrome-css.ts`, with the\n * `.ec-chrome` containment recipe on the root; no studio Tailwind utilities and\n * no studio React UI primitives cross into the iframe. The rect-tracking core\n * ({@link createRectTracker}) is a pure, reusable module — EC-405 rides this\n * overlay layer next, and EC-406 can share the tracking.\n */\n\n/** The overlay's full measured geometry (both frames + the bar placement). */\nexport interface OverlayGeometry {\n selected: Rect | null\n /** Chrome-bar placement for the selected frame (null when nothing to draw). */\n selectedBar: FrameBarPlacement | null\n hovered: Rect | null\n /** EC-408: the focused prop's marker rect (reverse highlight), or null. */\n propHighlight: Rect | null\n}\n\nconst EMPTY_GEOMETRY: OverlayGeometry = {\n selected: null,\n selectedBar: null,\n hovered: null,\n propHighlight: null,\n}\n\nfunction barsEqual(a: FrameBarPlacement | null, b: FrameBarPlacement | null): boolean {\n if (a === null || b === null) return a === b\n return a.top === b.top && a.left === b.left && a.placement === b.placement\n}\n\nfunction geometryEquals(a: OverlayGeometry, b: OverlayGeometry): boolean {\n return (\n rectsEqual(a.selected, b.selected) &&\n rectsEqual(a.hovered, b.hovered) &&\n rectsEqual(a.propHighlight, b.propHighlight) &&\n barsEqual(a.selectedBar, b.selectedBar)\n )\n}\n\n/** A drawn selection frame plus its control enablement. */\nexport interface SelectionFrameView {\n rect: Rect\n bar: FrameBarPlacement\n label: string\n canMoveUp: boolean\n canMoveDown: boolean\n}\n\n/** One \"+\" affordance button: its position and its placed anchor point. */\nexport interface AffordanceItemView extends AffordanceAnchor {\n position: InsertPosition\n}\n\n/**\n * The affordance buttons for one node — grouped so a single `onInsert` binds the\n * node id the inserts target (the selected node and, separately, a hovered one).\n */\nexport interface AffordanceGroupView {\n nodeId: string\n items: AffordanceItemView[]\n onInsert: (position: InsertPosition) => void\n}\n\nexport interface SelectionOverlayViewProps {\n selected: SelectionFrameView | null\n hovered: Rect | null\n /** In-canvas insertion affordances (EC-405); empty when suppressed (drag/editing). */\n affordances: AffordanceGroupView[]\n /** EC-408: the focused prop's marker rect (reverse highlight), or null. */\n propHighlight?: Rect | null\n onMoveUp: () => void\n onMoveDown: () => void\n onDuplicate: () => void\n onDelete: () => void\n}\n\nfunction frameStyle(rect: Rect): CSSProperties {\n return { left: rect.left, top: rect.top, width: rect.width, height: rect.height }\n}\n\nconst AFFORDANCE_LABELS: Record<InsertPosition, string> = {\n before: 'builder.canvas.insertBefore',\n after: 'builder.canvas.insertAfter',\n into: 'builder.canvas.insertInto',\n}\n\n/**\n * The overlay's rendered markup, portal-free so the AD-D chrome contract test\n * can walk it with `renderToStaticMarkup`: every class is `ec-*`, the root\n * carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).\n */\nexport function SelectionOverlayView(props: SelectionOverlayViewProps) {\n const { selected, hovered, affordances, propHighlight } = props\n return (\n <div className=\"ec-chrome ec-overlay\" data-ec-overlay=\"\">\n {/* EC-408 reverse highlight: a quiet filled tint on the focused prop's\n element, drawn UNDER the selection frame so its border stays crisp. */}\n {propHighlight ? (\n <div className=\"ec-overlay-prop\" data-ec-prop-highlight=\"\" style={frameStyle(propHighlight)} />\n ) : null}\n {hovered ? <div className=\"ec-overlay-frame\" data-ec-hover=\"\" style={frameStyle(hovered)} /> : null}\n {affordances.map((group) =>\n group.items.map((item) => (\n <AddButton\n key={`${group.nodeId}:${item.position}`}\n item={item}\n onInsert={() => group.onInsert(item.position)}\n />\n )),\n )}\n {selected ? (\n <>\n <div className=\"ec-overlay-frame\" data-ec-selected=\"\" style={frameStyle(selected.rect)} />\n <div\n className=\"ec-overlay-bar\"\n data-ec-frame-bar=\"\"\n data-ec-placement={selected.bar.placement}\n style={{ left: selected.bar.left, top: selected.bar.top }}\n >\n <span className=\"ec-overlay-label\" title={selected.label}>\n {selected.label}\n </span>\n <span className=\"ec-overlay-sep\" aria-hidden=\"true\" />\n <FrameButton\n label={translate('builder.pageTree.moveUp')}\n disabled={!selected.canMoveUp}\n onClick={props.onMoveUp}\n >\n <OverlayIcon kind=\"moveUp\" />\n </FrameButton>\n <FrameButton\n label={translate('builder.pageTree.moveDown')}\n disabled={!selected.canMoveDown}\n onClick={props.onMoveDown}\n >\n <OverlayIcon kind=\"moveDown\" />\n </FrameButton>\n <FrameButton label={translate('common.duplicate')} onClick={props.onDuplicate}>\n <OverlayIcon kind=\"duplicate\" />\n </FrameButton>\n <FrameButton label={translate('common.delete')} onClick={props.onDelete}>\n <OverlayIcon kind=\"delete\" />\n </FrameButton>\n </div>\n </>\n ) : null}\n </div>\n )\n}\n\nfunction FrameButton({\n label,\n disabled,\n onClick,\n children,\n}: {\n label: string\n disabled?: boolean\n onClick: () => void\n children: ReactNode\n}) {\n return (\n <button\n type=\"button\"\n className=\"ec-overlay-btn\"\n aria-label={label}\n title={label}\n disabled={disabled}\n // Keep the click on the control: it must not fall through to the canvas\n // selection listener (the bar sits over the page) nor start a text drag.\n onMouseDown={(e) => e.preventDefault()}\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n onClick()\n }}\n >\n {children}\n </button>\n )\n}\n\n/**\n * A \"+\" insertion affordance (EC-405): a small round button centered on a block\n * boundary (before/after edge, or the center of an empty container). Centered on\n * its anchor by the chrome CSS `translate(-50%,-50%)`; `pointer-events` re-enabled\n * on the button while the overlay root stays click-through. The gesture guards\n * mirror `FrameButton` — the press must not fall through to the canvas selection\n * listener nor start a native drag.\n */\nfunction AddButton({ item, onInsert }: { item: AffordanceItemView; onInsert: () => void }) {\n const label = translate(AFFORDANCE_LABELS[item.position])\n return (\n <button\n type=\"button\"\n className=\"ec-overlay-add\"\n data-ec-affordance={item.position}\n aria-label={label}\n title={label}\n style={{ left: item.left, top: item.top }}\n onMouseDown={(e) => e.preventDefault()}\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n onInsert()\n }}\n >\n <svg\n width={14}\n height={14}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.8}\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M8 3.5v9M3.5 8h9\" />\n </svg>\n </button>\n )\n}\n\n/** Hand-rolled inline icons — no `class` attribute (chrome contract). */\nfunction OverlayIcon({ kind }: { kind: FrameControlKind }) {\n const common = {\n width: 13,\n height: 13,\n viewBox: '0 0 16 16',\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 1.6,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n 'aria-hidden': true,\n }\n switch (kind) {\n case 'moveUp':\n return (\n <svg {...common}>\n <path d=\"M4 10l4-4 4 4\" />\n </svg>\n )\n case 'moveDown':\n return (\n <svg {...common}>\n <path d=\"M4 6l4 4 4-4\" />\n </svg>\n )\n case 'duplicate':\n return (\n <svg {...common}>\n <rect x=\"6\" y=\"6\" width=\"8\" height=\"8\" rx=\"1.5\" />\n <path d=\"M10 6V4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2\" />\n </svg>\n )\n case 'delete':\n return (\n <svg {...common}>\n <path d=\"M3 4.5h10\" />\n <path d=\"M6.5 4.5V3a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1.5\" />\n <path d=\"M5 4.5l.5 8a1 1 0 0 0 1 .9h3a1 1 0 0 0 1-.9l.5-8\" />\n </svg>\n )\n }\n}\n\n/**\n * The live overlay: tracks the selected + hovered node rects (glued through\n * scroll/resize/async layout via {@link subscribeCanvasLayout} + a coalescing\n * {@link createRectTracker}) and portals the frames into the iframe `body`.\n * Suppressed for the composition root (no frame around the whole page — mirrors\n * the tree, which offers the root no actions) and while an inline text/rich-text\n * edit is live (the `[data-ec-editing]` outline owns that state).\n */\nexport function SelectionOverlay({ doc, editing }: { doc: Document; editing: boolean }) {\n const builder = useBuilder()\n const { present, selectedId, hoveredId, dispatch } = builder\n const studio = getStudioRegistry()\n const affordance = useInsertAffordance()\n const propSync = usePropSync()\n const uiLocale = useUiLocale()\n const [geometry, setGeometry] = useState<OverlayGeometry>(EMPTY_GEOMETRY)\n\n // EC-408 reverse highlight: the focused prop's marker, but only for the\n // selected node (the inspector only shows that node's props) and never while\n // inline-editing (the [data-ec-editing] outline owns the node then).\n const highlight =\n propSync?.highlighted && propSync.highlighted.nodeId === selectedId && !editing\n ? propSync.highlighted\n : null\n const highlightNodeId = highlight?.nodeId ?? null\n const highlightPropName = highlight?.propName ?? null\n\n // EC-405 scope 4: insertion affordances hide while ANY drag is live (the\n // DnD before/after/into indicators own the \"where it lands\" story then).\n const [dragging, setDragging] = useState(() => activeDrag() !== null)\n useEffect(() => {\n const offStart = onDragSessionStart(() => setDragging(true))\n const offEnd = onDragSessionEnd(() => setDragging(false))\n return () => {\n offStart()\n offEnd()\n }\n }, [])\n\n // The selected node's controls/label come from the graph; the root and a\n // never-found selection draw nothing. Hover never frames the currently\n // selected node (one frame at a time) nor the root.\n const selectedNode = selectedId && !editing ? getNode(present, selectedId) : null\n const controls = selectedNode ? frameControlsFor(present, selectedId!) : null\n const drawSelected = selectedNode !== null && controls !== null && !controls.isRoot\n const hoveredNode = hoveredId && hoveredId !== selectedId ? getNode(present, hoveredId) : null\n const drawHover = hoveredNode !== null && !frameControlsFor(present, hoveredId!).isRoot\n\n useLayoutEffect(() => {\n const win = doc.defaultView\n if (!win) return\n const selId = drawSelected ? selectedId : null\n const hovId = drawHover ? hoveredId : null\n const measure = (): OverlayGeometry => {\n const selected = measureNodeRect(doc, selId)\n const hovered = measureNodeRect(doc, hovId)\n return {\n selected,\n selectedBar: selected ? placeFrameBar(selected, { barHeight: FRAME_BAR_HEIGHT, docTop: win.scrollY }) : null,\n hovered,\n propHighlight: measurePropRect(doc, highlightNodeId, highlightPropName),\n }\n }\n const targets = (): Element[] => {\n const out: Element[] = []\n for (const id of [selId, hovId]) {\n if (!id) continue\n const el = doc.querySelector(`[data-ec-node-id=\"${id}\"]`)\n if (el) out.push(el)\n }\n // Track the highlighted marker element too, so it re-glues on async layout.\n if (highlightNodeId) {\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${highlightNodeId}\"]`)\n if (wrapper) out.push(wrapper)\n }\n return out\n }\n // Coalesce on the PARENT window's frame loop (not the iframe's): this\n // component + its state live in the parent React tree, and the iframe's rAF\n // is throttled/paused whenever the preview is offscreen or backgrounded. The\n // compound rAF+timeout scheduler (scheduleFrame) keeps the flush paint-\n // aligned on screen while guaranteeing it still runs when rAF is paused.\n const host = win.parent ?? window\n const tracker = createRectTracker<OverlayGeometry>({\n measure,\n equals: geometryEquals,\n onChange: setGeometry,\n schedule: (flush) => scheduleFrame(host, flush),\n subscribe: (invalidate) => subscribeCanvasLayout(doc, targets, invalidate),\n })\n return () => tracker.dispose()\n // Re-subscribe when the tracked ids / graph / highlighted prop change so the\n // ResizeObserver re-points at the current (possibly remounted) wrappers and\n // the measure closure reads the current highlight target.\n }, [doc, drawSelected, drawHover, selectedId, hoveredId, present, highlightNodeId, highlightPropName])\n\n // EC-407: the frame label shares the tree row's resolution — the SAME\n // nodeDisplayTitle.primary the selected row shows (instance title when\n // derivable, else the localized component name).\n const label = selectedNode\n ? nodeDisplayTitle(studio.registry.getManifest(selectedNode.componentId), selectedNode, uiLocale).primary\n : ''\n\n const selectedView: SelectionFrameView | null =\n drawSelected && geometry.selected && geometry.selectedBar && controls\n ? {\n rect: geometry.selected,\n bar: geometry.selectedBar,\n label,\n canMoveUp: controls.canMoveUp,\n canMoveDown: controls.canMoveDown,\n }\n : null\n\n const run = (kind: FrameControlKind): void => {\n if (!selectedId) return\n dispatch(frameControlAction(kind, selectedId))\n }\n\n // EC-405: the \"+\" affordances for the selected node and (separately) a hovered\n // one — computed from the SAME measured rects the frames use. A group is drawn\n // only for a node with insertable positions, and only when no drag/inline edit\n // is live; a node with no provider above it (component preview) shows none.\n const affordanceGroups: AffordanceGroupView[] =\n affordance && !dragging && !editing\n ? [\n affordanceGroup(present, selectedId, drawSelected ? geometry.selected : null, affordance),\n affordanceGroup(present, hoveredId, drawHover ? geometry.hovered : null, affordance),\n ].filter((g): g is AffordanceGroupView => g !== null)\n : []\n\n return createPortal(\n <SelectionOverlayView\n selected={selectedView}\n hovered={drawHover ? geometry.hovered : null}\n affordances={affordanceGroups}\n propHighlight={highlight ? geometry.propHighlight : null}\n onMoveUp={() => run('moveUp')}\n onMoveDown={() => run('moveDown')}\n onDuplicate={() => run('duplicate')}\n onDelete={() => run('delete')}\n />,\n doc.body,\n )\n}\n\n/**\n * Build the affordance group for one node from its measured rect: the insertable\n * positions the provider allows (slot rules already applied), each placed on the\n * node's boundary along the parent's flow axis. Returns `null` when there is no\n * rect yet or no insertable position.\n */\nfunction affordanceGroup(\n present: ComponentNode,\n nodeId: string | null,\n rect: Rect | null,\n affordance: InsertAffordanceApi,\n): AffordanceGroupView | null {\n if (!nodeId || !rect) return null\n const positions = affordance.positionsFor(nodeId)\n if (positions.length === 0) return null\n const flow = containerFlowAxis(getParentInfo(present, nodeId)?.parent)\n return {\n nodeId,\n items: positions.map((position) => ({ position, ...placeAffordance(rect, position, flow) })),\n onInsert: (position) => affordance.openAt(nodeId, position),\n }\n}\n"]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ComponentNode } from '@elytracms/core/project-graph';
|
|
2
|
+
import type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
2
3
|
import type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag';
|
|
4
|
+
import type { FrameScheduler, RectTracker } from './canvas-rect-tracking.js';
|
|
3
5
|
/**
|
|
4
6
|
* Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid
|
|
5
7
|
* container is selected on the canvas, draggable gap handles appear between
|
|
@@ -14,9 +16,94 @@ import type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag';
|
|
|
14
16
|
*/
|
|
15
17
|
/** Plain-language label for a gap step — the inspector's vocabulary. */
|
|
16
18
|
export declare function gapStepLabel(step: GapStep): string;
|
|
17
|
-
|
|
19
|
+
export interface HandleSpotView {
|
|
20
|
+
index: number;
|
|
21
|
+
/** Document-coordinate position of the handle's center. */
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
crossStart: number;
|
|
25
|
+
crossLength: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The container's rendered element (the Stack/Grid div carrying the gap style).
|
|
29
|
+
* Resilient to legitimate intermediate wrappers between the editable
|
|
30
|
+
* `[data-ec-node-id]` wrapper and the primitive (an error-boundary/provider DOM
|
|
31
|
+
* node, a future decorator layer): the FIRST `[data-ec-primitive]` inside the
|
|
32
|
+
* wrapper whose NEAREST node wrapper is THIS node — so it tolerates a wrapper
|
|
33
|
+
* above the primitive but never descends into a CHILD node's subtree and grabs
|
|
34
|
+
* that nested container's own `[data-ec-primitive]`. Mirrors `measurePropRect`'s
|
|
35
|
+
* nearest-node scoping (EC-408). Today no such wrapper exists — `decorate` wraps
|
|
36
|
+
* `<RenderErrorBoundary>` which renders its children with no DOM element, so the
|
|
37
|
+
* primitive is a direct child — but the scoping makes 2(b) unregressable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function containerElementFor(doc: Document, nodeId: string): HTMLElement | null;
|
|
40
|
+
interface Measured {
|
|
41
|
+
spots: HandleSpotView[];
|
|
42
|
+
/** The axis the children are *rendered* along (drag direction follows it). */
|
|
43
|
+
axis: FlowAxis;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Measure the handle spots between the container's rendered children — the
|
|
47
|
+
* SINGLE source of truth for whether handles show (spots) and where. Eligibility
|
|
48
|
+
* and measurement agree on the SAME (DOM) child basis by construction: a
|
|
49
|
+
* null-rendering graph child (condition-gated → no `[data-ec-node-id]` wrapper)
|
|
50
|
+
* simply isn't counted, so it can neither block handles when ≥2 children render
|
|
51
|
+
* nor phantom them when <2 do; kept fresh by the live tracker (EC-404) so a child
|
|
52
|
+
* add/remove or resize re-runs this. The graph-level flow axis
|
|
53
|
+
* (`containerFlowAxis`) is tried first; when the rendered layout disagrees — a
|
|
54
|
+
* stacking Grid below the mobile breakpoint lays its "columns" out vertically —
|
|
55
|
+
* the cross axis is measured instead, so handles always sit in the gaps the
|
|
56
|
+
* editor actually sees.
|
|
57
|
+
*/
|
|
58
|
+
export declare function measureSpots(doc: Document, nodeId: string, flowAxis: FlowAxis): Measured;
|
|
59
|
+
/** True when two measurements are equivalent (same axis, same spots). */
|
|
60
|
+
export declare function measuredEqual(a: Measured, b: Measured): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Wire a live rect tracker for the spacing handles onto the shared EC-404 core:
|
|
63
|
+
* `subscribeCanvasLayout` re-measures under child resize (image/font decode),
|
|
64
|
+
* child add/remove (async population, conditional render — `MutationObserver`
|
|
65
|
+
* `childList`), container resize, and scroll/viewport change; `scheduleFrame`
|
|
66
|
+
* coalesces on the PARENT window's frame loop with the mandatory 100 ms timeout
|
|
67
|
+
* backstop (the iframe's rAF is paused offscreen / never fires under automation).
|
|
68
|
+
* `dispose` tears down every observer + listener — no leaks on unmount. The
|
|
69
|
+
* once-only `useLayoutEffect` measure this replaces went stale the instant any of
|
|
70
|
+
* those settled after first paint (EC-406 evidence).
|
|
71
|
+
*/
|
|
72
|
+
export declare function createSpacingTracker(doc: Document, nodeId: string, flowAxis: FlowAxis, host: FrameScheduler, onChange: (measured: Measured) => void): RectTracker;
|
|
73
|
+
/**
|
|
74
|
+
* True when the node is a layout container the handles can attach to — a cheap,
|
|
75
|
+
* NECESSARY graph precondition for the mount (the authoritative "are there ≥2
|
|
76
|
+
* gaps to space" answer is the live DOM {@link measureSpots}). Because each graph
|
|
77
|
+
* child renders at most one `[data-ec-node-id]` wrapper, rendered DOM children ⊆
|
|
78
|
+
* graph children, so requiring ≥2 graph children can never wrongly hide handles
|
|
79
|
+
* that a fresh DOM measure would show.
|
|
80
|
+
*/
|
|
18
81
|
export declare function hasSpacingHandles(node: ComponentNode | null): node is ComponentNode;
|
|
82
|
+
/** Everything the portal-free handle markup needs (chrome contract surface). */
|
|
83
|
+
export interface SpacingHandlesViewProps {
|
|
84
|
+
spots: HandleSpotView[];
|
|
85
|
+
horizontal: boolean;
|
|
86
|
+
/** The live drag, for the active-handle tint and the floating label. */
|
|
87
|
+
drag: {
|
|
88
|
+
index: number;
|
|
89
|
+
labelX: number;
|
|
90
|
+
labelY: number;
|
|
91
|
+
label: string;
|
|
92
|
+
} | null;
|
|
93
|
+
onPointerDown: (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => void;
|
|
94
|
+
onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => void;
|
|
95
|
+
onPointerUp: () => void;
|
|
96
|
+
onPointerCancel: () => void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The spacing handles' rendered markup, portal-free so the AD-D chrome
|
|
100
|
+
* contract test can walk it with `renderToStaticMarkup`. In-frame chrome
|
|
101
|
+
* contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root
|
|
102
|
+
* carries the `.ec-chrome` containment recipe.
|
|
103
|
+
*/
|
|
104
|
+
export declare function SpacingHandlesView(props: SpacingHandlesViewProps): import("react").JSX.Element;
|
|
19
105
|
export declare function SpacingHandles({ doc, node }: {
|
|
20
106
|
doc: Document;
|
|
21
107
|
node: ComponentNode;
|
|
22
108
|
}): import("react").ReactPortal;
|
|
109
|
+
export {};
|
|
@@ -7,6 +7,8 @@ import { containerGapStep, gapHandleSpots, gapStepCss, gapStepFromDrag, } from '
|
|
|
7
7
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
8
8
|
import { useBuilder } from './builder-store.js';
|
|
9
9
|
import { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector.js';
|
|
10
|
+
import { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session.js';
|
|
11
|
+
import { createRectTracker, measureElementRect, scheduleFrame, subscribeCanvasLayout, } from './canvas-rect-tracking.js';
|
|
10
12
|
/**
|
|
11
13
|
* Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid
|
|
12
14
|
* container is selected on the canvas, draggable gap handles appear between
|
|
@@ -23,38 +25,118 @@ import { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector.js';
|
|
|
23
25
|
export function gapStepLabel(step) {
|
|
24
26
|
return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step]);
|
|
25
27
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The container's rendered element (the Stack/Grid div carrying the gap style).
|
|
30
|
+
* Resilient to legitimate intermediate wrappers between the editable
|
|
31
|
+
* `[data-ec-node-id]` wrapper and the primitive (an error-boundary/provider DOM
|
|
32
|
+
* node, a future decorator layer): the FIRST `[data-ec-primitive]` inside the
|
|
33
|
+
* wrapper whose NEAREST node wrapper is THIS node — so it tolerates a wrapper
|
|
34
|
+
* above the primitive but never descends into a CHILD node's subtree and grabs
|
|
35
|
+
* that nested container's own `[data-ec-primitive]`. Mirrors `measurePropRect`'s
|
|
36
|
+
* nearest-node scoping (EC-408). Today no such wrapper exists — `decorate` wraps
|
|
37
|
+
* `<RenderErrorBoundary>` which renders its children with no DOM element, so the
|
|
38
|
+
* primitive is a direct child — but the scoping makes 2(b) unregressable.
|
|
39
|
+
*/
|
|
40
|
+
export function containerElementFor(doc, nodeId) {
|
|
28
41
|
const wrapper = doc.querySelector(`[data-ec-node-id="${nodeId}"]`);
|
|
29
|
-
|
|
42
|
+
if (!wrapper)
|
|
43
|
+
return null;
|
|
44
|
+
for (const primitive of wrapper.querySelectorAll('[data-ec-primitive]')) {
|
|
45
|
+
if (primitive.closest('[data-ec-node-id]') === wrapper)
|
|
46
|
+
return primitive;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/** The container's rendered child node-wrappers — the DOM child basis. */
|
|
51
|
+
function childWrappers(container) {
|
|
52
|
+
return Array.from(container.querySelectorAll(':scope > [data-ec-node-id]'));
|
|
30
53
|
}
|
|
54
|
+
const EMPTY_MEASURED = (axis) => ({ spots: [], axis });
|
|
31
55
|
/**
|
|
32
|
-
* Measure the handle spots between the container's rendered children
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
56
|
+
* Measure the handle spots between the container's rendered children — the
|
|
57
|
+
* SINGLE source of truth for whether handles show (spots) and where. Eligibility
|
|
58
|
+
* and measurement agree on the SAME (DOM) child basis by construction: a
|
|
59
|
+
* null-rendering graph child (condition-gated → no `[data-ec-node-id]` wrapper)
|
|
60
|
+
* simply isn't counted, so it can neither block handles when ≥2 children render
|
|
61
|
+
* nor phantom them when <2 do; kept fresh by the live tracker (EC-404) so a child
|
|
62
|
+
* add/remove or resize re-runs this. The graph-level flow axis
|
|
63
|
+
* (`containerFlowAxis`) is tried first; when the rendered layout disagrees — a
|
|
64
|
+
* stacking Grid below the mobile breakpoint lays its "columns" out vertically —
|
|
65
|
+
* the cross axis is measured instead, so handles always sit in the gaps the
|
|
66
|
+
* editor actually sees.
|
|
37
67
|
*/
|
|
38
|
-
function measureSpots(doc, nodeId, flowAxis) {
|
|
68
|
+
export function measureSpots(doc, nodeId, flowAxis) {
|
|
39
69
|
const container = containerElementFor(doc, nodeId);
|
|
40
70
|
const win = doc.defaultView;
|
|
41
71
|
if (!container || !win)
|
|
42
|
-
return
|
|
43
|
-
const children =
|
|
72
|
+
return EMPTY_MEASURED(flowAxis);
|
|
73
|
+
const children = childWrappers(container);
|
|
44
74
|
if (children.length < 2)
|
|
45
|
-
return
|
|
46
|
-
const rects = children.map((el) =>
|
|
47
|
-
const r = el.getBoundingClientRect();
|
|
48
|
-
return { left: r.left + win.scrollX, top: r.top + win.scrollY, width: r.width, height: r.height };
|
|
49
|
-
});
|
|
75
|
+
return EMPTY_MEASURED(flowAxis);
|
|
76
|
+
const rects = children.map((el) => measureElementRect(el, win));
|
|
50
77
|
const along = gapHandleSpots(rects, flowAxis);
|
|
51
78
|
if (along.length > 0)
|
|
52
79
|
return { spots: along, axis: flowAxis };
|
|
53
80
|
const cross = flowAxis === 'horizontal' ? 'vertical' : 'horizontal';
|
|
54
81
|
const across = gapHandleSpots(rects, cross);
|
|
55
|
-
return across.length > 0 ? { spots: across, axis: cross } :
|
|
82
|
+
return across.length > 0 ? { spots: across, axis: cross } : EMPTY_MEASURED(flowAxis);
|
|
83
|
+
}
|
|
84
|
+
/** Structural equality of two spot lists — suppresses redundant tracker emits. */
|
|
85
|
+
function spotsEqual(a, b) {
|
|
86
|
+
if (a.length !== b.length)
|
|
87
|
+
return false;
|
|
88
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
89
|
+
const x = a[i];
|
|
90
|
+
const y = b[i];
|
|
91
|
+
if (x.index !== y.index ||
|
|
92
|
+
x.x !== y.x ||
|
|
93
|
+
x.y !== y.y ||
|
|
94
|
+
x.crossStart !== y.crossStart ||
|
|
95
|
+
x.crossLength !== y.crossLength) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/** True when two measurements are equivalent (same axis, same spots). */
|
|
102
|
+
export function measuredEqual(a, b) {
|
|
103
|
+
return a.axis === b.axis && spotsEqual(a.spots, b.spots);
|
|
104
|
+
}
|
|
105
|
+
/** The elements the tracker's `ResizeObserver` watches: the container + its children. */
|
|
106
|
+
function spacingTargets(doc, nodeId) {
|
|
107
|
+
const container = containerElementFor(doc, nodeId);
|
|
108
|
+
if (!container)
|
|
109
|
+
return [];
|
|
110
|
+
return [container, ...childWrappers(container)];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Wire a live rect tracker for the spacing handles onto the shared EC-404 core:
|
|
114
|
+
* `subscribeCanvasLayout` re-measures under child resize (image/font decode),
|
|
115
|
+
* child add/remove (async population, conditional render — `MutationObserver`
|
|
116
|
+
* `childList`), container resize, and scroll/viewport change; `scheduleFrame`
|
|
117
|
+
* coalesces on the PARENT window's frame loop with the mandatory 100 ms timeout
|
|
118
|
+
* backstop (the iframe's rAF is paused offscreen / never fires under automation).
|
|
119
|
+
* `dispose` tears down every observer + listener — no leaks on unmount. The
|
|
120
|
+
* once-only `useLayoutEffect` measure this replaces went stale the instant any of
|
|
121
|
+
* those settled after first paint (EC-406 evidence).
|
|
122
|
+
*/
|
|
123
|
+
export function createSpacingTracker(doc, nodeId, flowAxis, host, onChange) {
|
|
124
|
+
return createRectTracker({
|
|
125
|
+
measure: () => measureSpots(doc, nodeId, flowAxis),
|
|
126
|
+
equals: measuredEqual,
|
|
127
|
+
onChange,
|
|
128
|
+
schedule: (flush) => scheduleFrame(host, flush),
|
|
129
|
+
subscribe: (invalidate) => subscribeCanvasLayout(doc, () => spacingTargets(doc, nodeId), invalidate),
|
|
130
|
+
});
|
|
56
131
|
}
|
|
57
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* True when the node is a layout container the handles can attach to — a cheap,
|
|
134
|
+
* NECESSARY graph precondition for the mount (the authoritative "are there ≥2
|
|
135
|
+
* gaps to space" answer is the live DOM {@link measureSpots}). Because each graph
|
|
136
|
+
* child renders at most one `[data-ec-node-id]` wrapper, rendered DOM children ⊆
|
|
137
|
+
* graph children, so requiring ≥2 graph children can never wrongly hide handles
|
|
138
|
+
* that a fresh DOM measure would show.
|
|
139
|
+
*/
|
|
58
140
|
export function hasSpacingHandles(node) {
|
|
59
141
|
return (node !== null &&
|
|
60
142
|
isLayoutContainerNode(node) &&
|
|
@@ -63,25 +145,105 @@ export function hasSpacingHandles(node) {
|
|
|
63
145
|
const HANDLE_THICKNESS = 14;
|
|
64
146
|
const HANDLE_BAR = 4;
|
|
65
147
|
const HANDLE_MAX_LENGTH = 48;
|
|
148
|
+
/**
|
|
149
|
+
* The spacing handles' rendered markup, portal-free so the AD-D chrome
|
|
150
|
+
* contract test can walk it with `renderToStaticMarkup`. In-frame chrome
|
|
151
|
+
* contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root
|
|
152
|
+
* carries the `.ec-chrome` containment recipe.
|
|
153
|
+
*/
|
|
154
|
+
export function SpacingHandlesView(props) {
|
|
155
|
+
const { spots, horizontal, drag } = props;
|
|
156
|
+
return (_jsxs("div", { "data-ec-spacing-handles": "", className: "ec-chrome", children: [spots.map((spot) => {
|
|
157
|
+
const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH);
|
|
158
|
+
const active = drag?.index === spot.index;
|
|
159
|
+
return (_jsx("div", { "data-ec-gap-handle": spot.index, role: "separator", "aria-label": translate('builder.spacing.gapAfterChild', { index: spot.index + 1 }), "aria-orientation": horizontal ? 'vertical' : 'horizontal', onPointerDown: props.onPointerDown(spot), onPointerMove: props.onPointerMove, onPointerUp: props.onPointerUp, onPointerCancel: props.onPointerCancel, style: {
|
|
160
|
+
position: 'absolute',
|
|
161
|
+
left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),
|
|
162
|
+
top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),
|
|
163
|
+
width: horizontal ? HANDLE_THICKNESS : length,
|
|
164
|
+
height: horizontal ? length : HANDLE_THICKNESS,
|
|
165
|
+
display: 'flex',
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
justifyContent: 'center',
|
|
168
|
+
cursor: horizontal ? 'col-resize' : 'row-resize',
|
|
169
|
+
zIndex: 40,
|
|
170
|
+
touchAction: 'none',
|
|
171
|
+
}, children: _jsx("div", { style: {
|
|
172
|
+
width: horizontal ? HANDLE_BAR : '100%',
|
|
173
|
+
height: horizontal ? '100%' : HANDLE_BAR,
|
|
174
|
+
borderRadius: 999,
|
|
175
|
+
backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',
|
|
176
|
+
boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',
|
|
177
|
+
} }) }, spot.index));
|
|
178
|
+
}), drag ? (_jsx("div", { "data-ec-gap-label": "", style: {
|
|
179
|
+
position: 'absolute',
|
|
180
|
+
left: drag.labelX + 12,
|
|
181
|
+
top: drag.labelY + 12,
|
|
182
|
+
zIndex: 50,
|
|
183
|
+
pointerEvents: 'none',
|
|
184
|
+
padding: '2px 8px',
|
|
185
|
+
borderRadius: 6,
|
|
186
|
+
fontSize: 11,
|
|
187
|
+
fontWeight: 600,
|
|
188
|
+
whiteSpace: 'nowrap',
|
|
189
|
+
color: '#fff',
|
|
190
|
+
backgroundColor: '#5b8cff',
|
|
191
|
+
boxShadow: '0 2px 8px rgba(8, 12, 24, 0.4)',
|
|
192
|
+
}, children: drag.label })) : null] }));
|
|
193
|
+
}
|
|
66
194
|
export function SpacingHandles({ doc, node }) {
|
|
67
195
|
const builder = useBuilder();
|
|
68
196
|
const { dispatch } = builder;
|
|
69
197
|
const flowAxis = containerFlowAxis(node);
|
|
70
|
-
const [measured, setMeasured] = useState({ spots: [], axis: flowAxis });
|
|
198
|
+
const [measured, setMeasured] = useState(() => ({ spots: [], axis: flowAxis }));
|
|
71
199
|
const [drag, setDrag] = useState(null);
|
|
72
200
|
const dragRef = useRef(null);
|
|
201
|
+
const trackerRef = useRef(null);
|
|
73
202
|
// The container's pre-drag inline gap, restored on cancel/commit (the graph
|
|
74
203
|
// re-render owns the committed value; the preview is imperative-only).
|
|
75
204
|
const inlineGapRef = useRef('');
|
|
76
|
-
//
|
|
205
|
+
// Measurement lifetime (EC-406): ride the shared rect-tracking core instead of
|
|
206
|
+
// the once-only useLayoutEffect + lone resize listener that went stale. The
|
|
207
|
+
// tracker re-measures under child resize (image/font decode), child add/remove
|
|
208
|
+
// (async population, conditional render), container resize, and scroll/viewport
|
|
209
|
+
// change — every settle the old code missed. Re-subscribed on a graph/axis/
|
|
210
|
+
// viewport change so the ResizeObserver re-points at the current children.
|
|
77
211
|
useLayoutEffect(() => {
|
|
78
|
-
const measure = () => setMeasured(measureSpots(doc, node.id, flowAxis));
|
|
79
|
-
measure();
|
|
80
212
|
const win = doc.defaultView;
|
|
81
|
-
win
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
213
|
+
if (!win)
|
|
214
|
+
return;
|
|
215
|
+
const host = (win.parent ?? win);
|
|
216
|
+
const tracker = createSpacingTracker(doc, node.id, flowAxis, host, setMeasured);
|
|
217
|
+
trackerRef.current = tracker;
|
|
218
|
+
return () => {
|
|
219
|
+
tracker.dispose();
|
|
220
|
+
trackerRef.current = null;
|
|
221
|
+
};
|
|
222
|
+
}, [doc, node.id, flowAxis, builder.present, builder.viewport]);
|
|
223
|
+
// The gap-drag previews imperatively on `container.style.gap` (the tracker's
|
|
224
|
+
// MutationObserver is childList-only and ignores attribute writes, by design),
|
|
225
|
+
// so nudge a coalesced re-measure per step to keep every handle glued to the
|
|
226
|
+
// previewed gap as the children shift. Harmless off-drag (an unchanged measure
|
|
227
|
+
// emits nothing).
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
trackerRef.current?.invalidate();
|
|
230
|
+
}, [drag?.step]);
|
|
231
|
+
// Scope 4: yield the handles during an active CONTENT drag (node move/insert) —
|
|
232
|
+
// the DnD before/after/into indicators own the "where it lands" story then. The
|
|
233
|
+
// handles' OWN gap drag uses pointer capture, never `beginDrag`, so it does not
|
|
234
|
+
// trip this. Inline-editing suppression is handled at the canvas mount (the
|
|
235
|
+
// handles unmount while a text/rich-text session owns the node).
|
|
236
|
+
const [contentDragging, setContentDragging] = useState(() => activeDrag() !== null);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const offStart = onDragSessionStart(() => setContentDragging(true));
|
|
239
|
+
const offEnd = onDragSessionEnd(() => setContentDragging(false));
|
|
240
|
+
return () => {
|
|
241
|
+
offStart();
|
|
242
|
+
offEnd();
|
|
243
|
+
};
|
|
244
|
+
}, []);
|
|
245
|
+
const { spots: measuredSpots, axis } = measured;
|
|
246
|
+
const spots = contentDragging ? [] : measuredSpots;
|
|
85
247
|
const applyPreview = useCallback((step) => {
|
|
86
248
|
const container = containerElementFor(doc, node.id);
|
|
87
249
|
if (container)
|
|
@@ -173,42 +335,8 @@ export function SpacingHandles({ doc, node }) {
|
|
|
173
335
|
applyPreview(step);
|
|
174
336
|
};
|
|
175
337
|
const horizontal = axis === 'horizontal';
|
|
176
|
-
return createPortal(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return (_jsx("div", { "data-ec-gap-handle": spot.index, role: "separator", "aria-label": translate('builder.spacing.gapAfterChild', { index: spot.index + 1 }), "aria-orientation": horizontal ? 'vertical' : 'horizontal', onPointerDown: onPointerDown(spot), onPointerMove: onPointerMove, onPointerUp: () => finish('commit'), onPointerCancel: () => finish('cancel'), style: {
|
|
180
|
-
position: 'absolute',
|
|
181
|
-
left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),
|
|
182
|
-
top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),
|
|
183
|
-
width: horizontal ? HANDLE_THICKNESS : length,
|
|
184
|
-
height: horizontal ? length : HANDLE_THICKNESS,
|
|
185
|
-
display: 'flex',
|
|
186
|
-
alignItems: 'center',
|
|
187
|
-
justifyContent: 'center',
|
|
188
|
-
cursor: horizontal ? 'col-resize' : 'row-resize',
|
|
189
|
-
zIndex: 40,
|
|
190
|
-
touchAction: 'none',
|
|
191
|
-
}, children: _jsx("div", { style: {
|
|
192
|
-
width: horizontal ? HANDLE_BAR : '100%',
|
|
193
|
-
height: horizontal ? '100%' : HANDLE_BAR,
|
|
194
|
-
borderRadius: 999,
|
|
195
|
-
backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',
|
|
196
|
-
boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',
|
|
197
|
-
} }) }, spot.index));
|
|
198
|
-
}), drag ? (_jsx("div", { "data-ec-gap-label": "", style: {
|
|
199
|
-
position: 'absolute',
|
|
200
|
-
left: drag.labelX + 12,
|
|
201
|
-
top: drag.labelY + 12,
|
|
202
|
-
zIndex: 50,
|
|
203
|
-
pointerEvents: 'none',
|
|
204
|
-
padding: '2px 8px',
|
|
205
|
-
borderRadius: 6,
|
|
206
|
-
fontSize: 11,
|
|
207
|
-
fontWeight: 600,
|
|
208
|
-
whiteSpace: 'nowrap',
|
|
209
|
-
color: '#fff',
|
|
210
|
-
backgroundColor: '#5b8cff',
|
|
211
|
-
boxShadow: '0 2px 8px rgba(8, 12, 24, 0.4)',
|
|
212
|
-
}, children: gapStepLabel(drag.step) })) : null] }), doc.body);
|
|
338
|
+
return createPortal(_jsx(SpacingHandlesView, { spots: spots, horizontal: horizontal, drag: drag
|
|
339
|
+
? { index: drag.index, labelX: drag.labelX, labelY: drag.labelY, label: gapStepLabel(drag.step) }
|
|
340
|
+
: null, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: () => finish('commit'), onPointerCancel: () => finish('cancel') }), doc.body);
|
|
213
341
|
}
|
|
214
342
|
//# sourceMappingURL=spacing-handles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spacing-handles.js","sourceRoot":"","sources":["../../../src/components/builder/spacing-handles.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { DEFAULT_SLOT, isLayoutContainerNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport {\n containerGapStep,\n gapHandleSpots,\n gapStepCss,\n gapStepFromDrag,\n} from '@elytracms/studio/lib/dnd/gap-drag'\nimport type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useBuilder } from './builder-store'\nimport { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector'\n\n/**\n * Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid\n * container is selected on the canvas, draggable gap handles appear between\n * its children, axis-aware via the shared `containerFlowAxis`. Dragging snaps\n * through the EC-161 spacing-token scale (`lib/dnd/gap-drag` — pure math, no\n * pixel value can escape it) with a live plain-language label (the layout\n * inspector's vocabulary). The drag previews imperatively on the rendered\n * element; releasing commits exactly ONE builder dispatch — `setProp` with the\n * token, or `removeProp` for \"no gap\", the identical values the inspector's\n * gap select writes — so one undo restores, and the graph stays the single\n * source of truth. Esc mid-drag cancels: preview reverts, nothing dispatches.\n */\n\n/** Plain-language label for a gap step — the inspector's vocabulary. */\nexport function gapStepLabel(step: GapStep): string {\n return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step])\n}\n\ninterface HandleSpotView {\n index: number\n /** Document-coordinate position of the handle's center. */\n x: number\n y: number\n crossStart: number\n crossLength: number\n}\n\ninterface ActiveDrag {\n index: number\n startStep: GapStep\n step: GapStep\n /** Pointer start along the flow axis (client coords). */\n startCoord: number\n /** Live pointer position (document coords) for the floating label. */\n labelX: number\n labelY: number\n}\n\n/** The container's rendered element (the Stack/Grid div carrying the gap style). */\nfunction containerElementFor(doc: Document, nodeId: string): HTMLElement | null {\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${nodeId}\"]`)\n return (wrapper?.querySelector(':scope > [data-ec-primitive]') as HTMLElement | null) ?? null\n}\n\ninterface Measured {\n spots: HandleSpotView[]\n /** The axis the children are *rendered* along (drag direction follows it). */\n axis: FlowAxis\n}\n\n/**\n * Measure the handle spots between the container's rendered children. The\n * graph-level flow axis (`containerFlowAxis`) is tried first; when the\n * rendered layout disagrees — a stacking Grid below the mobile breakpoint\n * lays its \"columns\" out vertically — the cross axis is measured instead, so\n * handles always sit in the gaps the editor actually sees.\n */\nfunction measureSpots(doc: Document, nodeId: string, flowAxis: FlowAxis): Measured {\n const container = containerElementFor(doc, nodeId)\n const win = doc.defaultView\n if (!container || !win) return { spots: [], axis: flowAxis }\n const children = Array.from(\n container.querySelectorAll(':scope > [data-ec-node-id]'),\n ) as HTMLElement[]\n if (children.length < 2) return { spots: [], axis: flowAxis }\n const rects = children.map((el) => {\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 const along = gapHandleSpots(rects, flowAxis)\n if (along.length > 0) return { spots: along, axis: flowAxis }\n const cross: FlowAxis = flowAxis === 'horizontal' ? 'vertical' : 'horizontal'\n const across = gapHandleSpots(rects, cross)\n return across.length > 0 ? { spots: across, axis: cross } : { spots: [], axis: flowAxis }\n}\n\n/** True when the node is a container with at least two children to space. */\nexport function hasSpacingHandles(node: ComponentNode | null): node is ComponentNode {\n return (\n node !== null &&\n isLayoutContainerNode(node) &&\n (node.slots?.[DEFAULT_SLOT]?.length ?? 0) >= 2\n )\n}\n\nconst HANDLE_THICKNESS = 14\nconst HANDLE_BAR = 4\nconst HANDLE_MAX_LENGTH = 48\n\nexport function SpacingHandles({ doc, node }: { doc: Document; node: ComponentNode }) {\n const builder = useBuilder()\n const { dispatch } = builder\n const flowAxis = containerFlowAxis(node)\n const [measured, setMeasured] = useState<Measured>({ spots: [], axis: flowAxis })\n const [drag, setDrag] = useState<ActiveDrag | null>(null)\n const dragRef = useRef<ActiveDrag | null>(null)\n // The container's pre-drag inline gap, restored on cancel/commit (the graph\n // re-render owns the committed value; the preview is imperative-only).\n const inlineGapRef = useRef<string>('')\n\n // Re-measure when the graph, selection, drag preview, or iframe size change.\n useLayoutEffect(() => {\n const measure = () => setMeasured(measureSpots(doc, node.id, flowAxis))\n measure()\n const win = doc.defaultView\n win?.addEventListener('resize', measure)\n return () => win?.removeEventListener('resize', measure)\n }, [doc, node, flowAxis, builder.present, builder.viewport, drag?.step])\n const { spots, axis } = measured\n\n const applyPreview = useCallback(\n (step: GapStep) => {\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = gapStepCss(step)\n },\n [doc, node.id],\n )\n\n const finish = useCallback(\n (mode: 'commit' | 'cancel') => {\n const active = dragRef.current\n if (!active) return\n dragRef.current = null\n setDrag(null)\n // Drop the imperative preview; the rendered graph value takes over.\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = inlineGapRef.current\n if (mode === 'commit' && active.step !== active.startStep) {\n // Exactly one dispatch — one operations command, one undo step — and\n // the identical prop value the inspector's gap select produces.\n if (active.step === null) {\n dispatch({ type: 'removeProp', id: node.id, name: 'gap' })\n } else {\n dispatch({ type: 'setProp', id: node.id, name: 'gap', value: staticProp(active.step) })\n }\n }\n },\n [doc, node.id, dispatch],\n )\n\n // Esc cancels mid-drag — listen on both documents (focus may sit either side\n // of the iframe boundary while the pointer is captured).\n useEffect(() => {\n if (!drag) return\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n doc.addEventListener('keydown', onKeyDown, true)\n document.addEventListener('keydown', onKeyDown, true)\n return () => {\n doc.removeEventListener('keydown', onKeyDown, true)\n document.removeEventListener('keydown', onKeyDown, true)\n }\n }, [drag, doc, finish])\n\n // Cleanup on unmount/selection change mid-drag: revert, never dispatch.\n useEffect(() => () => finish('cancel'), [finish])\n\n const onPointerDown = (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return\n e.preventDefault()\n e.stopPropagation()\n const startStep = containerGapStep(node)\n const container = containerElementFor(doc, node.id)\n inlineGapRef.current = container?.style.gap ?? ''\n try {\n e.currentTarget.setPointerCapture(e.pointerId)\n } catch {\n // Synthetic pointer events (tests, automation) have no active pointer\n // to capture; move/up still arrive via the handle's own listeners.\n }\n const next: ActiveDrag = {\n index: spot.index,\n startStep,\n step: startStep,\n startCoord: axis === 'horizontal' ? e.clientX : e.clientY,\n labelX: spot.x,\n labelY: spot.y,\n }\n dragRef.current = next\n setDrag(next)\n }\n\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const active = dragRef.current\n if (!active) return\n const win = doc.defaultView\n const coord = axis === 'horizontal' ? e.clientX : e.clientY\n const step = gapStepFromDrag(active.startStep, coord - active.startCoord)\n const next: ActiveDrag = {\n ...active,\n step,\n labelX: e.clientX + (win?.scrollX ?? 0),\n labelY: e.clientY + (win?.scrollY ?? 0),\n }\n dragRef.current = next\n setDrag(next)\n applyPreview(step)\n }\n\n const horizontal = axis === 'horizontal'\n\n return createPortal(\n <div data-ec-spacing-handles=\"\">\n {spots.map((spot) => {\n const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH)\n const active = drag?.index === spot.index\n return (\n <div\n key={spot.index}\n data-ec-gap-handle={spot.index}\n role=\"separator\"\n aria-label={translate('builder.spacing.gapAfterChild', { index: spot.index + 1 })}\n aria-orientation={horizontal ? 'vertical' : 'horizontal'}\n onPointerDown={onPointerDown(spot)}\n onPointerMove={onPointerMove}\n onPointerUp={() => finish('commit')}\n onPointerCancel={() => finish('cancel')}\n style={{\n position: 'absolute',\n left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),\n top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),\n width: horizontal ? HANDLE_THICKNESS : length,\n height: horizontal ? length : HANDLE_THICKNESS,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: horizontal ? 'col-resize' : 'row-resize',\n zIndex: 40,\n touchAction: 'none',\n }}\n >\n <div\n style={{\n width: horizontal ? HANDLE_BAR : '100%',\n height: horizontal ? '100%' : HANDLE_BAR,\n borderRadius: 999,\n backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',\n boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',\n }}\n />\n </div>\n )\n })}\n {drag ? (\n <div\n data-ec-gap-label=\"\"\n style={{\n position: 'absolute',\n left: drag.labelX + 12,\n top: drag.labelY + 12,\n zIndex: 50,\n pointerEvents: 'none',\n padding: '2px 8px',\n borderRadius: 6,\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n color: '#fff',\n backgroundColor: '#5b8cff',\n boxShadow: '0 2px 8px rgba(8, 12, 24, 0.4)',\n }}\n >\n {gapStepLabel(drag.step)}\n </div>\n ) : null}\n </div>,\n doc.body,\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"spacing-handles.js","sourceRoot":"","sources":["../../../src/components/builder/spacing-handles.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAE/F,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAExE,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,oCAAoC,CAAA;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACjF,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,qBAAqB,GACtB,MAAM,wBAAwB,CAAA;AAG/B;;;;;;;;;;;GAWG;AAEH,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;AACxF,CAAC;AAsBD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAa,EAAE,MAAc;IAC/D,MAAM,OAAO,GAAG,GAAG,CAAC,aAAa,CAAC,qBAAqB,MAAM,IAAI,CAAC,CAAA;IAClE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IACzB,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,gBAAgB,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACxE,IAAI,SAAS,CAAC,OAAO,CAAC,mBAAmB,CAAC,KAAK,OAAO;YAAE,OAAO,SAAwB,CAAA;IACzF,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,0EAA0E;AAC1E,SAAS,aAAa,CAAC,SAAkB;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,4BAA4B,CAAC,CAAkB,CAAA;AAC9F,CAAC;AAQD,MAAM,cAAc,GAAG,CAAC,IAAc,EAAY,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AAE1E;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,GAAa,EAAE,MAAc,EAAE,QAAkB;IAC5E,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAClD,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAA;IAC3B,IAAI,CAAC,SAAS,IAAI,CAAC,GAAG;QAAE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;IACzC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAA;IACxD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,kBAAkB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAA;IAC/D,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC7C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;IAC7D,MAAM,KAAK,GAAa,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAA;IAC7E,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAC3C,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAA;AACtF,CAAC;AAED,kFAAkF;AAClF,SAAS,UAAU,CAAC,CAA4B,EAAE,CAA4B;IAC5E,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAA;QACf,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAA;QACf,IACE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;YACnB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACX,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACX,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU;YAC7B,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,EAC/B,CAAC;YACD,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,CAAW,EAAE,CAAW;IACpD,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;AAC1D,CAAC;AAED,yFAAyF;AACzF,SAAS,cAAc,CAAC,GAAa,EAAE,MAAc;IACnD,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAClD,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAA;IACzB,OAAO,CAAC,SAAS,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAA;AACjD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAa,EACb,MAAc,EACd,QAAkB,EAClB,IAAoB,EACpB,QAAsC;IAEtC,OAAO,iBAAiB,CAAW;QACjC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC;QAClD,MAAM,EAAE,aAAa;QACrB,QAAQ;QACR,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC;QAC/C,SAAS,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,UAAU,CAAC;KACrG,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAA0B;IAC1D,OAAO,CACL,IAAI,KAAK,IAAI;QACb,qBAAqB,CAAC,IAAI,CAAC;QAC3B,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAC/C,CAAA;AACH,CAAC;AAED,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAC3B,MAAM,UAAU,GAAG,CAAC,CAAA;AACpB,MAAM,iBAAiB,GAAG,EAAE,CAAA;AAc5B;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAA8B;IAC/D,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;IACzC,OAAO,CACL,0CAA6B,EAAE,EAAC,SAAS,EAAC,WAAW,aAClD,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gBAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAA;gBAC5D,MAAM,MAAM,GAAG,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC,KAAK,CAAA;gBACzC,OAAO,CACL,oCAEsB,IAAI,CAAC,KAAK,EAC9B,IAAI,EAAC,WAAW,gBACJ,SAAS,CAAC,+BAA+B,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,sBAC/D,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,EACxD,aAAa,EAAE,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,EACxC,aAAa,EAAE,KAAK,CAAC,aAAa,EAClC,WAAW,EAAE,KAAK,CAAC,WAAW,EAC9B,eAAe,EAAE,KAAK,CAAC,eAAe,EACtC,KAAK,EAAE;wBACL,QAAQ,EAAE,UAAU;wBACpB,IAAI,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;wBAC/D,GAAG,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,GAAG,CAAC,CAAC;wBAC9D,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM;wBAC7C,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB;wBAC9C,OAAO,EAAE,MAAM;wBACf,UAAU,EAAE,QAAQ;wBACpB,cAAc,EAAE,QAAQ;wBACxB,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,YAAY;wBAChD,MAAM,EAAE,EAAE;wBACV,WAAW,EAAE,MAAM;qBACpB,YAED,cACE,KAAK,EAAE;4BACL,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM;4BACvC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU;4BACxC,YAAY,EAAE,GAAG;4BACjB,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,0BAA0B;4BAChE,SAAS,EAAE,iCAAiC;yBAC7C,GACD,IA/BG,IAAI,CAAC,KAAK,CAgCX,CACP,CAAA;YACH,CAAC,CAAC,EACD,IAAI,CAAC,CAAC,CAAC,CACN,mCACoB,EAAE,EACpB,KAAK,EAAE;oBACL,QAAQ,EAAE,UAAU;oBACpB,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE;oBACtB,GAAG,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE;oBACrB,MAAM,EAAE,EAAE;oBACV,aAAa,EAAE,MAAM;oBACrB,OAAO,EAAE,SAAS;oBAClB,YAAY,EAAE,CAAC;oBACf,QAAQ,EAAE,EAAE;oBACZ,UAAU,EAAE,GAAG;oBACf,UAAU,EAAE,QAAQ;oBACpB,KAAK,EAAE,MAAM;oBACb,eAAe,EAAE,SAAS;oBAC1B,SAAS,EAAE,gCAAgC;iBAC5C,YAEA,IAAI,CAAC,KAAK,GACP,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAE,GAAG,EAAE,IAAI,EAA0C;IAClF,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAC5B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAW,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;IACzF,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAoB,IAAI,CAAC,CAAA;IACzD,MAAM,OAAO,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAC/C,MAAM,UAAU,GAAG,MAAM,CAAqB,IAAI,CAAC,CAAA;IACnD,4EAA4E;IAC5E,uEAAuE;IACvE,MAAM,YAAY,GAAG,MAAM,CAAS,EAAE,CAAC,CAAA;IAEvC,+EAA+E;IAC/E,4EAA4E;IAC5E,+EAA+E;IAC/E,gFAAgF;IAChF,4EAA4E;IAC5E,2EAA2E;IAC3E,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAA;QAC3B,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAA8B,CAAA;QAC7D,MAAM,OAAO,GAAG,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;QAC/E,UAAU,CAAC,OAAO,GAAG,OAAO,CAAA;QAC5B,OAAO,GAAG,EAAE;YACV,OAAO,CAAC,OAAO,EAAE,CAAA;YACjB,UAAU,CAAC,OAAO,GAAG,IAAI,CAAA;QAC3B,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAE/D,6EAA6E;IAC7E,+EAA+E;IAC/E,6EAA6E;IAC7E,+EAA+E;IAC/E,kBAAkB;IAClB,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,CAAA;IAClC,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;IAEhB,gFAAgF;IAChF,gFAAgF;IAChF,gFAAgF;IAChF,4EAA4E;IAC5E,iEAAiE;IACjE,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,CAAA;IACnF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAA;QACnE,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAA;QAChE,OAAO,GAAG,EAAE;YACV,QAAQ,EAAE,CAAA;YACV,MAAM,EAAE,CAAA;QACV,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAA;IAC/C,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAA;IAElD,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,IAAa,EAAE,EAAE;QAChB,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;QACnD,IAAI,SAAS;YAAE,SAAS,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IACvD,CAAC,EACD,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CACf,CAAA;IAED,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,IAAyB,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAA;QAC9B,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;QACtB,OAAO,CAAC,IAAI,CAAC,CAAA;QACb,oEAAoE;QACpE,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;QACnD,IAAI,SAAS;YAAE,SAAS,CAAC,KAAK,CAAC,GAAG,GAAG,YAAY,CAAC,OAAO,CAAA;QACzD,IAAI,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;YAC1D,qEAAqE;YACrE,gEAAgE;YAChE,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;gBACzB,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YAC5D,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YACzF,CAAC;QACH,CAAC;IACH,CAAC,EACD,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CACzB,CAAA;IAED,6EAA6E;IAC7E,yDAAyD;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,MAAM,SAAS,GAAG,CAAC,CAAgB,EAAE,EAAE;YACrC,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBACvB,CAAC,CAAC,cAAc,EAAE,CAAA;gBAClB,CAAC,CAAC,eAAe,EAAE,CAAA;gBACnB,MAAM,CAAC,QAAQ,CAAC,CAAA;YAClB,CAAC;QACH,CAAC,CAAA;QACD,GAAG,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;QAChD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;QACrD,OAAO,GAAG,EAAE;YACV,GAAG,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;YACnD,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;QAC1D,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;IAEvB,wEAAwE;IACxE,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IAEjD,MAAM,aAAa,GAAG,CAAC,IAAoB,EAAE,EAAE,CAAC,CAAC,CAAqC,EAAE,EAAE;QACxF,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC1B,CAAC,CAAC,cAAc,EAAE,CAAA;QAClB,CAAC,CAAC,eAAe,EAAE,CAAA;QACnB,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;QACxC,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;QACnD,YAAY,CAAC,OAAO,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,IAAI,EAAE,CAAA;QACjD,IAAI,CAAC;YACH,CAAC,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,mEAAmE;QACrE,CAAC;QACD,MAAM,IAAI,GAAe;YACvB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS;YACT,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;YACzD,MAAM,EAAE,IAAI,CAAC,CAAC;YACd,MAAM,EAAE,IAAI,CAAC,CAAC;SACf,CAAA;QACD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;QACtB,OAAO,CAAC,IAAI,CAAC,CAAA;IACf,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CAAC,CAAqC,EAAE,EAAE;QAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAA;QAC9B,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAA;QAC3B,MAAM,KAAK,GAAG,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QAC3D,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;QACzE,MAAM,IAAI,GAAe;YACvB,GAAG,MAAM;YACT,IAAI;YACJ,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,OAAO,IAAI,CAAC,CAAC;YACvC,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,OAAO,IAAI,CAAC,CAAC;SACxC,CAAA;QACD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;QACtB,OAAO,CAAC,IAAI,CAAC,CAAA;QACb,YAAY,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,IAAI,KAAK,YAAY,CAAA;IAExC,OAAO,YAAY,CACjB,KAAC,kBAAkB,IACjB,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,UAAU,EACtB,IAAI,EACF,IAAI;YACF,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YACjG,CAAC,CAAC,IAAI,EAEV,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,aAAa,EAC5B,WAAW,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EACnC,eAAe,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,GACvC,EACF,GAAG,CAAC,IAAI,CACT,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { DEFAULT_SLOT, isLayoutContainerNode, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport type { FlowAxis } from '@elytracms/studio/lib/dnd/drop-rules'\nimport {\n containerGapStep,\n gapHandleSpots,\n gapStepCss,\n gapStepFromDrag,\n} from '@elytracms/studio/lib/dnd/gap-drag'\nimport type { GapStep } from '@elytracms/studio/lib/dnd/gap-drag'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useBuilder } from './builder-store'\nimport { GAP_LABEL_KEYS, GAP_NONE_LABEL_KEY } from './layout-inspector'\nimport { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session'\nimport {\n createRectTracker,\n measureElementRect,\n scheduleFrame,\n subscribeCanvasLayout,\n} from './canvas-rect-tracking'\nimport type { FrameScheduler, RectTracker } from './canvas-rect-tracking'\n\n/**\n * Tokenized spacing handles (EC-163, vision §3.1 / AD-10): when a Stack/Grid\n * container is selected on the canvas, draggable gap handles appear between\n * its children, axis-aware via the shared `containerFlowAxis`. Dragging snaps\n * through the EC-161 spacing-token scale (`lib/dnd/gap-drag` — pure math, no\n * pixel value can escape it) with a live plain-language label (the layout\n * inspector's vocabulary). The drag previews imperatively on the rendered\n * element; releasing commits exactly ONE builder dispatch — `setProp` with the\n * token, or `removeProp` for \"no gap\", the identical values the inspector's\n * gap select writes — so one undo restores, and the graph stays the single\n * source of truth. Esc mid-drag cancels: preview reverts, nothing dispatches.\n */\n\n/** Plain-language label for a gap step — the inspector's vocabulary. */\nexport function gapStepLabel(step: GapStep): string {\n return step === null ? translate(GAP_NONE_LABEL_KEY) : translate(GAP_LABEL_KEYS[step])\n}\n\nexport interface HandleSpotView {\n index: number\n /** Document-coordinate position of the handle's center. */\n x: number\n y: number\n crossStart: number\n crossLength: number\n}\n\ninterface ActiveDrag {\n index: number\n startStep: GapStep\n step: GapStep\n /** Pointer start along the flow axis (client coords). */\n startCoord: number\n /** Live pointer position (document coords) for the floating label. */\n labelX: number\n labelY: number\n}\n\n/**\n * The container's rendered element (the Stack/Grid div carrying the gap style).\n * Resilient to legitimate intermediate wrappers between the editable\n * `[data-ec-node-id]` wrapper and the primitive (an error-boundary/provider DOM\n * node, a future decorator layer): the FIRST `[data-ec-primitive]` inside the\n * wrapper whose NEAREST node wrapper is THIS node — so it tolerates a wrapper\n * above the primitive but never descends into a CHILD node's subtree and grabs\n * that nested container's own `[data-ec-primitive]`. Mirrors `measurePropRect`'s\n * nearest-node scoping (EC-408). Today no such wrapper exists — `decorate` wraps\n * `<RenderErrorBoundary>` which renders its children with no DOM element, so the\n * primitive is a direct child — but the scoping makes 2(b) unregressable.\n */\nexport function containerElementFor(doc: Document, nodeId: string): HTMLElement | null {\n const wrapper = doc.querySelector(`[data-ec-node-id=\"${nodeId}\"]`)\n if (!wrapper) return null\n for (const primitive of wrapper.querySelectorAll('[data-ec-primitive]')) {\n if (primitive.closest('[data-ec-node-id]') === wrapper) return primitive as HTMLElement\n }\n return null\n}\n\n/** The container's rendered child node-wrappers — the DOM child basis. */\nfunction childWrappers(container: Element): HTMLElement[] {\n return Array.from(container.querySelectorAll(':scope > [data-ec-node-id]')) as HTMLElement[]\n}\n\ninterface Measured {\n spots: HandleSpotView[]\n /** The axis the children are *rendered* along (drag direction follows it). */\n axis: FlowAxis\n}\n\nconst EMPTY_MEASURED = (axis: FlowAxis): Measured => ({ spots: [], axis })\n\n/**\n * Measure the handle spots between the container's rendered children — the\n * SINGLE source of truth for whether handles show (spots) and where. Eligibility\n * and measurement agree on the SAME (DOM) child basis by construction: a\n * null-rendering graph child (condition-gated → no `[data-ec-node-id]` wrapper)\n * simply isn't counted, so it can neither block handles when ≥2 children render\n * nor phantom them when <2 do; kept fresh by the live tracker (EC-404) so a child\n * add/remove or resize re-runs this. The graph-level flow axis\n * (`containerFlowAxis`) is tried first; when the rendered layout disagrees — a\n * stacking Grid below the mobile breakpoint lays its \"columns\" out vertically —\n * the cross axis is measured instead, so handles always sit in the gaps the\n * editor actually sees.\n */\nexport function measureSpots(doc: Document, nodeId: string, flowAxis: FlowAxis): Measured {\n const container = containerElementFor(doc, nodeId)\n const win = doc.defaultView\n if (!container || !win) return EMPTY_MEASURED(flowAxis)\n const children = childWrappers(container)\n if (children.length < 2) return EMPTY_MEASURED(flowAxis)\n const rects = children.map((el) => measureElementRect(el, win))\n const along = gapHandleSpots(rects, flowAxis)\n if (along.length > 0) return { spots: along, axis: flowAxis }\n const cross: FlowAxis = flowAxis === 'horizontal' ? 'vertical' : 'horizontal'\n const across = gapHandleSpots(rects, cross)\n return across.length > 0 ? { spots: across, axis: cross } : EMPTY_MEASURED(flowAxis)\n}\n\n/** Structural equality of two spot lists — suppresses redundant tracker emits. */\nfunction spotsEqual(a: readonly HandleSpotView[], b: readonly HandleSpotView[]): boolean {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n const x = a[i]!\n const y = b[i]!\n if (\n x.index !== y.index ||\n x.x !== y.x ||\n x.y !== y.y ||\n x.crossStart !== y.crossStart ||\n x.crossLength !== y.crossLength\n ) {\n return false\n }\n }\n return true\n}\n\n/** True when two measurements are equivalent (same axis, same spots). */\nexport function measuredEqual(a: Measured, b: Measured): boolean {\n return a.axis === b.axis && spotsEqual(a.spots, b.spots)\n}\n\n/** The elements the tracker's `ResizeObserver` watches: the container + its children. */\nfunction spacingTargets(doc: Document, nodeId: string): Element[] {\n const container = containerElementFor(doc, nodeId)\n if (!container) return []\n return [container, ...childWrappers(container)]\n}\n\n/**\n * Wire a live rect tracker for the spacing handles onto the shared EC-404 core:\n * `subscribeCanvasLayout` re-measures under child resize (image/font decode),\n * child add/remove (async population, conditional render — `MutationObserver`\n * `childList`), container resize, and scroll/viewport change; `scheduleFrame`\n * coalesces on the PARENT window's frame loop with the mandatory 100 ms timeout\n * backstop (the iframe's rAF is paused offscreen / never fires under automation).\n * `dispose` tears down every observer + listener — no leaks on unmount. The\n * once-only `useLayoutEffect` measure this replaces went stale the instant any of\n * those settled after first paint (EC-406 evidence).\n */\nexport function createSpacingTracker(\n doc: Document,\n nodeId: string,\n flowAxis: FlowAxis,\n host: FrameScheduler,\n onChange: (measured: Measured) => void,\n): RectTracker {\n return createRectTracker<Measured>({\n measure: () => measureSpots(doc, nodeId, flowAxis),\n equals: measuredEqual,\n onChange,\n schedule: (flush) => scheduleFrame(host, flush),\n subscribe: (invalidate) => subscribeCanvasLayout(doc, () => spacingTargets(doc, nodeId), invalidate),\n })\n}\n\n/**\n * True when the node is a layout container the handles can attach to — a cheap,\n * NECESSARY graph precondition for the mount (the authoritative \"are there ≥2\n * gaps to space\" answer is the live DOM {@link measureSpots}). Because each graph\n * child renders at most one `[data-ec-node-id]` wrapper, rendered DOM children ⊆\n * graph children, so requiring ≥2 graph children can never wrongly hide handles\n * that a fresh DOM measure would show.\n */\nexport function hasSpacingHandles(node: ComponentNode | null): node is ComponentNode {\n return (\n node !== null &&\n isLayoutContainerNode(node) &&\n (node.slots?.[DEFAULT_SLOT]?.length ?? 0) >= 2\n )\n}\n\nconst HANDLE_THICKNESS = 14\nconst HANDLE_BAR = 4\nconst HANDLE_MAX_LENGTH = 48\n\n/** Everything the portal-free handle markup needs (chrome contract surface). */\nexport interface SpacingHandlesViewProps {\n spots: HandleSpotView[]\n horizontal: boolean\n /** The live drag, for the active-handle tint and the floating label. */\n drag: { index: number; labelX: number; labelY: number; label: string } | null\n onPointerDown: (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => void\n onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => void\n onPointerUp: () => void\n onPointerCancel: () => void\n}\n\n/**\n * The spacing handles' rendered markup, portal-free so the AD-D chrome\n * contract test can walk it with `renderToStaticMarkup`. In-frame chrome\n * contract (EC-414 AD-B): `ec-*`/`data-ec-*` plus inline styles only; the root\n * carries the `.ec-chrome` containment recipe.\n */\nexport function SpacingHandlesView(props: SpacingHandlesViewProps) {\n const { spots, horizontal, drag } = props\n return (\n <div data-ec-spacing-handles=\"\" className=\"ec-chrome\">\n {spots.map((spot) => {\n const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH)\n const active = drag?.index === spot.index\n return (\n <div\n key={spot.index}\n data-ec-gap-handle={spot.index}\n role=\"separator\"\n aria-label={translate('builder.spacing.gapAfterChild', { index: spot.index + 1 })}\n aria-orientation={horizontal ? 'vertical' : 'horizontal'}\n onPointerDown={props.onPointerDown(spot)}\n onPointerMove={props.onPointerMove}\n onPointerUp={props.onPointerUp}\n onPointerCancel={props.onPointerCancel}\n style={{\n position: 'absolute',\n left: spot.x - (horizontal ? HANDLE_THICKNESS / 2 : length / 2),\n top: spot.y - (horizontal ? length / 2 : HANDLE_THICKNESS / 2),\n width: horizontal ? HANDLE_THICKNESS : length,\n height: horizontal ? length : HANDLE_THICKNESS,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: horizontal ? 'col-resize' : 'row-resize',\n zIndex: 40,\n touchAction: 'none',\n }}\n >\n <div\n style={{\n width: horizontal ? HANDLE_BAR : '100%',\n height: horizontal ? '100%' : HANDLE_BAR,\n borderRadius: 999,\n backgroundColor: active ? '#5b8cff' : 'rgba(91, 140, 255, 0.65)',\n boxShadow: '0 0 0 1px rgba(8, 12, 24, 0.55)',\n }}\n />\n </div>\n )\n })}\n {drag ? (\n <div\n data-ec-gap-label=\"\"\n style={{\n position: 'absolute',\n left: drag.labelX + 12,\n top: drag.labelY + 12,\n zIndex: 50,\n pointerEvents: 'none',\n padding: '2px 8px',\n borderRadius: 6,\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n color: '#fff',\n backgroundColor: '#5b8cff',\n boxShadow: '0 2px 8px rgba(8, 12, 24, 0.4)',\n }}\n >\n {drag.label}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport function SpacingHandles({ doc, node }: { doc: Document; node: ComponentNode }) {\n const builder = useBuilder()\n const { dispatch } = builder\n const flowAxis = containerFlowAxis(node)\n const [measured, setMeasured] = useState<Measured>(() => ({ spots: [], axis: flowAxis }))\n const [drag, setDrag] = useState<ActiveDrag | null>(null)\n const dragRef = useRef<ActiveDrag | null>(null)\n const trackerRef = useRef<RectTracker | null>(null)\n // The container's pre-drag inline gap, restored on cancel/commit (the graph\n // re-render owns the committed value; the preview is imperative-only).\n const inlineGapRef = useRef<string>('')\n\n // Measurement lifetime (EC-406): ride the shared rect-tracking core instead of\n // the once-only useLayoutEffect + lone resize listener that went stale. The\n // tracker re-measures under child resize (image/font decode), child add/remove\n // (async population, conditional render), container resize, and scroll/viewport\n // change — every settle the old code missed. Re-subscribed on a graph/axis/\n // viewport change so the ResizeObserver re-points at the current children.\n useLayoutEffect(() => {\n const win = doc.defaultView\n if (!win) return\n const host = (win.parent ?? win) as unknown as FrameScheduler\n const tracker = createSpacingTracker(doc, node.id, flowAxis, host, setMeasured)\n trackerRef.current = tracker\n return () => {\n tracker.dispose()\n trackerRef.current = null\n }\n }, [doc, node.id, flowAxis, builder.present, builder.viewport])\n\n // The gap-drag previews imperatively on `container.style.gap` (the tracker's\n // MutationObserver is childList-only and ignores attribute writes, by design),\n // so nudge a coalesced re-measure per step to keep every handle glued to the\n // previewed gap as the children shift. Harmless off-drag (an unchanged measure\n // emits nothing).\n useEffect(() => {\n trackerRef.current?.invalidate()\n }, [drag?.step])\n\n // Scope 4: yield the handles during an active CONTENT drag (node move/insert) —\n // the DnD before/after/into indicators own the \"where it lands\" story then. The\n // handles' OWN gap drag uses pointer capture, never `beginDrag`, so it does not\n // trip this. Inline-editing suppression is handled at the canvas mount (the\n // handles unmount while a text/rich-text session owns the node).\n const [contentDragging, setContentDragging] = useState(() => activeDrag() !== null)\n useEffect(() => {\n const offStart = onDragSessionStart(() => setContentDragging(true))\n const offEnd = onDragSessionEnd(() => setContentDragging(false))\n return () => {\n offStart()\n offEnd()\n }\n }, [])\n\n const { spots: measuredSpots, axis } = measured\n const spots = contentDragging ? [] : measuredSpots\n\n const applyPreview = useCallback(\n (step: GapStep) => {\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = gapStepCss(step)\n },\n [doc, node.id],\n )\n\n const finish = useCallback(\n (mode: 'commit' | 'cancel') => {\n const active = dragRef.current\n if (!active) return\n dragRef.current = null\n setDrag(null)\n // Drop the imperative preview; the rendered graph value takes over.\n const container = containerElementFor(doc, node.id)\n if (container) container.style.gap = inlineGapRef.current\n if (mode === 'commit' && active.step !== active.startStep) {\n // Exactly one dispatch — one operations command, one undo step — and\n // the identical prop value the inspector's gap select produces.\n if (active.step === null) {\n dispatch({ type: 'removeProp', id: node.id, name: 'gap' })\n } else {\n dispatch({ type: 'setProp', id: node.id, name: 'gap', value: staticProp(active.step) })\n }\n }\n },\n [doc, node.id, dispatch],\n )\n\n // Esc cancels mid-drag — listen on both documents (focus may sit either side\n // of the iframe boundary while the pointer is captured).\n useEffect(() => {\n if (!drag) return\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n finish('cancel')\n }\n }\n doc.addEventListener('keydown', onKeyDown, true)\n document.addEventListener('keydown', onKeyDown, true)\n return () => {\n doc.removeEventListener('keydown', onKeyDown, true)\n document.removeEventListener('keydown', onKeyDown, true)\n }\n }, [drag, doc, finish])\n\n // Cleanup on unmount/selection change mid-drag: revert, never dispatch.\n useEffect(() => () => finish('cancel'), [finish])\n\n const onPointerDown = (spot: HandleSpotView) => (e: React.PointerEvent<HTMLDivElement>) => {\n if (e.button !== 0) return\n e.preventDefault()\n e.stopPropagation()\n const startStep = containerGapStep(node)\n const container = containerElementFor(doc, node.id)\n inlineGapRef.current = container?.style.gap ?? ''\n try {\n e.currentTarget.setPointerCapture(e.pointerId)\n } catch {\n // Synthetic pointer events (tests, automation) have no active pointer\n // to capture; move/up still arrive via the handle's own listeners.\n }\n const next: ActiveDrag = {\n index: spot.index,\n startStep,\n step: startStep,\n startCoord: axis === 'horizontal' ? e.clientX : e.clientY,\n labelX: spot.x,\n labelY: spot.y,\n }\n dragRef.current = next\n setDrag(next)\n }\n\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const active = dragRef.current\n if (!active) return\n const win = doc.defaultView\n const coord = axis === 'horizontal' ? e.clientX : e.clientY\n const step = gapStepFromDrag(active.startStep, coord - active.startCoord)\n const next: ActiveDrag = {\n ...active,\n step,\n labelX: e.clientX + (win?.scrollX ?? 0),\n labelY: e.clientY + (win?.scrollY ?? 0),\n }\n dragRef.current = next\n setDrag(next)\n applyPreview(step)\n }\n\n const horizontal = axis === 'horizontal'\n\n return createPortal(\n <SpacingHandlesView\n spots={spots}\n horizontal={horizontal}\n drag={\n drag\n ? { index: drag.index, labelX: drag.labelX, labelY: drag.labelY, label: gapStepLabel(drag.step) }\n : null\n }\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={() => finish('commit')}\n onPointerCancel={() => finish('cancel')}\n />,\n doc.body,\n )\n}\n"]}
|