@elytracms/studio 0.0.69 → 0.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/components/app-views/studio-config.d.ts +22 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/builder/blocks-model.d.ts +91 -29
  4. package/dist/components/builder/blocks-model.js +83 -54
  5. package/dist/components/builder/blocks-model.js.map +1 -1
  6. package/dist/components/builder/blocks-panel.d.ts +40 -12
  7. package/dist/components/builder/blocks-panel.js +88 -34
  8. package/dist/components/builder/blocks-panel.js.map +1 -1
  9. package/dist/components/builder/fixture-thumbnail.d.ts +13 -23
  10. package/dist/components/builder/fixture-thumbnail.js +27 -99
  11. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  12. package/dist/components/builder/spacing-handles.d.ts +14 -11
  13. package/dist/components/builder/spacing-handles.js +33 -57
  14. package/dist/components/builder/spacing-handles.js.map +1 -1
  15. package/dist/components/session-expired-overlay.d.ts +16 -0
  16. package/dist/components/session-expired-overlay.js +22 -6
  17. package/dist/components/session-expired-overlay.js.map +1 -1
  18. package/dist/components/sign-in-view.d.ts +10 -1
  19. package/dist/components/sign-in-view.js +14 -3
  20. package/dist/components/sign-in-view.js.map +1 -1
  21. package/dist/customization.d.ts +9 -1
  22. package/dist/customization.js.map +1 -1
  23. package/dist/lib/cms-views/view-model.d.ts +6 -6
  24. package/dist/lib/i18n/messages.de.js +6 -0
  25. package/dist/lib/i18n/messages.de.js.map +1 -1
  26. package/dist/lib/i18n/messages.en.d.ts +5 -0
  27. package/dist/lib/i18n/messages.en.js +6 -0
  28. package/dist/lib/i18n/messages.en.js.map +1 -1
  29. package/dist/lib/workspace/index.d.ts +10 -0
  30. package/dist/lib/workspace/index.js +11 -0
  31. package/dist/lib/workspace/index.js.map +1 -1
  32. package/dist/ui/dialog.d.ts +9 -1
  33. package/dist/ui/dialog.js +8 -2
  34. package/dist/ui/dialog.js.map +1 -1
  35. package/package.json +2 -2
@@ -4,32 +4,34 @@ import { useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { buildFragmentFromRegistryFixture, buildFragmentFromSubtree, } from '@elytracms/core/clipboard';
5
5
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
6
6
  import { translate } from '@elytracms/studio/lib/i18n';
7
+ import { resolveDeploymentGeneratedPreviews } from '@elytracms/studio/lib/workspace';
7
8
  import { useCollab } from '../collab/collab-context.js';
8
9
  import { useStudioClipboard } from './clipboard-actions.js';
9
10
  import { beginDrag, endDrag, onDragSessionEnd } from './drag-session.js';
10
- import { Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Row, Text, } from '@elytracms/studio/ui';
11
+ import { Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Popover, PopoverContent, PopoverTrigger, Row, Text, } from '@elytracms/studio/ui';
11
12
  import { cn } from '@elytracms/studio/lib/utils';
12
13
  import { useBuilder } from './builder-store.js';
13
14
  import { blockGroups, presetTiles } from './blocks-model.js';
14
- import { PreviewImageThumbnail, ScaledFixtureThumbnail } from './fixture-thumbnail.js';
15
+ import { PlaceholderThumbnail, PreviewImageThumbnail } from './fixture-thumbnail.js';
15
16
  /**
16
17
  * "Copy exact configuration" (EC-175, vision 3.3): the EC-174 fragment for a
17
- * blocks-panel tile. Tiles backed by a declared manifest fixture go through
18
- * `buildFragmentFromRegistryFixture` (the fixture's exact configuration);
19
- * preset/default tiles fragment the same subtree the tile inserts. Both paths
20
- * share `buildFragmentFromSubtree`, so the fragment shape — and the paste
21
- * path — is identical to copying a live canvas instance.
18
+ * blocks-panel variant. Variants backed by a declared manifest fixture go
19
+ * through `buildFragmentFromRegistryFixture` (the fixture's exact
20
+ * configuration); implicit-default and preset variants fragment the same
21
+ * subtree they insert. Both paths share `buildFragmentFromSubtree`, so the
22
+ * fragment shape — and the paste path — is identical to copying a live canvas
23
+ * instance.
22
24
  */
23
- export function tileFragment(studio, tile, projectId) {
25
+ export function tileFragment(studio, tile, variant, projectId) {
24
26
  const manifest = studio.registry.getManifest(tile.componentId);
25
- const fixture = manifest?.fixtures?.find((candidate) => candidate.name === tile.variant);
27
+ const fixture = manifest?.fixtures?.find((candidate) => candidate.name === variant.name);
26
28
  if (manifest && fixture) {
27
29
  const built = buildFragmentFromRegistryFixture(manifest, fixture, {
28
30
  source: { projectId },
29
31
  });
30
32
  return built.ok ? built.fragment : null;
31
33
  }
32
- return buildFragmentFromSubtree(tile.build(), { source: { projectId } });
34
+ return buildFragmentFromSubtree(variant.build(), { source: { projectId } });
33
35
  }
34
36
  /**
35
37
  * The Insert dialog never dismisses on pointer interactions outside its
@@ -55,7 +57,10 @@ export function BlocksPanel({ context, open, onOpenChange, }) {
55
57
  }, [open]);
56
58
  const slotAllow = context?.slotAllow;
57
59
  const trimmed = query.trim();
58
- const groups = useMemo(() => blockGroups(studio, { query: trimmed || undefined, slotAllow }), [studio, trimmed, slotAllow]);
60
+ // EC-439 AD-2: the generated-preview seam (EC-440) — a config-sourced pure
61
+ // lookup, `undefined` until a consumer wires `defineStudio({ previews })`.
62
+ const generatedPreview = resolveDeploymentGeneratedPreviews();
63
+ const groups = useMemo(() => blockGroups(studio, { query: trimmed || undefined, slotAllow }, generatedPreview), [studio, trimmed, slotAllow, generatedPreview]);
59
64
  // Layout helpers keep their dedicated group while browsing (hidden once a
60
65
  // query narrows the panel to registry matches), filtered by slot.allow.
61
66
  const presets = useMemo(() => (trimmed ? [] : presetTiles(slotAllow)), [trimmed, slotAllow]);
@@ -64,7 +69,7 @@ export function BlocksPanel({ context, open, onOpenChange, }) {
64
69
  // the way so the canvas and the page tree can receive the drop; the session
65
70
  // ending restores it — and a successful drop closes the panel. The drop
66
71
  // surfaces dispatch the single insert action; the panel only carries the
67
- // payload (`tile.build`, the same fixture-subtree model click-insert uses).
72
+ // payload (`tile.build`, the same default-subtree model click-insert uses).
68
73
  const dragRestoreRef = useRef(null);
69
74
  useEffect(() => onDragSessionEnd((outcome, session) => {
70
75
  if (session.source !== 'blocks')
@@ -76,22 +81,23 @@ export function BlocksPanel({ context, open, onOpenChange, }) {
76
81
  }), [onOpenChange]);
77
82
  if (!context)
78
83
  return null;
79
- function insertTile(tile) {
80
- builder.dispatch({ type: 'insert', target: context.target, node: tile.build() });
84
+ function insertVariant(variant) {
85
+ builder.dispatch({ type: 'insert', target: context.target, node: variant.build() });
81
86
  onOpenChange(false);
82
87
  }
83
88
  // "Copy exact configuration" (EC-175): same fragment + paste path as
84
- // copying a live canvas instance.
85
- function copyTile(tile) {
86
- const fragment = tileFragment(studio, tile, collab.projectId);
89
+ // copying a live canvas instance, per variant.
90
+ function copyVariant(tile, variant) {
91
+ const fragment = tileFragment(studio, tile, variant, collab.projectId);
87
92
  if (fragment)
88
- clipboard.copyFragment(fragment, `${tile.title} — ${tile.variant}`);
93
+ clipboard.copyFragment(fragment, `${tile.title} — ${variant.name}`);
89
94
  }
90
95
  function handleTileDragStart(tile, e) {
91
96
  beginDrag({
92
97
  subject: { kind: 'component', componentId: tile.componentId },
93
98
  source: 'blocks',
94
- label: `${tile.title} — ${tile.variant}`,
99
+ // AD-4: one tile per component — the drag labels the component title.
100
+ label: tile.title,
95
101
  buildNode: tile.build,
96
102
  }, e.dataTransfer);
97
103
  // Radix's modal layer locks pointer events on the body and the overlay
@@ -125,32 +131,80 @@ export function BlocksPanel({ context, open, onOpenChange, }) {
125
131
  endDrag('cancelled');
126
132
  }
127
133
  const empty = groups.length === 0 && presets.length === 0;
128
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-3xl", onInteractOutside: preventOutsideDismiss, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('builder.blocksPanel.title') }), _jsxs(DialogDescription, { children: ["Adds into ", _jsx(Code, { tone: "muted", children: context.destinationLabel }), "."] })] }), _jsxs("div", { className: "relative", 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('builder.blocksPanel.searchPlaceholder'), "aria-label": translate('builder.blocksPanel.searchAria'), className: "pl-8" })] }), _jsxs("div", { className: "max-h-[min(30rem,calc(100dvh-14rem))] overflow-auto pr-1", children: [presets.length > 0 ? (_jsx(TileGroup, { heading: translate('builder.blocksPanel.layoutHelpers'), tiles: presets, onInsert: insertTile, onCopy: copyTile, onTileDragStart: handleTileDragStart, onTileDragEnd: handleTileDragEnd })) : null, groups.map((group) => (_jsx(TileGroup, { heading: group.category, tiles: group.tiles, onInsert: insertTile, onCopy: copyTile, onTileDragStart: handleTileDragStart, onTileDragEnd: handleTileDragEnd }, group.category))), empty ? (_jsx(Text, { size: "sm", tone: "muted", className: "p-3", children: slotAllow
134
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-3xl", onInteractOutside: preventOutsideDismiss, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('builder.blocksPanel.title') }), _jsxs(DialogDescription, { children: ["Adds into ", _jsx(Code, { tone: "muted", children: context.destinationLabel }), "."] })] }), _jsxs("div", { className: "relative", 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('builder.blocksPanel.searchPlaceholder'), "aria-label": translate('builder.blocksPanel.searchAria'), className: "pl-8" })] }), _jsxs("div", { className: "max-h-[min(30rem,calc(100dvh-14rem))] overflow-auto pr-1", children: [presets.length > 0 ? (_jsx(TileGroup, { heading: translate('builder.blocksPanel.layoutHelpers'), tiles: presets, onInsert: insertVariant, onCopy: copyVariant, onTileDragStart: handleTileDragStart, onTileDragEnd: handleTileDragEnd })) : null, groups.map((group) => (_jsx(TileGroup, { heading: group.category, tiles: group.tiles, onInsert: insertVariant, onCopy: copyVariant, onTileDragStart: handleTileDragStart, onTileDragEnd: handleTileDragEnd }, group.category))), empty ? (_jsx(Text, { size: "sm", tone: "muted", className: "p-3", children: slotAllow
129
135
  ? translate('builder.blocksPanel.noAllowed')
130
136
  : translate('builder.blocksPanel.noMatch', { query }) })) : null] })] }) }));
131
137
  }
132
138
  function TileGroup({ heading, tiles, onInsert, onCopy, onTileDragStart, onTileDragEnd, }) {
133
- 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, onClick: () => onInsert(tile), onCopy: () => onCopy(tile), onDragStart: (e) => onTileDragStart(tile, e), onDragEnd: onTileDragEnd }, tile.key))) })] }));
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))) })] }));
134
140
  }
135
- function BlockTileButton({ tile, onClick, onCopy, onDragStart, onDragEnd, }) {
141
+ /**
142
+ * One component tile (exported for markup tests). Not a native `<button>`: the
143
+ * tile CONTAINS interactive controls (the copy-exact button and the variants
144
+ * chip), and nested `<button>`s are invalid HTML — so it stays a keyboard-
145
+ * operable `role="button"` element whose own key handler ignores events
146
+ * bubbling from those inner controls.
147
+ */
148
+ export function BlockTileButton({ tile, onInsert, onCopy, onDragStart, onDragEnd, }) {
136
149
  const degraded = tile.issues.length > 0;
137
- // Not a native <button>: live thumbnails are real renders and may contain
138
- // interactive elements (e.g. the Button primitive), and a nested <button>
139
- // is invalid HTML. The thumbnail itself is aria-hidden and pointer-inert.
140
- return (_jsxs("div", { role: "button", tabIndex: 0, draggable: true, onDragStart: onDragStart, onDragEnd: onDragEnd, onClick: onClick, onKeyDown: (event) => {
150
+ const defaultVariant = tile.variants[0];
151
+ return (_jsxs("div", { role: "button", tabIndex: 0, draggable: true, onDragStart: onDragStart, onDragEnd: onDragEnd, onClick: () => onInsert(defaultVariant), onKeyDown: (event) => {
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;
141
156
  if (event.key === 'Enter' || event.key === ' ') {
142
157
  event.preventDefault();
143
- onClick();
158
+ onInsert(defaultVariant);
144
159
  }
145
- }, "data-block-tile": tile.key, "data-block-degraded": degraded || undefined, "aria-label": translate('builder.blocksPanel.insertTileAria', {
146
- title: tile.title,
147
- variant: tile.variant,
148
- }), title: tile.description, className: cn('group/tile relative flex cursor-pointer flex-col gap-1.5 rounded-lg border bg-card p-2 text-left outline-none transition-colors', 'hover:border-ring/60 hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring/60', degraded ? 'border-amber-500/50' : 'border-border'), children: [_jsx("button", { type: "button", "aria-label": translate('builder.blocksPanel.copyExactAria', {
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(
161
+ // AD-3: this outer border is the tile's ONE frame — the preview inset
162
+ // 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 outline-none transition-colors', 'hover:border-ring/60 hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring/60', degraded ? 'border-amber-500/50' : 'border-border'), children: [_jsx("button", { type: "button", "aria-label": translate('builder.blocksPanel.copyExactAria', {
149
164
  title: tile.title,
150
- variant: tile.variant,
165
+ variant: defaultVariant.name,
151
166
  }), title: translate('builder.blocksPanel.copyExactConfiguration'), "data-block-copy": tile.key, onClick: (event) => {
152
167
  event.stopPropagation();
153
- onCopy();
154
- }, 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.previewImage ? (_jsx(PreviewImageThumbnail, { src: tile.previewImage, alt: tile.title })) : (_jsx(ScaledFixtureThumbnail, { node: tile.previewNode })), _jsxs(Row, { gap: "1", className: "w-full min-w-0", children: [_jsx("span", { className: "truncate text-xs font-medium", children: tile.title }), _jsx("span", { className: "ml-auto shrink-0 text-[0.7rem] text-muted-foreground", children: tile.variant })] }), 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] }));
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] }));
170
+ }
171
+ /**
172
+ * The compact variant affordance (EC-439 AD-1): an "n variants" chip in the
173
+ * tile footer opening a flyout of per-fixture rows. When the active search
174
+ * query matched fixture names ({@link BlockTile.matchedVariants}) the chip is
175
+ * highlighted so "found by fixture name" is visible at the grid.
176
+ */
177
+ function VariantsChip({ tile, onInsert, onCopy, }) {
178
+ const matched = (tile.matchedVariants?.length ?? 0) > 0;
179
+ // The tile already surfaces the DEFAULT variant's issues; the chip warns
180
+ // when a deeper variant is degraded so it is visible before the flyout opens.
181
+ const laterVariantDegraded = tile.variants.slice(1).some((variant) => variant.issues.length > 0);
182
+ 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
+ count: tile.variants.length,
184
+ title: tile.title,
185
+ }), title: laterVariantDegraded ? translate('builder.blocksPanel.variantIssuesHint') : undefined,
186
+ // The chip sits inside the role="button" tile: opening the flyout
187
+ // must never ALSO insert the default.
188
+ onClick: (event) => event.stopPropagation(), className: cn('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
+ ? 'bg-primary/15 text-primary'
190
+ : '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
+ }
192
+ /**
193
+ * The flyout's variant rows (exported for markup tests): one insert row +
194
+ * copy-exact button per fixture (EC-175 semantics per variant). Rows matched
195
+ * by the active fixture-name search are highlighted.
196
+ */
197
+ export function VariantRows({ tile, onInsert, onCopy, }) {
198
+ return (_jsx("div", { className: "flex flex-col gap-0.5", children: tile.variants.map((variant) => {
199
+ const matched = tile.matchedVariants?.includes(variant.name) ?? false;
200
+ const variantDegraded = variant.issues.length > 0;
201
+ return (_jsxs(Row, { gap: "1", className: "w-full min-w-0", children: [_jsxs("button", { type: "button", "data-block-variant": `${tile.componentId}::${variant.name}`, "data-block-variant-match": matched || undefined, "data-block-variant-degraded": variantDegraded || undefined, "aria-label": translate('builder.blocksPanel.insertTileAria', {
202
+ title: tile.title,
203
+ variant: variant.name,
204
+ }), title: variantDegraded ? variant.issues.join('\n') : undefined, onClick: () => onInsert(variant), className: cn('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', matched ? 'text-primary' : 'text-foreground'), children: [variant.previewUrl ? (_jsx("img", { src: variant.previewUrl, alt: "", draggable: false, className: "pointer-events-none h-7 w-11 shrink-0 select-none rounded bg-muted/40 object-cover" })) : null, _jsx("span", { className: "truncate", children: variant.name }), variantDegraded ? (_jsx(AlertTriangle, { "aria-hidden": true, className: "ml-auto size-3 shrink-0 text-amber-600 dark:text-amber-400" })) : null] }), _jsx("button", { type: "button", "data-block-variant-copy": `${tile.componentId}::${variant.name}`, "aria-label": translate('builder.blocksPanel.copyExactAria', {
205
+ title: tile.title,
206
+ variant: variant.name,
207
+ }), title: translate('builder.blocksPanel.copyExactConfiguration'), onClick: () => onCopy(tile, variant), 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", children: _jsx(ClipboardCopy, { className: "size-3.5", "aria-hidden": true }) })] }, variant.name));
208
+ }) }));
155
209
  }
156
210
  //# sourceMappingURL=blocks-panel.js.map
@@ -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 { 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 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 } from './blocks-model'\nimport { PreviewImageThumbnail, ScaledFixtureThumbnail } from './fixture-thumbnail'\n\n/**\n * Blocks panel (EC-164): the visual insert surface. Every registry component\n * and each of its fixture variations is a tile with a live, scaled render —\n * the component library *is* the insert surface. Clicking a tile inserts the\n * fixture's configured subtree through the builder's `insert` action (one\n * history entry, mirrored onto the `graph.insertNode` operation).\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 tile. Tiles backed by a declared manifest fixture go through\n * `buildFragmentFromRegistryFixture` (the fixture's exact configuration);\n * preset/default tiles fragment the same subtree the tile inserts. Both paths\n * share `buildFragmentFromSubtree`, so the fragment shape — and the paste\n * path — is identical to copying a live canvas instance.\n */\nexport function tileFragment(\n studio: StudioRegistry,\n tile: BlockTile,\n projectId: string,\n): ClipboardFragment | null {\n const manifest = studio.registry.getManifest(tile.componentId)\n const fixture = manifest?.fixtures?.find((candidate) => candidate.name === tile.variant)\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(tile.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 const groups = useMemo(\n () => blockGroups(studio, { query: trimmed || undefined, slotAllow }),\n [studio, trimmed, slotAllow],\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 fixture-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 insertTile(tile: BlockTile) {\n builder.dispatch({ type: 'insert', target: context!.target, node: tile.build() })\n onOpenChange(false)\n }\n\n // \"Copy exact configuration\" (EC-175): same fragment + paste path as\n // copying a live canvas instance.\n function copyTile(tile: BlockTile) {\n const fragment = tileFragment(studio, tile, collab.projectId)\n if (fragment) clipboard.copyFragment(fragment, `${tile.title} — ${tile.variant}`)\n }\n\n function handleTileDragStart(tile: BlockTile, e: React.DragEvent) {\n beginDrag(\n {\n subject: { kind: 'component', componentId: tile.componentId },\n source: 'blocks',\n label: `${tile.title} — ${tile.variant}`,\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={insertTile}\n onCopy={copyTile}\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={insertTile}\n onCopy={copyTile}\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: (tile: BlockTile) => void\n onCopy: (tile: BlockTile) => 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 onClick={() => onInsert(tile)}\n onCopy={() => onCopy(tile)}\n onDragStart={(e) => onTileDragStart(tile, e)}\n onDragEnd={onTileDragEnd}\n />\n ))}\n </div>\n </section>\n )\n}\n\nfunction BlockTileButton({\n tile,\n onClick,\n onCopy,\n onDragStart,\n onDragEnd,\n}: {\n tile: BlockTile\n onClick: () => void\n onCopy: () => void\n onDragStart: (e: React.DragEvent) => void\n onDragEnd: () => void\n}) {\n const degraded = tile.issues.length > 0\n // Not a native <button>: live thumbnails are real renders and may contain\n // interactive elements (e.g. the Button primitive), and a nested <button>\n // is invalid HTML. The thumbnail itself is aria-hidden and pointer-inert.\n return (\n <div\n role=\"button\"\n tabIndex={0}\n draggable\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n onClick={onClick}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n onClick()\n }\n }}\n data-block-tile={tile.key}\n data-block-degraded={degraded || undefined}\n aria-label={translate('builder.blocksPanel.insertTileAria', {\n title: tile.title,\n variant: tile.variant,\n })}\n title={tile.description}\n className={cn(\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 this tile, pasteable anywhere the canvas/tree paste path reaches. */}\n <button\n type=\"button\"\n aria-label={translate('builder.blocksPanel.copyExactAria', {\n title: tile.title,\n variant: tile.variant,\n })}\n title={translate('builder.blocksPanel.copyExactConfiguration')}\n data-block-copy={tile.key}\n onClick={(event) => {\n event.stopPropagation()\n onCopy()\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 {/* EC-409: a curated `manifest.previewImage` replaces the live fixture\n render — the escape hatch for tiles that would preview as \"just\n text\" (data-dependent renders). Everything else about the tile —\n insert, drag, copy — is unchanged. */}\n {tile.previewImage ? (\n <PreviewImageThumbnail src={tile.previewImage} alt={tile.title} />\n ) : (\n <ScaledFixtureThumbnail node={tile.previewNode} />\n )}\n <Row gap=\"1\" className=\"w-full min-w-0\">\n <span className=\"truncate text-xs font-medium\">{tile.title}</span>\n <span className=\"ml-auto shrink-0 text-[0.7rem] text-muted-foreground\">{tile.variant}</span>\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"]}
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,33 +1,23 @@
1
- import type { ComponentNode } from '@elytracms/core/project-graph';
2
- import type { StudioRegistry } from '@elytracms/studio/lib/registry';
3
1
  /**
4
- * Curated preview-image tile (EC-409): renders `manifest.previewImage` in the
5
- * SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.
6
- * The escape hatch for components whose live render needs data/context the
7
- * tile can't have — fixtures stay the primary curation lever. `alt` is the
8
- * component title (already-localized manifest data); the image is never a
9
- * fetch the engine performs, just an `<img>` with the consumer's URL string.
2
+ * Preview-image tile (EC-409 curated / EC-440 generated): renders the resolved
3
+ * `previewUrl` in the shared 16/10 inset, object-fit cover. The image is never
4
+ * a fetch the engine performs, just an `<img>` with the consumer's URL string;
5
+ * `alt` is the component title (already-localized manifest data).
10
6
  */
11
7
  export declare function PreviewImageThumbnail({ src, alt, className, }: {
12
- /** The manifest's `previewImage` URL string, passed through verbatim. */
8
+ /** The resolved preview URL (curated `manifest.previewImage` or a generated artifact), passed through verbatim. */
13
9
  src: string;
14
10
  /** The component title (localized manifest data — no new catalog string). */
15
11
  alt: string;
16
12
  className?: string;
17
13
  }): import("react").JSX.Element;
18
- export declare function ScaledFixtureThumbnail({ node, studio, designWidth, eager, className, }: {
19
- /** The preview subtree to render live (deterministic ids). */
20
- node: ComponentNode;
21
- /**
22
- * Registry the preview renders against. Defaults to the active project's live
23
- * registry (`getStudioRegistry()`) — the blocks panel's tiles and their
24
- * thumbnails share one registry. Pass an explicit registry to render against a
25
- * specific component set (deterministic previews/tests).
26
- */
27
- studio?: StudioRegistry;
28
- /** Layout width of the unscaled preview, in CSS pixels. */
29
- designWidth?: number;
30
- /** Mount the live render immediately instead of waiting for visibility. */
31
- eager?: boolean;
14
+ /**
15
+ * The icon+name placeholder (EC-439 AD-2, tier 3): a deliberate glyph
16
+ * treatment — a centered two-letter monogram derived from the component title
17
+ * in a muted disc — never an empty box and never a lying render. Decorative
18
+ * only (`aria-hidden`): the tile itself carries the accessible name.
19
+ */
20
+ export declare function PlaceholderThumbnail({ title, className }: {
21
+ title: string;
32
22
  className?: string;
33
23
  }): import("react").JSX.Element;
@@ -1,112 +1,40 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Component, useEffect, useMemo, useRef, useState } from 'react';
3
- import { AlertTriangle } from 'lucide-react';
4
- import { renderNode } from '@elytracms/core/runtime-renderer';
5
- import { getStudioRegistry, previewContext } from '@elytracms/studio/lib/registry';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
6
2
  import { cn } from '@elytracms/studio/lib/utils';
7
- import { translate } from '@elytracms/studio/lib/i18n';
3
+ import { tileMonogram } from './blocks-model.js';
8
4
  /**
9
- * Live, scaled fixture thumbnail (EC-164). Renders a preview node through the
10
- * runtime renderer — the exact render path the canvas, fixture runner, and
11
- * export use — inside a fixed-aspect tile, scaled down with a CSS transform.
12
- * Never a screenshot, never interactive (pointer events are disabled), and
13
- * never a crash: render errors degrade to a visible fallback inside the tile.
5
+ * Blocks-panel tile previews (EC-439 AD-2): previews are ARTIFACTS, never
6
+ * runtime renders. A tile shows a curated `manifest.previewImage` (EC-409) or
7
+ * an EC-440-generated fixture image through {@link PreviewImageThumbnail}, and
8
+ * falls back to the deliberate icon+name placeholder
9
+ * ({@link PlaceholderThumbnail}) when neither exists. The in-document live
10
+ * fixture render (`ScaledFixtureThumbnail`, EC-164) is gone: since EC-414 the
11
+ * consumer theme lives only in the canvas iframe, so no in-studio-document
12
+ * render of consumer components can ever be styled correctly again.
14
13
  *
15
- * Off-screen thumbnails mount lazily via IntersectionObserver so a large
16
- * registry doesn't render its whole library up front; pass `eager` to render
17
- * immediately (first-fold tiles, non-DOM tests).
14
+ * One frame per tile (AD-3): the tile's outer border is the ONLY border — the
15
+ * preview inset is a borderless, tinted, rounded area.
18
16
  */
19
- /** Width the preview is laid out at before being scaled into the tile. */
20
- const DESIGN_WIDTH = 420;
21
- /** Fallback scale used for the single frame before the tile is measured. */
22
- const FALLBACK_SCALE = 0.45;
23
- function LiveRender({ node, studio }) {
24
- const ctx = useMemo(() => previewContext(studio), [studio]);
25
- // Called inside a component render so the boundary catches sync throws too.
26
- return _jsx(_Fragment, { children: renderNode(node, ctx) });
27
- }
28
- function ThumbnailFallback({ message }) {
29
- return (_jsxs("div", { "data-block-thumbnail-fallback": true, className: "flex h-full w-full items-start gap-1.5 p-2 text-[0.7rem] leading-snug text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "mt-px size-3 shrink-0" }), _jsx("span", { className: "line-clamp-3", children: message })] }));
30
- }
31
- /** Tile-local error boundary: one broken fixture never takes down the panel. */
32
- class ThumbnailErrorBoundary extends Component {
33
- state = { error: null };
34
- static getDerivedStateFromError(error) {
35
- return { error };
36
- }
37
- render() {
38
- if (this.state.error) {
39
- return (_jsx(ThumbnailFallback, { message: translate('builder.blocksPanel.previewFailed', { error: this.state.error.message }) }));
40
- }
41
- return this.props.children;
42
- }
43
- }
17
+ /** The shared borderless 16/10 inset both preview tiers and the placeholder render into. */
18
+ const INSET_CLASSES = 'relative w-full overflow-hidden rounded-md bg-muted/40';
44
19
  /**
45
- * Curated preview-image tile (EC-409): renders `manifest.previewImage` in the
46
- * SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.
47
- * The escape hatch for components whose live render needs data/context the
48
- * tile can't have — fixtures stay the primary curation lever. `alt` is the
49
- * component title (already-localized manifest data); the image is never a
50
- * fetch the engine performs, just an `<img>` with the consumer's URL string.
20
+ * Preview-image tile (EC-409 curated / EC-440 generated): renders the resolved
21
+ * `previewUrl` in the shared 16/10 inset, object-fit cover. The image is never
22
+ * a fetch the engine performs, just an `<img>` with the consumer's URL string;
23
+ * `alt` is the component title (already-localized manifest data).
51
24
  */
52
25
  export function PreviewImageThumbnail({ src, alt, className, }) {
53
- return (_jsx("div", { "data-block-thumbnail": true, "data-block-thumbnail-image": true, className: cn('relative w-full overflow-hidden rounded-md border border-border bg-background', className), style: { aspectRatio: '16 / 10' }, children: _jsx("img", { src: src, alt: alt,
26
+ return (_jsx("div", { "data-block-thumbnail": true, "data-block-thumbnail-image": true, className: cn(INSET_CLASSES, className), style: { aspectRatio: '16 / 10' }, children: _jsx("img", { src: src, alt: alt,
54
27
  // The surrounding tile is the drag source; a natively draggable <img>
55
28
  // would hijack the tile drag.
56
29
  draggable: false, className: "pointer-events-none h-full w-full select-none object-cover" }) }));
57
30
  }
58
- export function ScaledFixtureThumbnail({ node, studio, designWidth = DESIGN_WIDTH, eager = false, className, }) {
59
- const registry = studio ?? getStudioRegistry();
60
- const ref = useRef(null);
61
- const [inView, setInView] = useState(eager);
62
- const [scale, setScale] = useState(null);
63
- // Lazy mount: render the live preview only once the tile scrolls into view.
64
- // IntersectionObserver always delivers an initial report (intersecting or
65
- // not) within a rendering frame — when no report arrives at all the page
66
- // isn't producing frames (occluded/headless automation windows), so a
67
- // safety-net timer mounts the preview rather than leaving tiles blank.
68
- // Frame-producing browsers report long before the timer and keep laziness.
69
- useEffect(() => {
70
- if (inView)
71
- return;
72
- const el = ref.current;
73
- if (!el || typeof IntersectionObserver === 'undefined') {
74
- setInView(true);
75
- return;
76
- }
77
- let reported = false;
78
- const observer = new IntersectionObserver((entries) => {
79
- reported = true;
80
- if (entries.some((entry) => entry.isIntersecting))
81
- setInView(true);
82
- });
83
- observer.observe(el);
84
- const fallback = setTimeout(() => {
85
- if (!reported)
86
- setInView(true);
87
- }, 400);
88
- return () => {
89
- observer.disconnect();
90
- clearTimeout(fallback);
91
- };
92
- }, [inView]);
93
- // Fit-to-tile scale, kept in sync with the tile's measured width.
94
- useEffect(() => {
95
- const el = ref.current;
96
- if (!el)
97
- return;
98
- const update = () => {
99
- const width = el.clientWidth;
100
- if (width > 0)
101
- setScale(width / designWidth);
102
- };
103
- update();
104
- if (typeof ResizeObserver === 'undefined')
105
- return;
106
- const observer = new ResizeObserver(update);
107
- observer.observe(el);
108
- return () => observer.disconnect();
109
- }, [designWidth]);
110
- return (_jsx("div", { ref: ref, "aria-hidden": true, "data-block-thumbnail": true, className: cn('relative w-full overflow-hidden rounded-md border border-border bg-background', className), style: { aspectRatio: '16 / 10' }, children: inView ? (_jsx(ThumbnailErrorBoundary, { children: _jsx("div", { className: "pointer-events-none absolute left-0 top-0 origin-top-left select-none p-3", style: { width: designWidth, transform: `scale(${scale ?? FALLBACK_SCALE})` }, children: _jsx(LiveRender, { node: node, studio: registry }) }) })) : null }));
31
+ /**
32
+ * The icon+name placeholder (EC-439 AD-2, tier 3): a deliberate glyph
33
+ * treatment — a centered two-letter monogram derived from the component title
34
+ * in a muted disc — never an empty box and never a lying render. Decorative
35
+ * only (`aria-hidden`): the tile itself carries the accessible name.
36
+ */
37
+ export function PlaceholderThumbnail({ title, className }) {
38
+ return (_jsx("div", { "aria-hidden": true, "data-block-thumbnail": true, "data-block-thumbnail-placeholder": true, className: cn(INSET_CLASSES, 'grid place-items-center', className), style: { aspectRatio: '16 / 10' }, children: _jsx("span", { className: "grid size-10 select-none place-items-center rounded-full bg-muted text-sm font-semibold tracking-wide text-muted-foreground", children: tileMonogram(title) }) }));
111
39
  }
112
40
  //# sourceMappingURL=fixture-thumbnail.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-thumbnail.js","sourceRoot":"","sources":["../../../src/components/builder/fixture-thumbnail.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { renderNode } from '@elytracms/core/runtime-renderer'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry, previewContext } from '@elytracms/studio/lib/registry'\nimport type { StudioRegistry } from '@elytracms/studio/lib/registry'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Live, scaled fixture thumbnail (EC-164). Renders a preview node through the\n * runtime renderer — the exact render path the canvas, fixture runner, and\n * export use — inside a fixed-aspect tile, scaled down with a CSS transform.\n * Never a screenshot, never interactive (pointer events are disabled), and\n * never a crash: render errors degrade to a visible fallback inside the tile.\n *\n * Off-screen thumbnails mount lazily via IntersectionObserver so a large\n * registry doesn't render its whole library up front; pass `eager` to render\n * immediately (first-fold tiles, non-DOM tests).\n */\n\n/** Width the preview is laid out at before being scaled into the tile. */\nconst DESIGN_WIDTH = 420\n/** Fallback scale used for the single frame before the tile is measured. */\nconst FALLBACK_SCALE = 0.45\n\nfunction LiveRender({ node, studio }: { node: ComponentNode; studio: StudioRegistry }) {\n const ctx = useMemo(() => previewContext(studio), [studio])\n // Called inside a component render so the boundary catches sync throws too.\n return <>{renderNode(node, ctx)}</>\n}\n\nfunction ThumbnailFallback({ message }: { message: string }) {\n return (\n <div\n data-block-thumbnail-fallback\n className=\"flex h-full w-full items-start gap-1.5 p-2 text-[0.7rem] leading-snug text-amber-700 dark:text-amber-400\"\n >\n <AlertTriangle aria-hidden className=\"mt-px size-3 shrink-0\" />\n <span className=\"line-clamp-3\">{message}</span>\n </div>\n )\n}\n\ninterface BoundaryState {\n error: Error | null\n}\n\n/** Tile-local error boundary: one broken fixture never takes down the panel. */\nclass ThumbnailErrorBoundary extends Component<{ children: ReactNode }, BoundaryState> {\n override state: BoundaryState = { error: null }\n\n static getDerivedStateFromError(error: Error): BoundaryState {\n return { error }\n }\n\n override render(): ReactNode {\n if (this.state.error) {\n return (\n <ThumbnailFallback\n message={translate('builder.blocksPanel.previewFailed', { error: this.state.error.message })}\n />\n )\n }\n return this.props.children\n }\n}\n\n/**\n * Curated preview-image tile (EC-409): renders `manifest.previewImage` in the\n * SAME 16/10 tile frame as {@link ScaledFixtureThumbnail}, object-fit cover.\n * The escape hatch for components whose live render needs data/context the\n * tile can't have — fixtures stay the primary curation lever. `alt` is the\n * component title (already-localized manifest data); the image is never a\n * fetch the engine performs, just an `<img>` with the consumer's URL string.\n */\nexport function PreviewImageThumbnail({\n src,\n alt,\n className,\n}: {\n /** The manifest's `previewImage` URL string, passed through verbatim. */\n src: string\n /** The component title (localized manifest data — no new catalog string). */\n alt: string\n className?: string\n}) {\n return (\n <div\n data-block-thumbnail\n data-block-thumbnail-image\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {/* Deliberately NOT loading=\"lazy\": native lazy images need rendering\n frames to trigger (occluded/headless windows never load them — the\n same hazard the live thumbnail's safety-net timer covers), and a\n curated preview is a small asset on a handful of tiles. */}\n <img\n src={src}\n alt={alt}\n // The surrounding tile is the drag source; a natively draggable <img>\n // would hijack the tile drag.\n draggable={false}\n className=\"pointer-events-none h-full w-full select-none object-cover\"\n />\n </div>\n )\n}\n\nexport function ScaledFixtureThumbnail({\n node,\n studio,\n designWidth = DESIGN_WIDTH,\n eager = false,\n className,\n}: {\n /** The preview subtree to render live (deterministic ids). */\n node: ComponentNode\n /**\n * Registry the preview renders against. Defaults to the active project's live\n * registry (`getStudioRegistry()`) — the blocks panel's tiles and their\n * thumbnails share one registry. Pass an explicit registry to render against a\n * specific component set (deterministic previews/tests).\n */\n studio?: StudioRegistry\n /** Layout width of the unscaled preview, in CSS pixels. */\n designWidth?: number\n /** Mount the live render immediately instead of waiting for visibility. */\n eager?: boolean\n className?: string\n}) {\n const registry = studio ?? getStudioRegistry()\n const ref = useRef<HTMLDivElement>(null)\n const [inView, setInView] = useState(eager)\n const [scale, setScale] = useState<number | null>(null)\n\n // Lazy mount: render the live preview only once the tile scrolls into view.\n // IntersectionObserver always delivers an initial report (intersecting or\n // not) within a rendering frame — when no report arrives at all the page\n // isn't producing frames (occluded/headless automation windows), so a\n // safety-net timer mounts the preview rather than leaving tiles blank.\n // Frame-producing browsers report long before the timer and keep laziness.\n useEffect(() => {\n if (inView) return\n const el = ref.current\n if (!el || typeof IntersectionObserver === 'undefined') {\n setInView(true)\n return\n }\n let reported = false\n const observer = new IntersectionObserver((entries) => {\n reported = true\n if (entries.some((entry) => entry.isIntersecting)) setInView(true)\n })\n observer.observe(el)\n const fallback = setTimeout(() => {\n if (!reported) setInView(true)\n }, 400)\n return () => {\n observer.disconnect()\n clearTimeout(fallback)\n }\n }, [inView])\n\n // Fit-to-tile scale, kept in sync with the tile's measured width.\n useEffect(() => {\n const el = ref.current\n if (!el) return\n const update = () => {\n const width = el.clientWidth\n if (width > 0) setScale(width / designWidth)\n }\n update()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(update)\n observer.observe(el)\n return () => observer.disconnect()\n }, [designWidth])\n\n return (\n <div\n ref={ref}\n aria-hidden\n data-block-thumbnail\n className={cn(\n 'relative w-full overflow-hidden rounded-md border border-border bg-background',\n className,\n )}\n style={{ aspectRatio: '16 / 10' }}\n >\n {inView ? (\n <ThumbnailErrorBoundary>\n <div\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left select-none p-3\"\n style={{ width: designWidth, transform: `scale(${scale ?? FALLBACK_SCALE})` }}\n >\n <LiveRender node={node} studio={registry} />\n </div>\n </ThumbnailErrorBoundary>\n ) : null}\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"fixture-thumbnail.js","sourceRoot":"","sources":["../../../src/components/builder/fixture-thumbnail.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C;;;;;;;;;;;;GAYG;AAEH,4FAA4F;AAC5F,MAAM,aAAa,GAAG,wDAAwD,CAAA;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,GAAG,EACH,GAAG,EACH,SAAS,GAOV;IACC,OAAO,CACL,gFAGE,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,KAAK,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,YAKjC,cACE,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG;YACR,sEAAsE;YACtE,8BAA8B;YAC9B,SAAS,EAAE,KAAK,EAChB,SAAS,EAAC,4DAA4D,GACtE,GACE,CACP,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAyC;IAC9F,OAAO,CACL,2GAIE,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,yBAAyB,EAAE,SAAS,CAAC,EAClE,KAAK,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,YAEjC,eAAM,SAAS,EAAC,6HAA6H,YAC1I,YAAY,CAAC,KAAK,CAAC,GACf,GACH,CACP,CAAA;AACH,CAAC","sourcesContent":["import { cn } from '@elytracms/studio/lib/utils'\nimport { tileMonogram } from './blocks-model'\n\n/**\n * Blocks-panel tile previews (EC-439 AD-2): previews are ARTIFACTS, never\n * runtime renders. A tile shows a curated `manifest.previewImage` (EC-409) or\n * an EC-440-generated fixture image through {@link PreviewImageThumbnail}, and\n * falls back to the deliberate icon+name placeholder\n * ({@link PlaceholderThumbnail}) when neither exists. The in-document live\n * fixture render (`ScaledFixtureThumbnail`, EC-164) is gone: since EC-414 the\n * consumer theme lives only in the canvas iframe, so no in-studio-document\n * render of consumer components can ever be styled correctly again.\n *\n * One frame per tile (AD-3): the tile's outer border is the ONLY border — the\n * preview inset is a borderless, tinted, rounded area.\n */\n\n/** The shared borderless 16/10 inset both preview tiers and the placeholder render into. */\nconst INSET_CLASSES = 'relative w-full overflow-hidden rounded-md bg-muted/40'\n\n/**\n * Preview-image tile (EC-409 curated / EC-440 generated): renders the resolved\n * `previewUrl` in the shared 16/10 inset, object-fit cover. The image is never\n * a fetch the engine performs, just an `<img>` with the consumer's URL string;\n * `alt` is the component title (already-localized manifest data).\n */\nexport function PreviewImageThumbnail({\n src,\n alt,\n className,\n}: {\n /** The resolved preview URL (curated `manifest.previewImage` or a generated artifact), passed through verbatim. */\n src: string\n /** The component title (localized manifest data — no new catalog string). */\n alt: string\n className?: string\n}) {\n return (\n <div\n data-block-thumbnail\n data-block-thumbnail-image\n className={cn(INSET_CLASSES, className)}\n style={{ aspectRatio: '16 / 10' }}\n >\n {/* Deliberately NOT loading=\"lazy\": native lazy images need rendering\n frames to trigger (occluded/headless windows never load them), and a\n tile preview is a small asset. */}\n <img\n src={src}\n alt={alt}\n // The surrounding tile is the drag source; a natively draggable <img>\n // would hijack the tile drag.\n draggable={false}\n className=\"pointer-events-none h-full w-full select-none object-cover\"\n />\n </div>\n )\n}\n\n/**\n * The icon+name placeholder (EC-439 AD-2, tier 3): a deliberate glyph\n * treatment — a centered two-letter monogram derived from the component title\n * in a muted disc — never an empty box and never a lying render. Decorative\n * only (`aria-hidden`): the tile itself carries the accessible name.\n */\nexport function PlaceholderThumbnail({ title, className }: { title: string; className?: string }) {\n return (\n <div\n aria-hidden\n data-block-thumbnail\n data-block-thumbnail-placeholder\n className={cn(INSET_CLASSES, 'grid place-items-center', className)}\n style={{ aspectRatio: '16 / 10' }}\n >\n <span className=\"grid size-10 select-none place-items-center rounded-full bg-muted text-sm font-semibold tracking-wide text-muted-foreground\">\n {tileMonogram(title)}\n </span>\n </div>\n )\n}\n"]}