@elytracms/studio 0.0.67 → 0.0.69
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/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/canvas-chrome-css.js +6 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/property-inspector.d.ts +0 -21
- package/dist/components/builder/property-inspector.js +5 -22
- package/dist/components/builder/property-inspector.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 +26 -1
- package/dist/components/builder/spacing-handles.js +66 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +52 -0
- package/dist/components/cms/asset-picker-core.js +69 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -0
- package/dist/components/cms/asset-picker.d.ts +16 -4
- package/dist/components/cms/asset-picker.js +34 -50
- package/dist/components/cms/asset-picker.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/detail-editor.js +45 -54
- package/dist/components/cms/detail-editor.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/prop-visibility.d.ts +3 -0
- package/dist/components/cms/prop-visibility.js +31 -0
- package/dist/components/cms/prop-visibility.js.map +1 -0
- 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/component-embed.d.ts +8 -6
- package/dist/components/cms/rich-text/component-embed.js +21 -62
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +33 -4
- package/dist/components/cms/rich-text/editor.js +52 -22
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +60 -27
- package/dist/components/cms/rich-text/embed-props.js +102 -45
- package/dist/components/cms/rich-text/embed-props.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/users/users-collection-view-source.js +1 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- 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 +3 -2
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +1 -1
- package/dist/lib/i18n/messages.en.js +4 -2
- 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/ui/expandable-surface.d.ts +35 -25
- package/dist/ui/expandable-surface.js +41 -21
- package/dist/ui/expandable-surface.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/surface-geometry.d.ts +71 -0
- package/dist/ui/surface-geometry.js +64 -0
- package/dist/ui/surface-geometry.js.map +1 -0
- package/package.json +2 -2
- package/src/styles/studio.css +30 -0
- package/dist/components/cms/rich-text/embed-inspector-context.d.ts +0 -50
- package/dist/components/cms/rich-text/embed-inspector-context.js +0 -12
- package/dist/components/cms/rich-text/embed-inspector-context.js.map +0 -1
- package/dist/components/cms/rich-text/embed-inspector.d.ts +0 -25
- package/dist/components/cms/rich-text/embed-inspector.js +0 -48
- package/dist/components/cms/rich-text/embed-inspector.js.map +0 -1
- package/dist/components/cms/use-embed-inspector-host.d.ts +0 -12
- package/dist/components/cms/use-embed-inspector-host.js +0 -17
- package/dist/components/cms/use-embed-inspector-host.js.map +0 -1
|
@@ -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'\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"]}
|
|
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 { GapRect, 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 /** Leading edge of the gap region along the flow axis (EC-435 hatch). */\n gapStart: number\n /** Rendered extent of the gap along the flow axis (0 for a closed gap). */\n gapLength: number\n}\n\n/**\n * The hatched gap REGION for a spot (EC-435 AD-2): the rectangle between child\n * N's trailing edge and child N+1's leading edge along the flow axis, spanning\n * the pair's shared cross extent — the same measured child-rect basis the\n * handle spots come from (`gapHandleSpots`), in the same document coordinates.\n * Pure over the spot; a closed gap yields a zero-extent rect (nothing to paint).\n */\nexport function gapRegionRect(spot: HandleSpotView, axis: FlowAxis): GapRect {\n return axis === 'horizontal'\n ? { left: spot.gapStart, top: spot.crossStart, width: spot.gapLength, height: spot.crossLength }\n : { left: spot.crossStart, top: spot.gapStart, width: spot.crossLength, height: spot.gapLength }\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 x.gapStart !== y.gapStart ||\n x.gapLength !== y.gapLength\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 /**\n * Token label for the container's current gap (EC-435 AD-2) — the SAME\n * plain-language vocabulary the layout inspector and the drag label resolve\n * (`gapStepLabel`), stamped on every hatched gap region as a static size tag.\n */\n gapLabel: string\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 *\n * EC-435 AD-2: every open gap region renders a pointer-transparent hatched\n * rectangle (diagonal accent stripes, Figma-grid style) under the handles,\n * each with a small static token-label size tag beside its handle. During a\n * gap drag the hatches keep tracking the previewed gap while the static tags\n * yield to the floating drag label (one live readout, no double vocabulary);\n * the gap handles remain the only interactive element.\n */\nexport function SpacingHandlesView(props: SpacingHandlesViewProps) {\n const { spots, horizontal, gapLabel, drag } = props\n const axis: FlowAxis = horizontal ? 'horizontal' : 'vertical'\n return (\n <div data-ec-spacing-handles=\"\" className=\"ec-chrome\">\n {spots.map((spot) => {\n const region = gapRegionRect(spot, axis)\n if (spot.gapLength < 1) return null\n return (\n <div\n key={spot.index}\n data-ec-gap-hatch={spot.index}\n style={{\n position: 'absolute',\n left: region.left,\n top: region.top,\n width: region.width,\n height: region.height,\n zIndex: 10,\n pointerEvents: 'none',\n background:\n 'repeating-linear-gradient(45deg, rgba(91,140,255,0.28) 0px, rgba(91,140,255,0.28) 3px, rgba(91,140,255,0.06) 3px, rgba(91,140,255,0.06) 9px)',\n boxShadow: 'inset 0 0 0 1px rgba(91,140,255,0.3)',\n }}\n />\n )\n })}\n {drag === null\n ? spots.map((spot) => {\n if (spot.gapLength < 1) return null\n const length = Math.min(spot.crossLength, HANDLE_MAX_LENGTH)\n return (\n <div\n key={spot.index}\n data-ec-gap-hatch-label={spot.index}\n style={{\n position: 'absolute',\n left: horizontal ? spot.x : spot.x + length / 2 + 6,\n top: horizontal ? spot.y + length / 2 + 6 : spot.y,\n transform: horizontal ? 'translateX(-50%)' : 'translateY(-50%)',\n zIndex: 45,\n pointerEvents: 'none',\n padding: '1px 6px',\n borderRadius: 5,\n fontSize: 10,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n color: '#fff',\n backgroundColor: 'rgba(91,140,255,0.92)',\n boxShadow: '0 1px 4px rgba(8,12,24,0.3)',\n }}\n >\n {gapLabel} · {Math.round(spot.gapLength)}px\n </div>\n )\n })\n : null}\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 // The static size tags share ONE vocabulary with the inspector's gap\n // select and the floating drag label: the token label of the node's\n // current gap step (EC-435 AD-2).\n gapLabel={gapStepLabel(containerGapStep(node))}\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"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AssetRecord } from '@elytracms/core/persistence';
|
|
2
|
+
/**
|
|
3
|
+
* Pure selection/filter core for the asset picker (EC-432). DOM-free and
|
|
4
|
+
* side-effect-free so the media browser's "what shows" and "what a pick/upload
|
|
5
|
+
* commits" are unit-testable without a browser — the picker component is just the
|
|
6
|
+
* stage shell around these.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The stage width preset the media browser mounts with (EC-432 / EC-431 AD-A):
|
|
10
|
+
* the roomy `grid` measure (`min(1240px, 94vw)`) — browsing assets is spatial.
|
|
11
|
+
* Exported so the geometry contract is pinnable without rendering the portal
|
|
12
|
+
* (which is `null` in the node test env).
|
|
13
|
+
*/
|
|
14
|
+
export declare const ASSET_PICKER_STAGE_PRESET: "grid";
|
|
15
|
+
/**
|
|
16
|
+
* Split a file-input `accept` string ("image/*", "image/png,image/jpeg",
|
|
17
|
+
* ".pdf") into the token list the core matcher understands. The picker receives
|
|
18
|
+
* `accept` as the file-input attribute string (from `assetAcceptAttr(field.accept)`,
|
|
19
|
+
* a literal like `"image/*"`, or `undefined`), not a field's structured list —
|
|
20
|
+
* so we re-split it here. Empty/undefined ⇒ no tokens (no restriction).
|
|
21
|
+
*/
|
|
22
|
+
export declare function pickerAcceptTokens(accept: string | undefined): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Does an asset satisfy the picker's `accept`? Reuses the core
|
|
25
|
+
* {@link assetMatchesAccept} matcher over the split tokens — an empty/absent
|
|
26
|
+
* `accept` accepts everything, so a picker opened WITHOUT an accept (the editable
|
|
27
|
+
* cell) still browses the whole library, unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export declare function matchesPickerAccept(asset: Pick<AssetRecord, 'contentType' | 'filename'>, accept: string | undefined): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The assets shown in the picker grid: the shipped search filter (EC-152) then
|
|
32
|
+
* the `accept` filter (EC-432) — an image field's picker browses only images,
|
|
33
|
+
* never the project's PDFs. Order follows the library.
|
|
34
|
+
*/
|
|
35
|
+
export declare function visiblePickerAssets(assets: readonly AssetRecord[], query: string, accept: string | undefined): AssetRecord[];
|
|
36
|
+
/**
|
|
37
|
+
* The id an upload just added to the library, if any (upload-then-select, EC-301):
|
|
38
|
+
* the `upload()` API is fire-and-forget, so the picker diffs the library against
|
|
39
|
+
* the pre-upload id set to learn the new asset. `null` when nothing new landed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function uploadedSelection(before: Iterable<string>, after: readonly string[]): string | null;
|
|
42
|
+
/** The picker's commit callbacks — a pick fires `onChange` then closes the stage. */
|
|
43
|
+
export interface PickerCommit {
|
|
44
|
+
onChange: (assetId: string | null) => void;
|
|
45
|
+
onOpenChange: (open: boolean) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Commit a picked asset (or a clear, `assetId === null`) and close the picker —
|
|
49
|
+
* the selection semantic (click = pick, fire `onChange`, close). Both the grid
|
|
50
|
+
* card and the Clear affordance route through this.
|
|
51
|
+
*/
|
|
52
|
+
export declare function commitPickerSelection(assetId: string | null, { onChange, onOpenChange }: PickerCommit): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { assetMatchesAccept } from '@elytracms/core/cms-core';
|
|
2
|
+
import { filterAssets } from '../assets/asset-search.js';
|
|
3
|
+
import { newlyAddedAssetIds } from '../assets/focal-point.js';
|
|
4
|
+
/**
|
|
5
|
+
* Pure selection/filter core for the asset picker (EC-432). DOM-free and
|
|
6
|
+
* side-effect-free so the media browser's "what shows" and "what a pick/upload
|
|
7
|
+
* commits" are unit-testable without a browser — the picker component is just the
|
|
8
|
+
* stage shell around these.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The stage width preset the media browser mounts with (EC-432 / EC-431 AD-A):
|
|
12
|
+
* the roomy `grid` measure (`min(1240px, 94vw)`) — browsing assets is spatial.
|
|
13
|
+
* Exported so the geometry contract is pinnable without rendering the portal
|
|
14
|
+
* (which is `null` in the node test env).
|
|
15
|
+
*/
|
|
16
|
+
export const ASSET_PICKER_STAGE_PRESET = 'grid';
|
|
17
|
+
/**
|
|
18
|
+
* Split a file-input `accept` string ("image/*", "image/png,image/jpeg",
|
|
19
|
+
* ".pdf") into the token list the core matcher understands. The picker receives
|
|
20
|
+
* `accept` as the file-input attribute string (from `assetAcceptAttr(field.accept)`,
|
|
21
|
+
* a literal like `"image/*"`, or `undefined`), not a field's structured list —
|
|
22
|
+
* so we re-split it here. Empty/undefined ⇒ no tokens (no restriction).
|
|
23
|
+
*/
|
|
24
|
+
export function pickerAcceptTokens(accept) {
|
|
25
|
+
if (!accept)
|
|
26
|
+
return [];
|
|
27
|
+
return accept
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((token) => token.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Does an asset satisfy the picker's `accept`? Reuses the core
|
|
34
|
+
* {@link assetMatchesAccept} matcher over the split tokens — an empty/absent
|
|
35
|
+
* `accept` accepts everything, so a picker opened WITHOUT an accept (the editable
|
|
36
|
+
* cell) still browses the whole library, unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function matchesPickerAccept(asset, accept) {
|
|
39
|
+
const tokens = pickerAcceptTokens(accept);
|
|
40
|
+
if (tokens.length === 0)
|
|
41
|
+
return true;
|
|
42
|
+
return assetMatchesAccept({ contentType: asset.contentType, filename: asset.filename }, tokens);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The assets shown in the picker grid: the shipped search filter (EC-152) then
|
|
46
|
+
* the `accept` filter (EC-432) — an image field's picker browses only images,
|
|
47
|
+
* never the project's PDFs. Order follows the library.
|
|
48
|
+
*/
|
|
49
|
+
export function visiblePickerAssets(assets, query, accept) {
|
|
50
|
+
return filterAssets(assets, query).filter((asset) => matchesPickerAccept(asset, accept));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The id an upload just added to the library, if any (upload-then-select, EC-301):
|
|
54
|
+
* the `upload()` API is fire-and-forget, so the picker diffs the library against
|
|
55
|
+
* the pre-upload id set to learn the new asset. `null` when nothing new landed.
|
|
56
|
+
*/
|
|
57
|
+
export function uploadedSelection(before, after) {
|
|
58
|
+
return newlyAddedAssetIds(before, after)[0] ?? null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Commit a picked asset (or a clear, `assetId === null`) and close the picker —
|
|
62
|
+
* the selection semantic (click = pick, fire `onChange`, close). Both the grid
|
|
63
|
+
* card and the Clear affordance route through this.
|
|
64
|
+
*/
|
|
65
|
+
export function commitPickerSelection(assetId, { onChange, onOpenChange }) {
|
|
66
|
+
onChange(assetId);
|
|
67
|
+
onOpenChange(false);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=asset-picker-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-picker-core.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker-core.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAE1D;;;;;GAKG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAe,CAAA;AAExD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA0B;IAC3D,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAA;IACtB,OAAO,MAAM;SACV,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAoD,EACpD,MAA0B;IAE1B,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAA;IACzC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpC,OAAO,kBAAkB,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAA;AACjG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA8B,EAC9B,KAAa,EACb,MAA0B;IAE1B,OAAO,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;AAC1F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAwB,EAAE,KAAwB;IAClF,OAAO,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AACrD,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAsB,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAgB;IACpG,QAAQ,CAAC,OAAO,CAAC,CAAA;IACjB,YAAY,CAAC,KAAK,CAAC,CAAA;AACrB,CAAC","sourcesContent":["import type { AssetRecord } from '@elytracms/core/persistence'\nimport { assetMatchesAccept } from '@elytracms/core/cms-core'\nimport { filterAssets } from '../assets/asset-search'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\n\n/**\n * Pure selection/filter core for the asset picker (EC-432). DOM-free and\n * side-effect-free so the media browser's \"what shows\" and \"what a pick/upload\n * commits\" are unit-testable without a browser — the picker component is just the\n * stage shell around these.\n */\n\n/**\n * The stage width preset the media browser mounts with (EC-432 / EC-431 AD-A):\n * the roomy `grid` measure (`min(1240px, 94vw)`) — browsing assets is spatial.\n * Exported so the geometry contract is pinnable without rendering the portal\n * (which is `null` in the node test env).\n */\nexport const ASSET_PICKER_STAGE_PRESET = 'grid' as const\n\n/**\n * Split a file-input `accept` string (\"image/*\", \"image/png,image/jpeg\",\n * \".pdf\") into the token list the core matcher understands. The picker receives\n * `accept` as the file-input attribute string (from `assetAcceptAttr(field.accept)`,\n * a literal like `\"image/*\"`, or `undefined`), not a field's structured list —\n * so we re-split it here. Empty/undefined ⇒ no tokens (no restriction).\n */\nexport function pickerAcceptTokens(accept: string | undefined): string[] {\n if (!accept) return []\n return accept\n .split(',')\n .map((token) => token.trim())\n .filter(Boolean)\n}\n\n/**\n * Does an asset satisfy the picker's `accept`? Reuses the core\n * {@link assetMatchesAccept} matcher over the split tokens — an empty/absent\n * `accept` accepts everything, so a picker opened WITHOUT an accept (the editable\n * cell) still browses the whole library, unchanged.\n */\nexport function matchesPickerAccept(\n asset: Pick<AssetRecord, 'contentType' | 'filename'>,\n accept: string | undefined,\n): boolean {\n const tokens = pickerAcceptTokens(accept)\n if (tokens.length === 0) return true\n return assetMatchesAccept({ contentType: asset.contentType, filename: asset.filename }, tokens)\n}\n\n/**\n * The assets shown in the picker grid: the shipped search filter (EC-152) then\n * the `accept` filter (EC-432) — an image field's picker browses only images,\n * never the project's PDFs. Order follows the library.\n */\nexport function visiblePickerAssets(\n assets: readonly AssetRecord[],\n query: string,\n accept: string | undefined,\n): AssetRecord[] {\n return filterAssets(assets, query).filter((asset) => matchesPickerAccept(asset, accept))\n}\n\n/**\n * The id an upload just added to the library, if any (upload-then-select, EC-301):\n * the `upload()` API is fire-and-forget, so the picker diffs the library against\n * the pre-upload id set to learn the new asset. `null` when nothing new landed.\n */\nexport function uploadedSelection(before: Iterable<string>, after: readonly string[]): string | null {\n return newlyAddedAssetIds(before, after)[0] ?? null\n}\n\n/** The picker's commit callbacks — a pick fires `onChange` then closes the stage. */\nexport interface PickerCommit {\n onChange: (assetId: string | null) => void\n onOpenChange: (open: boolean) => void\n}\n\n/**\n * Commit a picked asset (or a clear, `assetId === null`) and close the picker —\n * the selection semantic (click = pick, fire `onChange`, close). Both the grid\n * card and the Clear affordance route through this.\n */\nexport function commitPickerSelection(assetId: string | null, { onChange, onOpenChange }: PickerCommit): void {\n onChange(assetId)\n onOpenChange(false)\n}\n"]}
|
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
* Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).
|
|
3
3
|
* Backed by the *real* asset library (the assets store over the operations
|
|
4
4
|
* client) — the same records the Assets section lists — not by fixture
|
|
5
|
-
* documents. Selecting stores the asset id on the document field.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* documents. Selecting stores the asset id on the document field.
|
|
6
|
+
*
|
|
7
|
+
* EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}
|
|
8
|
+
* `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the
|
|
9
|
+
* old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the
|
|
10
|
+
* assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and
|
|
11
|
+
* the upload-then-select path. It is a SELECTION browser, not a form — click = pick
|
|
12
|
+
* (fires `onChange`, closes) plus the shipped clear/cancel affordances; the
|
|
13
|
+
* forms-never-modal rule governs document forms and is untouched. Opened from the
|
|
14
|
+
* expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer
|
|
15
|
+
* stack — Escape closes the browser first, the rich-text stage stays.
|
|
16
|
+
*
|
|
17
|
+
* The component CONTRACT is unchanged (`{ open, onOpenChange, value, accept,
|
|
18
|
+
* onChange }`) so every call site — asset fields, rich-text image insertion,
|
|
19
|
+
* editable cells — works with zero edits, just a bigger surface.
|
|
8
20
|
*/
|
|
9
21
|
export declare function AssetPicker({ value, open, onOpenChange, onChange, accept, }: {
|
|
10
22
|
value: string | null;
|
|
11
23
|
open: boolean;
|
|
12
24
|
onOpenChange: (open: boolean) => void;
|
|
13
25
|
onChange: (assetId: string | null) => void;
|
|
14
|
-
/** The file input's `accept` attribute for in-picker upload (field-derived). */
|
|
26
|
+
/** The file input's `accept` attribute for in-picker upload + grid filter (field-derived). */
|
|
15
27
|
accept?: string;
|
|
16
28
|
}): import("react").JSX.Element | null;
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { AlertTriangle, Check, ImageIcon,
|
|
2
|
+
import { AlertTriangle, Check, ImageIcon, Search, Upload } from 'lucide-react';
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Row, Spacer,
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, ExpandableSurface, Input, Row, Spacer, Text, } from '@elytracms/studio/ui';
|
|
5
5
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
6
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
-
import {
|
|
7
|
+
import { AssetThumb } from '../assets/asset-thumb.js';
|
|
8
8
|
import { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store.js';
|
|
9
|
-
import { newlyAddedAssetIds } from '../assets/focal-point.js';
|
|
10
9
|
import { useCollectionView } from '../collection-view/collection-view-context.js';
|
|
10
|
+
import { ASSET_PICKER_STAGE_PRESET, commitPickerSelection, uploadedSelection, visiblePickerAssets, } from './asset-picker-core.js';
|
|
11
11
|
/**
|
|
12
12
|
* Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).
|
|
13
13
|
* Backed by the *real* asset library (the assets store over the operations
|
|
14
14
|
* client) — the same records the Assets section lists — not by fixture
|
|
15
|
-
* documents. Selecting stores the asset id on the document field.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* documents. Selecting stores the asset id on the document field.
|
|
16
|
+
*
|
|
17
|
+
* EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}
|
|
18
|
+
* `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the
|
|
19
|
+
* old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the
|
|
20
|
+
* assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and
|
|
21
|
+
* the upload-then-select path. It is a SELECTION browser, not a form — click = pick
|
|
22
|
+
* (fires `onChange`, closes) plus the shipped clear/cancel affordances; the
|
|
23
|
+
* forms-never-modal rule governs document forms and is untouched. Opened from the
|
|
24
|
+
* expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer
|
|
25
|
+
* stack — Escape closes the browser first, the rich-text stage stays.
|
|
26
|
+
*
|
|
27
|
+
* The component CONTRACT is unchanged (`{ open, onOpenChange, value, accept,
|
|
28
|
+
* onChange }`) so every call site — asset fields, rich-text image insertion,
|
|
29
|
+
* editable cells — works with zero edits, just a bigger surface.
|
|
18
30
|
*/
|
|
19
31
|
export function AssetPicker({ value, open, onOpenChange, onChange, accept, }) {
|
|
20
32
|
const source = useCollectionView();
|
|
@@ -24,21 +36,20 @@ export function AssetPicker({ value, open, onOpenChange, onChange, accept, }) {
|
|
|
24
36
|
if (!open)
|
|
25
37
|
return null;
|
|
26
38
|
if (ambient) {
|
|
27
|
-
return (_jsx(
|
|
39
|
+
return (_jsx(AssetPickerStage, { value: value, onOpenChange: onOpenChange, onChange: onChange, accept: accept }));
|
|
28
40
|
}
|
|
29
41
|
if (source.projectId === undefined) {
|
|
30
42
|
// No project scope (fixture-only CMS session): the library is
|
|
31
43
|
// unreachable — say so instead of silently showing nothing.
|
|
32
44
|
return (_jsx(Dialog, { open: true, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('cms.asset.pickTitle') }), _jsx(DialogDescription, { children: translate('cms.asset.pickDescription') })] }), _jsxs(Row, { gap: "2", "data-testid": "asset-picker-no-project", children: [_jsx(AlertTriangle, { className: "size-4 text-warning" }), _jsx(Text, { size: "sm", tone: "muted", children: translate('cms.asset.noProject') })] })] }) }));
|
|
33
45
|
}
|
|
34
|
-
return (_jsx(AssetsProvider, { projectId: source.projectId, children: _jsx(
|
|
46
|
+
return (_jsx(AssetsProvider, { projectId: source.projectId, children: _jsx(AssetPickerStage, { value: value, onOpenChange: onOpenChange, onChange: onChange, accept: accept }) }));
|
|
35
47
|
}
|
|
36
|
-
/** The open
|
|
37
|
-
function
|
|
48
|
+
/** The open media-browser stage over a mounted assets store. */
|
|
49
|
+
function AssetPickerStage({ value, onOpenChange, onChange, accept, }) {
|
|
38
50
|
const store = useAssets();
|
|
39
51
|
// Fresh per open: the picker unmounts entirely while closed.
|
|
40
52
|
const [query, setQuery] = useState('');
|
|
41
|
-
const [maximized, setMaximized] = useState(false);
|
|
42
53
|
const [dragging, setDragging] = useState(false);
|
|
43
54
|
const fileInputRef = useRef(null);
|
|
44
55
|
// Library ids when an upload started; the effect below diffs against it to
|
|
@@ -48,18 +59,17 @@ function AssetPickerDialog({ value, onOpenChange, onChange, accept, }) {
|
|
|
48
59
|
// child boundaries — the same per-element dragenter/dragleave flicker the field
|
|
49
60
|
// fixes. Only hide when the count returns to zero.
|
|
50
61
|
const dragDepth = useRef(0);
|
|
51
|
-
const assets =
|
|
62
|
+
const assets = visiblePickerAssets(store.assets, query, accept);
|
|
52
63
|
const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets]);
|
|
53
64
|
// Auto-select an asset uploaded from within the picker, then close.
|
|
54
65
|
useEffect(() => {
|
|
55
66
|
if (!pendingRef.current)
|
|
56
67
|
return;
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
68
|
+
const picked = uploadedSelection(pendingRef.current, libraryIds);
|
|
69
|
+
if (picked === null)
|
|
59
70
|
return;
|
|
60
71
|
pendingRef.current = null;
|
|
61
|
-
onChange
|
|
62
|
-
onOpenChange(false);
|
|
72
|
+
commitPickerSelection(picked, { onChange, onOpenChange });
|
|
63
73
|
}, [libraryIds, onChange, onOpenChange]);
|
|
64
74
|
function handleUpload(files) {
|
|
65
75
|
if (files.length === 0)
|
|
@@ -67,10 +77,10 @@ function AssetPickerDialog({ value, onOpenChange, onChange, accept, }) {
|
|
|
67
77
|
pendingRef.current = new Set(libraryIds);
|
|
68
78
|
store.upload(files);
|
|
69
79
|
}
|
|
70
|
-
//
|
|
80
|
+
// The stage portals to document.body, but React synthetic events still bubble up
|
|
71
81
|
// the COMPONENT tree — and this picker is rendered as a child of the asset field,
|
|
72
82
|
// which has its own drag-to-upload handlers. Without stopPropagation a drop inside
|
|
73
|
-
// the
|
|
83
|
+
// the stage would fire BOTH the picker's and the field's `onDrop`, uploading the
|
|
74
84
|
// file twice. Stop every DnD event here so the picker owns its drop entirely.
|
|
75
85
|
function onDragEnter(e) {
|
|
76
86
|
e.preventDefault();
|
|
@@ -98,40 +108,14 @@ function AssetPickerDialog({ value, onOpenChange, onChange, accept, }) {
|
|
|
98
108
|
setDragging(false);
|
|
99
109
|
handleUpload(Array.from(e.dataTransfer.files));
|
|
100
110
|
}
|
|
101
|
-
return (_jsx(
|
|
111
|
+
return (_jsx(ExpandableSurface, { title: translate('cms.asset.pickTitle'), closeLabel: translate('common.close'), variant: "stage", preset: ASSET_PICKER_STAGE_PRESET, onDismiss: () => onOpenChange(false), headerLeading: _jsx(ImageIcon, { className: "size-4 text-muted-foreground", "aria-hidden": true }), children: _jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col gap-3 p-4", onDragEnter: onDragEnter, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsxs("span", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: query, onChange: (e) => setQuery(e.target.value), placeholder: translate('cms.asset.searchPlaceholder'), className: "pl-8", "data-testid": "asset-picker-search" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => fileInputRef.current?.click(), children: [_jsx(Upload, {}), " ", translate('common.upload')] }), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: accept ?? 'image/*', className: "hidden", onChange: (e) => {
|
|
102
112
|
handleUpload(Array.from(e.target.files ?? []));
|
|
103
113
|
e.target.value = '';
|
|
104
|
-
} })] }), _jsx("div", { className:
|
|
105
|
-
? 'max-h-[78vh] grid-cols-4 lg:grid-cols-6'
|
|
106
|
-
: 'max-h-[60vh] grid-cols-3 sm:grid-cols-4'), "data-testid": "asset-picker-grid", children: store.status === 'loading' ? (_jsx(Text, { size: "sm", tone: "muted", className: "col-span-full p-3", children: translate('cms.asset.loading') })) : store.status === 'error' ? (_jsxs(Row, { gap: "2", className: "col-span-full p-3", "data-testid": "asset-picker-error", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { size: "sm", tone: "muted", children: store.errorMessage ?? translate('cms.asset.loadError') })] })) : assets.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", className: "col-span-full p-3", children: query.trim()
|
|
107
|
-
? translate('cms.asset.noMatches')
|
|
108
|
-
: translate('cms.asset.empty') })) : (assets.map((asset) => (_jsx(AssetPickerCard, { asset: asset, selected: asset.id === value, onPick: () => {
|
|
109
|
-
onChange(asset.id);
|
|
110
|
-
onOpenChange(false);
|
|
111
|
-
} }, asset.id)))) }), _jsxs(Row, { gap: "2", children: [_jsx(Spacer, {}), value ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
112
|
-
onChange(null);
|
|
113
|
-
onOpenChange(false);
|
|
114
|
-
}, children: translate('common.clear') })) : null] }), dragging ? (_jsx("div", { className: "pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-background/85", children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Upload, { className: "size-5 text-primary" }), _jsx(Text, { className: "font-medium", children: translate('cms.asset.dropToUpload') })] }) })) : null] }) }));
|
|
114
|
+
} })] }), _jsx("div", { className: "grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5", "data-testid": "asset-picker-grid", children: store.status === 'loading' ? (_jsx(Text, { size: "sm", tone: "muted", className: "col-span-full p-3", children: translate('cms.asset.loading') })) : store.status === 'error' ? (_jsxs(Row, { gap: "2", className: "col-span-full p-3", "data-testid": "asset-picker-error", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { size: "sm", tone: "muted", children: store.errorMessage ?? translate('cms.asset.loadError') })] })) : assets.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", className: "col-span-full p-3", children: query.trim() ? translate('cms.asset.noMatches') : translate('cms.asset.empty') })) : (assets.map((asset) => (_jsx(AssetPickerCard, { asset: asset, selected: asset.id === value, onPick: () => commitPickerSelection(asset.id, { onChange, onOpenChange }) }, asset.id)))) }), _jsxs(Row, { gap: "2", children: [_jsx(Spacer, {}), value ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => commitPickerSelection(null, { onChange, onOpenChange }), children: translate('common.clear') })) : null] }), dragging ? (_jsx("div", { className: "pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-lg border-2 border-dashed border-primary bg-background/85", children: _jsxs(Row, { gap: "2", align: "center", children: [_jsx(Upload, { className: "size-5 text-primary" }), _jsx(Text, { className: "font-medium", children: translate('cms.asset.dropToUpload') })] }) })) : null] }) }));
|
|
115
115
|
}
|
|
116
|
-
/** One selectable asset:
|
|
117
|
-
* asset id is intentionally not shown — it's noise to editors (EC-301). */
|
|
116
|
+
/** One selectable asset: EC-400 thumb tile + title/filename + alt text. The
|
|
117
|
+
* internal asset id is intentionally not shown — it's noise to editors (EC-301). */
|
|
118
118
|
function AssetPickerCard({ asset, selected, onPick, }) {
|
|
119
|
-
return (_jsxs("button", { type: "button", onClick: onPick, className: cn('flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors', selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted'), "data-testid": "asset-picker-card", children: [_jsx(
|
|
120
|
-
}
|
|
121
|
-
/** Image preview: the EC-400 downscaled thumb when resolvable, else the full
|
|
122
|
-
* original; icon fallback for other types. In-memory/opaque locators
|
|
123
|
-
* (memory://, convex://) are not fetchable by <img>, so those degrade too. */
|
|
124
|
-
function PickerThumbnail({ asset }) {
|
|
125
|
-
const isImage = asset.contentType.startsWith('image/');
|
|
126
|
-
const thumbUrl = asset.thumb?.url;
|
|
127
|
-
const src = thumbUrl?.startsWith('http') === true
|
|
128
|
-
? thumbUrl
|
|
129
|
-
: asset.url?.startsWith('http') === true
|
|
130
|
-
? asset.url
|
|
131
|
-
: undefined;
|
|
132
|
-
if (isImage && src) {
|
|
133
|
-
return (_jsx("img", { src: src, alt: asset.alt ?? asset.filename, className: "aspect-video w-full rounded-md bg-muted object-cover" }));
|
|
134
|
-
}
|
|
135
|
-
return (_jsx("span", { className: "grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(ImageIcon, { className: "size-6" }) }));
|
|
119
|
+
return (_jsxs("button", { type: "button", onClick: onPick, className: cn('flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors', selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted'), "data-testid": "asset-picker-card", "aria-pressed": selected, children: [_jsxs("span", { className: "relative block", children: [_jsx(AssetThumb, { record: asset, className: "aspect-video w-full" }), selected ? (_jsx("span", { className: "absolute right-1 top-1 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow", children: _jsx(Check, { className: "size-3" }) })) : null] }), _jsx(Text, { size: "xs", className: "min-w-0 truncate font-medium", children: asset.title ?? asset.filename }), _jsx(Text, { size: "xs", tone: asset.alt ? 'muted' : 'subtle', className: "truncate", children: asset.alt ?? translate('cms.asset.noAltText') })] }));
|
|
136
120
|
}
|
|
137
121
|
//# sourceMappingURL=asset-picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Maximize2, Minimize2, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { filterAssets } from '../assets/asset-search'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field. The dialog\n * is form-width with a maximize toggle and supports drag/click upload that\n * auto-selects the new asset (EC-301).\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** The file input's `accept` attribute for in-picker upload (field-derived). */\n accept?: string\n}) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n </AssetsProvider>\n )\n}\n\n/** The open dialog over a mounted assets store. */\nfunction AssetPickerDialog({\n value,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [maximized, setMaximized] = useState(false)\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // Library ids when an upload started; the effect below diffs against it to\n // learn the new asset and select it (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = filterAssets(store.assets, query)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n\n // Auto-select an asset uploaded from within the picker, then close.\n useEffect(() => {\n if (!pendingRef.current) return\n const added = newlyAddedAssetIds(pendingRef.current, libraryIds)\n if (added.length === 0) return\n pendingRef.current = null\n onChange(added[0]!)\n onOpenChange(false)\n }, [libraryIds, onChange, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = new Set(libraryIds)\n store.upload(files)\n }\n // Radix portals the dialog in the DOM, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the dialog would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(maximized ? 'max-w-[95vw]' : 'max-w-2xl')}\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <DialogHeader>\n <Row gap=\"2\" align=\"center\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </Stack>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={maximized ? translate('cms.asset.restoreDialogSize') : translate('cms.asset.maximizeDialog')}\n onClick={() => setMaximized((v) => !v)}\n className=\"mr-7\"\n >\n {maximized ? <Minimize2 className=\"size-4\" /> : <Maximize2 className=\"size-4\" />}\n </Button>\n </Row>\n </DialogHeader>\n\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className={cn(\n 'grid gap-2 overflow-auto',\n maximized\n ? 'max-h-[78vh] grid-cols-4 lg:grid-cols-6'\n : 'max-h-[60vh] grid-cols-3 sm:grid-cols-4',\n )}\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim()\n ? translate('cms.asset.noMatches')\n : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => {\n onChange(asset.id)\n onOpenChange(false)\n }}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n onChange(null)\n onOpenChange(false)\n }}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** One selectable asset: thumbnail + title/filename + alt text. The internal\n * asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n >\n <PickerThumbnail asset={asset} />\n <Row gap=\"1\">\n <Text size=\"xs\" className=\"min-w-0 flex-1 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {selected ? <Check className=\"size-3.5 text-primary\" /> : null}\n </Row>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n\n/** Image preview: the EC-400 downscaled thumb when resolvable, else the full\n * original; icon fallback for other types. In-memory/opaque locators\n * (memory://, convex://) are not fetchable by <img>, so those degrade too. */\nfunction PickerThumbnail({ asset }: { asset: AssetRecord }) {\n const isImage = asset.contentType.startsWith('image/')\n const thumbUrl = asset.thumb?.url\n const src =\n thumbUrl?.startsWith('http') === true\n ? thumbUrl\n : asset.url?.startsWith('http') === true\n ? asset.url\n : undefined\n if (isImage && src) {\n return (\n <img\n src={src}\n alt={asset.alt ?? asset.filename}\n className=\"aspect-video w-full rounded-md bg-muted object-cover\"\n />\n )\n }\n return (\n <span className=\"grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground\">\n <ImageIcon className=\"size-6\" />\n </span>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n ExpandableSurface,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { AssetThumb } from '../assets/asset-thumb'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport {\n ASSET_PICKER_STAGE_PRESET,\n commitPickerSelection,\n uploadedSelection,\n visiblePickerAssets,\n} from './asset-picker-core'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field.\n *\n * EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}\n * `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the\n * old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the\n * assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and\n * the upload-then-select path. It is a SELECTION browser, not a form — click = pick\n * (fires `onChange`, closes) plus the shipped clear/cancel affordances; the\n * forms-never-modal rule governs document forms and is untouched. Opened from the\n * expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer\n * stack — Escape closes the browser first, the rich-text stage stays.\n *\n * The component CONTRACT is unchanged (`{ open, onOpenChange, value, accept,\n * onChange }`) so every call site — asset fields, rich-text image insertion,\n * editable cells — works with zero edits, just a bigger surface.\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** The file input's `accept` attribute for in-picker upload + grid filter (field-derived). */\n accept?: string\n}) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerStage value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerStage value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n </AssetsProvider>\n )\n}\n\n/** The open media-browser stage over a mounted assets store. */\nfunction AssetPickerStage({\n value,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // Library ids when an upload started; the effect below diffs against it to\n // learn the new asset and select it (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = visiblePickerAssets(store.assets, query, accept)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n\n // Auto-select an asset uploaded from within the picker, then close.\n useEffect(() => {\n if (!pendingRef.current) return\n const picked = uploadedSelection(pendingRef.current, libraryIds)\n if (picked === null) return\n pendingRef.current = null\n commitPickerSelection(picked, { onChange, onOpenChange })\n }, [libraryIds, onChange, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = new Set(libraryIds)\n store.upload(files)\n }\n // The stage portals to document.body, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the stage would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <ExpandableSurface\n title={translate('cms.asset.pickTitle')}\n closeLabel={translate('common.close')}\n variant=\"stage\"\n preset={ASSET_PICKER_STAGE_PRESET}\n onDismiss={() => onOpenChange(false)}\n headerLeading={<ImageIcon className=\"size-4 text-muted-foreground\" aria-hidden />}\n >\n <div\n className=\"relative flex min-h-0 flex-1 flex-col gap-3 p-4\"\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className=\"grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\"\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim() ? translate('cms.asset.noMatches') : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => commitPickerSelection(asset.id, { onChange, onOpenChange })}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => commitPickerSelection(null, { onChange, onOpenChange })}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-lg border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </div>\n </ExpandableSurface>\n )\n}\n\n/** One selectable asset: EC-400 thumb tile + title/filename + alt text. The\n * internal asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n aria-pressed={selected}\n >\n <span className=\"relative block\">\n <AssetThumb record={asset} className=\"aspect-video w-full\" />\n {selected ? (\n <span className=\"absolute right-1 top-1 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow\">\n <Check className=\"size-3\" />\n </span>\n ) : null}\n </span>\n <Text size=\"xs\" className=\"min-w-0 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n"]}
|