@elytracms/studio 0.0.77 → 0.0.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/contract-parity.test.ts +238 -0
- package/convex/tsconfig.json +3 -1
- package/dist/components/app-views/default-modules.js +5 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +12 -11
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +11 -5
- package/dist/components/builder/blocks-panel.js +18 -24
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +22 -0
- package/dist/components/builder/canvas-render-context.js +65 -9
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +25 -0
- package/dist/components/builder/reuse.js +11 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +52 -19
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +40 -0
- package/dist/components/cms/asset-picker-core.js +21 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.d.ts +11 -2
- package/dist/components/cms/asset-picker.js +34 -13
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell.js +2 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +8 -1
- package/dist/components/cms/cms-store.js +8 -7
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +8 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/date-input-core.d.ts +64 -0
- package/dist/components/cms/date-input-core.js +78 -0
- package/dist/components/cms/date-input-core.js.map +1 -0
- package/dist/components/cms/date-input.d.ts +43 -0
- package/dist/components/cms/date-input.js +109 -0
- package/dist/components/cms/date-input.js.map +1 -0
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.js +2 -1
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/document-staging.d.ts +15 -4
- package/dist/components/cms/document-staging.js +26 -15
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/editable-cell.js +39 -5
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +38 -7
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.js +4 -1
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/slug-auto-derive.d.ts +41 -0
- package/dist/components/cms/slug-auto-derive.js +14 -0
- package/dist/components/cms/slug-auto-derive.js.map +1 -0
- package/dist/components/components/transfer-dialog.js +3 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/project-session.d.ts +44 -22
- package/dist/components/project-session.js +72 -36
- package/dist/components/project-session.js.map +1 -1
- package/dist/lib/assets/asset-url.d.ts +20 -0
- package/dist/lib/assets/asset-url.js +25 -0
- package/dist/lib/assets/asset-url.js.map +1 -0
- package/dist/lib/assets/browser-blob-storage.d.ts +80 -0
- package/dist/lib/assets/browser-blob-storage.js +86 -0
- package/dist/lib/assets/browser-blob-storage.js.map +1 -0
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +20 -1
- package/dist/lib/data/collection-content.js +51 -1
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/content-session-refresh.d.ts +74 -0
- package/dist/lib/data/content-session-refresh.js +138 -0
- package/dist/lib/data/content-session-refresh.js.map +1 -0
- package/dist/lib/data/index.d.ts +2 -0
- package/dist/lib/data/index.js +2 -0
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/data/playground-unload-guard.d.ts +22 -0
- package/dist/lib/data/playground-unload-guard.js +73 -0
- package/dist/lib/data/playground-unload-guard.js.map +1 -0
- package/dist/lib/i18n/messages.de.js +3 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +2 -0
- package/dist/lib/i18n/messages.en.js +5 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +3 -3
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/ui/input.d.ts +8 -3
- package/dist/ui/input.js +6 -1
- package/dist/ui/input.js.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-thumb.js","sourceRoot":"","sources":["../../../src/components/assets/asset-thumb.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useState } from 'react'\nimport { Image } from 'lucide-react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useOptionalAssets } from './assets-store'\nimport {\n CENTER_FOCAL_POINT,\n focalPointFromPoint,\n focalPointObjectPosition,\n} from './focal-point'\nimport type { FocalPoint } from './focal-point'\n\n/** Resolve an asset record by id from the ambient library, if one is mounted. */\nexport function useAssetRecord(assetId: string | null | undefined): AssetRecord | undefined {\n const store = useOptionalAssets()\n return useMemo(() => {\n if (!assetId || !store || store.status !== 'ready') return undefined\n return store.assets.find((a) => a.id === assetId)\n }, [assetId, store])\n}\n\n/** True when the record is an image with a browser-resolvable URL (EC-212). */\nfunction isResolvableImage(record: AssetRecord | undefined): record is AssetRecord & { url: string } {\n return (\n record !== undefined &&\n record.contentType.startsWith('image/') &&\n typeof record.url === 'string' &&\n record.url.startsWith('http')\n )\n}\n\nconst isHttp = (url: string | undefined): url is string =>\n typeof url === 'string' && url.startsWith('http')\n\n/**\n * The best studio thumbnail src for an image record (EC-400): prefer the stored\n * downscaled `thumb` variant when it is browser-resolvable, else fall back to the\n * full original `url`. A missing/broken thumb therefore degrades to the original —\n * never a broken tile (scope §4). Non-image / unresolvable records yield\n * `undefined` (the icon placeholder). Focal-point editing + the canvas keep the\n * original elsewhere (delivery fidelity) — this preference is thumbnail-only.\n */\nfunction thumbnailSrc(record: AssetRecord | undefined): string | undefined {\n if (!record || !record.contentType.startsWith('image/')) return undefined\n const thumbUrl = record.thumb?.url\n if (isHttp(thumbUrl)) return thumbUrl\n if (isHttp(record.url)) return record.url\n return undefined\n}\n\n/**\n * A read-only image thumbnail honouring the asset's focal point (EC-248): the\n * preview crops with the same `object-position` delivery uses (EC-230), so the\n * editor sees exactly what a focal-point change does. Prefers the EC-400\n * downscaled `thumb` variant (light egress) and falls back to the original.\n * Non-image / unresolvable assets fall back to the labelled icon, matching the\n * table cell's preview.\n */\nexport function AssetThumb({\n record,\n className,\n alt,\n}: {\n record: AssetRecord | undefined\n className?: string\n alt?: string\n}) {\n const src = thumbnailSrc(record)\n if (record && src) {\n return (\n <img\n src={src}\n alt={alt ?? record.alt ?? record.title ?? record.filename}\n loading=\"lazy\"\n style={{ objectPosition: focalPointObjectPosition(record.focalPoint) }}\n className={cn('rounded border border-border bg-muted object-cover', className)}\n />\n )\n }\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n <Image className=\"size-4\" aria-hidden />\n </div>\n )\n}\n\n/**\n * The focal-point picker (EC-248/EC-301): click or **drag** anywhere on the image\n * to place the normalized 0–1 hotspot delivery threads to `object-position`\n * (EC-230). The marker follows the pointer live while dragging and only commits on\n * release (`onPreview` updates the marker, `onChange` commits). `onChange(null)`\n * resets to centre. Shows a placeholder until the image loads; renders a muted note\n * when the asset has no resolvable image.\n */\nexport function FocalPointPicker({\n record,\n point,\n onChange,\n onPreview,\n disabled,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n onChange: (point: FocalPoint | null) => void\n /** Called continuously while dragging to move the marker without committing. */\n onPreview?: (point: FocalPoint) => void\n disabled?: boolean\n}) {\n const marker = point ?? CENTER_FOCAL_POINT\n const dragging = useRef(false)\n const [loaded, setLoaded] = useState(false)\n\n if (!isResolvableImage(record)) {\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('assets.focalPoint.noPreview')}\n </Text>\n )\n }\n\n const pointFrom = (e: React.PointerEvent<HTMLElement>): FocalPoint =>\n focalPointFromPoint(e.currentTarget.getBoundingClientRect(), e.clientX, e.clientY)\n\n function onPointerDown(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled) return\n dragging.current = true\n e.currentTarget.setPointerCapture(e.pointerId)\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerMove(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerUp(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n dragging.current = false\n e.currentTarget.releasePointerCapture(e.pointerId)\n onChange(pointFrom(e)) // commit on release only\n }\n\n return (\n <button\n type=\"button\"\n aria-label={translate('assets.focalPoint.setLabel')}\n disabled={disabled}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n className={cn(\n 'relative block w-full touch-none select-none overflow-hidden rounded border border-border bg-muted',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n disabled ? 'cursor-default opacity-60' : 'cursor-crosshair',\n )}\n >\n {/* Width-constrained, natural aspect (no object-fit), so the button wraps\n the image tightly and the bounding rect equals the image box — the\n normalized pointer coordinates are then exact, with no letterbox offset. */}\n <img\n src={record.url}\n alt={record.alt ?? record.filename}\n draggable={false}\n onLoad={() => setLoaded(true)}\n className={cn('block h-auto w-full select-none transition-opacity', loaded ? 'opacity-100' : 'opacity-0')}\n />\n {!loaded ? (\n <span className=\"absolute inset-0 grid aspect-video place-items-center bg-muted\">\n <Image className=\"size-6 animate-pulse text-muted-foreground\" aria-hidden />\n </span>\n ) : (\n <span\n aria-hidden\n className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.55)] ring-1 ring-black/40\"\n style={{ left: `${marker.x * 100}%`, top: `${marker.y * 100}%` }}\n />\n )}\n </button>\n )\n}\n\n/** Common crop aspect ratios shown as focal-aware previews (EC-301). */\nconst FOCAL_CROP_RATIOS = [\n { label: '9:16', className: 'aspect-[9/16]' },\n { label: '3:4', className: 'aspect-[3/4]' },\n { label: '1:1', className: 'aspect-square' },\n { label: '4:3', className: 'aspect-[4/3]' },\n { label: '16:9', className: 'aspect-video' },\n] as const\n\n/**\n * A row of common-ratio crop previews (EC-301), each cropping the image with the\n * same `object-position` delivery uses — so the editor sees how the focal point\n * holds the subject across portrait/square/landscape crops. Updates live as the\n * point is dragged.\n */\nexport function FocalCropPreviews({\n record,\n point,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n}) {\n if (!isResolvableImage(record)) return null\n const objectPosition = focalPointObjectPosition(point)\n return (\n <div className=\"flex items-end gap-2\">\n {FOCAL_CROP_RATIOS.map((ratio) => (\n <div key={ratio.label} className=\"flex min-w-0 flex-col items-center gap-1\">\n <div className={cn('h-14 overflow-hidden rounded border border-border bg-muted', ratio.className)}>\n <img\n src={record.url}\n alt=\"\"\n draggable={false}\n style={{ objectPosition }}\n className=\"h-full w-full object-cover\"\n />\n </div>\n <Text size=\"xs\" tone=\"subtle\">\n {ratio.label}\n </Text>\n </div>\n ))}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asset-thumb.js","sourceRoot":"","sources":["../../../src/components/assets/asset-thumb.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useRef, useState } from 'react'\nimport { Image } from 'lucide-react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useOptionalAssets } from './assets-store'\nimport { isDisplayableAssetUrl } from '@elytracms/studio/lib/assets'\nimport {\n CENTER_FOCAL_POINT,\n focalPointFromPoint,\n focalPointObjectPosition,\n} from './focal-point'\nimport type { FocalPoint } from './focal-point'\n\n/** Resolve an asset record by id from the ambient library, if one is mounted. */\nexport function useAssetRecord(assetId: string | null | undefined): AssetRecord | undefined {\n const store = useOptionalAssets()\n return useMemo(() => {\n if (!assetId || !store || store.status !== 'ready') return undefined\n return store.assets.find((a) => a.id === assetId)\n }, [assetId, store])\n}\n\n/** True when the record is an image with a browser-displayable URL (EC-212, EC-459). */\nfunction isResolvableImage(record: AssetRecord | undefined): record is AssetRecord & { url: string } {\n return (\n record !== undefined &&\n record.contentType.startsWith('image/') &&\n isDisplayableAssetUrl(record.url)\n )\n}\n\n/**\n * The best studio thumbnail src for an image record (EC-400): prefer the stored\n * downscaled `thumb` variant when it is browser-displayable, else fall back to\n * the full original `url` (EC-459: `http(s)`, `blob:`, and `data:` all qualify —\n * an opaque backend locator like `memory://` does not). A missing/broken thumb\n * therefore degrades to the original — never a broken tile (scope §4). Non-image\n * / unresolvable records yield `undefined` (the icon placeholder). Focal-point\n * editing + the canvas keep the original elsewhere (delivery fidelity) — this\n * preference is thumbnail-only.\n */\nfunction thumbnailSrc(record: AssetRecord | undefined): string | undefined {\n if (!record || !record.contentType.startsWith('image/')) return undefined\n const thumbUrl = record.thumb?.url\n if (isDisplayableAssetUrl(thumbUrl)) return thumbUrl\n if (isDisplayableAssetUrl(record.url)) return record.url\n return undefined\n}\n\n/**\n * A read-only image thumbnail honouring the asset's focal point (EC-248): the\n * preview crops with the same `object-position` delivery uses (EC-230), so the\n * editor sees exactly what a focal-point change does. Prefers the EC-400\n * downscaled `thumb` variant (light egress) and falls back to the original.\n * Non-image / unresolvable assets fall back to the labelled icon, matching the\n * table cell's preview.\n */\nexport function AssetThumb({\n record,\n className,\n alt,\n}: {\n record: AssetRecord | undefined\n className?: string\n alt?: string\n}) {\n const src = thumbnailSrc(record)\n if (record && src) {\n return (\n <img\n src={src}\n alt={alt ?? record.alt ?? record.title ?? record.filename}\n loading=\"lazy\"\n style={{ objectPosition: focalPointObjectPosition(record.focalPoint) }}\n className={cn('rounded border border-border bg-muted object-cover', className)}\n />\n )\n }\n return (\n <div\n className={cn(\n 'flex items-center justify-center rounded border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n <Image className=\"size-4\" aria-hidden />\n </div>\n )\n}\n\n/**\n * The focal-point picker (EC-248/EC-301): click or **drag** anywhere on the image\n * to place the normalized 0–1 hotspot delivery threads to `object-position`\n * (EC-230). The marker follows the pointer live while dragging and only commits on\n * release (`onPreview` updates the marker, `onChange` commits). `onChange(null)`\n * resets to centre. Shows a placeholder until the image loads; renders a muted note\n * when the asset has no resolvable image.\n */\nexport function FocalPointPicker({\n record,\n point,\n onChange,\n onPreview,\n disabled,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n onChange: (point: FocalPoint | null) => void\n /** Called continuously while dragging to move the marker without committing. */\n onPreview?: (point: FocalPoint) => void\n disabled?: boolean\n}) {\n const marker = point ?? CENTER_FOCAL_POINT\n const dragging = useRef(false)\n const [loaded, setLoaded] = useState(false)\n\n if (!isResolvableImage(record)) {\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('assets.focalPoint.noPreview')}\n </Text>\n )\n }\n\n const pointFrom = (e: React.PointerEvent<HTMLElement>): FocalPoint =>\n focalPointFromPoint(e.currentTarget.getBoundingClientRect(), e.clientX, e.clientY)\n\n function onPointerDown(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled) return\n dragging.current = true\n e.currentTarget.setPointerCapture(e.pointerId)\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerMove(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n ;(onPreview ?? onChange)(pointFrom(e))\n }\n function onPointerUp(e: React.PointerEvent<HTMLButtonElement>) {\n if (disabled || !dragging.current) return\n dragging.current = false\n e.currentTarget.releasePointerCapture(e.pointerId)\n onChange(pointFrom(e)) // commit on release only\n }\n\n return (\n <button\n type=\"button\"\n aria-label={translate('assets.focalPoint.setLabel')}\n disabled={disabled}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n className={cn(\n 'relative block w-full touch-none select-none overflow-hidden rounded border border-border bg-muted',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',\n disabled ? 'cursor-default opacity-60' : 'cursor-crosshair',\n )}\n >\n {/* Width-constrained, natural aspect (no object-fit), so the button wraps\n the image tightly and the bounding rect equals the image box — the\n normalized pointer coordinates are then exact, with no letterbox offset. */}\n <img\n src={record.url}\n alt={record.alt ?? record.filename}\n draggable={false}\n onLoad={() => setLoaded(true)}\n className={cn('block h-auto w-full select-none transition-opacity', loaded ? 'opacity-100' : 'opacity-0')}\n />\n {!loaded ? (\n <span className=\"absolute inset-0 grid aspect-video place-items-center bg-muted\">\n <Image className=\"size-6 animate-pulse text-muted-foreground\" aria-hidden />\n </span>\n ) : (\n <span\n aria-hidden\n className=\"pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-[0_0_0_1.5px_rgba(0,0,0,0.55)] ring-1 ring-black/40\"\n style={{ left: `${marker.x * 100}%`, top: `${marker.y * 100}%` }}\n />\n )}\n </button>\n )\n}\n\n/** Common crop aspect ratios shown as focal-aware previews (EC-301). */\nconst FOCAL_CROP_RATIOS = [\n { label: '9:16', className: 'aspect-[9/16]' },\n { label: '3:4', className: 'aspect-[3/4]' },\n { label: '1:1', className: 'aspect-square' },\n { label: '4:3', className: 'aspect-[4/3]' },\n { label: '16:9', className: 'aspect-video' },\n] as const\n\n/**\n * A row of common-ratio crop previews (EC-301), each cropping the image with the\n * same `object-position` delivery uses — so the editor sees how the focal point\n * holds the subject across portrait/square/landscape crops. Updates live as the\n * point is dragged.\n */\nexport function FocalCropPreviews({\n record,\n point,\n}: {\n record: AssetRecord | undefined\n point: FocalPoint | undefined\n}) {\n if (!isResolvableImage(record)) return null\n const objectPosition = focalPointObjectPosition(point)\n return (\n <div className=\"flex items-end gap-2\">\n {FOCAL_CROP_RATIOS.map((ratio) => (\n <div key={ratio.label} className=\"flex min-w-0 flex-col items-center gap-1\">\n <div className={cn('h-14 overflow-hidden rounded border border-border bg-muted', ratio.className)}>\n <img\n src={record.url}\n alt=\"\"\n draggable={false}\n style={{ objectPosition }}\n className=\"h-full w-full object-cover\"\n />\n </div>\n <Text size=\"xs\" tone=\"subtle\">\n {ratio.label}\n </Text>\n </div>\n ))}\n </div>\n )\n}\n"]}
|
|
@@ -47,11 +47,17 @@ export declare function BlocksPanel({ context, open, onOpenChange, }: {
|
|
|
47
47
|
onOpenChange: (open: boolean) => void;
|
|
48
48
|
}): import("react").JSX.Element | null;
|
|
49
49
|
/**
|
|
50
|
-
* One component tile (exported for markup tests).
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* One component tile (exported for markup tests).
|
|
51
|
+
*
|
|
52
|
+
* EC-463: the tile's insert affordance is a REAL `<button>`. It used to be a
|
|
53
|
+
* `div[role="button"]` with a hand-rolled Enter/Space handler, because a tile
|
|
54
|
+
* carries two more controls (copy-exact, the variants chip) and nested
|
|
55
|
+
* `<button>`s are invalid HTML — a nested interactive control is also an
|
|
56
|
+
* accessibility failure, not just a validator complaint. The fix is the standard
|
|
57
|
+
* "stretched primary action" card: the button is an inset overlay UNDER the
|
|
58
|
+
* visual content, the content is `pointer-events-none`, and the two secondary
|
|
59
|
+
* controls sit above it. One frame, identical layout, native activation
|
|
60
|
+
* semantics — and `find role button` resolves it like every other control.
|
|
55
61
|
*/
|
|
56
62
|
export declare function BlockTileButton({ tile, onInsert, onCopy, onDragStart, onDragEnd, }: {
|
|
57
63
|
tile: BlockTile;
|
|
@@ -139,34 +139,28 @@ function TileGroup({ heading, tiles, onInsert, onCopy, onTileDragStart, onTileDr
|
|
|
139
139
|
return (_jsxs("section", { className: "mb-3", "aria-label": heading, children: [_jsxs(Row, { gap: "2", className: "px-1 py-1.5", children: [_jsx(Code, { tone: "muted", className: "capitalize", children: heading }), _jsx(Text, { size: "xs", tone: "muted", children: tiles.length })] }), _jsx("div", { className: "grid grid-cols-3 gap-2", children: tiles.map((tile) => (_jsx(BlockTileButton, { tile: tile, onInsert: onInsert, onCopy: onCopy, onDragStart: (e) => onTileDragStart(tile, e), onDragEnd: onTileDragEnd }, tile.key))) })] }));
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
|
-
* One component tile (exported for markup tests).
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
142
|
+
* One component tile (exported for markup tests).
|
|
143
|
+
*
|
|
144
|
+
* EC-463: the tile's insert affordance is a REAL `<button>`. It used to be a
|
|
145
|
+
* `div[role="button"]` with a hand-rolled Enter/Space handler, because a tile
|
|
146
|
+
* carries two more controls (copy-exact, the variants chip) and nested
|
|
147
|
+
* `<button>`s are invalid HTML — a nested interactive control is also an
|
|
148
|
+
* accessibility failure, not just a validator complaint. The fix is the standard
|
|
149
|
+
* "stretched primary action" card: the button is an inset overlay UNDER the
|
|
150
|
+
* visual content, the content is `pointer-events-none`, and the two secondary
|
|
151
|
+
* controls sit above it. One frame, identical layout, native activation
|
|
152
|
+
* semantics — and `find role button` resolves it like every other control.
|
|
147
153
|
*/
|
|
148
154
|
export function BlockTileButton({ tile, onInsert, onCopy, onDragStart, onDragEnd, }) {
|
|
149
155
|
const degraded = tile.issues.length > 0;
|
|
150
156
|
const defaultVariant = tile.variants[0];
|
|
151
|
-
return (_jsxs("div", {
|
|
152
|
-
// Only the tile itself: Enter/Space on the inner copy button or the
|
|
153
|
-
// variants chip must operate THAT control, never also insert.
|
|
154
|
-
if (event.target !== event.currentTarget)
|
|
155
|
-
return;
|
|
156
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
157
|
-
event.preventDefault();
|
|
158
|
-
onInsert(defaultVariant);
|
|
159
|
-
}
|
|
160
|
-
}, "data-block-tile": tile.key, "data-block-degraded": degraded || undefined, "aria-label": translate('builder.blocksPanel.insertComponentAria', { title: tile.title }), title: tile.description, className: cn(
|
|
157
|
+
return (_jsxs("div", { draggable: true, onDragStart: onDragStart, onDragEnd: onDragEnd, "data-block-tile": tile.key, "data-block-degraded": degraded || undefined, className: cn(
|
|
161
158
|
// AD-3: this outer border is the tile's ONE frame — the preview inset
|
|
162
159
|
// below is deliberately borderless.
|
|
163
|
-
'group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left
|
|
160
|
+
'group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left transition-colors', 'hover:border-ring/60 hover:bg-muted/60', degraded ? 'border-amber-500/50' : 'border-border'), children: [_jsx("button", { type: "button", "data-block-insert": tile.key, onClick: () => onInsert(defaultVariant), "aria-label": translate('builder.blocksPanel.insertComponentAria', { title: tile.title }), title: tile.description, className: "absolute inset-0 z-0 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/60" }), _jsx("button", { type: "button", "aria-label": translate('builder.blocksPanel.copyExactAria', {
|
|
164
161
|
title: tile.title,
|
|
165
162
|
variant: defaultVariant.name,
|
|
166
|
-
}), title: translate('builder.blocksPanel.copyExactConfiguration'), "data-block-copy": tile.key, onClick: (
|
|
167
|
-
event.stopPropagation();
|
|
168
|
-
onCopy(tile, defaultVariant);
|
|
169
|
-
}, className: cn('absolute right-1.5 top-1.5 z-10 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground', 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100'), children: _jsx(ClipboardCopy, { className: "size-3.5", "aria-hidden": true }) }), tile.previewUrl ? (_jsx(PreviewImageThumbnail, { src: tile.previewUrl, alt: tile.title })) : (_jsx(PlaceholderThumbnail, { title: tile.title })), _jsxs(Row, { gap: "1", className: "w-full min-w-0", children: [_jsx("span", { className: "truncate text-xs font-medium", children: tile.title }), tile.variants.length > 1 ? (_jsx(VariantsChip, { tile: tile, onInsert: onInsert, onCopy: onCopy })) : null] }), degraded ? (_jsxs(Row, { gap: "1", className: "w-full min-w-0 text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate text-[0.7rem]", title: tile.issues.join('\n'), children: tile.issues[0] })] })) : null] }));
|
|
163
|
+
}), title: translate('builder.blocksPanel.copyExactConfiguration'), "data-block-copy": tile.key, onClick: () => onCopy(tile, defaultVariant), className: cn('absolute right-1.5 top-1.5 z-20 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground', 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100'), children: _jsx(ClipboardCopy, { className: "size-3.5", "aria-hidden": true }) }), _jsxs("div", { className: "pointer-events-none relative z-10 flex flex-col gap-1.5", children: [tile.previewUrl ? (_jsx(PreviewImageThumbnail, { src: tile.previewUrl, alt: tile.title })) : (_jsx(PlaceholderThumbnail, { title: tile.title })), _jsxs(Row, { gap: "1", className: "w-full min-w-0", children: [_jsx("span", { className: "truncate text-xs font-medium", children: tile.title }), tile.variants.length > 1 ? (_jsx(VariantsChip, { tile: tile, onInsert: onInsert, onCopy: onCopy })) : null] }), degraded ? (_jsxs(Row, { gap: "1", className: "w-full min-w-0 text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate text-[0.7rem]", title: tile.issues.join('\n'), children: tile.issues[0] })] })) : null] })] }));
|
|
170
164
|
}
|
|
171
165
|
/**
|
|
172
166
|
* The compact variant affordance (EC-439 AD-1): an "n variants" chip in the
|
|
@@ -182,10 +176,10 @@ function VariantsChip({ tile, onInsert, onCopy, }) {
|
|
|
182
176
|
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-block-variants": tile.key, "data-block-variants-match": matched || undefined, "aria-label": translate('builder.blocksPanel.variantsAria', {
|
|
183
177
|
count: tile.variants.length,
|
|
184
178
|
title: tile.title,
|
|
185
|
-
}), title: laterVariantDegraded ? translate('builder.blocksPanel.variantIssuesHint') : undefined,
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
179
|
+
}), title: laterVariantDegraded ? translate('builder.blocksPanel.variantIssuesHint') : undefined, className: cn(
|
|
180
|
+
// EC-463: the tile body is `pointer-events-none` so the stretched
|
|
181
|
+
// insert button underneath owns plain clicks — the chip opts back in.
|
|
182
|
+
'pointer-events-auto relative z-20', 'ml-auto flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[0.65rem] leading-none transition-colors', matched
|
|
189
183
|
? 'bg-primary/15 text-primary'
|
|
190
184
|
: 'bg-muted text-muted-foreground hover:bg-muted/70 hover:text-foreground'), children: [laterVariantDegraded ? (_jsx(AlertTriangle, { "aria-hidden": true, className: "size-2.5 text-amber-600 dark:text-amber-400" })) : null, translate('builder.blocksPanel.variantCount', { count: tile.variants.length })] }) }), _jsxs(PopoverContent, { className: "w-64 p-1.5", onClick: (event) => event.stopPropagation(), children: [_jsx(Text, { size: "xs", tone: "muted", className: "px-1.5 pb-1 pt-0.5", children: translate('builder.blocksPanel.variantsHeading') }), _jsx(VariantRows, { tile: tile, onInsert: onInsert, onCopy: onCopy })] })] }));
|
|
191
185
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks-panel.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, ClipboardCopy, Search } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { InsertTarget } from '@elytracms/core/project-graph'\nimport {\n buildFragmentFromRegistryFixture,\n buildFragmentFromSubtree,\n} from '@elytracms/core/clipboard'\nimport type { ClipboardFragment } from '@elytracms/core/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentGeneratedPreviews } from '@elytracms/studio/lib/workspace'\nimport { useCollab } from '../collab/collab-context'\nimport { useStudioClipboard } from './clipboard-actions'\nimport { beginDrag, endDrag, onDragSessionEnd } from './drag-session'\nimport {\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Row,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useBuilder } from './builder-store'\nimport { blockGroups, presetTiles } from './blocks-model'\nimport type { BlockTile, BlockVariant } from './blocks-model'\nimport { PlaceholderThumbnail, PreviewImageThumbnail } from './fixture-thumbnail'\n\n/**\n * Blocks panel (EC-164, redesigned by EC-439): the visual insert surface.\n * Every canvas-eligible registry component is ONE tile; a component with 2+\n * fixtures carries a compact \"n variants\" chip opening a flyout with one\n * insert/copy row per fixture. Clicking a tile (or dragging it) inserts the\n * DEFAULT — the first declared fixture's configured subtree, else the seeded\n * manifest default — through the builder's `insert` action (one history entry).\n * Previews are artifacts (curated `previewImage` → generated artifact →\n * icon+name placeholder), never in-document runtime renders.\n */\n\nexport interface InsertContext {\n target: InsertTarget\n /** When set, only these component ids may be inserted into the slot (slot.allow). */\n slotAllow?: string[]\n /** Human label of where the insert lands. */\n destinationLabel: string\n}\n\n/**\n * \"Copy exact configuration\" (EC-175, vision 3.3): the EC-174 fragment for a\n * blocks-panel variant. Variants backed by a declared manifest fixture go\n * through `buildFragmentFromRegistryFixture` (the fixture's exact\n * configuration); implicit-default and preset variants fragment the same\n * subtree they insert. Both paths share `buildFragmentFromSubtree`, so the\n * fragment shape — and the paste path — is identical to copying a live canvas\n * instance.\n */\nexport function tileFragment(\n studio: StudioRegistry,\n tile: BlockTile,\n variant: BlockVariant,\n projectId: string,\n): ClipboardFragment | null {\n const manifest = studio.registry.getManifest(tile.componentId)\n const fixture = manifest?.fixtures?.find((candidate) => candidate.name === variant.name)\n if (manifest && fixture) {\n const built = buildFragmentFromRegistryFixture(manifest, fixture, {\n source: { projectId },\n })\n return built.ok ? built.fragment : null\n }\n return buildFragmentFromSubtree(variant.build(), { source: { projectId } })\n}\n\n/**\n * The Insert dialog never dismisses on pointer interactions outside its\n * content. Rows scrolled below the list's fold keep their layout position\n * directly above the modal overlay, so a coordinate-targeted click on a\n * clipped row (agents click what the accessibility tree lists; humans can hit\n * the boundary too) lands on the overlay — with default Radix behaviour that\n * pointerdown dismissed the dialog and silently dropped the insert. Cancel\n * stays explicit: Escape or the Close button.\n */\nexport function preventOutsideDismiss(event: { preventDefault: () => void }): void {\n event.preventDefault()\n}\n\nexport function BlocksPanel({\n context,\n open,\n onOpenChange,\n}: {\n context: InsertContext | null\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const collab = useCollab()\n const clipboard = useStudioClipboard()\n const [query, setQuery] = useState('')\n\n useEffect(() => {\n if (open) setQuery('')\n }, [open])\n\n const slotAllow = context?.slotAllow\n const trimmed = query.trim()\n // EC-439 AD-2: the generated-preview seam (EC-440) — a config-sourced pure\n // lookup, `undefined` until a consumer wires `defineStudio({ previews })`.\n const generatedPreview = resolveDeploymentGeneratedPreviews()\n const groups = useMemo(\n () => blockGroups(studio, { query: trimmed || undefined, slotAllow }, generatedPreview),\n [studio, trimmed, slotAllow, generatedPreview],\n )\n // Layout helpers keep their dedicated group while browsing (hidden once a\n // query narrows the panel to registry matches), filtered by slot.allow.\n const presets = useMemo(\n () => (trimmed ? [] : presetTiles(slotAllow)),\n [trimmed, slotAllow],\n )\n\n // Drag-to-insert (EC-160): a tile is also a drag source. While the drag is\n // live the modal (content + overlay + Radix body pointer lock) gets out of\n // the way so the canvas and the page tree can receive the drop; the session\n // ending restores it — and a successful drop closes the panel. The drop\n // surfaces dispatch the single insert action; the panel only carries the\n // payload (`tile.build`, the same default-subtree model click-insert uses).\n const dragRestoreRef = useRef<(() => void) | null>(null)\n useEffect(\n () =>\n onDragSessionEnd((outcome, session) => {\n if (session.source !== 'blocks') return\n dragRestoreRef.current?.()\n dragRestoreRef.current = null\n if (outcome === 'dropped') onOpenChange(false)\n }),\n [onOpenChange],\n )\n\n if (!context) return null\n\n function insertVariant(variant: BlockVariant) {\n builder.dispatch({ type: 'insert', target: context!.target, node: variant.build() })\n onOpenChange(false)\n }\n\n // \"Copy exact configuration\" (EC-175): same fragment + paste path as\n // copying a live canvas instance, per variant.\n function copyVariant(tile: BlockTile, variant: BlockVariant) {\n const fragment = tileFragment(studio, tile, variant, collab.projectId)\n if (fragment) clipboard.copyFragment(fragment, `${tile.title} — ${variant.name}`)\n }\n\n function handleTileDragStart(tile: BlockTile, e: React.DragEvent) {\n beginDrag(\n {\n subject: { kind: 'component', componentId: tile.componentId },\n source: 'blocks',\n // AD-4: one tile per component — the drag labels the component title.\n label: tile.title,\n buildNode: tile.build,\n },\n e.dataTransfer,\n )\n // Radix's modal layer locks pointer events on the body and the overlay\n // sits above every drop surface. Lift both for the duration of the drag —\n // after a tick, so the browser captures the tile as the drag image first.\n const content = (e.currentTarget as HTMLElement).closest('[role=\"dialog\"]') as HTMLElement | null\n const overlay = (content?.previousElementSibling ?? null) as HTMLElement | null\n const previousBodyPointerEvents = document.body.style.pointerEvents\n const hide = window.setTimeout(() => {\n document.body.style.pointerEvents = 'auto'\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = 'none'\n el.style.opacity = '0'\n }\n }\n }, 0)\n dragRestoreRef.current = () => {\n window.clearTimeout(hide)\n document.body.style.pointerEvents = previousBodyPointerEvents\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = ''\n el.style.opacity = ''\n }\n }\n }\n }\n\n function handleTileDragEnd() {\n // Trailing dragend after a handled drop is a no-op (endDrag is idempotent).\n endDrag('cancelled')\n }\n\n const empty = groups.length === 0 && presets.length === 0\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\" onInteractOutside={preventOutsideDismiss}>\n <DialogHeader>\n <DialogTitle>{translate('builder.blocksPanel.title')}</DialogTitle>\n <DialogDescription>\n Adds into <Code tone=\"muted\">{context.destinationLabel}</Code>.\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\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('builder.blocksPanel.searchPlaceholder')}\n aria-label={translate('builder.blocksPanel.searchAria')}\n className=\"pl-8\"\n />\n </div>\n\n {/* Viewport-bounded so the dialog never extends past the fold on short windows\n (a grid taller than the screen would put tiles outside the clickable area). */}\n <div className=\"max-h-[min(30rem,calc(100dvh-14rem))] overflow-auto pr-1\">\n {presets.length > 0 ? (\n <TileGroup\n heading={translate('builder.blocksPanel.layoutHelpers')}\n tiles={presets}\n onInsert={insertVariant}\n onCopy={copyVariant}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ) : null}\n {groups.map((group) => (\n <TileGroup\n key={group.category}\n heading={group.category}\n tiles={group.tiles}\n onInsert={insertVariant}\n onCopy={copyVariant}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ))}\n {empty ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {slotAllow\n ? translate('builder.blocksPanel.noAllowed')\n : translate('builder.blocksPanel.noMatch', { query })}\n </Text>\n ) : null}\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction TileGroup({\n heading,\n tiles,\n onInsert,\n onCopy,\n onTileDragStart,\n onTileDragEnd,\n}: {\n heading: string\n tiles: BlockTile[]\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n onTileDragStart: (tile: BlockTile, e: React.DragEvent) => void\n onTileDragEnd: () => void\n}) {\n return (\n <section className=\"mb-3\" aria-label={heading}>\n <Row gap=\"2\" className=\"px-1 py-1.5\">\n <Code tone=\"muted\" className=\"capitalize\">\n {heading}\n </Code>\n <Text size=\"xs\" tone=\"muted\">\n {tiles.length}\n </Text>\n </Row>\n <div className=\"grid grid-cols-3 gap-2\">\n {tiles.map((tile) => (\n <BlockTileButton\n key={tile.key}\n tile={tile}\n onInsert={onInsert}\n onCopy={onCopy}\n onDragStart={(e) => onTileDragStart(tile, e)}\n onDragEnd={onTileDragEnd}\n />\n ))}\n </div>\n </section>\n )\n}\n\n/**\n * One component tile (exported for markup tests). Not a native `<button>`: the\n * tile CONTAINS interactive controls (the copy-exact button and the variants\n * chip), and nested `<button>`s are invalid HTML — so it stays a keyboard-\n * operable `role=\"button\"` element whose own key handler ignores events\n * bubbling from those inner controls.\n */\nexport function BlockTileButton({\n tile,\n onInsert,\n onCopy,\n onDragStart,\n onDragEnd,\n}: {\n tile: BlockTile\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n onDragStart: (e: React.DragEvent) => void\n onDragEnd: () => void\n}) {\n const degraded = tile.issues.length > 0\n const defaultVariant = tile.variants[0]!\n return (\n <div\n role=\"button\"\n tabIndex={0}\n draggable\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n onClick={() => onInsert(defaultVariant)}\n onKeyDown={(event) => {\n // Only the tile itself: Enter/Space on the inner copy button or the\n // variants chip must operate THAT control, never also insert.\n if (event.target !== event.currentTarget) return\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onInsert(defaultVariant)\n }\n }}\n data-block-tile={tile.key}\n data-block-degraded={degraded || undefined}\n aria-label={translate('builder.blocksPanel.insertComponentAria', { title: tile.title })}\n title={tile.description}\n className={cn(\n // AD-3: this outer border is the tile's ONE frame — the preview inset\n // below is deliberately borderless.\n 'group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left outline-none transition-colors',\n 'hover:border-ring/60 hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring/60',\n degraded ? 'border-amber-500/50' : 'border-border',\n )}\n >\n {/* Copy exact configuration (EC-175/vision 3.3): the EC-174 fragment of\n the tile's DEFAULT variant; per-fixture copies live in the flyout. */}\n <button\n type=\"button\"\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: defaultVariant.name,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n data-block-copy={tile.key}\n onClick={(event) => {\n event.stopPropagation()\n onCopy(tile, defaultVariant)\n }}\n className={cn(\n 'absolute right-1.5 top-1.5 z-10 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground',\n 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100',\n )}\n >\n <ClipboardCopy className=\"size-3.5\" aria-hidden />\n </button>\n {/* AD-2 preview resolution: curated/generated artifact, else the\n icon+name placeholder — never an in-document live render. */}\n {tile.previewUrl ? (\n <PreviewImageThumbnail src={tile.previewUrl} alt={tile.title} />\n ) : (\n <PlaceholderThumbnail title={tile.title} />\n )}\n <Row gap=\"1\" className=\"w-full min-w-0\">\n <span className=\"truncate text-xs font-medium\">{tile.title}</span>\n {tile.variants.length > 1 ? (\n <VariantsChip tile={tile} onInsert={onInsert} onCopy={onCopy} />\n ) : null}\n </Row>\n {degraded ? (\n <Row gap=\"1\" className=\"w-full min-w-0 text-amber-700 dark:text-amber-400\">\n <AlertTriangle aria-hidden className=\"size-3 shrink-0\" />\n <span className=\"truncate text-[0.7rem]\" title={tile.issues.join('\\n')}>\n {tile.issues[0]}\n </span>\n </Row>\n ) : null}\n </div>\n )\n}\n\n/**\n * The compact variant affordance (EC-439 AD-1): an \"n variants\" chip in the\n * tile footer opening a flyout of per-fixture rows. When the active search\n * query matched fixture names ({@link BlockTile.matchedVariants}) the chip is\n * highlighted so \"found by fixture name\" is visible at the grid.\n */\nfunction VariantsChip({\n tile,\n onInsert,\n onCopy,\n}: {\n tile: BlockTile\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n}) {\n const matched = (tile.matchedVariants?.length ?? 0) > 0\n // The tile already surfaces the DEFAULT variant's issues; the chip warns\n // when a deeper variant is degraded so it is visible before the flyout opens.\n const laterVariantDegraded = tile.variants.slice(1).some((variant) => variant.issues.length > 0)\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n data-block-variants={tile.key}\n data-block-variants-match={matched || undefined}\n aria-label={translate('builder.blocksPanel.variantsAria', {\n count: tile.variants.length,\n title: tile.title,\n })}\n title={laterVariantDegraded ? translate('builder.blocksPanel.variantIssuesHint') : undefined}\n // The chip sits inside the role=\"button\" tile: opening the flyout\n // must never ALSO insert the default.\n onClick={(event) => event.stopPropagation()}\n className={cn(\n 'ml-auto flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[0.65rem] leading-none transition-colors',\n matched\n ? 'bg-primary/15 text-primary'\n : 'bg-muted text-muted-foreground hover:bg-muted/70 hover:text-foreground',\n )}\n >\n {laterVariantDegraded ? (\n <AlertTriangle aria-hidden className=\"size-2.5 text-amber-600 dark:text-amber-400\" />\n ) : null}\n {translate('builder.blocksPanel.variantCount', { count: tile.variants.length })}\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-64 p-1.5\" onClick={(event) => event.stopPropagation()}>\n <Text size=\"xs\" tone=\"muted\" className=\"px-1.5 pb-1 pt-0.5\">\n {translate('builder.blocksPanel.variantsHeading')}\n </Text>\n <VariantRows tile={tile} onInsert={onInsert} onCopy={onCopy} />\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * The flyout's variant rows (exported for markup tests): one insert row +\n * copy-exact button per fixture (EC-175 semantics per variant). Rows matched\n * by the active fixture-name search are highlighted.\n */\nexport function VariantRows({\n tile,\n onInsert,\n onCopy,\n}: {\n tile: BlockTile\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n}) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n {tile.variants.map((variant) => {\n const matched = tile.matchedVariants?.includes(variant.name) ?? false\n const variantDegraded = variant.issues.length > 0\n return (\n <Row key={variant.name} gap=\"1\" className=\"w-full min-w-0\">\n <button\n type=\"button\"\n data-block-variant={`${tile.componentId}::${variant.name}`}\n data-block-variant-match={matched || undefined}\n data-block-variant-degraded={variantDegraded || undefined}\n aria-label={translate('builder.blocksPanel.insertTileAria', {\n title: tile.title,\n variant: variant.name,\n })}\n title={variantDegraded ? variant.issues.join('\\n') : undefined}\n onClick={() => onInsert(variant)}\n className={cn(\n 'flex min-w-0 flex-1 items-center gap-2 rounded-md px-1.5 py-1 text-left text-xs transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none',\n matched ? 'text-primary' : 'text-foreground',\n )}\n >\n {variant.previewUrl ? (\n <img\n src={variant.previewUrl}\n alt=\"\"\n draggable={false}\n className=\"pointer-events-none h-7 w-11 shrink-0 select-none rounded bg-muted/40 object-cover\"\n />\n ) : null}\n <span className=\"truncate\">{variant.name}</span>\n {variantDegraded ? (\n <AlertTriangle\n aria-hidden\n className=\"ml-auto size-3 shrink-0 text-amber-600 dark:text-amber-400\"\n />\n ) : null}\n </button>\n <button\n type=\"button\"\n data-block-variant-copy={`${tile.componentId}::${variant.name}`}\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: variant.name,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n onClick={() => onCopy(tile, variant)}\n className=\"shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:outline-none\"\n >\n <ClipboardCopy className=\"size-3.5\" aria-hidden />\n </button>\n </Row>\n )\n })}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"blocks-panel.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, ClipboardCopy, Search } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { InsertTarget } from '@elytracms/core/project-graph'\nimport {\n buildFragmentFromRegistryFixture,\n buildFragmentFromSubtree,\n} from '@elytracms/core/clipboard'\nimport type { ClipboardFragment } from '@elytracms/core/clipboard'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentGeneratedPreviews } from '@elytracms/studio/lib/workspace'\nimport { useCollab } from '../collab/collab-context'\nimport { useStudioClipboard } from './clipboard-actions'\nimport { beginDrag, endDrag, onDragSessionEnd } from './drag-session'\nimport {\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Row,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useBuilder } from './builder-store'\nimport { blockGroups, presetTiles } from './blocks-model'\nimport type { BlockTile, BlockVariant } from './blocks-model'\nimport { PlaceholderThumbnail, PreviewImageThumbnail } from './fixture-thumbnail'\n\n/**\n * Blocks panel (EC-164, redesigned by EC-439): the visual insert surface.\n * Every canvas-eligible registry component is ONE tile; a component with 2+\n * fixtures carries a compact \"n variants\" chip opening a flyout with one\n * insert/copy row per fixture. Clicking a tile (or dragging it) inserts the\n * DEFAULT — the first declared fixture's configured subtree, else the seeded\n * manifest default — through the builder's `insert` action (one history entry).\n * Previews are artifacts (curated `previewImage` → generated artifact →\n * icon+name placeholder), never in-document runtime renders.\n */\n\nexport interface InsertContext {\n target: InsertTarget\n /** When set, only these component ids may be inserted into the slot (slot.allow). */\n slotAllow?: string[]\n /** Human label of where the insert lands. */\n destinationLabel: string\n}\n\n/**\n * \"Copy exact configuration\" (EC-175, vision 3.3): the EC-174 fragment for a\n * blocks-panel variant. Variants backed by a declared manifest fixture go\n * through `buildFragmentFromRegistryFixture` (the fixture's exact\n * configuration); implicit-default and preset variants fragment the same\n * subtree they insert. Both paths share `buildFragmentFromSubtree`, so the\n * fragment shape — and the paste path — is identical to copying a live canvas\n * instance.\n */\nexport function tileFragment(\n studio: StudioRegistry,\n tile: BlockTile,\n variant: BlockVariant,\n projectId: string,\n): ClipboardFragment | null {\n const manifest = studio.registry.getManifest(tile.componentId)\n const fixture = manifest?.fixtures?.find((candidate) => candidate.name === variant.name)\n if (manifest && fixture) {\n const built = buildFragmentFromRegistryFixture(manifest, fixture, {\n source: { projectId },\n })\n return built.ok ? built.fragment : null\n }\n return buildFragmentFromSubtree(variant.build(), { source: { projectId } })\n}\n\n/**\n * The Insert dialog never dismisses on pointer interactions outside its\n * content. Rows scrolled below the list's fold keep their layout position\n * directly above the modal overlay, so a coordinate-targeted click on a\n * clipped row (agents click what the accessibility tree lists; humans can hit\n * the boundary too) lands on the overlay — with default Radix behaviour that\n * pointerdown dismissed the dialog and silently dropped the insert. Cancel\n * stays explicit: Escape or the Close button.\n */\nexport function preventOutsideDismiss(event: { preventDefault: () => void }): void {\n event.preventDefault()\n}\n\nexport function BlocksPanel({\n context,\n open,\n onOpenChange,\n}: {\n context: InsertContext | null\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const builder = useBuilder()\n const studio = getStudioRegistry()\n const collab = useCollab()\n const clipboard = useStudioClipboard()\n const [query, setQuery] = useState('')\n\n useEffect(() => {\n if (open) setQuery('')\n }, [open])\n\n const slotAllow = context?.slotAllow\n const trimmed = query.trim()\n // EC-439 AD-2: the generated-preview seam (EC-440) — a config-sourced pure\n // lookup, `undefined` until a consumer wires `defineStudio({ previews })`.\n const generatedPreview = resolveDeploymentGeneratedPreviews()\n const groups = useMemo(\n () => blockGroups(studio, { query: trimmed || undefined, slotAllow }, generatedPreview),\n [studio, trimmed, slotAllow, generatedPreview],\n )\n // Layout helpers keep their dedicated group while browsing (hidden once a\n // query narrows the panel to registry matches), filtered by slot.allow.\n const presets = useMemo(\n () => (trimmed ? [] : presetTiles(slotAllow)),\n [trimmed, slotAllow],\n )\n\n // Drag-to-insert (EC-160): a tile is also a drag source. While the drag is\n // live the modal (content + overlay + Radix body pointer lock) gets out of\n // the way so the canvas and the page tree can receive the drop; the session\n // ending restores it — and a successful drop closes the panel. The drop\n // surfaces dispatch the single insert action; the panel only carries the\n // payload (`tile.build`, the same default-subtree model click-insert uses).\n const dragRestoreRef = useRef<(() => void) | null>(null)\n useEffect(\n () =>\n onDragSessionEnd((outcome, session) => {\n if (session.source !== 'blocks') return\n dragRestoreRef.current?.()\n dragRestoreRef.current = null\n if (outcome === 'dropped') onOpenChange(false)\n }),\n [onOpenChange],\n )\n\n if (!context) return null\n\n function insertVariant(variant: BlockVariant) {\n builder.dispatch({ type: 'insert', target: context!.target, node: variant.build() })\n onOpenChange(false)\n }\n\n // \"Copy exact configuration\" (EC-175): same fragment + paste path as\n // copying a live canvas instance, per variant.\n function copyVariant(tile: BlockTile, variant: BlockVariant) {\n const fragment = tileFragment(studio, tile, variant, collab.projectId)\n if (fragment) clipboard.copyFragment(fragment, `${tile.title} — ${variant.name}`)\n }\n\n function handleTileDragStart(tile: BlockTile, e: React.DragEvent) {\n beginDrag(\n {\n subject: { kind: 'component', componentId: tile.componentId },\n source: 'blocks',\n // AD-4: one tile per component — the drag labels the component title.\n label: tile.title,\n buildNode: tile.build,\n },\n e.dataTransfer,\n )\n // Radix's modal layer locks pointer events on the body and the overlay\n // sits above every drop surface. Lift both for the duration of the drag —\n // after a tick, so the browser captures the tile as the drag image first.\n const content = (e.currentTarget as HTMLElement).closest('[role=\"dialog\"]') as HTMLElement | null\n const overlay = (content?.previousElementSibling ?? null) as HTMLElement | null\n const previousBodyPointerEvents = document.body.style.pointerEvents\n const hide = window.setTimeout(() => {\n document.body.style.pointerEvents = 'auto'\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = 'none'\n el.style.opacity = '0'\n }\n }\n }, 0)\n dragRestoreRef.current = () => {\n window.clearTimeout(hide)\n document.body.style.pointerEvents = previousBodyPointerEvents\n for (const el of [content, overlay]) {\n if (el) {\n el.style.pointerEvents = ''\n el.style.opacity = ''\n }\n }\n }\n }\n\n function handleTileDragEnd() {\n // Trailing dragend after a handled drop is a no-op (endDrag is idempotent).\n endDrag('cancelled')\n }\n\n const empty = groups.length === 0 && presets.length === 0\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-3xl\" onInteractOutside={preventOutsideDismiss}>\n <DialogHeader>\n <DialogTitle>{translate('builder.blocksPanel.title')}</DialogTitle>\n <DialogDescription>\n Adds into <Code tone=\"muted\">{context.destinationLabel}</Code>.\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\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('builder.blocksPanel.searchPlaceholder')}\n aria-label={translate('builder.blocksPanel.searchAria')}\n className=\"pl-8\"\n />\n </div>\n\n {/* Viewport-bounded so the dialog never extends past the fold on short windows\n (a grid taller than the screen would put tiles outside the clickable area). */}\n <div className=\"max-h-[min(30rem,calc(100dvh-14rem))] overflow-auto pr-1\">\n {presets.length > 0 ? (\n <TileGroup\n heading={translate('builder.blocksPanel.layoutHelpers')}\n tiles={presets}\n onInsert={insertVariant}\n onCopy={copyVariant}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ) : null}\n {groups.map((group) => (\n <TileGroup\n key={group.category}\n heading={group.category}\n tiles={group.tiles}\n onInsert={insertVariant}\n onCopy={copyVariant}\n onTileDragStart={handleTileDragStart}\n onTileDragEnd={handleTileDragEnd}\n />\n ))}\n {empty ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {slotAllow\n ? translate('builder.blocksPanel.noAllowed')\n : translate('builder.blocksPanel.noMatch', { query })}\n </Text>\n ) : null}\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction TileGroup({\n heading,\n tiles,\n onInsert,\n onCopy,\n onTileDragStart,\n onTileDragEnd,\n}: {\n heading: string\n tiles: BlockTile[]\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n onTileDragStart: (tile: BlockTile, e: React.DragEvent) => void\n onTileDragEnd: () => void\n}) {\n return (\n <section className=\"mb-3\" aria-label={heading}>\n <Row gap=\"2\" className=\"px-1 py-1.5\">\n <Code tone=\"muted\" className=\"capitalize\">\n {heading}\n </Code>\n <Text size=\"xs\" tone=\"muted\">\n {tiles.length}\n </Text>\n </Row>\n <div className=\"grid grid-cols-3 gap-2\">\n {tiles.map((tile) => (\n <BlockTileButton\n key={tile.key}\n tile={tile}\n onInsert={onInsert}\n onCopy={onCopy}\n onDragStart={(e) => onTileDragStart(tile, e)}\n onDragEnd={onTileDragEnd}\n />\n ))}\n </div>\n </section>\n )\n}\n\n/**\n * One component tile (exported for markup tests).\n *\n * EC-463: the tile's insert affordance is a REAL `<button>`. It used to be a\n * `div[role=\"button\"]` with a hand-rolled Enter/Space handler, because a tile\n * carries two more controls (copy-exact, the variants chip) and nested\n * `<button>`s are invalid HTML — a nested interactive control is also an\n * accessibility failure, not just a validator complaint. The fix is the standard\n * \"stretched primary action\" card: the button is an inset overlay UNDER the\n * visual content, the content is `pointer-events-none`, and the two secondary\n * controls sit above it. One frame, identical layout, native activation\n * semantics — and `find role button` resolves it like every other control.\n */\nexport function BlockTileButton({\n tile,\n onInsert,\n onCopy,\n onDragStart,\n onDragEnd,\n}: {\n tile: BlockTile\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n onDragStart: (e: React.DragEvent) => void\n onDragEnd: () => void\n}) {\n const degraded = tile.issues.length > 0\n const defaultVariant = tile.variants[0]!\n return (\n <div\n draggable\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n data-block-tile={tile.key}\n data-block-degraded={degraded || undefined}\n className={cn(\n // AD-3: this outer border is the tile's ONE frame — the preview inset\n // below is deliberately borderless.\n 'group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left transition-colors',\n 'hover:border-ring/60 hover:bg-muted/60',\n degraded ? 'border-amber-500/50' : 'border-border',\n )}\n >\n {/* The tile's primary action, covering the whole frame. */}\n <button\n type=\"button\"\n data-block-insert={tile.key}\n onClick={() => onInsert(defaultVariant)}\n aria-label={translate('builder.blocksPanel.insertComponentAria', { title: tile.title })}\n title={tile.description}\n className=\"absolute inset-0 z-0 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/60\"\n />\n {/* Copy exact configuration (EC-175/vision 3.3): the EC-174 fragment of\n the tile's DEFAULT variant; per-fixture copies live in the flyout. */}\n <button\n type=\"button\"\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: defaultVariant.name,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n data-block-copy={tile.key}\n onClick={() => onCopy(tile, defaultVariant)}\n className={cn(\n 'absolute right-1.5 top-1.5 z-20 rounded-md border border-border bg-popover/90 p-1 text-muted-foreground',\n 'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/tile:opacity-100',\n )}\n >\n <ClipboardCopy className=\"size-3.5\" aria-hidden />\n </button>\n {/* AD-2 preview resolution: curated/generated artifact, else the\n icon+name placeholder — never an in-document live render. The visual\n body never takes the pointer: clicks belong to the insert button. */}\n <div className=\"pointer-events-none relative z-10 flex flex-col gap-1.5\">\n {tile.previewUrl ? (\n <PreviewImageThumbnail src={tile.previewUrl} alt={tile.title} />\n ) : (\n <PlaceholderThumbnail title={tile.title} />\n )}\n <Row gap=\"1\" className=\"w-full min-w-0\">\n <span className=\"truncate text-xs font-medium\">{tile.title}</span>\n {tile.variants.length > 1 ? (\n <VariantsChip tile={tile} onInsert={onInsert} onCopy={onCopy} />\n ) : null}\n </Row>\n {degraded ? (\n <Row gap=\"1\" className=\"w-full min-w-0 text-amber-700 dark:text-amber-400\">\n <AlertTriangle aria-hidden className=\"size-3 shrink-0\" />\n <span className=\"truncate text-[0.7rem]\" title={tile.issues.join('\\n')}>\n {tile.issues[0]}\n </span>\n </Row>\n ) : null}\n </div>\n </div>\n )\n}\n\n/**\n * The compact variant affordance (EC-439 AD-1): an \"n variants\" chip in the\n * tile footer opening a flyout of per-fixture rows. When the active search\n * query matched fixture names ({@link BlockTile.matchedVariants}) the chip is\n * highlighted so \"found by fixture name\" is visible at the grid.\n */\nfunction VariantsChip({\n tile,\n onInsert,\n onCopy,\n}: {\n tile: BlockTile\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n}) {\n const matched = (tile.matchedVariants?.length ?? 0) > 0\n // The tile already surfaces the DEFAULT variant's issues; the chip warns\n // when a deeper variant is degraded so it is visible before the flyout opens.\n const laterVariantDegraded = tile.variants.slice(1).some((variant) => variant.issues.length > 0)\n return (\n <Popover>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n data-block-variants={tile.key}\n data-block-variants-match={matched || undefined}\n aria-label={translate('builder.blocksPanel.variantsAria', {\n count: tile.variants.length,\n title: tile.title,\n })}\n title={laterVariantDegraded ? translate('builder.blocksPanel.variantIssuesHint') : undefined}\n className={cn(\n // EC-463: the tile body is `pointer-events-none` so the stretched\n // insert button underneath owns plain clicks — the chip opts back in.\n 'pointer-events-auto relative z-20',\n 'ml-auto flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[0.65rem] leading-none transition-colors',\n matched\n ? 'bg-primary/15 text-primary'\n : 'bg-muted text-muted-foreground hover:bg-muted/70 hover:text-foreground',\n )}\n >\n {laterVariantDegraded ? (\n <AlertTriangle aria-hidden className=\"size-2.5 text-amber-600 dark:text-amber-400\" />\n ) : null}\n {translate('builder.blocksPanel.variantCount', { count: tile.variants.length })}\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-64 p-1.5\" onClick={(event) => event.stopPropagation()}>\n <Text size=\"xs\" tone=\"muted\" className=\"px-1.5 pb-1 pt-0.5\">\n {translate('builder.blocksPanel.variantsHeading')}\n </Text>\n <VariantRows tile={tile} onInsert={onInsert} onCopy={onCopy} />\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * The flyout's variant rows (exported for markup tests): one insert row +\n * copy-exact button per fixture (EC-175 semantics per variant). Rows matched\n * by the active fixture-name search are highlighted.\n */\nexport function VariantRows({\n tile,\n onInsert,\n onCopy,\n}: {\n tile: BlockTile\n onInsert: (variant: BlockVariant) => void\n onCopy: (tile: BlockTile, variant: BlockVariant) => void\n}) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n {tile.variants.map((variant) => {\n const matched = tile.matchedVariants?.includes(variant.name) ?? false\n const variantDegraded = variant.issues.length > 0\n return (\n <Row key={variant.name} gap=\"1\" className=\"w-full min-w-0\">\n <button\n type=\"button\"\n data-block-variant={`${tile.componentId}::${variant.name}`}\n data-block-variant-match={matched || undefined}\n data-block-variant-degraded={variantDegraded || undefined}\n aria-label={translate('builder.blocksPanel.insertTileAria', {\n title: tile.title,\n variant: variant.name,\n })}\n title={variantDegraded ? variant.issues.join('\\n') : undefined}\n onClick={() => onInsert(variant)}\n className={cn(\n 'flex min-w-0 flex-1 items-center gap-2 rounded-md px-1.5 py-1 text-left text-xs transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none',\n matched ? 'text-primary' : 'text-foreground',\n )}\n >\n {variant.previewUrl ? (\n <img\n src={variant.previewUrl}\n alt=\"\"\n draggable={false}\n className=\"pointer-events-none h-7 w-11 shrink-0 select-none rounded bg-muted/40 object-cover\"\n />\n ) : null}\n <span className=\"truncate\">{variant.name}</span>\n {variantDegraded ? (\n <AlertTriangle\n aria-hidden\n className=\"ml-auto size-3 shrink-0 text-amber-600 dark:text-amber-400\"\n />\n ) : null}\n </button>\n <button\n type=\"button\"\n data-block-variant-copy={`${tile.componentId}::${variant.name}`}\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: variant.name,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n onClick={() => onCopy(tile, variant)}\n className=\"shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:outline-none\"\n >\n <ClipboardCopy className=\"size-3.5\" aria-hidden />\n </button>\n </Row>\n )\n })}\n </div>\n )\n}\n"]}
|
|
@@ -57,6 +57,14 @@ export interface CanvasRenderContextDeps {
|
|
|
57
57
|
richEditEpochs: Record<string, number>;
|
|
58
58
|
unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null;
|
|
59
59
|
localeFallbackOf(node: ComponentNode): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* EC-460 (never-silent): a human-readable message when the node declares a
|
|
62
|
+
* `listing` whose built query is STATICALLY invalid against the active session
|
|
63
|
+
* schema (unknown collection, non-filterable filter/sort field) — the canvas
|
|
64
|
+
* then shows a visible notice above the block instead of an empty grid that
|
|
65
|
+
* is indistinguishable from "no content yet". `null` = healthy (or no listing).
|
|
66
|
+
*/
|
|
67
|
+
listingIssueOf(node: ComponentNode): string | null;
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
62
70
|
* Assemble the canvas `RenderContext`. Pure (no hooks): the same deps produce
|
|
@@ -91,6 +99,20 @@ export declare function useCanvasRenderContext(args: {
|
|
|
91
99
|
* selectable; the inspector's `element` relation chip (EC-296) deep-links to
|
|
92
100
|
* the reusable to edit or publish it.
|
|
93
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* In-canvas notice for a listing block whose query is statically invalid
|
|
104
|
+
* against the active session schema (EC-460 — the never-silent rule): the
|
|
105
|
+
* message renders ABOVE the block (which keeps its own empty state), so a
|
|
106
|
+
* misdeclared `listing` manifest (non-filterable sort/filter field, unknown
|
|
107
|
+
* collection) is an explicit, visible state — never an empty grid an editor
|
|
108
|
+
* can't tell apart from "no content yet". Rendered INTO the iframe (project
|
|
109
|
+
* stylesheet only), so it uses inline styles + the `.ec-chrome` containment
|
|
110
|
+
* recipe, exactly like {@link UnresolvedReusablePlaceholder}. Exported for the
|
|
111
|
+
* chrome contract test.
|
|
112
|
+
*/
|
|
113
|
+
export declare function CanvasListingIssueNotice({ message }: {
|
|
114
|
+
message: string;
|
|
115
|
+
}): import("react").JSX.Element;
|
|
94
116
|
export declare function UnresolvedReusablePlaceholder({ status }: {
|
|
95
117
|
status: 'unpublished' | 'missing';
|
|
96
118
|
}): import("react").JSX.Element;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useMemo } from 'react';
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { listingQueryOf } from '@elytracms/core/runtime-renderer';
|
|
3
4
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
-
import { resolveCanvasLink, resolveCanvasListing, resolveCanvasRelation, useData, } from '@elytracms/studio/lib/data';
|
|
5
|
+
import { acquireContentSessionConsumer, contentSessionEpoch, resolveCanvasLink, resolveCanvasListing, resolveCanvasRelation, subscribeContentSession, useData, validateCanvasListingQuery, } from '@elytracms/studio/lib/data';
|
|
5
6
|
import { cmsBindingFallback, useStudioLocale } from '@elytracms/studio/lib/locale';
|
|
6
7
|
import { useOptionalAssets } from '../assets/assets-store.js';
|
|
7
8
|
import { useReuse } from './reuse-context.js';
|
|
@@ -83,7 +84,10 @@ export function createCanvasRenderContext(deps) {
|
|
|
83
84
|
// transclusion; the node stays selectable, so its inspector relation chip
|
|
84
85
|
// (EC-296) deep-links to the reusable to edit/publish it.
|
|
85
86
|
const unresolved = deps.unresolvedReuse(node);
|
|
86
|
-
|
|
87
|
+
// EC-460: a config-invalid listing query gets a visible notice ABOVE the
|
|
88
|
+
// block (which keeps its own empty state below) — never a silent empty grid.
|
|
89
|
+
const listingIssue = deps.listingIssueOf(node);
|
|
90
|
+
return (_jsx("div", { "data-ec-node-id": node.id, className: "ec-editable", "data-selected": node.id === deps.selectedId || undefined, "data-hovered": node.id === deps.hoveredId || undefined, "data-ec-locale-fallback": deps.localeFallbackOf(node), children: unresolved ? (_jsx(UnresolvedReusablePlaceholder, { status: unresolved })) : (_jsxs(_Fragment, { children: [listingIssue !== null ? _jsx(CanvasListingIssueNotice, { message: listingIssue }) : null, rendered] })) }, `${node.id}#${deps.richEditEpochs[node.id] ?? 0}`));
|
|
87
91
|
},
|
|
88
92
|
};
|
|
89
93
|
}
|
|
@@ -96,6 +100,16 @@ export function createCanvasRenderContext(deps) {
|
|
|
96
100
|
export function useCanvasRenderContext(args) {
|
|
97
101
|
const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args;
|
|
98
102
|
const data = useData();
|
|
103
|
+
// EC-460: the canvas follows the preview content session reactively. The
|
|
104
|
+
// epoch bumps whenever `configureContentSession` re-points the session (the
|
|
105
|
+
// write-sync refresh, a CMS-surface hydration, boot) — it keys the resolver
|
|
106
|
+
// callbacks below, so the context identity changes and the canvas re-renders
|
|
107
|
+
// against the fresh session. Registering as a CONSUMER tells the write-sync a
|
|
108
|
+
// preview surface is live, so a stale session actually refreshes (with no
|
|
109
|
+
// consumer it only flags — connected mode never pays a per-autosave query
|
|
110
|
+
// while nobody is previewing).
|
|
111
|
+
const sessionEpoch = useSyncExternalStore(subscribeContentSession, contentSessionEpoch);
|
|
112
|
+
useEffect(() => acquireContentSessionConsumer(), []);
|
|
99
113
|
// Only page nodes are editable; layout chrome (rendered around the page) is
|
|
100
114
|
// read-only context and must not be selectable (it isn't in the page tree).
|
|
101
115
|
const pageNodeIds = useMemo(() => collectIds(present), [present]);
|
|
@@ -135,19 +149,37 @@ export function useCanvasRenderContext(args) {
|
|
|
135
149
|
// picked documents exactly as the host renders them. The studio analog of the
|
|
136
150
|
// host's baked relations map. A missing/invisible target → null → the block's
|
|
137
151
|
// own empty state.
|
|
138
|
-
const resolveRelation = useCallback((ref) => resolveCanvasRelation(ref, activeLocale, perspective),
|
|
152
|
+
const resolveRelation = useCallback((ref) => resolveCanvasRelation(ref, activeLocale, perspective),
|
|
153
|
+
// sessionEpoch: re-resolve when the content session was re-pointed (EC-460).
|
|
154
|
+
[activeLocale, perspective, sessionEpoch]);
|
|
139
155
|
// EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to
|
|
140
156
|
// its documents from the active content session, in the canvas's active locale, so
|
|
141
157
|
// the canvas previews a dynamic grid exactly as the host renders it. The studio
|
|
142
158
|
// analog of the host's baked listings map; an empty/invalid query → [] → the
|
|
143
159
|
// block's own empty state.
|
|
144
|
-
const resolveListing = useCallback((query) => resolveCanvasListing(query, activeLocale, perspective), [activeLocale, perspective]);
|
|
160
|
+
const resolveListing = useCallback((query) => resolveCanvasListing(query, activeLocale, perspective), [activeLocale, perspective, sessionEpoch]);
|
|
145
161
|
// EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's
|
|
146
162
|
// URL path from the active content session, in the canvas's active locale — the
|
|
147
163
|
// studio analog of the host's baked links map. A missing/unpublished/unroutable
|
|
148
164
|
// target → null → the rich-text renderer shows the visible `unresolved-ref` issue.
|
|
149
|
-
const resolveLink = useCallback((ref) => resolveCanvasLink(ref, activeLocale, perspective), [activeLocale, perspective]);
|
|
150
|
-
const unresolvedReuse = useCallback((node) => unresolvedReuseStatus(node, reuse?.capability, (ref, p) => resolveCanvasRelation(ref, activeLocale, p), perspective), [reuse, activeLocale, perspective]);
|
|
165
|
+
const resolveLink = useCallback((ref) => resolveCanvasLink(ref, activeLocale, perspective), [activeLocale, perspective, sessionEpoch]);
|
|
166
|
+
const unresolvedReuse = useCallback((node) => unresolvedReuseStatus(node, reuse?.capability, (ref, p) => resolveCanvasRelation(ref, activeLocale, p), perspective), [reuse, activeLocale, perspective, sessionEpoch]);
|
|
167
|
+
// EC-460 (never-silent): flag a listing whose query is statically invalid
|
|
168
|
+
// against the active session schema. Pure over the registry + the shared
|
|
169
|
+
// `listingQueryOf` (the same query builder the renderer/host use), so notice
|
|
170
|
+
// and resolution can never disagree about WHICH query was judged.
|
|
171
|
+
const listingIssueOf = useCallback((node) => {
|
|
172
|
+
const manifest = studio.registry.getManifest(node.componentId);
|
|
173
|
+
if (!manifest?.listing)
|
|
174
|
+
return null;
|
|
175
|
+
const query = listingQueryOf(node, manifest.listing);
|
|
176
|
+
if (!query)
|
|
177
|
+
return null; // awaiting the filter pick — the block's own empty state
|
|
178
|
+
const errors = validateCanvasListingQuery(query);
|
|
179
|
+
if (errors.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
return errors.map((error) => error.message).join(' ');
|
|
182
|
+
}, [studio, sessionEpoch]);
|
|
151
183
|
const localeFallbackOf = useCallback((node) => firstBindingFallbackLocale(node, (binding) => cmsBindingFallback(binding, data.getSource(binding.sourceId), activeLocale)), [data, activeLocale]);
|
|
152
184
|
const ctx = useMemo(() => createCanvasRenderContext({
|
|
153
185
|
registry: studio.registry,
|
|
@@ -163,7 +195,8 @@ export function useCanvasRenderContext(args) {
|
|
|
163
195
|
richEditEpochs,
|
|
164
196
|
unresolvedReuse,
|
|
165
197
|
localeFallbackOf,
|
|
166
|
-
|
|
198
|
+
listingIssueOf,
|
|
199
|
+
}), [studio, resolveBinding, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, localeFallbackOf, richEditEpochs, listingIssueOf]);
|
|
167
200
|
return { ctx, pageNodeIds };
|
|
168
201
|
}
|
|
169
202
|
/**
|
|
@@ -176,6 +209,29 @@ export function useCanvasRenderContext(args) {
|
|
|
176
209
|
* selectable; the inspector's `element` relation chip (EC-296) deep-links to
|
|
177
210
|
* the reusable to edit or publish it.
|
|
178
211
|
*/
|
|
212
|
+
/**
|
|
213
|
+
* In-canvas notice for a listing block whose query is statically invalid
|
|
214
|
+
* against the active session schema (EC-460 — the never-silent rule): the
|
|
215
|
+
* message renders ABOVE the block (which keeps its own empty state), so a
|
|
216
|
+
* misdeclared `listing` manifest (non-filterable sort/filter field, unknown
|
|
217
|
+
* collection) is an explicit, visible state — never an empty grid an editor
|
|
218
|
+
* can't tell apart from "no content yet". Rendered INTO the iframe (project
|
|
219
|
+
* stylesheet only), so it uses inline styles + the `.ec-chrome` containment
|
|
220
|
+
* recipe, exactly like {@link UnresolvedReusablePlaceholder}. Exported for the
|
|
221
|
+
* chrome contract test.
|
|
222
|
+
*/
|
|
223
|
+
export function CanvasListingIssueNotice({ message }) {
|
|
224
|
+
return (_jsxs("div", { className: "ec-chrome", "data-testid": "canvas-listing-issue", style: {
|
|
225
|
+
boxSizing: 'border-box',
|
|
226
|
+
border: '1px dashed rgba(200,140,40,0.65)',
|
|
227
|
+
borderRadius: 8,
|
|
228
|
+
padding: '10px 14px',
|
|
229
|
+
margin: '8px 16px',
|
|
230
|
+
background: 'rgba(230,160,40,0.08)',
|
|
231
|
+
color: 'rgba(120,80,10,0.95)',
|
|
232
|
+
font: '13px/1.5 system-ui, -apple-system, sans-serif',
|
|
233
|
+
}, children: [_jsxs("span", { style: { fontWeight: 600 }, children: ["\u26A0 ", translate('builder.canvas.listingInvalidTitle')] }), ' ', _jsx("span", { style: { opacity: 0.9 }, children: message })] }));
|
|
234
|
+
}
|
|
179
235
|
export function UnresolvedReusablePlaceholder({ status }) {
|
|
180
236
|
const title = status === 'unpublished'
|
|
181
237
|
? translate('builder.canvas.reusableUnpublishedTitle')
|