@elytracms/studio 0.0.86 → 0.0.87

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 (203) hide show
  1. package/convex/asset-revalidate.test.ts +186 -0
  2. package/convex/assets.ts +53 -19
  3. package/convex/cms.ts +22 -0
  4. package/convex/delivery.ts +33 -116
  5. package/convex/functions.ts +3 -3
  6. package/convex/rail-write-parity.test.ts +180 -0
  7. package/convex/redirects.ts +33 -11
  8. package/convex/revalidate.ts +7 -5
  9. package/convex/triggers.ts +53 -3
  10. package/dist/components/app-views/default-modules.d.ts +20 -20
  11. package/dist/components/app-views/default-modules.js +27 -48
  12. package/dist/components/app-views/default-modules.js.map +1 -1
  13. package/dist/components/app-views/studio-config.d.ts +5 -6
  14. package/dist/components/app-views/studio-config.js.map +1 -1
  15. package/dist/components/assets/asset-metadata.d.ts +28 -19
  16. package/dist/components/assets/asset-metadata.js +24 -31
  17. package/dist/components/assets/asset-metadata.js.map +1 -1
  18. package/dist/components/assets/asset-selection.d.ts +24 -5
  19. package/dist/components/assets/asset-selection.js +39 -9
  20. package/dist/components/assets/asset-selection.js.map +1 -1
  21. package/dist/components/assets/assets-collection-view-source.d.ts +17 -14
  22. package/dist/components/assets/assets-collection-view-source.js +25 -31
  23. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  24. package/dist/components/builder/blocks-panel.js +3 -3
  25. package/dist/components/builder/blocks-panel.js.map +1 -1
  26. package/dist/components/builder/builder-store.d.ts +25 -1
  27. package/dist/components/builder/builder-store.js +3 -2
  28. package/dist/components/builder/builder-store.js.map +1 -1
  29. package/dist/components/builder/canvas-chrome-css.js +0 -17
  30. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  31. package/dist/components/builder/canvas-chrome.d.ts +10 -12
  32. package/dist/components/builder/canvas-chrome.js +12 -13
  33. package/dist/components/builder/canvas-chrome.js.map +1 -1
  34. package/dist/components/builder/canvas-dnd.d.ts +8 -0
  35. package/dist/components/builder/canvas-dnd.js +5 -3
  36. package/dist/components/builder/canvas-dnd.js.map +1 -1
  37. package/dist/components/builder/canvas-render-context.d.ts +12 -24
  38. package/dist/components/builder/canvas-render-context.js +15 -44
  39. package/dist/components/builder/canvas-render-context.js.map +1 -1
  40. package/dist/components/builder/canvas.js +5 -11
  41. package/dist/components/builder/canvas.js.map +1 -1
  42. package/dist/components/builder/clipboard-actions.d.ts +14 -6
  43. package/dist/components/builder/clipboard-actions.js +29 -20
  44. package/dist/components/builder/clipboard-actions.js.map +1 -1
  45. package/dist/components/builder/composition-field-editor.d.ts +19 -26
  46. package/dist/components/builder/composition-field-editor.js +30 -52
  47. package/dist/components/builder/composition-field-editor.js.map +1 -1
  48. package/dist/components/builder/insert-context.d.ts +3 -1
  49. package/dist/components/builder/insert-context.js +15 -10
  50. package/dist/components/builder/insert-context.js.map +1 -1
  51. package/dist/components/builder/page-tree.d.ts +29 -0
  52. package/dist/components/builder/page-tree.js +56 -58
  53. package/dist/components/builder/page-tree.js.map +1 -1
  54. package/dist/components/builder/reuse.js +5 -2
  55. package/dist/components/builder/reuse.js.map +1 -1
  56. package/dist/components/cms/asset-field.js +20 -32
  57. package/dist/components/cms/asset-field.js.map +1 -1
  58. package/dist/components/cms/cms-composition-validators.js +11 -2
  59. package/dist/components/cms/cms-composition-validators.js.map +1 -1
  60. package/dist/components/cms/editable-cell.js +5 -2
  61. package/dist/components/cms/editable-cell.js.map +1 -1
  62. package/dist/components/cms/use-document-versions.js +1 -0
  63. package/dist/components/cms/use-document-versions.js.map +1 -1
  64. package/dist/components/cms/versions-model.d.ts +0 -12
  65. package/dist/components/cms/versions-model.js +3 -15
  66. package/dist/components/cms/versions-model.js.map +1 -1
  67. package/dist/components/components/component-detail.js +1 -1
  68. package/dist/components/components/component-detail.js.map +1 -1
  69. package/dist/components/components/transfer-dialog.js +2 -4
  70. package/dist/components/components/transfer-dialog.js.map +1 -1
  71. package/dist/components/components/transfer.d.ts +13 -10
  72. package/dist/components/components/transfer.js +21 -19
  73. package/dist/components/components/transfer.js.map +1 -1
  74. package/dist/components/project-session.d.ts +9 -0
  75. package/dist/components/project-session.js +1 -1
  76. package/dist/components/project-session.js.map +1 -1
  77. package/dist/components/redirects/redirects-collection-view-source.d.ts +17 -2
  78. package/dist/components/redirects/redirects-collection-view-source.js +20 -6
  79. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  80. package/dist/components/section-surface.js +3 -2
  81. package/dist/components/section-surface.js.map +1 -1
  82. package/dist/components/settings-form.js +1 -2
  83. package/dist/components/settings-form.js.map +1 -1
  84. package/dist/components/studio-context.js +1 -18
  85. package/dist/components/studio-context.js.map +1 -1
  86. package/dist/components/studio-project-screen.js +3 -5
  87. package/dist/components/studio-project-screen.js.map +1 -1
  88. package/dist/lib/ai/apply-content.js +73 -29
  89. package/dist/lib/ai/apply-content.js.map +1 -1
  90. package/dist/lib/ai/constraints.d.ts +35 -0
  91. package/dist/lib/ai/constraints.js +104 -4
  92. package/dist/lib/ai/constraints.js.map +1 -1
  93. package/dist/lib/ai/route.js +4 -1
  94. package/dist/lib/ai/route.js.map +1 -1
  95. package/dist/lib/ai/shape.d.ts +3 -2
  96. package/dist/lib/ai/shape.js +16 -24
  97. package/dist/lib/ai/shape.js.map +1 -1
  98. package/dist/lib/ai/types.d.ts +8 -0
  99. package/dist/lib/ai/types.js.map +1 -1
  100. package/dist/lib/ai/update-fields.js +33 -12
  101. package/dist/lib/ai/update-fields.js.map +1 -1
  102. package/dist/lib/assets/asset-placements.d.ts +35 -0
  103. package/dist/lib/assets/asset-placements.js +74 -0
  104. package/dist/lib/assets/asset-placements.js.map +1 -0
  105. package/dist/lib/assets/backfill-thumbs.d.ts +9 -8
  106. package/dist/lib/assets/backfill-thumbs.js +13 -25
  107. package/dist/lib/assets/backfill-thumbs.js.map +1 -1
  108. package/dist/lib/clipboard/paste-issues.d.ts +17 -15
  109. package/dist/lib/clipboard/paste-issues.js +8 -45
  110. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  111. package/dist/lib/clipboard/paste-target.d.ts +25 -1
  112. package/dist/lib/clipboard/paste-target.js +13 -6
  113. package/dist/lib/clipboard/paste-target.js.map +1 -1
  114. package/dist/lib/cms/published-documents.js +1 -0
  115. package/dist/lib/cms/published-documents.js.map +1 -1
  116. package/dist/lib/cms-views/view-model.d.ts +3 -3
  117. package/dist/lib/data/index.d.ts +0 -3
  118. package/dist/lib/data/index.js +4 -6
  119. package/dist/lib/data/index.js.map +1 -1
  120. package/dist/lib/dnd/drop-rules.d.ts +45 -2
  121. package/dist/lib/dnd/drop-rules.js +64 -9
  122. package/dist/lib/dnd/drop-rules.js.map +1 -1
  123. package/dist/lib/engine-version.d.ts +1 -1
  124. package/dist/lib/engine-version.js +1 -1
  125. package/dist/lib/engine-version.js.map +1 -1
  126. package/dist/lib/i18n/messages.de.js +4 -56
  127. package/dist/lib/i18n/messages.de.js.map +1 -1
  128. package/dist/lib/i18n/messages.en.d.ts +4 -53
  129. package/dist/lib/i18n/messages.en.js +8 -57
  130. package/dist/lib/i18n/messages.en.js.map +1 -1
  131. package/dist/lib/locale/fallback.js +8 -2
  132. package/dist/lib/locale/fallback.js.map +1 -1
  133. package/dist/lib/locale/index.d.ts +0 -1
  134. package/dist/lib/locale/index.js +0 -1
  135. package/dist/lib/locale/index.js.map +1 -1
  136. package/dist/lib/locale/locale-store.d.ts +10 -3
  137. package/dist/lib/locale/locale-store.js +12 -5
  138. package/dist/lib/locale/locale-store.js.map +1 -1
  139. package/dist/lib/operations/session-hydration.d.ts +9 -5
  140. package/dist/lib/operations/session-hydration.js +10 -10
  141. package/dist/lib/operations/session-hydration.js.map +1 -1
  142. package/dist/lib/persistence/convex-adapter.js +63 -75
  143. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  144. package/dist/lib/persona/labels.d.ts +0 -2
  145. package/dist/lib/persona/labels.js +1 -7
  146. package/dist/lib/persona/labels.js.map +1 -1
  147. package/dist/lib/registry/index.d.ts +4 -2
  148. package/dist/lib/registry/index.js +26 -21
  149. package/dist/lib/registry/index.js.map +1 -1
  150. package/dist/lib/registry/project-components.js +0 -3
  151. package/dist/lib/registry/project-components.js.map +1 -1
  152. package/dist/lib/routes/route-issues.js +4 -5
  153. package/dist/lib/routes/route-issues.js.map +1 -1
  154. package/dist/lib/theme.js +3 -2
  155. package/dist/lib/theme.js.map +1 -1
  156. package/dist/lib/webhooks/tags.js +11 -14
  157. package/dist/lib/webhooks/tags.js.map +1 -1
  158. package/dist/lib/workspace/index.d.ts +1 -1
  159. package/dist/lib/workspace/index.js +1 -1
  160. package/dist/lib/workspace/index.js.map +1 -1
  161. package/dist/previews/enumerate.d.ts +27 -9
  162. package/dist/previews/enumerate.js +27 -6
  163. package/dist/previews/enumerate.js.map +1 -1
  164. package/dist/previews/manifest-format.d.ts +9 -0
  165. package/dist/previews/manifest-format.js.map +1 -1
  166. package/dist/router/match.d.ts +0 -2
  167. package/dist/router/match.js +0 -2
  168. package/dist/router/match.js.map +1 -1
  169. package/dist/ui/badge.d.ts +1 -1
  170. package/package.json +2 -2
  171. package/src/styles/studio.css +5 -4
  172. package/src/styles/tokens.css +72 -55
  173. package/convex/content.ts +0 -291
  174. package/dist/components/collab/collab-context.d.ts +0 -27
  175. package/dist/components/collab/collab-context.js +0 -39
  176. package/dist/components/collab/collab-context.js.map +0 -1
  177. package/dist/components/collab/collab-seed.d.ts +0 -15
  178. package/dist/components/collab/collab-seed.js +0 -20
  179. package/dist/components/collab/collab-seed.js.map +0 -1
  180. package/dist/components/extensions/extensions-view.d.ts +0 -7
  181. package/dist/components/extensions/extensions-view.js +0 -127
  182. package/dist/components/extensions/extensions-view.js.map +0 -1
  183. package/dist/lib/bindings/section-bindings.d.ts +0 -24
  184. package/dist/lib/bindings/section-bindings.js +0 -9
  185. package/dist/lib/bindings/section-bindings.js.map +0 -1
  186. package/dist/lib/data/data-store.d.ts +0 -98
  187. package/dist/lib/data/data-store.js +0 -211
  188. package/dist/lib/data/data-store.js.map +0 -1
  189. package/dist/lib/data/providers.d.ts +0 -143
  190. package/dist/lib/data/providers.js +0 -255
  191. package/dist/lib/data/providers.js.map +0 -1
  192. package/dist/lib/data/sample-capture.d.ts +0 -30
  193. package/dist/lib/data/sample-capture.js +0 -70
  194. package/dist/lib/data/sample-capture.js.map +0 -1
  195. package/dist/lib/data/sources-store.d.ts +0 -42
  196. package/dist/lib/data/sources-store.js +0 -120
  197. package/dist/lib/data/sources-store.js.map +0 -1
  198. package/dist/lib/locale/binding-fallback.d.ts +0 -35
  199. package/dist/lib/locale/binding-fallback.js +0 -73
  200. package/dist/lib/locale/binding-fallback.js.map +0 -1
  201. package/dist/lib/starters/kit-session.d.ts +0 -5
  202. package/dist/lib/starters/kit-session.js +0 -17
  203. package/dist/lib/starters/kit-session.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord, AssetRepository, NewAssetInput } from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\nexport interface AssetMetadataPatch {\n /**\n * New alt text. A present key overwrites; a trimmed-empty string clears\n * the field. An absent key leaves the current value untouched.\n */\n alt?: string\n /** New display title; same present/empty/absent semantics as `alt`. */\n title?: string\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). A present `{x,y}` sets it, `null` clears it, an\n * absent key leaves the current value untouched.\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * Schema-driven asset metadata bag (EC-301): alt, copyright, credit, … keyed by\n * the asset collection's `fields`. A present object **replaces** the whole bag\n * (the image options dialog holds the complete edited meta); an absent key\n * leaves the current bag untouched.\n */\n meta?: Record<string, unknown>\n}\n\n/** Merge one patch field: present+empty clears, present+value sets, absent keeps. */\nfunction mergeField(\n patchValue: string | undefined,\n patchHasKey: boolean,\n currentValue: string | undefined,\n): string | undefined {\n if (!patchHasKey) return currentValue\n const trimmed = patchValue?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/** The full upsert input that re-states `current` with the patch applied. */\nexport function assetMetadataUpsertInput(\n current: AssetRecord,\n patch: AssetMetadataPatch,\n): NewAssetInput {\n const alt = mergeField(patch.alt, 'alt' in patch, current.alt)\n const title = mergeField(patch.title, 'title' in patch, current.title)\n // present `{x,y}` sets, present `null` clears, absent keeps the current point.\n const focalPoint = 'focalPoint' in patch ? (patch.focalPoint ?? undefined) : current.focalPoint\n // present object replaces the whole bag; absent keeps the current one (EC-301).\n const meta = 'meta' in patch ? patch.meta : current.meta\n return {\n id: current.id,\n filename: current.filename,\n contentType: current.contentType,\n byteSize: current.byteSize,\n storageKey: current.storageKey,\n ...(current.width !== undefined ? { width: current.width } : {}),\n ...(current.height !== undefined ? { height: current.height } : {}),\n ...(current.url !== undefined ? { url: current.url } : {}),\n ...(alt !== undefined ? { alt } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(focalPoint !== undefined ? { focalPoint } : {}),\n ...(meta !== undefined ? { meta } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Load the current record, apply the patch, and persist it via the\n * repository (the operations facade in the studio). Failures come back as a\n * structured result — never a throw the UI has to guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const current = await deps.assets.getAsset(projectId, assetId)\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataUpsertInput(current, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
1
+ {"version":3,"file":"asset-metadata.js","sourceRoot":"","sources":["../../../src/components/assets/asset-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord, AssetRepository, AssetWriteInput } from '@elytracms/core/persistence'\n\n/**\n * Asset metadata editing (EC-152). A patch is applied through the\n * {@link AssetRepository} interface — in the studio that is the EC-141\n * operations facade, so every edit runs the `assets.upsert` command\n * (validate → authorize → mutate → event). Pure orchestration; injectable\n * for tests.\n */\n\n/**\n * System columns the upload pipeline owns — never editable metadata, so they're\n * excluded from the asset collection's schema-driven forms: the image field's\n * options dialog (EC-301, `field-control.tsx`) and the assets rail's detail\n * surface (EC-319, `asset-detail.tsx`) share this ONE reserved set so neither\n * drifts from the other. Everything else a project declares on the `kind:'asset'`\n * collection round-trips through the record's open `meta` bag.\n */\nexport const SYSTEM_ASSET_FIELDS: ReadonlySet<string> = new Set([\n 'filename',\n 'url',\n 'contentType',\n 'byteSize',\n 'width',\n 'height',\n 'storageKey',\n 'title',\n])\n\n/**\n * The asset collection's EDITABLE metadata fields — declared fields minus the\n * system columns above and anything explicitly `readOnly` (EC-301/EC-319).\n */\nexport function assetMetaFieldsOf(collection: CollectionDef): FieldDef[] {\n return collection.fields.filter((f) => !SYSTEM_ASSET_FIELDS.has(f.name) && !f.form?.readOnly)\n}\n\n/** Drop `undefined` values so a persisted meta bag stays JSON/Convex-clean. */\nexport function pruneMeta(meta: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined))\n}\n\n/** Human-readable byte size (`1536` → `1.5 KB`). Shared by the asset grid + detail. */\nexport function formatBytes(byteSize: number): string {\n if (byteSize < 1024) return `${byteSize} B`\n if (byteSize < 1024 * 1024) return `${(byteSize / 1024).toFixed(1)} KB`\n return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`\n}\n\n/**\n * An editor's metadata change, in the EC-577 write rule: an ABSENT key leaves the\n * stored value untouched, `null` clears it, a value sets it.\n */\nexport interface AssetMetadataPatch {\n /** New alt text: a value sets (trimmed), `null` or a trimmed-empty string clears. */\n alt?: string | null\n /** New display title; same semantics as `alt`. */\n title?: string | null\n /**\n * New focal point (EC-248), the normalized 0–1 hotspot threaded to delivery's\n * `object-position` (EC-230). `{x,y}` sets it, `null` clears it (\"Reset to\n * centre\").\n */\n focalPoint?: { x: number; y: number } | null\n /**\n * Schema-driven asset metadata bag (EC-301): alt, copyright, credit, … keyed by\n * the asset collection's `fields`. An object **replaces** the whole bag (the\n * image options dialog holds the complete edited meta); `null` clears it.\n */\n meta?: Record<string, unknown> | null\n}\n\n/** A text column's write: a trimmed value sets, `null`/empty clears. */\nfunction textWrite(value: string | null): string | null {\n const trimmed = value?.trim() ?? ''\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * The asset write for a metadata edit (EC-577): ONLY the keys the editor changed,\n * with `null` for a clear. It deliberately restates nothing else — not the\n * upload-owned columns, and never `thumb`/`lqip`: a stale copy of those would\n * overwrite a preview backfill that finished while the dialog was open, and a\n * clear expressed as an omitted key is exactly what never persisted on Convex.\n */\nexport function assetMetadataWrite(assetId: string, patch: AssetMetadataPatch): AssetWriteInput {\n return {\n id: assetId,\n ...(patch.alt !== undefined ? { alt: textWrite(patch.alt) } : {}),\n ...(patch.title !== undefined ? { title: textWrite(patch.title) } : {}),\n ...(patch.focalPoint !== undefined ? { focalPoint: patch.focalPoint } : {}),\n ...(patch.meta !== undefined ? { meta: patch.meta } : {}),\n }\n}\n\nexport type UpdateAssetMetadataResult =\n | { ok: true; asset: AssetRecord }\n | { ok: false; message: string }\n\n/**\n * Persist a metadata edit via the repository (the operations facade in the\n * studio). No read first: the write carries only the change, and the adapter\n * resolves it against the stored record inside its own transaction (EC-577). An\n * asset that no longer exists is refused there (a metadata write cannot create\n * one). Failures come back as a structured result — never a throw the UI has to\n * guess about.\n */\nexport async function updateAssetMetadata(\n deps: { assets: AssetRepository },\n projectId: string,\n assetId: string,\n patch: AssetMetadataPatch,\n): Promise<UpdateAssetMetadataResult> {\n try {\n const asset = await deps.assets.upsertAsset(projectId, assetMetadataWrite(assetId, patch))\n return { ok: true, asset }\n } catch (cause) {\n return { ok: false, message: cause instanceof Error ? cause.message : String(cause) }\n }\n}\n"]}
@@ -1,11 +1,30 @@
1
+ import type { AssetFieldDef } from '@elytracms/core/cms-core';
2
+ import type { NewPlacementForm } from '@elytracms/studio/lib/assets/asset-placements';
1
3
  /** The id list a `many` asset field currently holds (unreadable entries dropped). */
2
4
  export declare function assetIdsOf(value: unknown): string[];
3
5
  /** The single id a `one` asset field currently holds, if any. */
4
6
  export declare function singleAssetIdOf(value: unknown): string | null;
5
7
  /**
6
- * Apply a picker selection to the stored field value.
7
- * - `one`: the picked id replaces the value; `null` clears it (`undefined`).
8
- * - `many`: the picked id is appended once (already-present ids are a
9
- * no-op); `null` leaves the list untouched.
8
+ * How a picker writes a NEW placement on this field (EC-301 → EC-558): an envelope
9
+ * when the field declares per-use sub-fields (so the placement's sub-field form has
10
+ * something to write into), otherwise a bare id (self-typed on save). The table cell
11
+ * and the detail editor both read it here, so they cannot disagree.
10
12
  */
11
- export declare function applyAssetSelection(current: unknown, picked: string | null, multiple: boolean): unknown;
13
+ export declare function assetPickForm(field: Pick<AssetFieldDef, 'fields'>): NewPlacementForm;
14
+ /**
15
+ * Apply a picker selection to the stored field value, keeping every placement's
16
+ * per-use sub-fields (EC-558).
17
+ * - `one`: the picked id replaces the image and keeps the placement's sub-fields.
18
+ * Picking the id it already shows is a no-op, and `null` clears it (`undefined`).
19
+ * - `many`: the picked id is appended once, and held entries stay as they are.
20
+ * An already-present id and `null` are no-ops.
21
+ *
22
+ * A no-op returns `current` itself, so a caller can skip the write.
23
+ */
24
+ export declare function applyAssetSelection(current: unknown, picked: string | null, multiple: boolean, form: NewPlacementForm): unknown;
25
+ /**
26
+ * Attach several picked ids to a `many` field at once: an in-picker or drop upload
27
+ * batch (EC-460). Each id not already held is appended once, in arrival order, and
28
+ * held entries stay as they are. Returns `current` itself when nothing is new.
29
+ */
30
+ export declare function appendAssetSelections(current: unknown, picked: readonly string[], form: NewPlacementForm): unknown;
@@ -4,8 +4,14 @@
4
4
  * normalize stored values and apply a picker selection without the UI owning the
5
5
  * rules. EC-277: reads tolerate self-typed `{ type:'asset', id }` envelopes AND
6
6
  * legacy bare ids (the field is self-typed on save by the operations write lens).
7
+ *
8
+ * EC-558: the pick operations below are the ones the table cell and the detail
9
+ * editor BOTH commit. They work out which ids the field holds next, and hand the
10
+ * per-use sub-field carry to the one rule, `carryAssetPlacements` (in `lib/`,
11
+ * because AI `updateFields` uses it too). Neither writer rebuilds the value itself.
7
12
  */
8
13
  import { readAssetId } from '@elytracms/core/cms-core';
14
+ import { carryAssetPlacements } from '@elytracms/studio/lib/assets/asset-placements';
9
15
  /** The id list a `many` asset field currently holds (unreadable entries dropped). */
10
16
  export function assetIdsOf(value) {
11
17
  if (!Array.isArray(value))
@@ -22,17 +28,41 @@ export function singleAssetIdOf(value) {
22
28
  return null;
23
29
  }
24
30
  /**
25
- * Apply a picker selection to the stored field value.
26
- * - `one`: the picked id replaces the value; `null` clears it (`undefined`).
27
- * - `many`: the picked id is appended once (already-present ids are a
28
- * no-op); `null` leaves the list untouched.
31
+ * How a picker writes a NEW placement on this field (EC-301 → EC-558): an envelope
32
+ * when the field declares per-use sub-fields (so the placement's sub-field form has
33
+ * something to write into), otherwise a bare id (self-typed on save). The table cell
34
+ * and the detail editor both read it here, so they cannot disagree.
29
35
  */
30
- export function applyAssetSelection(current, picked, multiple) {
36
+ export function assetPickForm(field) {
37
+ return (field.fields?.length ?? 0) > 0 ? 'envelope' : 'bare';
38
+ }
39
+ /**
40
+ * Apply a picker selection to the stored field value, keeping every placement's
41
+ * per-use sub-fields (EC-558).
42
+ * - `one`: the picked id replaces the image and keeps the placement's sub-fields.
43
+ * Picking the id it already shows is a no-op, and `null` clears it (`undefined`).
44
+ * - `many`: the picked id is appended once, and held entries stay as they are.
45
+ * An already-present id and `null` are no-ops.
46
+ *
47
+ * A no-op returns `current` itself, so a caller can skip the write.
48
+ */
49
+ export function applyAssetSelection(current, picked, multiple, form) {
31
50
  if (!multiple)
32
- return picked ?? undefined;
51
+ return carryAssetPlacements(current, { multiple: false, id: picked }, form);
52
+ if (picked === null)
53
+ return current;
54
+ return appendAssetSelections(current, [picked], form);
55
+ }
56
+ /**
57
+ * Attach several picked ids to a `many` field at once: an in-picker or drop upload
58
+ * batch (EC-460). Each id not already held is appended once, in arrival order, and
59
+ * held entries stay as they are. Returns `current` itself when nothing is new.
60
+ */
61
+ export function appendAssetSelections(current, picked, form) {
33
62
  const ids = assetIdsOf(current);
34
- if (picked === null || ids.includes(picked))
35
- return ids;
36
- return [...ids, picked];
63
+ const fresh = [...new Set(picked)].filter((id) => !ids.includes(id));
64
+ if (fresh.length === 0)
65
+ return current;
66
+ return carryAssetPlacements(current, { multiple: true, ids: [...ids, ...fresh] }, form);
37
67
  }
38
68
  //# sourceMappingURL=asset-selection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,QAAiB;IAEjB,IAAI,CAAC,QAAQ;QAAE,OAAO,MAAM,IAAI,SAAS,CAAA;IACzC,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/B,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,GAAG,CAAA;IACvD,OAAO,CAAC,GAAG,GAAG,EAAE,MAAM,CAAC,CAAA;AACzB,CAAC","sourcesContent":["/**\n * Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores the\n * asset *id* (cardinality `one`) or an array of ids (`many`). These helpers\n * normalize stored values and apply a picker selection without the UI owning the\n * rules. EC-277: reads tolerate self-typed `{ type:'asset', id }` envelopes AND\n * legacy bare ids (the field is self-typed on save by the operations write lens).\n */\nimport { readAssetId } from '@elytracms/core/cms-core'\n\n/** The id list a `many` asset field currently holds (unreadable entries dropped). */\nexport function assetIdsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return value.map(readAssetId).filter((id): id is string => id !== undefined)\n}\n\n/** The single id a `one` asset field currently holds, if any. */\nexport function singleAssetIdOf(value: unknown): string | null {\n const direct = readAssetId(value)\n if (direct !== undefined) return direct\n if (Array.isArray(value)) return readAssetId(value[0]) ?? null\n return null\n}\n\n/**\n * Apply a picker selection to the stored field value.\n * - `one`: the picked id replaces the value; `null` clears it (`undefined`).\n * - `many`: the picked id is appended once (already-present ids are a\n * no-op); `null` leaves the list untouched.\n */\nexport function applyAssetSelection(\n current: unknown,\n picked: string | null,\n multiple: boolean,\n): unknown {\n if (!multiple) return picked ?? undefined\n const ids = assetIdsOf(current)\n if (picked === null || ids.includes(picked)) return ids\n return [...ids, picked]\n}\n"]}
1
+ {"version":3,"file":"asset-selection.js","sourceRoot":"","sources":["../../../src/components/assets/asset-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+CAA+C,CAAA;AAGpF,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,OAAO,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;AAC9E,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,KAAoC;IAChE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAA;AAC9D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAqB,EACrB,QAAiB,EACjB,IAAsB;IAEtB,IAAI,CAAC,QAAQ;QAAE,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,CAAA;IAC1F,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,OAAO,CAAA;IACnC,OAAO,qBAAqB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAA;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAgB,EAChB,MAAyB,EACzB,IAAsB;IAEtB,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;IACpE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,OAAO,oBAAoB,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;AACzF,CAAC","sourcesContent":["/**\n * Pure value logic for CMS `asset` fields (EC-075 → EC-152): a field stores the\n * asset *id* (cardinality `one`) or an array of ids (`many`). These helpers\n * normalize stored values and apply a picker selection without the UI owning the\n * rules. EC-277: reads tolerate self-typed `{ type:'asset', id }` envelopes AND\n * legacy bare ids (the field is self-typed on save by the operations write lens).\n *\n * EC-558: the pick operations below are the ones the table cell and the detail\n * editor BOTH commit. They work out which ids the field holds next, and hand the\n * per-use sub-field carry to the one rule, `carryAssetPlacements` (in `lib/`,\n * because AI `updateFields` uses it too). Neither writer rebuilds the value itself.\n */\nimport { readAssetId } from '@elytracms/core/cms-core'\nimport type { AssetFieldDef } from '@elytracms/core/cms-core'\nimport { carryAssetPlacements } from '@elytracms/studio/lib/assets/asset-placements'\nimport type { NewPlacementForm } from '@elytracms/studio/lib/assets/asset-placements'\n\n/** The id list a `many` asset field currently holds (unreadable entries dropped). */\nexport function assetIdsOf(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return value.map(readAssetId).filter((id): id is string => id !== undefined)\n}\n\n/** The single id a `one` asset field currently holds, if any. */\nexport function singleAssetIdOf(value: unknown): string | null {\n const direct = readAssetId(value)\n if (direct !== undefined) return direct\n if (Array.isArray(value)) return readAssetId(value[0]) ?? null\n return null\n}\n\n/**\n * How a picker writes a NEW placement on this field (EC-301 → EC-558): an envelope\n * when the field declares per-use sub-fields (so the placement's sub-field form has\n * something to write into), otherwise a bare id (self-typed on save). The table cell\n * and the detail editor both read it here, so they cannot disagree.\n */\nexport function assetPickForm(field: Pick<AssetFieldDef, 'fields'>): NewPlacementForm {\n return (field.fields?.length ?? 0) > 0 ? 'envelope' : 'bare'\n}\n\n/**\n * Apply a picker selection to the stored field value, keeping every placement's\n * per-use sub-fields (EC-558).\n * - `one`: the picked id replaces the image and keeps the placement's sub-fields.\n * Picking the id it already shows is a no-op, and `null` clears it (`undefined`).\n * - `many`: the picked id is appended once, and held entries stay as they are.\n * An already-present id and `null` are no-ops.\n *\n * A no-op returns `current` itself, so a caller can skip the write.\n */\nexport function applyAssetSelection(\n current: unknown,\n picked: string | null,\n multiple: boolean,\n form: NewPlacementForm,\n): unknown {\n if (!multiple) return carryAssetPlacements(current, { multiple: false, id: picked }, form)\n if (picked === null) return current\n return appendAssetSelections(current, [picked], form)\n}\n\n/**\n * Attach several picked ids to a `many` field at once: an in-picker or drop upload\n * batch (EC-460). Each id not already held is appended once, in arrival order, and\n * held entries stay as they are. Returns `current` itself when nothing is new.\n */\nexport function appendAssetSelections(\n current: unknown,\n picked: readonly string[],\n form: NewPlacementForm,\n): unknown {\n const ids = assetIdsOf(current)\n const fresh = [...new Set(picked)].filter((id) => !ids.includes(id))\n if (fresh.length === 0) return current\n return carryAssetPlacements(current, { multiple: true, ids: [...ids, ...fresh] }, form)\n}\n"]}
@@ -23,24 +23,27 @@ import type { CollectionViewSource, CollectionViewCapabilities } from '@elytracm
23
23
  */
24
24
  export declare const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities;
25
25
  /**
26
- * Project a stored asset record into the lens's `values` shape (EC-319), reading
27
- * only the schema's DECLARED fields — the same "schema is a lens" projection the
28
- * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)
29
- * reads straight off the record; everything else — the built-in `alt`, a
30
- * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips
31
- * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO
32
- * has a legacy record column: the meta value wins, falling back to the column
33
- * (matches delivery's `meta.alt ?? record.alt` read and the image field's
34
- * options dialog).
26
+ * Project a stored asset record into the lens's `values` shape (EC-319) — the
27
+ * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY
28
+ * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the
29
+ * schema or not, so a save hands the bag back whole (D22). Declared fields then
30
+ * take their canonical reading on top — a column never loses to a same-named meta
31
+ * key: a system column (`filename`, `url`, dimensions, …) reads straight off the
32
+ * record, and `alt`, schema-driven but ALSO a legacy record column, reads the meta
33
+ * value falling back to the column (matches delivery's `meta.alt ?? record.alt`
34
+ * read and the image field's options dialog).
35
35
  */
36
36
  export declare function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument;
37
37
  /**
38
- * The inverse of {@link assetToDocument}: the schema-driven meta bag a save
39
- * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),
40
- * keyed exactly as the record stores it. System columns are upload-managed and
41
- * never written here.
38
+ * The inverse of {@link assetToDocument}: the meta bag a save writes back — every
39
+ * value outside the system set (EC-301/EC-311), declared or not, through the ONE
40
+ * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d).
41
+ * `meta` is written whole (it replaces the stored bag), so a key the lens never
42
+ * declared must be in it: the schema-driven projection this replaced dropped them
43
+ * on every rail save (D22), while the options dialog and asset detail kept them.
44
+ * System columns are upload-managed and never written here.
42
45
  */
43
- export declare function documentToAssetMetaPatch(collection: CollectionDef, doc: CmsDocument): Record<string, unknown>;
46
+ export declare function documentToAssetMetaPatch(doc: CmsDocument): Record<string, unknown>;
44
47
  /** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}
45
48
  * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;
46
49
  * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces
@@ -2,8 +2,8 @@ import { useCallback, useMemo } from 'react';
2
2
  import { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core';
3
3
  import { studioPath } from '@elytracms/studio/router';
4
4
  import { useAssets } from './assets-store.js';
5
- import { SYSTEM_ASSET_FIELDS } from './asset-metadata.js';
6
- import { normalizeOkResult, useRailViewSource } from '../rail-view-source.js';
5
+ import { SYSTEM_ASSET_FIELDS, pruneMeta } from './asset-metadata.js';
6
+ import { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source.js';
7
7
  /** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */
8
8
  const ASSETS_LOCALE = 'en';
9
9
  const ASSETS_LOCALE_CONFIG = {
@@ -66,46 +66,40 @@ function systemFieldValue(record, name) {
66
66
  }
67
67
  }
68
68
  /**
69
- * Project a stored asset record into the lens's `values` shape (EC-319), reading
70
- * only the schema's DECLARED fields — the same "schema is a lens" projection the
71
- * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)
72
- * reads straight off the record; everything else — the built-in `alt`, a
73
- * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips
74
- * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO
75
- * has a legacy record column: the meta value wins, falling back to the column
76
- * (matches delivery's `meta.alt ?? record.alt` read and the image field's
77
- * options dialog).
69
+ * Project a stored asset record into the lens's `values` shape (EC-319) — the
70
+ * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY
71
+ * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the
72
+ * schema or not, so a save hands the bag back whole (D22). Declared fields then
73
+ * take their canonical reading on top — a column never loses to a same-named meta
74
+ * key: a system column (`filename`, `url`, dimensions, …) reads straight off the
75
+ * record, and `alt`, schema-driven but ALSO a legacy record column, reads the meta
76
+ * value falling back to the column (matches delivery's `meta.alt ?? record.alt`
77
+ * read and the image field's options dialog).
78
78
  */
79
79
  export function assetToDocument(collection, record) {
80
- const values = {};
80
+ const values = { ...(record.meta ?? {}) };
81
81
  for (const field of collection.fields) {
82
82
  if (field.name === 'alt') {
83
83
  const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined;
84
84
  values.alt = metaAlt ?? record.alt;
85
- continue;
86
85
  }
87
- values[field.name] = SYSTEM_ASSET_FIELDS.has(field.name)
88
- ? systemFieldValue(record, field.name)
89
- : record.meta?.[field.name];
86
+ else if (SYSTEM_ASSET_FIELDS.has(field.name)) {
87
+ values[field.name] = systemFieldValue(record, field.name);
88
+ }
90
89
  }
91
90
  return { collection: collection.id, id: record.id, values };
92
91
  }
93
92
  /**
94
- * The inverse of {@link assetToDocument}: the schema-driven meta bag a save
95
- * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),
96
- * keyed exactly as the record stores it. System columns are upload-managed and
97
- * never written here.
93
+ * The inverse of {@link assetToDocument}: the meta bag a save writes back — every
94
+ * value outside the system set (EC-301/EC-311), declared or not, through the ONE
95
+ * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d).
96
+ * `meta` is written whole (it replaces the stored bag), so a key the lens never
97
+ * declared must be in it: the schema-driven projection this replaced dropped them
98
+ * on every rail save (D22), while the options dialog and asset detail kept them.
99
+ * System columns are upload-managed and never written here.
98
100
  */
99
- export function documentToAssetMetaPatch(collection, doc) {
100
- const values = doc.values ?? {};
101
- const meta = {};
102
- for (const field of collection.fields) {
103
- if (SYSTEM_ASSET_FIELDS.has(field.name))
104
- continue;
105
- if (field.name in values)
106
- meta[field.name] = values[field.name];
107
- }
108
- return meta;
101
+ export function documentToAssetMetaPatch(doc) {
102
+ return pruneMeta(metaBagProjection(doc.values ?? {}, SYSTEM_ASSET_FIELDS));
109
103
  }
110
104
  /** `create` when `capabilities.create === false` — never reached from the UI
111
105
  * (list/detail never call it); an explicit throw rather than a silent no-op. */
@@ -170,7 +164,7 @@ export function useAssetsCollectionViewSource(projectId, schema) {
170
164
  return record?.title ?? record?.filename ?? ref.id;
171
165
  }, [store.assets]);
172
166
  const validate = useCallback((doc) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }), [registry]);
173
- const save = useCallback((doc) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(schema, doc) }), [schema, store]);
167
+ const save = useCallback((doc) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(doc) }), [store]);
174
168
  // The unconfirmed delete (the store's usage gate still applies — a referenced
175
169
  // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths
176
170
  // never land here: row/bulk deletes confirm through the reference dialog and
@@ -1 +1 @@
1
- {"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '@elytracms/studio/lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS } from './asset-metadata'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */\nconst ASSETS_LOCALE: Locale = 'en'\nconst ASSETS_LOCALE_CONFIG: LocaleConfig = {\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319), reading\n * only the schema's DECLARED fields — the same \"schema is a lens\" projection the\n * redirects/users rails use. A system column (`filename`, `url`, dimensions, …)\n * reads straight off the record; everything else — the built-in `alt`, a\n * project's own `copyright`/`credit`, or an EC-311 additive field — round-trips\n * through the record's open `meta` bag (EC-301). `alt` is schema-driven but ALSO\n * has a legacy record column: the meta value wins, falling back to the column\n * (matches delivery's `meta.alt ?? record.alt` read and the image field's\n * options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined\n values.alt = metaAlt ?? record.alt\n continue\n }\n values[field.name] = SYSTEM_ASSET_FIELDS.has(field.name)\n ? systemFieldValue(record, field.name)\n : record.meta?.[field.name]\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the schema-driven meta bag a save\n * writes back — every declared field OUTSIDE the system set (EC-301/EC-311),\n * keyed exactly as the record stores it. System columns are upload-managed and\n * never written here.\n */\nexport function documentToAssetMetaPatch(\n collection: CollectionDef,\n doc: CmsDocument,\n): Record<string, unknown> {\n const values = doc.values ?? {}\n const meta: Record<string, unknown> = {}\n for (const field of collection.fields) {\n if (SYSTEM_ASSET_FIELDS.has(field.name)) continue\n if (field.name in values) meta[field.name] = values[field.name]\n }\n return meta\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record)),\n [store.assets, schema],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record) : undefined\n },\n [store.assets, schema],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),\n [registry],\n )\n const save = useCallback(\n (doc: CmsDocument) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(schema, doc) }),\n [schema, store],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: ASSETS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
1
+ {"version":3,"file":"assets-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/assets/assets-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { AssetRecord, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport type { DeleteAssetResult } from '@elytracms/studio/lib/assets'\nimport type { CollectionViewSource, CollectionViewCapabilities } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useAssets } from './assets-store'\nimport { SYSTEM_ASSET_FIELDS, pruneMeta } from './asset-metadata'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */\nconst ASSETS_LOCALE: Locale = 'en'\nconst ASSETS_LOCALE_CONFIG: LocaleConfig = {\n default: ASSETS_LOCALE,\n locales: [ASSETS_LOCALE],\n}\n\n/**\n * The capability profile of the assets rail (EC-319, EC-433). Hoisted to a\n * module-level constant (no prop/state dependency) so it's both the value the\n * hook returns AND independently unit-testable.\n *\n * `create` is off — a new asset comes from an upload, never a blank draft.\n * `remove` + `bulk` are ON since EC-433: the list's row/bulk delete used to be\n * off deliberately because the generic `ReferenceConfirmDialog` enumerated\n * inbound references as `{ type: 'document' }` targets — the wrong index shape\n * for an asset — which would have shown \"not referenced\" for an asset that IS\n * referenced. That pre-flight now queries the shape THIS source declares via\n * {@link assetReferenceTarget} (`{ type: 'asset' }`, the same lookup the\n * detail surface runs), and the confirmed delete dispatches the SAME checked\n * store delete the detail surface uses (`deleteAsset`: original + EC-400 thumb\n * blobs, then the record — EC-420 lqips ride on the record). Nothing else\n * applies to assets: no publish/versions/locales/inline-edit/live-URL/\n * hierarchy/import-export.\n */\nexport const ASSET_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false, // EC-442: blob duplication is a different feature, out of scope\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\n/** Read one of the record's fixed, upload-managed columns by field name. */\nfunction systemFieldValue(record: AssetRecord, name: string): unknown {\n switch (name) {\n case 'filename':\n return record.filename\n case 'url':\n return record.url\n case 'contentType':\n return record.contentType\n case 'byteSize':\n return record.byteSize\n case 'width':\n return record.width\n case 'height':\n return record.height\n case 'storageKey':\n return record.storageKey\n case 'title':\n return record.title\n default:\n return undefined\n }\n}\n\n/**\n * Project a stored asset record into the lens's `values` shape (EC-319) — the\n * value-keyed projection the redirects/users rails use (EC-577, RESIDUE-3d): EVERY\n * key of the record's open `meta` bag (EC-301) rides in `values`, declared by the\n * schema or not, so a save hands the bag back whole (D22). Declared fields then\n * take their canonical reading on top — a column never loses to a same-named meta\n * key: a system column (`filename`, `url`, dimensions, …) reads straight off the\n * record, and `alt`, schema-driven but ALSO a legacy record column, reads the meta\n * value falling back to the column (matches delivery's `meta.alt ?? record.alt`\n * read and the image field's options dialog).\n */\nexport function assetToDocument(collection: CollectionDef, record: AssetRecord): CmsDocument {\n const values: Record<string, unknown> = { ...(record.meta ?? {}) }\n for (const field of collection.fields) {\n if (field.name === 'alt') {\n const metaAlt = typeof record.meta?.alt === 'string' ? record.meta.alt : undefined\n values.alt = metaAlt ?? record.alt\n } else if (SYSTEM_ASSET_FIELDS.has(field.name)) {\n values[field.name] = systemFieldValue(record, field.name)\n }\n }\n return { collection: collection.id, id: record.id, values }\n}\n\n/**\n * The inverse of {@link assetToDocument}: the meta bag a save writes back — every\n * value outside the system set (EC-301/EC-311), declared or not, through the ONE\n * value-keyed projection the users/redirects rails share (EC-577, RESIDUE-3d).\n * `meta` is written whole (it replaces the stored bag), so a key the lens never\n * declared must be in it: the schema-driven projection this replaced dropped them\n * on every rail save (D22), while the options dialog and asset detail kept them.\n * System columns are upload-managed and never written here.\n */\nexport function documentToAssetMetaPatch(doc: CmsDocument): Record<string, unknown> {\n return pruneMeta(metaBagProjection(doc.values ?? {}, SYSTEM_ASSET_FIELDS))\n}\n\n/** `create` when `capabilities.create === false` — never reached from the UI\n * (list/detail never call it); an explicit throw rather than a silent no-op. */\nfunction createUnsupported(): DocumentRef {\n throw new Error('assets are created by upload, not the assets rail')\n}\n\n/** Assets' `remove()` returns an `outcome`-discriminated {@link DeleteAssetResult}\n * (not the `{ ok }` shape users/redirects use, EC-327). `'error'` surfaces as-is;\n * `'blocked'` (the unconfirmed usage gate fired — nothing was removed) surfaces\n * as an explicit failure too (EC-433): a delete that left the asset in place\n * must NEVER look like success. It is unreachable from this rail's UI — every\n * delete path confirms through the reference dialog and dispatches with\n * `confirmInUse` — but stays honest for any direct caller. `'not-found'` maps\n * to success (idempotent delete: the desired end state — gone — holds). */\nexport function normalizeAssetRemoveResult(result: DeleteAssetResult): OperationError | null {\n if (result.outcome === 'error') return { code: 'unknown', message: result.message }\n if (result.outcome === 'blocked') {\n return {\n code: 'unknown',\n message: `Asset is referenced in ${result.usageCount} ${result.usageCount === 1 ? 'place' : 'places'} and was not deleted.`,\n }\n }\n return null\n}\n\n/**\n * How an asset row is addressed in the EC-155 reference index (EC-433):\n * `{ type: 'asset' }`, never the document shape — so the delete confirmation's\n * inbound pre-flight counts the SAME references `loadInboundAssetReferenceViews`\n * (the detail surface's check) does.\n */\nexport function assetReferenceTarget(ref: DocumentRef): ReferenceTarget {\n return { type: 'asset', id: ref.id }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **assets rail** (EC-319) — the\n * portable list surface over the existing asset library store instead of a\n * bespoke listing. `schema` is the DEPLOYMENT-resolved `kind:'asset'` collection\n * (EC-311: a project's own declared fields, or a mount's `nav.assets({ fields })`\n * override merged over it — see `resolveAssetsSchema` in `app-views/default-modules.tsx`),\n * never a hardcoded engine default — so a project's `copyright`/`credit`/… fields\n * (and any EC-311 override) drive the list's columns/filters exactly as they\n * drive the image field's options dialog.\n *\n * Must be mounted under an `<AssetsProvider>` (for {@link useAssets}) — already\n * ambient at the project level (EC-195), so callers rarely mount their own.\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the asset-store\n * reads/writes, the capability profile, and the projections above.\n */\nexport function useAssetsCollectionViewSource(\n projectId: string,\n schema: CollectionDef,\n): CollectionViewSource {\n const store = useAssets()\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.assets.map((record) => assetToDocument(schema, record)),\n [store.assets, schema],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record ? assetToDocument(schema, record) : undefined\n },\n [store.assets, schema],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => {\n const record = store.assets.find((a) => a.id === ref.id)\n return record?.title ?? record?.filename ?? ref.id\n },\n [store.assets],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: ASSETS_LOCALE_CONFIG }),\n [registry],\n )\n const save = useCallback(\n (doc: CmsDocument) => store.updateMetadata(doc.id, { meta: documentToAssetMetaPatch(doc) }),\n [store],\n )\n // The unconfirmed delete (the store's usage gate still applies — a referenced\n // asset comes back `blocked`, surfaced via `lastError`). The UI's delete paths\n // never land here: row/bulk deletes confirm through the reference dialog and\n // dispatch `bulkRemove` below.\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n // The CONFIRMED delete the bulk/row path dispatches (EC-433): the reference\n // dialog has already enumerated inbound usages (asset-shaped, see\n // `assetReferenceTarget`), so this passes `confirmInUse` exactly like the\n // detail surface's confirmed delete — same blob + thumb + cache handling.\n const bulkRemove = useCallback(\n (ref: DocumentRef) => store.remove(ref.id, { confirmInUse: true }),\n [store],\n )\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: ASSET_COLLECTION_CAPABILITIES,\n localeConfig: ASSETS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections`/`isFieldReadOnly` override: assets carry\n // no relation/asset fields of their own (v1) — nothing cross-collection to\n // resolve — and read-only follows the factory's default `form.readOnly` check.\n detailPath: (id: string) => studioPath.asset(id),\n listPath: studioPath.assets(),\n // EC-433: the delete confirmation must enumerate ASSET-shaped inbound\n // references — the document-shaped default would report \"not referenced\"\n // for a referenced asset.\n referenceTarget: assetReferenceTarget,\n validate,\n create: createUnsupported,\n save,\n remove,\n bulkRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeAssetRemoveResult,\n })\n}\n"]}
@@ -5,7 +5,7 @@ import { buildFragmentFromRegistryFixture, buildFragmentFromSubtree, } from '@el
5
5
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
6
6
  import { translate } from '@elytracms/studio/lib/i18n';
7
7
  import { resolveDeploymentGeneratedPreviews } from '@elytracms/studio/lib/workspace';
8
- import { useCollab } from '../collab/collab-context.js';
8
+ import { useProjectSession } from '../project-session.js';
9
9
  import { useStudioClipboard } from './clipboard-actions.js';
10
10
  import { beginDrag, endDrag, onDragSessionEnd } from './drag-session.js';
11
11
  import { Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Popover, PopoverContent, PopoverTrigger, Row, Text, } from '@elytracms/studio/ui';
@@ -48,7 +48,7 @@ export function preventOutsideDismiss(event) {
48
48
  export function BlocksPanel({ context, open, onOpenChange, }) {
49
49
  const builder = useBuilder();
50
50
  const studio = getStudioRegistry();
51
- const collab = useCollab();
51
+ const { projectId } = useProjectSession();
52
52
  const clipboard = useStudioClipboard();
53
53
  const [query, setQuery] = useState('');
54
54
  useEffect(() => {
@@ -88,7 +88,7 @@ export function BlocksPanel({ context, open, onOpenChange, }) {
88
88
  // "Copy exact configuration" (EC-175): same fragment + paste path as
89
89
  // copying a live canvas instance, per variant.
90
90
  function copyVariant(tile, variant) {
91
- const fragment = tileFragment(studio, tile, variant, collab.projectId);
91
+ const fragment = tileFragment(studio, tile, variant, projectId);
92
92
  if (fragment)
93
93
  clipboard.copyFragment(fragment, `${tile.title} — ${variant.name}`);
94
94
  }
@@ -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).\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"]}
1
+ {"version":3,"file":"blocks-panel.js","sourceRoot":"","sources":["../../../src/components/builder/blocks-panel.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE5D,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAElE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,kCAAkC,EAAE,MAAM,iCAAiC,CAAA;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,KAAK,EACL,OAAO,EACP,cAAc,EACd,cAAc,EACd,GAAG,EACH,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEzD,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAqBjF;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAsB,EACtB,IAAe,EACf,OAAqB,EACrB,SAAiB;IAEjB,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC9D,MAAM,OAAO,GAAG,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxF,IAAI,QAAQ,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,gCAAgC,CAAC,QAAQ,EAAE,OAAO,EAAE;YAChE,MAAM,EAAE,EAAE,SAAS,EAAE;SACtB,CAAC,CAAA;QACF,OAAO,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;IACzC,CAAC;IACD,OAAO,wBAAwB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;AAC7E,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAqC;IACzE,KAAK,CAAC,cAAc,EAAE,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAC1B,OAAO,EACP,IAAI,EACJ,YAAY,GAKb;IACC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,EAAE,SAAS,EAAE,GAAG,iBAAiB,EAAE,CAAA;IACzC,MAAM,SAAS,GAAG,kBAAkB,EAAE,CAAA;IACtC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAEtC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI;YAAE,QAAQ,CAAC,EAAE,CAAC,CAAA;IACxB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEV,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,CAAA;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM,gBAAgB,GAAG,kCAAkC,EAAE,CAAA;IAC7D,MAAM,MAAM,GAAG,OAAO,CACpB,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,EAAE,gBAAgB,CAAC,EACvF,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAC/C,CAAA;IACD,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,OAAO,GAAG,OAAO,CACrB,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,EAC7C,CAAC,OAAO,EAAE,SAAS,CAAC,CACrB,CAAA;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,4EAA4E;IAC5E,MAAM,cAAc,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAA;IACxD,SAAS,CACP,GAAG,EAAE,CACH,gBAAgB,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;QACpC,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAM;QACvC,cAAc,CAAC,OAAO,EAAE,EAAE,CAAA;QAC1B,cAAc,CAAC,OAAO,GAAG,IAAI,CAAA;QAC7B,IAAI,OAAO,KAAK,SAAS;YAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IAChD,CAAC,CAAC,EACJ,CAAC,YAAY,CAAC,CACf,CAAA;IAED,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IAEzB,SAAS,aAAa,CAAC,OAAqB;QAC1C,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QACpF,YAAY,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IAED,qEAAqE;IACrE,+CAA+C;IAC/C,SAAS,WAAW,CAAC,IAAe,EAAE,OAAqB;QACzD,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QAC/D,IAAI,QAAQ;YAAE,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IACnF,CAAC;IAED,SAAS,mBAAmB,CAAC,IAAe,EAAE,CAAkB;QAC9D,SAAS,CACP;YACE,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE;YAC7D,MAAM,EAAE,QAAQ;YAChB,sEAAsE;YACtE,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,KAAK;SACtB,EACD,CAAC,CAAC,YAAY,CACf,CAAA;QACD,uEAAuE;QACvE,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,OAAO,GAAI,CAAC,CAAC,aAA6B,CAAC,OAAO,CAAC,iBAAiB,CAAuB,CAAA;QACjG,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,sBAAsB,IAAI,IAAI,CAAuB,CAAA;QAC/E,MAAM,yBAAyB,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAA;QACnE,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YAClC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAA;YAC1C,KAAK,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;gBACpC,IAAI,EAAE,EAAE,CAAC;oBACP,EAAE,CAAC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAA;oBAC/B,EAAE,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAA;gBACxB,CAAC;YACH,CAAC;QACH,CAAC,EAAE,CAAC,CAAC,CAAA;QACL,cAAc,CAAC,OAAO,GAAG,GAAG,EAAE;YAC5B,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;YACzB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,yBAAyB,CAAA;YAC7D,KAAK,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;gBACpC,IAAI,EAAE,EAAE,CAAC;oBACP,EAAE,CAAC,KAAK,CAAC,aAAa,GAAG,EAAE,CAAA;oBAC3B,EAAE,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAA;gBACvB,CAAC;YACH,CAAC;QACH,CAAC,CAAA;IACH,CAAC;IAED,SAAS,iBAAiB;QACxB,4EAA4E;QAC5E,OAAO,CAAC,WAAW,CAAC,CAAA;IACtB,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAA;IAEzD,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,YAC5C,MAAC,aAAa,IAAC,SAAS,EAAC,WAAW,EAAC,iBAAiB,EAAE,qBAAqB,aAC3E,MAAC,YAAY,eACX,KAAC,WAAW,cAAE,SAAS,CAAC,2BAA2B,CAAC,GAAe,EACnE,MAAC,iBAAiB,6BACN,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,OAAO,CAAC,gBAAgB,GAAQ,SAC5C,IACP,EAEf,eAAK,SAAS,EAAC,UAAU,aACvB,KAAC,MAAM,IAAC,SAAS,EAAC,+FAA+F,GAAG,EACpH,KAAC,KAAK,IACJ,SAAS,QACT,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAE,SAAS,CAAC,uCAAuC,CAAC,gBACnD,SAAS,CAAC,gCAAgC,CAAC,EACvD,SAAS,EAAC,MAAM,GAChB,IACE,EAIN,eAAK,SAAS,EAAC,0DAA0D,aACtE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACpB,KAAC,SAAS,IACR,OAAO,EAAE,SAAS,CAAC,mCAAmC,CAAC,EACvD,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,WAAW,EACnB,eAAe,EAAE,mBAAmB,EACpC,aAAa,EAAE,iBAAiB,GAChC,CACH,CAAC,CAAC,CAAC,IAAI,EACP,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACrB,KAAC,SAAS,IAER,OAAO,EAAE,KAAK,CAAC,QAAQ,EACvB,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,WAAW,EACnB,eAAe,EAAE,mBAAmB,EACpC,aAAa,EAAE,iBAAiB,IAN3B,KAAK,CAAC,QAAQ,CAOnB,CACH,CAAC,EACD,KAAK,CAAC,CAAC,CAAC,CACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,KAAK,YACzC,SAAS;gCACR,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC;gCAC5C,CAAC,CAAC,SAAS,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,CAAC,GAClD,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,IACQ,GACT,CACV,CAAA;AACH,CAAC;AAED,SAAS,SAAS,CAAC,EACjB,OAAO,EACP,KAAK,EACL,QAAQ,EACR,MAAM,EACN,eAAe,EACf,aAAa,GAQd;IACC,OAAO,CACL,mBAAS,SAAS,EAAC,MAAM,gBAAa,OAAO,aAC3C,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,aAAa,aAClC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,YAAY,YACtC,OAAO,GACH,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,KAAK,CAAC,MAAM,GACR,IACH,EACN,cAAK,SAAS,EAAC,wBAAwB,YACpC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,KAAC,eAAe,IAEd,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,EAC5C,SAAS,EAAE,aAAa,IALnB,IAAI,CAAC,GAAG,CAMb,CACH,CAAC,GACE,IACE,CACX,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,WAAW,EACX,SAAS,GAOV;IACC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IACvC,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAA;IACxC,OAAO,CACL,eACE,SAAS,QACT,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,qBACH,IAAI,CAAC,GAAG,yBACJ,QAAQ,IAAI,SAAS,EAC1C,SAAS,EAAE,EAAE;QACX,sEAAsE;QACtE,oCAAoC;QACpC,oHAAoH,EACpH,wCAAwC,EACxC,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe,CACnD,aAGD,iBACE,IAAI,EAAC,QAAQ,uBACM,IAAI,CAAC,GAAG,EAC3B,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,gBAC3B,SAAS,CAAC,yCAAyC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,EACvF,KAAK,EAAE,IAAI,CAAC,WAAW,EACvB,SAAS,EAAC,8FAA8F,GACxG,EAGF,iBACE,IAAI,EAAC,QAAQ,gBACD,SAAS,CAAC,mCAAmC,EAAE;oBACzD,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,OAAO,EAAE,cAAc,CAAC,IAAI;iBAC7B,CAAC,EACF,KAAK,EAAE,SAAS,CAAC,4CAA4C,CAAC,qBAC7C,IAAI,CAAC,GAAG,EACzB,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc,CAAC,EAC3C,SAAS,EAAE,EAAE,CACX,yGAAyG,EACzG,2GAA2G,CAC5G,YAED,KAAC,aAAa,IAAC,SAAS,EAAC,UAAU,wBAAe,GAC3C,EAIT,eAAK,SAAS,EAAC,yDAAyD,aACrE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CACjB,KAAC,qBAAqB,IAAC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,GAAI,CACjE,CAAC,CAAC,CAAC,CACF,KAAC,oBAAoB,IAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAI,CAC5C,EACD,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACrC,eAAM,SAAS,EAAC,8BAA8B,YAAE,IAAI,CAAC,KAAK,GAAQ,EACjE,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAC1B,KAAC,YAAY,IAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAI,CACjE,CAAC,CAAC,CAAC,IAAI,IACJ,EACL,QAAQ,CAAC,CAAC,CAAC,CACV,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,mDAAmD,aACxE,KAAC,aAAa,yBAAa,SAAS,EAAC,iBAAiB,GAAG,EACzD,eAAM,SAAS,EAAC,wBAAwB,EAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YACnE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GACV,IACH,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,IACF,CACP,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,EACpB,IAAI,EACJ,QAAQ,EACR,MAAM,GAKP;IACC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IACvD,yEAAyE;IACzE,8EAA8E;IAC9E,MAAM,oBAAoB,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAChG,OAAO,CACL,MAAC,OAAO,eACN,KAAC,cAAc,IAAC,OAAO,kBACrB,kBACE,IAAI,EAAC,QAAQ,yBACQ,IAAI,CAAC,GAAG,+BACF,OAAO,IAAI,SAAS,gBACnC,SAAS,CAAC,kCAAkC,EAAE;wBACxD,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;wBAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;qBAClB,CAAC,EACF,KAAK,EAAE,oBAAoB,CAAC,CAAC,CAAC,SAAS,CAAC,uCAAuC,CAAC,CAAC,CAAC,CAAC,SAAS,EAC5F,SAAS,EAAE,EAAE;oBACX,kEAAkE;oBAClE,sEAAsE;oBACtE,mCAAmC,EACnC,mHAAmH,EACnH,OAAO;wBACL,CAAC,CAAC,4BAA4B;wBAC9B,CAAC,CAAC,wEAAwE,CAC7E,aAEA,oBAAoB,CAAC,CAAC,CAAC,CACtB,KAAC,aAAa,yBAAa,SAAS,EAAC,6CAA6C,GAAG,CACtF,CAAC,CAAC,CAAC,IAAI,EACP,SAAS,CAAC,kCAAkC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,IACxE,GACM,EACjB,MAAC,cAAc,IAAC,SAAS,EAAC,YAAY,EAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,aAChF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,oBAAoB,YACxD,SAAS,CAAC,qCAAqC,CAAC,GAC5C,EACP,KAAC,WAAW,IAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAI,IAChD,IACT,CACX,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,EACR,MAAM,GAKP;IACC,OAAO,CACL,cAAK,SAAS,EAAC,uBAAuB,YACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,CAAA;YACrE,MAAM,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;YACjD,OAAO,CACL,MAAC,GAAG,IAAoB,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,gBAAgB,aACxD,kBACE,IAAI,EAAC,QAAQ,wBACO,GAAG,IAAI,CAAC,WAAW,KAAK,OAAO,CAAC,IAAI,EAAE,8BAChC,OAAO,IAAI,SAAS,iCACjB,eAAe,IAAI,SAAS,gBAC7C,SAAS,CAAC,oCAAoC,EAAE;4BAC1D,KAAK,EAAE,IAAI,CAAC,KAAK;4BACjB,OAAO,EAAE,OAAO,CAAC,IAAI;yBACtB,CAAC,EACF,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,EAC9D,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAChC,SAAS,EAAE,EAAE,CACX,oKAAoK,EACpK,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,iBAAiB,CAC7C,aAEA,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CACpB,cACE,GAAG,EAAE,OAAO,CAAC,UAAU,EACvB,GAAG,EAAC,EAAE,EACN,SAAS,EAAE,KAAK,EAChB,SAAS,EAAC,oFAAoF,GAC9F,CACH,CAAC,CAAC,CAAC,IAAI,EACR,eAAM,SAAS,EAAC,UAAU,YAAE,OAAO,CAAC,IAAI,GAAQ,EAC/C,eAAe,CAAC,CAAC,CAAC,CACjB,KAAC,aAAa,yBAEZ,SAAS,EAAC,4DAA4D,GACtE,CACH,CAAC,CAAC,CAAC,IAAI,IACD,EACT,iBACE,IAAI,EAAC,QAAQ,6BACY,GAAG,IAAI,CAAC,WAAW,KAAK,OAAO,CAAC,IAAI,EAAE,gBACnD,SAAS,CAAC,mCAAmC,EAAE;4BACzD,KAAK,EAAE,IAAI,CAAC,KAAK;4BACjB,OAAO,EAAE,OAAO,CAAC,IAAI;yBACtB,CAAC,EACF,KAAK,EAAE,SAAS,CAAC,4CAA4C,CAAC,EAC9D,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,EACpC,SAAS,EAAC,wJAAwJ,YAElK,KAAC,aAAa,IAAC,SAAS,EAAC,UAAU,wBAAe,GAC3C,KA7CD,OAAO,CAAC,IAAI,CA8ChB,CACP,CAAA;QACH,CAAC,CAAC,GACE,CACP,CAAA;AACH,CAAC","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 { useProjectSession } from '../project-session'\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 { projectId } = useProjectSession()\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, 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"]}