@elytracms/studio 0.0.95 → 0.0.97

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 (104) hide show
  1. package/convex/cms.ts +272 -16
  2. package/convex/delivery-folded-drift.test.ts +104 -0
  3. package/convex/delivery.ts +24 -8
  4. package/convex/depth-codec-lookalike.test.ts +192 -0
  5. package/convex/fixture-accounts.test.ts +305 -0
  6. package/convex/fixtureAccounts.ts +298 -0
  7. package/convex/incarnation.test.ts +149 -0
  8. package/convex/ingest.test.ts +145 -3
  9. package/convex/ingest.ts +123 -17
  10. package/convex/receipt-races.test.ts +184 -0
  11. package/convex/records-zod-copy.test.ts +76 -0
  12. package/convex/schema.ts +20 -1
  13. package/convex/strict-delete.test.ts +129 -0
  14. package/convex/strict-reference-race.test.ts +139 -0
  15. package/dist/components/ai/ai-chat-panel.js +15 -2
  16. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  17. package/dist/components/ai/translate-document-action.d.ts +6 -0
  18. package/dist/components/ai/translate-document-action.js.map +1 -1
  19. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  20. package/dist/components/ai/translate-document-dialog.js +5 -5
  21. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  22. package/dist/components/assets/asset-detail.js +5 -2
  23. package/dist/components/assets/asset-detail.js.map +1 -1
  24. package/dist/components/assets/asset-locale.d.ts +7 -0
  25. package/dist/components/assets/asset-locale.js +16 -2
  26. package/dist/components/assets/asset-locale.js.map +1 -1
  27. package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
  28. package/dist/components/assets/asset-meta-drift-notice.js +22 -0
  29. package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
  30. package/dist/components/assets/asset-meta-label.d.ts +2 -1
  31. package/dist/components/assets/asset-meta-label.js +4 -2
  32. package/dist/components/assets/asset-meta-label.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +33 -13
  34. package/dist/components/assets/asset-metadata.js +46 -22
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.d.ts +6 -2
  37. package/dist/components/assets/asset-search.js +15 -5
  38. package/dist/components/assets/asset-search.js.map +1 -1
  39. package/dist/components/assets/assets-collection-view-source.d.ts +5 -3
  40. package/dist/components/assets/assets-collection-view-source.js +7 -5
  41. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  42. package/dist/components/builder/canvas-rebase.d.ts +36 -12
  43. package/dist/components/builder/canvas-rebase.js +13 -3
  44. package/dist/components/builder/canvas-rebase.js.map +1 -1
  45. package/dist/components/builder/canvas-render-context.js +5 -4
  46. package/dist/components/builder/canvas-render-context.js.map +1 -1
  47. package/dist/components/builder/composition-field-editor.d.ts +12 -6
  48. package/dist/components/builder/composition-field-editor.js +6 -5
  49. package/dist/components/builder/composition-field-editor.js.map +1 -1
  50. package/dist/components/cms/asset-picker-core.d.ts +2 -2
  51. package/dist/components/cms/asset-picker-core.js +2 -2
  52. package/dist/components/cms/asset-picker-core.js.map +1 -1
  53. package/dist/components/cms/asset-picker.js +3 -1
  54. package/dist/components/cms/asset-picker.js.map +1 -1
  55. package/dist/components/cms/canvas-commit.d.ts +6 -0
  56. package/dist/components/cms/canvas-commit.js +5 -1
  57. package/dist/components/cms/canvas-commit.js.map +1 -1
  58. package/dist/components/cms/cms-store.d.ts +24 -8
  59. package/dist/components/cms/cms-store.js +21 -14
  60. package/dist/components/cms/cms-store.js.map +1 -1
  61. package/dist/components/cms/collection-table.js +4 -11
  62. package/dist/components/cms/collection-table.js.map +1 -1
  63. package/dist/components/cms/detail-editor.js +2 -2
  64. package/dist/components/cms/detail-editor.js.map +1 -1
  65. package/dist/components/cms/draft-commit.d.ts +16 -3
  66. package/dist/components/cms/draft-commit.js +3 -2
  67. package/dist/components/cms/draft-commit.js.map +1 -1
  68. package/dist/components/cms/use-document-draft.d.ts +107 -10
  69. package/dist/components/cms/use-document-draft.js +157 -42
  70. package/dist/components/cms/use-document-draft.js.map +1 -1
  71. package/dist/components/collection-view/cms-collection-view-source.js +6 -4
  72. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  73. package/dist/components/components/transfer-dialog.js +6 -4
  74. package/dist/components/components/transfer-dialog.js.map +1 -1
  75. package/dist/lib/ai/chat-state.d.ts +7 -0
  76. package/dist/lib/ai/chat-state.js.map +1 -1
  77. package/dist/lib/ai/tools.d.ts +8 -0
  78. package/dist/lib/ai/tools.js +4 -0
  79. package/dist/lib/ai/tools.js.map +1 -1
  80. package/dist/lib/cms-views/view-model.d.ts +15 -8
  81. package/dist/lib/cms-views/view-model.js +19 -5
  82. package/dist/lib/cms-views/view-model.js.map +1 -1
  83. package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
  84. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
  85. package/dist/lib/collection-view/collection-view-source.d.ts +18 -7
  86. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  87. package/dist/lib/data/collection-content.js +5 -0
  88. package/dist/lib/data/collection-content.js.map +1 -1
  89. package/dist/lib/engine-version.d.ts +1 -1
  90. package/dist/lib/engine-version.js +1 -1
  91. package/dist/lib/engine-version.js.map +1 -1
  92. package/dist/lib/i18n/messages.de.js +15 -1
  93. package/dist/lib/i18n/messages.de.js.map +1 -1
  94. package/dist/lib/i18n/messages.en.d.ts +13 -0
  95. package/dist/lib/i18n/messages.en.js +16 -1
  96. package/dist/lib/i18n/messages.en.js.map +1 -1
  97. package/dist/lib/operations/cms-persistence.d.ts +119 -35
  98. package/dist/lib/operations/cms-persistence.js +441 -91
  99. package/dist/lib/operations/cms-persistence.js.map +1 -1
  100. package/dist/lib/persistence/convex-adapter.js +80 -8
  101. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  102. package/dist/lib/persona/labels.js +2 -0
  103. package/dist/lib/persona/labels.js.map +1 -1
  104. package/package.json +4 -3
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'\nimport { DRAFT_PERSPECTIVE } from '@elytracms/core/cms-core'\nimport type { Perspective } from '@elytracms/core/cms-core'\nimport { resolvedAsset } from '@elytracms/core/content'\nimport { assetReadLocale } from '@elytracms/core/persistence'\nimport type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence'\nimport { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-renderer'\nimport type {\n ListingQuery,\n RenderAsset,\n RenderContext,\n ResolveAsset,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { ContentQueryError } from '@elytracms/core/content'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '../../lib/registry'\nimport { translate } from '../../lib/i18n'\nimport { contentQueryErrorMessage } from '../../lib/persona/labels'\nimport {\n acquireContentSessionConsumer,\n contentSessionEpoch,\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n subscribeContentSession,\n validateCanvasListingQuery,\n} from '../../lib/data'\nimport { useStudioLocale } from '../../lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * injected resolvers that make the iframe preview match delivery — assets\n * (EC-195), relations (EC-254), listings (EC-255), rich-text links (EC-291) —\n * plus the per-node decorator that stamps the editable wrappers (selection/hover\n * state, rich-text remount epochs, and the EC-300 unresolved-reusable\n * placeholder). The binding resolver and the EC-176 locale-fallback chips (which\n * read bound props) went with the data-binding subsystem (EC-574).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over a pure decision helper ({@link unresolvedReuseStatus}), so the decoration\n * and resolver wiring are unit-testable without providers;\n * {@link useCanvasRenderContext} binds them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: Perspective) => unknown,\n perspective: Perspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, DRAFT_PERSPECTIVE) ? 'unpublished' : 'missing'\n}\n\n/**\n * Everything the pure context factory needs — the live SOURCE (resolvers over the\n * content session, and the session schema's verdict on a listing query) plus the\n * editor's decoration state. EC-592: the factory is a call of core's ONE\n * `createRenderContext` (`mode: 'canvas'`) — the delivery host calls the same one\n * over its baked source.\n */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n /**\n * EC-460 (never-silent) / EC-581 6a: why a runnable listing query is STATICALLY\n * invalid against the active session schema (unknown collection, non-filterable\n * filter/sort field) — localized messages, or `null` when it can run. The render\n * context judges the declaration itself and shows the notice above the block —\n * the same judgment the delivery host reads from its baked refusals.\n */\n listingFailure(query: ListingQuery): readonly string[] | null\n}\n\n/**\n * The session schema's verdict on a listing query, localized (EC-494:\n * `ContentQueryError` carries a catalog ref) — the canvas's `listingFailure` source.\n */\nexport function sessionListingFailure(\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): (query: ListingQuery) => readonly string[] | null {\n return (query) => {\n const errors = validate(query)\n return errors.length === 0 ? null : errors.map((error) => contentQueryErrorMessage(error))\n }\n}\n\n/**\n * The canvas notice text for a listing node that cannot run, or `null` (EC-460, the\n * never-silent rule) — core's ONE judgment (`listingFailureOf`: the registry's verdict\n * on the declaration, then `validate`'s on the built query; a listing awaiting its\n * bound value keeps the block's own empty state). Pure, exported for the node test.\n */\nexport function canvasListingIssue(\n node: ComponentNode,\n registry: RenderContext['registry'],\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): string | null {\n const failure = listingFailureOf(node, registry, { listingFailure: sessionListingFailure(validate) })\n return failure ? failure.join(' ') : null\n}\n\n/**\n * Assemble the canvas `RenderContext` — core's ONE `createRenderContext` over the live\n * source, `mode: 'canvas'`, with the editor's decoration. Pure (no hooks): the same\n * deps produce the same context, so the decorator's wrapper markup is pinnable.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return createRenderContext({\n components: { registry: deps.registry, implementations: deps.implementations },\n mode: 'canvas',\n source: {\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n listingFailure: deps.listingFailure,\n },\n // EC-460: a listing that cannot run gets the canvas's own (chrome-styled,\n // localized) notice ABOVE the block, which keeps its empty state below.\n listingNotice: (_node, messages) => <CanvasListingIssueNotice message={messages.join(' ')} />,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n })\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (asset library, content session, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\n/**\n * The canvas's asset reader IS delivery's (NK-049 finding, 2026-10-23). This used to\n * hand-pick `url/width/height/alt/focalPoint`, read `alt` from the legacy column\n * only and drop `meta` — so a component's AI badge (`meta.aiGenerated`) and an\n * editor-set `meta.alt` rendered on the site but not on the canvas. One reader\n * (`resolvedAsset`, `content/assets.ts`) means the canvas and the host cannot\n * disagree about an asset's own facts; `RenderAsset` IS `ResolvedAsset` (EC-587 —\n * one resolved shape in every position). EC-632: in the canvas's locale `at` — the\n * locale the relations, listings and links beside it resolve in — so a localized alt\n * previews in the locale the editor has open, with delivery's fallback. Pure, exported\n * for the node test.\n */\nexport function canvasRenderAsset(record: AssetRecord | undefined, at: AssetReadLocale | null): RenderAsset | null {\n // EC-633: a record with no URL is decided by the constructor (no resolved asset), the\n // same answer delivery gets — so both render the same empty state.\n return record ? resolvedAsset(record, record.url, at) : null\n}\n\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: Perspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n\n // EC-460: the canvas follows the preview content session reactively. The\n // epoch bumps whenever `configureContentSession` re-points the session (the\n // write-sync refresh, a CMS-surface hydration, boot) — it keys the resolver\n // callbacks below, so the context identity changes and the canvas re-renders\n // against the fresh session. Registering as a CONSUMER tells the write-sync a\n // preview surface is live, so a stale session actually refreshes (with no\n // consumer it only flags — connected mode never pays a per-autosave query\n // while nobody is previewing).\n const sessionEpoch = useSyncExternalStore(subscribeContentSession, contentSessionEpoch)\n useEffect(() => acquireContentSessionConsumer(), [])\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) from the live asset\n // library, so the canvas previews exactly what the host renders. A record\n // without a resolved url (e.g. a never-served seed) → null → the\n // implementation's visible empty state.\n // The canvas's active locale for asset/relation/listing/link resolution below.\n // EC-479: the ambient locale is `null` until a project's config is installed (it\n // no longer opens on the engine's `en/de` fixture). `undefined` means \"you pick\" to\n // the content engine, which then resolves in the SESSION's own default locale —\n // never a fabricated one — so a canvas mounted outside a CMS session still previews\n // in the project's locale.\n const { locale, config: localeConfig } = useStudioLocale()\n const activeLocale = locale ?? undefined\n // EC-632: an asset's localized metadata (alt) reads in the same locale, falling back to\n // the project's default — `null` (shared metadata only) before a config is installed.\n const defaultLocale = localeConfig?.default\n const assetLocale = useMemo(\n () => (defaultLocale === undefined ? null : assetReadLocale({ default: defaultLocale }, activeLocale)),\n [defaultLocale, activeLocale],\n )\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId), assetLocale),\n [assetsStore, assetLocale],\n )\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n // sessionEpoch: re-resolve when the content session was re-pointed (EC-460).\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective, sessionEpoch],\n )\n\n // EC-460 (never-silent): the session schema's verdict on a listing query.\n const listingFailure = useMemo(\n () => sessionListingFailure((query) => validateCanvasListingQuery(query)),\n [sessionEpoch],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n listingFailure,\n }),\n [studio, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, richEditEpochs, listingFailure],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes\n * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe\n * on its root. Exported for the chrome contract test. The node stays\n * selectable; the inspector's `element` relation chip (EC-296) deep-links to\n * the reusable to edit or publish it.\n */\n/**\n * In-canvas notice for a listing block whose query is statically invalid\n * against the active session schema (EC-460 — the never-silent rule): the\n * message renders ABOVE the block (which keeps its own empty state), so a\n * misdeclared `listing` manifest (non-filterable sort/filter field, unknown\n * collection) is an explicit, visible state — never an empty grid an editor\n * can't tell apart from \"no content yet\". Rendered INTO the iframe (project\n * stylesheet only), so it uses inline styles + the `.ec-chrome` containment\n * recipe, exactly like {@link UnresolvedReusablePlaceholder}. Exported for the\n * chrome contract test.\n */\nexport function CanvasListingIssueNotice({ message }: { message: string }) {\n return (\n <div\n className=\"ec-chrome\"\n data-testid=\"canvas-listing-issue\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(200,140,40,0.65)',\n borderRadius: 8,\n padding: '10px 14px',\n margin: '8px 16px',\n background: 'rgba(230,160,40,0.08)',\n color: 'rgba(120,80,10,0.95)',\n font: '13px/1.5 system-ui, -apple-system, sans-serif',\n }}\n >\n <span style={{ fontWeight: 600 }}>⚠ {translate('builder.canvas.listingInvalidTitle')}</span>{' '}\n <span style={{ opacity: 0.9 }}>{message}</span>\n </div>\n )\n}\n\nexport function UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n className=\"ec-chrome\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"canvas-render-context.js","sourceRoot":"","sources":["../../../src/components/builder/canvas-render-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'\nimport { DRAFT_PERSPECTIVE } from '@elytracms/core/cms-core'\nimport type { Perspective } from '@elytracms/core/cms-core'\nimport { resolvedAsset } from '@elytracms/core/content'\nimport type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence'\nimport { createRenderContext, listingFailureOf } from '@elytracms/core/runtime-renderer'\nimport type {\n ListingQuery,\n RenderAsset,\n RenderContext,\n ResolveAsset,\n ResolveLink,\n ResolveListing,\n ResolveRelation,\n} from '@elytracms/core/runtime-renderer'\nimport type { ContentQueryError } from '@elytracms/core/content'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { StudioRegistry } from '../../lib/registry'\nimport { translate } from '../../lib/i18n'\nimport { contentQueryErrorMessage } from '../../lib/persona/labels'\nimport {\n acquireContentSessionConsumer,\n contentSessionEpoch,\n resolveCanvasLink,\n resolveCanvasListing,\n resolveCanvasRelation,\n subscribeContentSession,\n validateCanvasListingQuery,\n} from '../../lib/data'\nimport { useAssetReadLocale } from '../assets/asset-locale'\nimport { useStudioLocale } from '../../lib/locale'\nimport { useOptionalAssets } from '../assets/assets-store'\nimport { useReuse } from './reuse-context'\nimport { reuseEmbedRef } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * The canvas's `RenderContext` (EC-330, extracted from `BuilderCanvas`): the\n * injected resolvers that make the iframe preview match delivery — assets\n * (EC-195), relations (EC-254), listings (EC-255), rich-text links (EC-291) —\n * plus the per-node decorator that stamps the editable wrappers (selection/hover\n * state, rich-text remount epochs, and the EC-300 unresolved-reusable\n * placeholder). The binding resolver and the EC-176 locale-fallback chips (which\n * read bound props) went with the data-binding subsystem (EC-574).\n *\n * The context assembly is a pure factory ({@link createCanvasRenderContext})\n * over a pure decision helper ({@link unresolvedReuseStatus}), so the decoration\n * and resolver wiring are unit-testable without providers;\n * {@link useCanvasRenderContext} binds them to the live stores.\n */\n\n/** Collect every node id under a root (the page subtree). */\nexport function collectIds(node: ComponentNode, out: Set<string> = new Set()): Set<string> {\n out.add(node.id)\n for (const children of Object.values(node.slots ?? {})) {\n for (const child of children) collectIds(child, out)\n }\n return out\n}\n\n/**\n * EC-300/AD-16: a transcluding embed whose target doesn't resolve in the active\n * perspective renders a placeholder, never an empty section. `unpublished` = the\n * target exists as a draft but isn't live (Published view of a draft-only reusable);\n * `missing` = no resolvable target at all (deleted). `null` = resolves fine, or the\n * node isn't an embed / has no target picked yet (its own empty state owns that).\n */\nexport function unresolvedReuseStatus(\n node: ComponentNode,\n capability: ReuseCapability | null | undefined,\n resolveAt: (ref: { collection: string; id: string }, perspective: Perspective) => unknown,\n perspective: Perspective,\n): 'unpublished' | 'missing' | null {\n if (!capability) return null\n const ref = reuseEmbedRef(node, capability)\n if (!ref) return null\n if (resolveAt(ref, perspective)) return null\n return resolveAt(ref, DRAFT_PERSPECTIVE) ? 'unpublished' : 'missing'\n}\n\n/**\n * Everything the pure context factory needs — the live SOURCE (resolvers over the\n * content session, and the session schema's verdict on a listing query) plus the\n * editor's decoration state. EC-592: the factory is a call of core's ONE\n * `createRenderContext` (`mode: 'canvas'`) — the delivery host calls the same one\n * over its baked source.\n */\nexport interface CanvasRenderContextDeps {\n registry: RenderContext['registry']\n implementations: RenderContext['implementations']\n resolveAsset: ResolveAsset\n resolveRelation: ResolveRelation\n resolveListing: ResolveListing\n resolveLink: ResolveLink\n /** Only page nodes are editable; layout chrome must not be decorated. */\n pageNodeIds: ReadonlySet<string>\n selectedId: string | null\n hoveredId: string | null\n /** Per-node remount epochs from the inline rich-text editing (EC-159). */\n richEditEpochs: Record<string, number>\n unresolvedReuse(node: ComponentNode): 'unpublished' | 'missing' | null\n /**\n * EC-460 (never-silent) / EC-581 6a: why a runnable listing query is STATICALLY\n * invalid against the active session schema (unknown collection, non-filterable\n * filter/sort field) — localized messages, or `null` when it can run. The render\n * context judges the declaration itself and shows the notice above the block —\n * the same judgment the delivery host reads from its baked refusals.\n */\n listingFailure(query: ListingQuery): readonly string[] | null\n}\n\n/**\n * The session schema's verdict on a listing query, localized (EC-494:\n * `ContentQueryError` carries a catalog ref) — the canvas's `listingFailure` source.\n */\nexport function sessionListingFailure(\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): (query: ListingQuery) => readonly string[] | null {\n return (query) => {\n const errors = validate(query)\n return errors.length === 0 ? null : errors.map((error) => contentQueryErrorMessage(error))\n }\n}\n\n/**\n * The canvas notice text for a listing node that cannot run, or `null` (EC-460, the\n * never-silent rule) — core's ONE judgment (`listingFailureOf`: the registry's verdict\n * on the declaration, then `validate`'s on the built query; a listing awaiting its\n * bound value keeps the block's own empty state). Pure, exported for the node test.\n */\nexport function canvasListingIssue(\n node: ComponentNode,\n registry: RenderContext['registry'],\n validate: (query: ListingQuery) => readonly ContentQueryError[],\n): string | null {\n const failure = listingFailureOf(node, registry, { listingFailure: sessionListingFailure(validate) })\n return failure ? failure.join(' ') : null\n}\n\n/**\n * Assemble the canvas `RenderContext` — core's ONE `createRenderContext` over the live\n * source, `mode: 'canvas'`, with the editor's decoration. Pure (no hooks): the same\n * deps produce the same context, so the decorator's wrapper markup is pinnable.\n */\nexport function createCanvasRenderContext(deps: CanvasRenderContextDeps): RenderContext {\n return createRenderContext({\n components: { registry: deps.registry, implementations: deps.implementations },\n mode: 'canvas',\n source: {\n resolveAsset: deps.resolveAsset,\n resolveRelation: deps.resolveRelation,\n resolveListing: deps.resolveListing,\n resolveLink: deps.resolveLink,\n listingFailure: deps.listingFailure,\n },\n // EC-460: a listing that cannot run gets the canvas's own (chrome-styled,\n // localized) notice ABOVE the block, which keeps its empty state below.\n listingNotice: (_node, messages) => <CanvasListingIssueNotice message={messages.join(' ')} />,\n decorateNode: (node, rendered) => {\n if (!deps.pageNodeIds.has(node.id)) return rendered\n // An unresolved hard reference shows the placeholder in place of the empty\n // transclusion; the node stays selectable, so its inspector relation chip\n // (EC-296) deep-links to the reusable to edit/publish it.\n const unresolved = deps.unresolvedReuse(node)\n return (\n <div\n // The epoch suffix forces a remount after a rich-text editing\n // session mutated this node's rendered DOM (see richEditEpochs).\n key={`${node.id}#${deps.richEditEpochs[node.id] ?? 0}`}\n data-ec-node-id={node.id}\n className=\"ec-editable\"\n data-selected={node.id === deps.selectedId || undefined}\n data-hovered={node.id === deps.hoveredId || undefined}\n >\n {unresolved ? <UnresolvedReusablePlaceholder status={unresolved} /> : rendered}\n </div>\n )\n },\n })\n}\n\n/**\n * The live canvas render context: binds the pure factory to the studio's\n * stores (asset library, content session, active locale, reuse capability) for\n * the given perspective. Returns the context plus the page-node id set the\n * canvas also needs for spacing-handle gating.\n */\n/**\n * The canvas's asset reader IS delivery's (NK-049 finding, 2026-10-23). This used to\n * hand-pick `url/width/height/alt/focalPoint`, read `alt` from the legacy column\n * only and drop `meta` — so a component's AI badge (`meta.aiGenerated`) and an\n * editor-set `meta.alt` rendered on the site but not on the canvas. One reader\n * (`resolvedAsset`, `content/assets.ts`) means the canvas and the host cannot\n * disagree about an asset's own facts; `RenderAsset` IS `ResolvedAsset` (EC-587 —\n * one resolved shape in every position). EC-632: in the canvas's locale `at` — the\n * locale the relations, listings and links beside it resolve in — so a localized alt\n * previews in the locale the editor has open, with delivery's fallback. Pure, exported\n * for the node test.\n */\nexport function canvasRenderAsset(record: AssetRecord | undefined, at: AssetReadLocale | null): RenderAsset | null {\n // EC-633: a record with no URL is decided by the constructor (no resolved asset), the\n // same answer delivery gets — so both render the same empty state.\n return record ? resolvedAsset(record, record.url, at) : null\n}\n\nexport function useCanvasRenderContext(args: {\n /** The edited composition root (`builder.present`). */\n present: ComponentNode\n selectedId: string | null\n hoveredId: string | null\n studio: StudioRegistry\n perspective: Perspective\n richEditEpochs: Record<string, number>\n}): { ctx: RenderContext; pageNodeIds: ReadonlySet<string> } {\n const { present, selectedId, hoveredId, studio, perspective, richEditEpochs } = args\n\n // EC-460: the canvas follows the preview content session reactively. The\n // epoch bumps whenever `configureContentSession` re-points the session (the\n // write-sync refresh, a CMS-surface hydration, boot) — it keys the resolver\n // callbacks below, so the context identity changes and the canvas re-renders\n // against the fresh session. Registering as a CONSUMER tells the write-sync a\n // preview surface is live, so a stale session actually refreshes (with no\n // consumer it only flags — connected mode never pays a per-autosave query\n // while nobody is previewing).\n const sessionEpoch = useSyncExternalStore(subscribeContentSession, contentSessionEpoch)\n useEffect(() => acquireContentSessionConsumer(), [])\n\n // Only page nodes are editable; layout chrome (rendered around the page) is\n // read-only context and must not be selectable (it isn't in the page tree).\n const pageNodeIds = useMemo(() => collectIds(present), [present])\n\n // EC-195: resolve an `asset`-typed Image prop (an asset id) from the live asset\n // library, so the canvas previews exactly what the host renders. A record\n // without a resolved url (e.g. a never-served seed) → null → the\n // implementation's visible empty state.\n // The canvas's active locale for asset/relation/listing/link resolution below.\n // EC-479: the ambient locale is `null` until a project's config is installed (it\n // no longer opens on the engine's `en/de` fixture). `undefined` means \"you pick\" to\n // the content engine, which then resolves in the SESSION's own default locale —\n // never a fabricated one — so a canvas mounted outside a CMS session still previews\n // in the project's locale.\n const { locale } = useStudioLocale()\n const activeLocale = locale ?? undefined\n // EC-632: an asset's localized metadata (alt) reads in the same locale, falling back to\n // the project's default — `null` (shared metadata only) before a config is installed.\n // EC-659: the studio's ONE asset read locale, read through the project's asset collection\n // as declared now — a translation of a field it no longer localizes is never previewed.\n const assetLocale = useAssetReadLocale()\n const assetsStore = useOptionalAssets()\n const resolveAsset: ResolveAsset = useCallback(\n (assetId) => canvasRenderAsset(assetsStore?.assets.find((a) => a.id === assetId), assetLocale),\n [assetsStore, assetLocale],\n )\n // EC-300: the reuse capability identifies a transcluding embed + its relation prop,\n // so the canvas can detect a HARD reference whose target doesn't resolve and show a\n // placeholder instead of an empty section. Null-safe (no provider / no capability).\n const reuse = useReuse()\n\n // EC-254: populate a block's `relation` prop (the editor's document pick — a\n // ProductGrid's products, a Reusable's element) to delivery shape from the\n // active content session, in the canvas's active locale, so the canvas previews\n // picked documents exactly as the host renders them. The studio analog of the\n // host's baked relations map. A missing/invisible target → null → the block's\n // own empty state.\n const resolveRelation: ResolveRelation = useCallback(\n (ref) => resolveCanvasRelation(ref, activeLocale, perspective),\n // sessionEpoch: re-resolve when the content session was re-pointed (EC-460).\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-255: resolve a listing block's query (a ProductGrid keyed to a category) to\n // its documents from the active content session, in the canvas's active locale, so\n // the canvas previews a dynamic grid exactly as the host renders it. The studio\n // analog of the host's baked listings map; an empty/invalid query → [] → the\n // block's own empty state.\n const resolveListing: ResolveListing = useCallback(\n (query) => resolveCanvasListing(query, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n // EC-291: resolve a rich-text internal link (`{type:'reference'}`) to its target's\n // URL path from the active content session, in the canvas's active locale — the\n // studio analog of the host's baked links map. A missing/unpublished/unroutable\n // target → null → the rich-text renderer shows the visible `unresolved-ref` issue.\n const resolveLink: ResolveLink = useCallback(\n (ref) => resolveCanvasLink(ref, activeLocale, perspective),\n [activeLocale, perspective, sessionEpoch],\n )\n\n const unresolvedReuse = useCallback(\n (node: ComponentNode): 'unpublished' | 'missing' | null =>\n unresolvedReuseStatus(\n node,\n reuse?.capability,\n (ref, p) => resolveCanvasRelation(ref, activeLocale, p),\n perspective,\n ),\n [reuse, activeLocale, perspective, sessionEpoch],\n )\n\n // EC-460 (never-silent): the session schema's verdict on a listing query.\n const listingFailure = useMemo(\n () => sessionListingFailure((query) => validateCanvasListingQuery(query)),\n [sessionEpoch],\n )\n\n const ctx: RenderContext = useMemo(\n () =>\n createCanvasRenderContext({\n registry: studio.registry,\n implementations: studio.implementations,\n resolveAsset,\n resolveRelation,\n resolveListing,\n resolveLink,\n pageNodeIds,\n selectedId,\n hoveredId,\n richEditEpochs,\n unresolvedReuse,\n listingFailure,\n }),\n [studio, resolveAsset, resolveRelation, resolveListing, resolveLink, unresolvedReuse, pageNodeIds, selectedId, hoveredId, richEditEpochs, listingFailure],\n )\n\n return { ctx, pageNodeIds }\n}\n\n/**\n * In-canvas placeholder for a transcluding embed whose target doesn't resolve\n * (EC-300/AD-16) — shown in place of the empty section so a hard reference never\n * silently vanishes. Rendered INTO the iframe (portal), which carries only the\n * project's stylesheet, so it uses inline styles rather than studio UI classes\n * (the EC-414 AD-B chrome contract), with the `.ec-chrome` containment recipe\n * on its root. Exported for the chrome contract test. The node stays\n * selectable; the inspector's `element` relation chip (EC-296) deep-links to\n * the reusable to edit or publish it.\n */\n/**\n * In-canvas notice for a listing block whose query is statically invalid\n * against the active session schema (EC-460 — the never-silent rule): the\n * message renders ABOVE the block (which keeps its own empty state), so a\n * misdeclared `listing` manifest (non-filterable sort/filter field, unknown\n * collection) is an explicit, visible state — never an empty grid an editor\n * can't tell apart from \"no content yet\". Rendered INTO the iframe (project\n * stylesheet only), so it uses inline styles + the `.ec-chrome` containment\n * recipe, exactly like {@link UnresolvedReusablePlaceholder}. Exported for the\n * chrome contract test.\n */\nexport function CanvasListingIssueNotice({ message }: { message: string }) {\n return (\n <div\n className=\"ec-chrome\"\n data-testid=\"canvas-listing-issue\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(200,140,40,0.65)',\n borderRadius: 8,\n padding: '10px 14px',\n margin: '8px 16px',\n background: 'rgba(230,160,40,0.08)',\n color: 'rgba(120,80,10,0.95)',\n font: '13px/1.5 system-ui, -apple-system, sans-serif',\n }}\n >\n <span style={{ fontWeight: 600 }}>⚠ {translate('builder.canvas.listingInvalidTitle')}</span>{' '}\n <span style={{ opacity: 0.9 }}>{message}</span>\n </div>\n )\n}\n\nexport function UnresolvedReusablePlaceholder({ status }: { status: 'unpublished' | 'missing' }) {\n const title =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedTitle')\n : translate('builder.canvas.reusableMissingTitle')\n const hint =\n status === 'unpublished'\n ? translate('builder.canvas.reusableUnpublishedHint')\n : translate('builder.canvas.reusableMissingHint')\n return (\n <div\n className=\"ec-chrome\"\n style={{\n boxSizing: 'border-box',\n border: '1px dashed rgba(120,120,120,0.5)',\n borderRadius: 8,\n padding: '24px 28px',\n margin: '12px 16px',\n background: 'rgba(127,127,127,0.06)',\n color: 'rgba(90,90,90,0.95)',\n font: '14px/1.5 system-ui, -apple-system, sans-serif',\n textAlign: 'center',\n }}\n >\n <div style={{ fontWeight: 600 }}>⚠ {title}</div>\n <div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{hint}</div>\n </div>\n )\n}\n"]}
@@ -5,7 +5,7 @@ import type { ComponentRegistry } from '@elytracms/core/component-registry';
5
5
  import type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text';
6
6
  import type { MessageKey } from '../../lib/i18n/index.js';
7
7
  import type { CompositionFieldScope } from './builder-store.js';
8
- import type { CanvasFieldChange } from './canvas-rebase.js';
8
+ import type { CanvasFieldChange, CanvasLift } from './canvas-rebase.js';
9
9
  import type { CanvasCommitChannel } from './canvas-commit-boundary.js';
10
10
  import type { InspectorPane } from '../inspector/inspector-sidebar.js';
11
11
  /**
@@ -67,20 +67,26 @@ export declare function CanvasRoleNotices({ notices }: {
67
67
  * canvas without it. The value is not touched — nothing mounts that could write.
68
68
  */
69
69
  export declare function NoStackCanvasNotice(): import("react").JSX.Element;
70
- export declare function CompositionFieldEditor({ field, value, change, onChange, commits, readOnly, trailingPanes, conflicts, }: {
70
+ export declare function CompositionFieldEditor({ field, value, revision, change, onChange, commits, readOnly, trailingPanes, conflicts, }: {
71
71
  field: BlocksFieldDef;
72
72
  /** The field's value at mount (the store is the source of truth while open). */
73
73
  value: unknown;
74
+ /**
75
+ * EC-647 — the revision the draft was at when {@link value} was read (its value and its
76
+ * revision travel together): the revision the canvas's first edit is authored at.
77
+ */
78
+ revision: number | undefined;
74
79
  /**
75
80
  * EC-630 — the latest change of the field made UNDERNEATH the canvas (the document
76
- * draft reports it); the store rebases onto it.
81
+ * draft reports it); the store rebases onto it — and, EC-647, onto its revision.
77
82
  */
78
83
  change?: CanvasFieldChange;
79
84
  /**
80
- * The store's value, lifted, and the value it was last agreed on with the draft — the
81
- * base the canvas writer's delta is measured from.
85
+ * The store's value, lifted, with the value it was last agreed on with the draft — the
86
+ * base the canvas writer's delta is measured from — and the revision that was agreed
87
+ * at (EC-647: the edit's authoring revision).
82
88
  */
83
- onChange: (value: unknown, base: unknown) => void;
89
+ onChange: (lift: CanvasLift) => void;
84
90
  /**
85
91
  * EC-630 — the document's committer, for the inspector controls' commit boundaries
86
92
  * (focus / blur / Enter at a position): a burst of typing commits on its control kind's
@@ -125,7 +125,7 @@ export function CanvasRoleNotices({ notices }) {
125
125
  export function NoStackCanvasNotice() {
126
126
  return (_jsx(Panel, { "data-testid": "canvas-no-stack", className: "flex h-full min-h-0 items-center justify-center p-6", children: _jsxs(Row, { gap: "2", align: "center", className: "max-w-prose", children: [_jsx(AlertTriangle, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" }), _jsx(Text, { size: "sm", tone: "muted", children: translate('builder.roles.noStack') })] }) }));
127
127
  }
128
- export function CompositionFieldEditor({ field, value, change, onChange, commits, readOnly, trailingPanes, conflicts, }) {
128
+ export function CompositionFieldEditor({ field, value, revision, change, onChange, commits, readOnly, trailingPanes, conflicts, }) {
129
129
  // Split the value ONCE at mount (EC-547): component nodes become the canvas
130
130
  // roots; every other sequence item is preserved aside for write-back. The store
131
131
  // is the source of truth while open: it lifts its edits out via onChange, and a
@@ -144,7 +144,7 @@ export function CompositionFieldEditor({ field, value, change, onChange, commits
144
144
  const fieldScope = useMemo(() => compositionFieldScope(field), [field]);
145
145
  if (!initialRoot)
146
146
  return _jsx(NoStackCanvasNotice, {});
147
- return (_jsxs(BuilderProvider, { composition: initialRoot, preserved: initial.preserved, field: fieldScope, components: registry.lookup, layout: layout, children: [_jsx(CompositionSync, { change: change, onChange: onChange, readOnly: readOnly }), _jsx(CanvasCommitProvider, { value: commits ?? null, children: _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, trailingPanes: trailingPanes, roleNotices: notices, conflicts: conflicts }) }) }) })] }));
147
+ return (_jsxs(BuilderProvider, { composition: initialRoot, preserved: initial.preserved, field: fieldScope, components: registry.lookup, layout: layout, children: [_jsx(CompositionSync, { revision: revision, change: change, onChange: onChange, readOnly: readOnly }), _jsx(CanvasCommitProvider, { value: commits ?? null, children: _jsx(ReuseProvider, { children: _jsx(PropSyncProvider, { children: _jsx(CompositionEditorShell, { readOnly: readOnly, trailingPanes: trailingPanes, roleNotices: notices, conflicts: conflicts }) }) }) })] }));
148
148
  }
149
149
  /**
150
150
  * Keeps the scoped store and the CMS draft on ONE value, in both directions — the
@@ -167,7 +167,7 @@ export function CompositionFieldEditor({ field, value, change, onChange, commits
167
167
  * replaced the remount (`canvasNonce` on every change of the value), which reset
168
168
  * both. The rebase effect is declared first: in one commit it runs before the lift.
169
169
  */
170
- function CompositionSync({ change, onChange, readOnly, }) {
170
+ function CompositionSync({ revision, change, onChange, readOnly, }) {
171
171
  const builder = useBuilder();
172
172
  const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS;
173
173
  const preserved = builder.preserved;
@@ -175,7 +175,8 @@ function CompositionSync({ change, onChange, readOnly, }) {
175
175
  const dispatch = builder.dispatch;
176
176
  const onChangeRef = useRef(onChange);
177
177
  onChangeRef.current = onChange;
178
- const [sync] = useState(() => createCanvasSync(mergeCompositionValue(blocks, preserved), change?.seq));
178
+ // EC-647: the mount value and the revision the draft held it at — one agreed pair.
179
+ const [sync] = useState(() => createCanvasSync(mergeCompositionValue(blocks, preserved), revision, change?.seq));
179
180
  useEffect(() => {
180
181
  // EC-226: a READ-ONLY canvas (the Published view) shows another perspective's
181
182
  // value — the draft's changes are not its own.
@@ -196,7 +197,7 @@ function CompositionSync({ change, onChange, readOnly, }) {
196
197
  return;
197
198
  const lift = sync.stored(mergeCompositionValue(blocks, preserved), rebased);
198
199
  if (lift)
199
- onChangeRef.current(lift.value, lift.base);
200
+ onChangeRef.current(lift);
200
201
  }, [blocks, preserved, rebased, readOnly]);
201
202
  return null;
202
203
  }
@@ -1 +1 @@
1
- {"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode, PreservedSequenceItem, SplitBlockRoots } from '@elytracms/core/project-graph'\nimport { DRAFT_PERSPECTIVE, fieldAllowList } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport type { ComponentRegistry } from '@elytracms/core/component-registry'\nimport type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '../../lib/utils'\nimport { translate } from '../../lib/i18n'\nimport type { MessageKey } from '../../lib/i18n'\nimport { getStudioRegistry } from '../../lib/registry'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport type { CompositionFieldScope } from './builder-store'\nimport { createCanvasSync } from './canvas-rebase'\nimport type { CanvasFieldChange } from './canvas-rebase'\nimport { CanvasCommitProvider } from './canvas-commit-boundary'\nimport type { CanvasCommitChannel } from './canvas-commit-boundary'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context'\nimport type { InsertPosition } from './insert-context'\nimport { InsertAffordanceProvider } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport { PropSyncProvider } from './prop-sync-context'\nimport { ReuseProvider } from './reuse-context'\nimport { PlacementRefusalNotice } from './placement-refusal'\nimport { fieldLabel } from '../cms/cms-labels'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`).\n *\n * EC-547 (EC-500 D1–D3): the stored value is the converged v2 block sequence\n * `{ version: 2, content: BlockNode[] }`. The canvas edits the sequence's\n * `elytra/component` nodes; typography nodes survive the session byte-for-byte\n * and keep their order relative to components on write-back (see\n * {@link splitCompositionValue} / {@link mergeCompositionValue}). Legacy bare\n * node/array values are still ACCEPTED on read during the staging window, but\n * every write emits v2. This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n\n/**\n * A sequence item the canvas does not natively edit (EC-547 / EC-500 D3):\n * typography nodes, unknown/future node kinds, even entries the v2 schema would\n * reject — the editor is not a validator, so they are carried BYTE-FOR-BYTE and\n * re-merged on write-back rather than parsed, rewritten, or dropped.\n * `componentPrefix` records how many component nodes preceded the item in the\n * original sequence — the anchor {@link mergeCompositionValue} splices by.\n */\nexport type PreservedBlockNode = PreservedSequenceItem<BlockNode>\n\n/** A field value split for canvas editing: component roots + everything else. */\nexport type SplitCompositionValue = SplitBlockRoots<BlockNode>\n\n/**\n * Split a stored blocks-field value into the component nodes the canvas edits\n * and the sequence items it must preserve (EC-547, the EC-500 round-trip law:\n * a projection may only rewrite nodes it natively edits). Typography items are\n * deliberately NOT surfaced as editable canvas blocks in v1 (EC-500 D3) — they\n * survive the editing session and keep their order on write-back.\n *\n * EC-576: the logic is core's {@link splitBlockRoots} — the ONE reader every\n * blocks writer shares. This is the canvas's typed view of it: project-graph\n * sits below rich-text and cannot name {@link BlockNode}, so the carried items\n * arrive `unknown`; they are typed here, not checked (preservation over parsing).\n */\nexport function splitCompositionValue(value: unknown): SplitCompositionValue {\n return splitBlockRoots(value) as SplitCompositionValue\n}\n\n/**\n * Merge canvas-edited component roots back into the stored sequence (EC-547).\n * The merge rule — preserved items anchored to their ordinal position between\n * components, every root tagged, the write always v2 — is core's\n * {@link mergeBlockRoots} (EC-576: the same merge every blocks writer goes\n * through, via `editBlockRoots`). Typed here as the rich-text value.\n */\nexport function mergeCompositionValue(\n roots: readonly ComponentNode[],\n preserved: readonly PreservedBlockNode[],\n): BlockSequenceValue {\n return mergeBlockRoots(roots, preserved)\n}\n\n/**\n * The field scope the editor's store carries (EC-554): the field's label (named\n * by refusal notices) and its EFFECTIVE top-level vocabulary (EC-584 — read through\n * the per-FieldType definition, `fieldAllowList`: the declared `allow`, else the\n * `blocks` default, any canvas-eligible component).\n */\nexport function compositionFieldScope(field: BlocksFieldDef): CompositionFieldScope {\n // A blocks field always has a vocabulary (its type declares the default).\n return { label: fieldLabel(field), allow: fieldAllowList(field)! }\n}\n\n/**\n * Wrap the field value's component roots in a synthetic root container (EC-287).\n * The builder store edits this single root-bearing composition directly; its\n * children are the field's component blocks, lifted back out via `onChange`.\n * EC-591: the root is the component that fills the `layout.stack` role — the\n * project's own, from its component list — never a hardcoded engine id.\n */\nfunction buildSyntheticRoot(roots: readonly ComponentNode[], stackComponentId: string): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: stackComponentId,\n slots: { [DEFAULT_SLOT]: [...roots] },\n }\n}\n\n/**\n * The canvas notices for roles nobody in the component list fills (EC-591) — each\n * names the gesture that is OFF because of it, so a missing role is visible, never\n * a silent dead affordance. Only gestures the canvas actually has are listed: the\n * `layout.stack` role is the canvas itself (see {@link CompositionFieldEditor}),\n * `layout.grid` the wrap-beside gesture, `text` the seeded layout presets.\n * (`richText` — the typography projection, EC-549 — and `image` — asset drop —\n * have no canvas gesture yet, so there is nothing to switch off.) Pure.\n */\nexport function canvasRoleNotices(registry: ComponentRegistry): MessageKey[] {\n const notices: MessageKey[] = []\n if (!registry.roleComponent('layout.grid')) notices.push('builder.roles.noGrid')\n if (!registry.roleComponent('text')) notices.push('builder.roles.noText')\n return notices\n}\n\n/** The role notices above the canvas (EC-591) — a standing truth about the component list. */\nexport function CanvasRoleNotices({ notices }: { notices: readonly MessageKey[] }) {\n if (notices.length === 0) return null\n return (\n <div data-testid=\"canvas-role-notices\" className=\"shrink-0 border-b border-border bg-muted/40\">\n {notices.map((key) => (\n <Row key={key} gap=\"2\" align=\"center\" className=\"px-3 py-1.5\">\n <AlertTriangle aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"muted\">\n {translate(key)}\n </Text>\n </Row>\n ))}\n </div>\n )\n}\n\n/**\n * The canvas in place of the editor when nobody fills `layout.stack` (EC-591): the\n * blocks of a composition field compose inside that component, so there is no\n * canvas without it. The value is not touched — nothing mounts that could write.\n */\nexport function NoStackCanvasNotice() {\n return (\n <Panel data-testid=\"canvas-no-stack\" className=\"flex h-full min-h-0 items-center justify-center p-6\">\n <Row gap=\"2\" align=\"center\" className=\"max-w-prose\">\n <AlertTriangle aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.roles.noStack')}\n </Text>\n </Row>\n </Panel>\n )\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n change,\n onChange,\n commits,\n readOnly,\n trailingPanes,\n conflicts,\n}: {\n field: BlocksFieldDef\n /** The field's value at mount (the store is the source of truth while open). */\n value: unknown\n /**\n * EC-630 — the latest change of the field made UNDERNEATH the canvas (the document\n * draft reports it); the store rebases onto it.\n */\n change?: CanvasFieldChange\n /**\n * The store's value, lifted, and the value it was last agreed on with the draft — the\n * base the canvas writer's delta is measured from.\n */\n onChange: (value: unknown, base: unknown) => void\n /**\n * EC-630 — the document's committer, for the inspector controls' commit boundaries\n * (focus / blur / Enter at a position): a burst of typing commits on its control kind's\n * policy, never per keystroke. Absent → every edit commits as the canvas writes it.\n */\n commits?: CanvasCommitChannel\n readOnly: boolean\n /**\n * EC-595 A2 — the field's per-field conflicts (a refused node patch: \"changed by X —\n * take theirs / keep mine\"), rendered above the canvas: the canvas IS the field.\n */\n conflicts?: ReactNode\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Split the value ONCE at mount (EC-547): component nodes become the canvas\n // roots; every other sequence item is preserved aside for write-back. The store\n // is the source of truth while open: it lifts its edits out via onChange, and a\n // value that changes UNDERNEATH it (EC-630 — a colleague's patch, a write's\n // canonical row) is REBASED into it by `CompositionSync`, never remounted.\n // (BuilderProvider reads `composition` only in its lazy initial state.)\n const initial = useMemo(() => splitCompositionValue(value), [])\n // EC-591: the roles come from the project's component list (the registry).\n const registry = getStudioRegistry().registry\n const stack = registry.roleComponent('layout.stack')\n const layout = registry.layout\n const notices = useMemo(() => canvasRoleNotices(registry), [registry])\n const initialRoot = useMemo(\n () => (stack ? buildSyntheticRoot(initial.roots, stack.id) : null),\n [initial, stack],\n )\n // EC-554: the field's `allow` travels WITH the store, so every placement\n // surface (canvas + tree DnD, paste, the insert offer) judges the same value.\n const fieldScope = useMemo(() => compositionFieldScope(field), [field])\n\n if (!initialRoot) return <NoStackCanvasNotice />\n\n return (\n <BuilderProvider\n composition={initialRoot}\n preserved={initial.preserved}\n field={fieldScope}\n components={registry.lookup}\n layout={layout}\n >\n <CompositionSync change={change} onChange={onChange} readOnly={readOnly} />\n <CanvasCommitProvider value={commits ?? null}>\n <ReuseProvider>\n {/* EC-408: the field↔component selection-sync channel wraps BOTH the canvas\n and the inspector (they reach each other through nothing else). */}\n <PropSyncProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n trailingPanes={trailingPanes}\n roleNotices={notices}\n conflicts={conflicts}\n />\n </PropSyncProvider>\n </ReuseProvider>\n </CanvasCommitProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Keeps the scoped store and the CMS draft on ONE value, in both directions — the\n * React shell of {@link createCanvasSync} (EC-630).\n *\n * **Lift** (the store → the draft): opening the tab lifts nothing (the\n * container-normalized mount value is the first agreed value, so simply opening\n * never marks the document dirty); every later change to the block list OR to the\n * preserved typography (EC-549: a run deleted or edited, or an undo of either)\n * emits the merged v2 sequence value (EC-547) — the canvas roots spliced back\n * between the preserved typography items — with the value it was last agreed on,\n * which the draft measures the canvas writer's delta from. Cardinality\n * (`multiple`) no longer shapes the stored value: it is a validation constraint\n * over the one envelope (EC-500 D2).\n *\n * **Rebase** (the draft → the store): a change of the field made UNDERNEATH the\n * canvas (a colleague's patch by subscription, a write's canonical row, a resolved\n * conflict), reported by the draft as `change`, is dispatched as `rebase` — the\n * change lands, undo history and selection survive, undo never reverts it. This\n * replaced the remount (`canvasNonce` on every change of the value), which reset\n * both. The rebase effect is declared first: in one commit it runs before the lift.\n */\nfunction CompositionSync({\n change,\n onChange,\n readOnly,\n}: {\n change: CanvasFieldChange | undefined\n onChange: (value: unknown, base: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const preserved = builder.preserved as readonly PreservedBlockNode[]\n const rebased = builder.rebased\n const dispatch = builder.dispatch\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const [sync] = useState(() => createCanvasSync(mergeCompositionValue(blocks, preserved), change?.seq))\n\n useEffect(() => {\n // EC-226: a READ-ONLY canvas (the Published view) shows another perspective's\n // value — the draft's changes are not its own.\n if (readOnly || !change) return\n const action = sync.changed(change)\n if (action) dispatch(action)\n }, [change?.seq, readOnly])\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n const lift = sync.stored(mergeCompositionValue(blocks, preserved), rebased)\n if (lift) onChangeRef.current(lift.value, lift.base)\n }, [blocks, preserved, rebased, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n trailingPanes,\n roleNotices,\n conflicts,\n}: {\n readOnly: boolean\n /** EC-595 A2 — the field's conflict notices (see {@link CompositionFieldEditor}). */\n conflicts?: ReactNode\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n /** Notices for the roles nobody fills (EC-591) — see {@link canvasRoleNotices}. */\n roleNotices?: readonly MessageKey[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div className={cn('h-full min-h-0 overflow-hidden', readOnly && 'pointer-events-none opacity-60')}>\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n // The single insert entry point for BOTH the toolbar and the page tree's\n // \"Add child\" (EC-383). It selects its target first so `builder.selectedId`\n // is the one source of truth for where an insert lands: the toolbar resolves\n // `builder.selectedId ?? rootId` while Add-child passes an explicit row, and\n // without this sync clicking Add-child (which otherwise leaves selection put)\n // could target a different node than the toolbar — surfacing as a mismatched\n // (even empty \"No allowed components match\") insert dialog. Both now resolve\n // the identical target + allow set for the same selection.\n function openInsert(nodeId: string) {\n // EC-584: the offer reads the store's placement scope — the field's vocabulary at\n // the composition root, each slot's below — the same scope the reducer judges by.\n const ctx = resolveInsertContext(builder, nodeId)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n\n // EC-405: the in-canvas \"+\" affordances resolve the SAME slot rules through the\n // shared position-carrying insert-context resolution, and open the panel at the\n // clicked edge (an explicit position that survives to the insertion, rather than\n // the panel re-deriving it from the selection). Re-memoized on every store change\n // so the closures read the live graph.\n const affordanceApi = useMemo<InsertAffordanceApi>(\n () => ({\n positionsFor: (nodeId: string) => insertablePositions(builder, nodeId),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, dispatch],\n )\n\n return (\n <InsertAffordanceProvider value={affordanceApi}>\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}>\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div className={cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60')}>\n {conflicts ? <div className=\"shrink-0 border-b border-border p-2\">{conflicts}</div> : null}\n {/* EC-584: a placement the reducer refused, with its reason — never a silent no-op. */}\n <PlacementRefusalNotice\n refusal={builder.refusal}\n fieldLabel={builder.field.label}\n onDismiss={() => dispatch({ type: 'clearRefusal' })}\n />\n <CanvasRoleNotices notices={roleNotices ?? []} />\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : DRAFT_PERSPECTIVE} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n </InsertAffordanceProvider>\n )\n}\n"]}
1
+ {"version":3,"file":"composition-field-editor.js","sourceRoot":"","sources":["../../../src/components/builder/composition-field-editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, mergeBlockRoots, splitBlockRoots } from '@elytracms/core/project-graph'\nimport type { ComponentNode, PreservedSequenceItem, SplitBlockRoots } from '@elytracms/core/project-graph'\nimport { DRAFT_PERSPECTIVE, fieldAllowList } from '@elytracms/core/cms-core'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport type { ComponentRegistry } from '@elytracms/core/component-registry'\nimport type { BlockNode, BlockSequenceValue } from '@elytracms/core/rich-text'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '../../lib/utils'\nimport { translate } from '../../lib/i18n'\nimport type { MessageKey } from '../../lib/i18n'\nimport { getStudioRegistry } from '../../lib/registry'\nimport { BuilderProvider, useBuilder } from './builder-store'\nimport type { CompositionFieldScope } from './builder-store'\nimport { createCanvasSync } from './canvas-rebase'\nimport type { CanvasFieldChange, CanvasLift } from './canvas-rebase'\nimport { CanvasCommitProvider } from './canvas-commit-boundary'\nimport type { CanvasCommitChannel } from './canvas-commit-boundary'\nimport { useCanvasViewports } from './canvas-breakpoints'\nimport type { ViewportOption } from './canvas-breakpoints'\nimport { BuilderCanvas } from './canvas'\nimport { PageTree } from './page-tree'\nimport { PropertyInspector } from './property-inspector'\nimport { InspectorSidebar } from '../inspector/inspector-sidebar'\nimport type { InspectorPane } from '../inspector/inspector-sidebar'\nimport { InsertMenu } from './insert-menu'\nimport type { InsertContext } from './insert-menu'\nimport { insertablePositions, resolveInsertContext, resolveInsertContextAt } from './insert-context'\nimport type { InsertPosition } from './insert-context'\nimport { InsertAffordanceProvider } from './insert-affordance-context'\nimport type { InsertAffordanceApi } from './insert-affordance-context'\nimport { PropSyncProvider } from './prop-sync-context'\nimport { ReuseProvider } from './reuse-context'\nimport { PlacementRefusalNotice } from './placement-refusal'\nimport { fieldLabel } from '../cms/cms-labels'\n\n/**\n * EC-188 — the block-field surface: one canvas engine mounted on a composition\n * **field value** (AD-12), not the project graph.\n *\n * The composition tree is wrapped in a synthetic single-page graph and edited by\n * a SCOPED `BuilderProvider` with **no `projectId`** (so the graph persister is a\n * no-op — builder-store.tsx). Every committed edit is lifted out via `onChange`\n * into the CMS document draft; the document's own Save persists it (the tree is\n * stored as document data, `v.any()`).\n *\n * EC-547 (EC-500 D1–D3): the stored value is the converged v2 block sequence\n * `{ version: 2, content: BlockNode[] }`. The canvas edits the sequence's\n * `elytra/component` nodes; typography nodes survive the session byte-for-byte\n * and keep their order relative to components on write-back (see\n * {@link splitCompositionValue} / {@link mergeCompositionValue}). Legacy bare\n * node/array values are still ACCEPTED on read during the staging window, but\n * every write emits v2. This reuses the real canvas + inspector +\n * blocks panel unchanged — the same EC-158–164 mechanics — without touching the\n * project-wide builder store. Surfaced on a dedicated \"Canvas\" fieldset tab in\n * the CMS detail editor so the page builder gets full screen real estate rather\n * than being crammed into the field list.\n */\n\nconst SYNTH_ROOT_ID = 'composition.root'\n\n/**\n * A sequence item the canvas does not natively edit (EC-547 / EC-500 D3):\n * typography nodes, unknown/future node kinds, even entries the v2 schema would\n * reject — the editor is not a validator, so they are carried BYTE-FOR-BYTE and\n * re-merged on write-back rather than parsed, rewritten, or dropped.\n * `componentPrefix` records how many component nodes preceded the item in the\n * original sequence — the anchor {@link mergeCompositionValue} splices by.\n */\nexport type PreservedBlockNode = PreservedSequenceItem<BlockNode>\n\n/** A field value split for canvas editing: component roots + everything else. */\nexport type SplitCompositionValue = SplitBlockRoots<BlockNode>\n\n/**\n * Split a stored blocks-field value into the component nodes the canvas edits\n * and the sequence items it must preserve (EC-547, the EC-500 round-trip law:\n * a projection may only rewrite nodes it natively edits). Typography items are\n * deliberately NOT surfaced as editable canvas blocks in v1 (EC-500 D3) — they\n * survive the editing session and keep their order on write-back.\n *\n * EC-576: the logic is core's {@link splitBlockRoots} — the ONE reader every\n * blocks writer shares. This is the canvas's typed view of it: project-graph\n * sits below rich-text and cannot name {@link BlockNode}, so the carried items\n * arrive `unknown`; they are typed here, not checked (preservation over parsing).\n */\nexport function splitCompositionValue(value: unknown): SplitCompositionValue {\n return splitBlockRoots(value) as SplitCompositionValue\n}\n\n/**\n * Merge canvas-edited component roots back into the stored sequence (EC-547).\n * The merge rule — preserved items anchored to their ordinal position between\n * components, every root tagged, the write always v2 — is core's\n * {@link mergeBlockRoots} (EC-576: the same merge every blocks writer goes\n * through, via `editBlockRoots`). Typed here as the rich-text value.\n */\nexport function mergeCompositionValue(\n roots: readonly ComponentNode[],\n preserved: readonly PreservedBlockNode[],\n): BlockSequenceValue {\n return mergeBlockRoots(roots, preserved)\n}\n\n/**\n * The field scope the editor's store carries (EC-554): the field's label (named\n * by refusal notices) and its EFFECTIVE top-level vocabulary (EC-584 — read through\n * the per-FieldType definition, `fieldAllowList`: the declared `allow`, else the\n * `blocks` default, any canvas-eligible component).\n */\nexport function compositionFieldScope(field: BlocksFieldDef): CompositionFieldScope {\n // A blocks field always has a vocabulary (its type declares the default).\n return { label: fieldLabel(field), allow: fieldAllowList(field)! }\n}\n\n/**\n * Wrap the field value's component roots in a synthetic root container (EC-287).\n * The builder store edits this single root-bearing composition directly; its\n * children are the field's component blocks, lifted back out via `onChange`.\n * EC-591: the root is the component that fills the `layout.stack` role — the\n * project's own, from its component list — never a hardcoded engine id.\n */\nfunction buildSyntheticRoot(roots: readonly ComponentNode[], stackComponentId: string): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: stackComponentId,\n slots: { [DEFAULT_SLOT]: [...roots] },\n }\n}\n\n/**\n * The canvas notices for roles nobody in the component list fills (EC-591) — each\n * names the gesture that is OFF because of it, so a missing role is visible, never\n * a silent dead affordance. Only gestures the canvas actually has are listed: the\n * `layout.stack` role is the canvas itself (see {@link CompositionFieldEditor}),\n * `layout.grid` the wrap-beside gesture, `text` the seeded layout presets.\n * (`richText` — the typography projection, EC-549 — and `image` — asset drop —\n * have no canvas gesture yet, so there is nothing to switch off.) Pure.\n */\nexport function canvasRoleNotices(registry: ComponentRegistry): MessageKey[] {\n const notices: MessageKey[] = []\n if (!registry.roleComponent('layout.grid')) notices.push('builder.roles.noGrid')\n if (!registry.roleComponent('text')) notices.push('builder.roles.noText')\n return notices\n}\n\n/** The role notices above the canvas (EC-591) — a standing truth about the component list. */\nexport function CanvasRoleNotices({ notices }: { notices: readonly MessageKey[] }) {\n if (notices.length === 0) return null\n return (\n <div data-testid=\"canvas-role-notices\" className=\"shrink-0 border-b border-border bg-muted/40\">\n {notices.map((key) => (\n <Row key={key} gap=\"2\" align=\"center\" className=\"px-3 py-1.5\">\n <AlertTriangle aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <Text size=\"xs\" tone=\"muted\">\n {translate(key)}\n </Text>\n </Row>\n ))}\n </div>\n )\n}\n\n/**\n * The canvas in place of the editor when nobody fills `layout.stack` (EC-591): the\n * blocks of a composition field compose inside that component, so there is no\n * canvas without it. The value is not touched — nothing mounts that could write.\n */\nexport function NoStackCanvasNotice() {\n return (\n <Panel data-testid=\"canvas-no-stack\" className=\"flex h-full min-h-0 items-center justify-center p-6\">\n <Row gap=\"2\" align=\"center\" className=\"max-w-prose\">\n <AlertTriangle aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('builder.roles.noStack')}\n </Text>\n </Row>\n </Panel>\n )\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n revision,\n change,\n onChange,\n commits,\n readOnly,\n trailingPanes,\n conflicts,\n}: {\n field: BlocksFieldDef\n /** The field's value at mount (the store is the source of truth while open). */\n value: unknown\n /**\n * EC-647 — the revision the draft was at when {@link value} was read (its value and its\n * revision travel together): the revision the canvas's first edit is authored at.\n */\n revision: number | undefined\n /**\n * EC-630 — the latest change of the field made UNDERNEATH the canvas (the document\n * draft reports it); the store rebases onto it — and, EC-647, onto its revision.\n */\n change?: CanvasFieldChange\n /**\n * The store's value, lifted, with the value it was last agreed on with the draft — the\n * base the canvas writer's delta is measured from — and the revision that was agreed\n * at (EC-647: the edit's authoring revision).\n */\n onChange: (lift: CanvasLift) => void\n /**\n * EC-630 — the document's committer, for the inspector controls' commit boundaries\n * (focus / blur / Enter at a position): a burst of typing commits on its control kind's\n * policy, never per keystroke. Absent → every edit commits as the canvas writes it.\n */\n commits?: CanvasCommitChannel\n readOnly: boolean\n /**\n * EC-595 A2 — the field's per-field conflicts (a refused node patch: \"changed by X —\n * take theirs / keep mine\"), rendered above the canvas: the canvas IS the field.\n */\n conflicts?: ReactNode\n /** Inspector panes the document editor appends after the canvas-supplied\n * `Properties` pane (EC-224 — the frame-owned `Versions` pane). */\n trailingPanes?: readonly InspectorPane[]\n}) {\n // Split the value ONCE at mount (EC-547): component nodes become the canvas\n // roots; every other sequence item is preserved aside for write-back. The store\n // is the source of truth while open: it lifts its edits out via onChange, and a\n // value that changes UNDERNEATH it (EC-630 — a colleague's patch, a write's\n // canonical row) is REBASED into it by `CompositionSync`, never remounted.\n // (BuilderProvider reads `composition` only in its lazy initial state.)\n const initial = useMemo(() => splitCompositionValue(value), [])\n // EC-591: the roles come from the project's component list (the registry).\n const registry = getStudioRegistry().registry\n const stack = registry.roleComponent('layout.stack')\n const layout = registry.layout\n const notices = useMemo(() => canvasRoleNotices(registry), [registry])\n const initialRoot = useMemo(\n () => (stack ? buildSyntheticRoot(initial.roots, stack.id) : null),\n [initial, stack],\n )\n // EC-554: the field's `allow` travels WITH the store, so every placement\n // surface (canvas + tree DnD, paste, the insert offer) judges the same value.\n const fieldScope = useMemo(() => compositionFieldScope(field), [field])\n\n if (!initialRoot) return <NoStackCanvasNotice />\n\n return (\n <BuilderProvider\n composition={initialRoot}\n preserved={initial.preserved}\n field={fieldScope}\n components={registry.lookup}\n layout={layout}\n >\n <CompositionSync revision={revision} change={change} onChange={onChange} readOnly={readOnly} />\n <CanvasCommitProvider value={commits ?? null}>\n <ReuseProvider>\n {/* EC-408: the field↔component selection-sync channel wraps BOTH the canvas\n and the inspector (they reach each other through nothing else). */}\n <PropSyncProvider>\n <CompositionEditorShell\n readOnly={readOnly}\n trailingPanes={trailingPanes}\n roleNotices={notices}\n conflicts={conflicts}\n />\n </PropSyncProvider>\n </ReuseProvider>\n </CanvasCommitProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Keeps the scoped store and the CMS draft on ONE value, in both directions — the\n * React shell of {@link createCanvasSync} (EC-630).\n *\n * **Lift** (the store → the draft): opening the tab lifts nothing (the\n * container-normalized mount value is the first agreed value, so simply opening\n * never marks the document dirty); every later change to the block list OR to the\n * preserved typography (EC-549: a run deleted or edited, or an undo of either)\n * emits the merged v2 sequence value (EC-547) — the canvas roots spliced back\n * between the preserved typography items — with the value it was last agreed on,\n * which the draft measures the canvas writer's delta from. Cardinality\n * (`multiple`) no longer shapes the stored value: it is a validation constraint\n * over the one envelope (EC-500 D2).\n *\n * **Rebase** (the draft → the store): a change of the field made UNDERNEATH the\n * canvas (a colleague's patch by subscription, a write's canonical row, a resolved\n * conflict), reported by the draft as `change`, is dispatched as `rebase` — the\n * change lands, undo history and selection survive, undo never reverts it. This\n * replaced the remount (`canvasNonce` on every change of the value), which reset\n * both. The rebase effect is declared first: in one commit it runs before the lift.\n */\nfunction CompositionSync({\n revision,\n change,\n onChange,\n readOnly,\n}: {\n revision: number | undefined\n change: CanvasFieldChange | undefined\n onChange: (lift: CanvasLift) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const preserved = builder.preserved as readonly PreservedBlockNode[]\n const rebased = builder.rebased\n const dispatch = builder.dispatch\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // EC-647: the mount value and the revision the draft held it at — one agreed pair.\n const [sync] = useState(() => createCanvasSync(mergeCompositionValue(blocks, preserved), revision, change?.seq))\n\n useEffect(() => {\n // EC-226: a READ-ONLY canvas (the Published view) shows another perspective's\n // value — the draft's changes are not its own.\n if (readOnly || !change) return\n const action = sync.changed(change)\n if (action) dispatch(action)\n }, [change?.seq, readOnly])\n\n useEffect(() => {\n // EC-226: never lift edits out of a READ-ONLY canvas (the Published view).\n // The canvas's keyboard listeners (clipboard paste/cut) aren't blocked by the\n // visual `pointer-events-none`, so without this a Cmd+V could mutate the\n // graph; refusing to lift keeps the document draft intact regardless. The\n // editor remounts per perspective, so any transient in-canvas change is\n // discarded on switch.\n if (readOnly) return\n const lift = sync.stored(mergeCompositionValue(blocks, preserved), rebased)\n if (lift) onChangeRef.current(lift)\n }, [blocks, preserved, rebased, readOnly])\n\n return null\n}\n\nconst EMPTY_BLOCKS: readonly ComponentNode[] = []\n\n/** A breakpoint preview-width toggle (EC-211/EC-250). Switching the viewport\n * resizes only the preview frame (not an undoable edit, writes no graph command).\n * The label is the breakpoint name (`Full`/`sm`/`md`/…) with the real px width in\n * the tooltip — so the switcher reflects the site's actual screens, not a fixed\n * desktop/tablet/phone triple. */\nfunction ViewportButton({ option }: { option: ViewportOption }) {\n const builder = useBuilder()\n const active = builder.viewport === option.key\n return (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.viewportPreviewAria', { label: option.label })}\n aria-pressed={active}\n title={\n option.px !== undefined\n ? `${option.label} · ${option.px}px`\n : translate('builder.canvas.viewportWidthTitle', { label: option.label })\n }\n onClick={() => builder.dispatch({ type: 'viewport', viewport: option.key })}\n className={cn('px-2 text-xs', active && 'bg-accent text-accent-foreground')}\n >\n {option.label}\n </Button>\n )\n}\n\n/** The centered breakpoint switcher (EC-250): the resolved viewport set, plus a\n * visible validation issue when a project declared breakpoints but none are usable. */\nfunction BreakpointSwitcher() {\n const { viewports, issue } = useCanvasViewports()\n return (\n <Row gap=\"1\" align=\"center\">\n <ToolbarGroup>\n {viewports.map((v) => (\n <ViewportButton key={v.key} option={v} />\n ))}\n </ToolbarGroup>\n {issue ? (\n <Row gap=\"1\" align=\"center\" className=\"text-amber-600 dark:text-amber-400\" title={issue}>\n <AlertTriangle aria-hidden className=\"size-3.5\" />\n <Text size=\"xs\">{translate('builder.canvas.breakpointsIssueLabel')}</Text>\n </Row>\n ) : null}\n </Row>\n )\n}\n\n/**\n * The slim editor surface — the canvas + layers + inspector, plus Insert and\n * undo/redo. It deliberately drops the project-builder *lifecycle* chrome\n * (publish, versions, AI, page SEO): the document owns the lifecycle, not the\n * field. It keeps spatial/preview controls — including the breakpoint\n * preview-width switcher (EC-211), a canvas concern, not a lifecycle one.\n */\nfunction CompositionEditorShell({\n readOnly,\n trailingPanes,\n roleNotices,\n conflicts,\n}: {\n readOnly: boolean\n /** EC-595 A2 — the field's conflict notices (see {@link CompositionFieldEditor}). */\n conflicts?: ReactNode\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\n /** Notices for the roles nobody fills (EC-591) — see {@link canvasRoleNotices}. */\n roleNotices?: readonly MessageKey[]\n}) {\n const builder = useBuilder()\n const dispatch = builder.dispatch\n const [insertContext, setInsertContext] = useState<InsertContext | null>(null)\n const [insertOpen, setInsertOpen] = useState(false)\n const rootId = builder.present.id\n\n // The canvas SUPPLIES the `Properties` pane (EC-224); the document editor\n // appends its own (`Versions`). Properties dims under read-only just as the\n // whole inspector column used to — but the appended panes stay interactive.\n const propertiesPane = useMemo<InspectorPane>(\n () => ({\n id: 'properties',\n label: translate('builder.inspector.propertiesPane'),\n icon: <SlidersHorizontal className=\"size-3.5\" />,\n render: () => (\n <div className={cn('h-full min-h-0 overflow-hidden', readOnly && 'pointer-events-none opacity-60')}>\n <PropertyInspector />\n </div>\n ),\n }),\n [readOnly],\n )\n const panes = useMemo<InspectorPane[]>(\n () => [propertiesPane, ...(trailingPanes ?? [])],\n [propertiesPane, trailingPanes],\n )\n\n // The single insert entry point for BOTH the toolbar and the page tree's\n // \"Add child\" (EC-383). It selects its target first so `builder.selectedId`\n // is the one source of truth for where an insert lands: the toolbar resolves\n // `builder.selectedId ?? rootId` while Add-child passes an explicit row, and\n // without this sync clicking Add-child (which otherwise leaves selection put)\n // could target a different node than the toolbar — surfacing as a mismatched\n // (even empty \"No allowed components match\") insert dialog. Both now resolve\n // the identical target + allow set for the same selection.\n function openInsert(nodeId: string) {\n // EC-584: the offer reads the store's placement scope — the field's vocabulary at\n // the composition root, each slot's below — the same scope the reducer judges by.\n const ctx = resolveInsertContext(builder, nodeId)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n }\n\n // EC-405: the in-canvas \"+\" affordances resolve the SAME slot rules through the\n // shared position-carrying insert-context resolution, and open the panel at the\n // clicked edge (an explicit position that survives to the insertion, rather than\n // the panel re-deriving it from the selection). Re-memoized on every store change\n // so the closures read the live graph.\n const affordanceApi = useMemo<InsertAffordanceApi>(\n () => ({\n positionsFor: (nodeId: string) => insertablePositions(builder, nodeId),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, dispatch],\n )\n\n return (\n <InsertAffordanceProvider value={affordanceApi}>\n <div className=\"flex h-full min-h-0 flex-col\">\n <div className=\"shrink-0 border-b border-border\">\n <Toolbar>\n <ToolbarGroup>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.undo')}\n title={translate('builder.canvas.undo')}\n disabled={readOnly || !builder.canUndo}\n onClick={() => dispatch({ type: 'undo' })}\n >\n <Undo2 />\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={translate('builder.canvas.redo')}\n title={translate('builder.canvas.redo')}\n disabled={readOnly || !builder.canRedo}\n onClick={() => dispatch({ type: 'redo' })}\n >\n <Redo2 />\n </Button>\n </ToolbarGroup>\n <ToolbarSeparator />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={readOnly}\n onClick={() => openInsert(builder.selectedId ?? rootId)}\n >\n <Plus /> {translate('builder.canvas.insert')}\n </Button>\n {/* Breakpoint preview-width switcher (EC-211/EC-250), centered over the\n canvas via flanking spacers. A preview control, not an edit — stays\n active even in read-only published preview. */}\n <Spacer />\n <BreakpointSwitcher />\n <Spacer />\n </Toolbar>\n </div>\n\n <div className=\"grid min-h-0 flex-1 grid-cols-[minmax(200px,240px)_1fr_minmax(280px,320px)] gap-px bg-border\">\n <Panel className={cn('rounded-none border-0', readOnly && 'pointer-events-none opacity-60')}>\n <PageTree onAddChild={openInsert} />\n </Panel>\n <div className={cn('flex min-h-0 flex-col bg-background', readOnly && 'pointer-events-none opacity-60')}>\n {conflicts ? <div className=\"shrink-0 border-b border-border p-2\">{conflicts}</div> : null}\n {/* EC-584: a placement the reducer refused, with its reason — never a silent no-op. */}\n <PlacementRefusalNotice\n refusal={builder.refusal}\n fieldLabel={builder.field.label}\n onDismiss={() => dispatch({ type: 'clearRefusal' })}\n />\n <CanvasRoleNotices notices={roleNotices ?? []} />\n {/* EC-300/AD-16: the read-only Published view resolves references against\n the published perspective (honest preview); the editable Draft view\n resolves drafts (a just-made reusable shows immediately). */}\n <BuilderCanvas perspective={readOnly ? 'published' : DRAFT_PERSPECTIVE} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n </InsertAffordanceProvider>\n )\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { AssetFieldDef } from '@elytracms/core/cms-core';
2
- import type { AssetRecord } from '@elytracms/core/persistence';
2
+ import type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence';
3
3
  /**
4
4
  * Pure selection/filter core for the asset picker (EC-432). DOM-free and
5
5
  * side-effect-free so the media browser's "what shows", "what an upload may send" and
@@ -25,7 +25,7 @@ export declare const ASSET_PICKER_STAGE_PRESET: "grid";
25
25
  * the `accept` filter (EC-432) — an image field's picker browses only images,
26
26
  * never the project's PDFs. Order follows the library.
27
27
  */
28
- export declare function visiblePickerAssets(assets: readonly AssetRecord[], query: string, accept: readonly string[] | undefined): AssetRecord[];
28
+ export declare function visiblePickerAssets(assets: readonly AssetRecord[], query: string, accept: readonly string[] | undefined, at: AssetReadLocale | null): AssetRecord[];
29
29
  /**
30
30
  * THE drop rule (EC-301 → EC-588): split files about to be uploaded into the ones the
31
31
  * field's `accept` admits and the NAMES of the ones it refuses. A drag-drop bypasses
@@ -26,8 +26,8 @@ export const ASSET_PICKER_STAGE_PRESET = 'grid';
26
26
  * the `accept` filter (EC-432) — an image field's picker browses only images,
27
27
  * never the project's PDFs. Order follows the library.
28
28
  */
29
- export function visiblePickerAssets(assets, query, accept) {
30
- return filterAssets(assets, query).filter((asset) => assetMatchesAccept(asset, accept));
29
+ export function visiblePickerAssets(assets, query, accept, at) {
30
+ return filterAssets(assets, query, at).filter((asset) => assetMatchesAccept(asset, accept));
31
31
  }
32
32
  /**
33
33
  * THE drop rule (EC-301 → EC-588): split files about to be uploaded into the ones the
@@ -1 +1 @@
1
- {"version":3,"file":"asset-picker-core.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker-core.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssetFieldDef } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport { assetMatchesAccept } from '@elytracms/core/cms-core'\nimport { filterAssets } from '../assets/asset-search'\nimport {\n appendAssetSelections,\n applyAssetSelection,\n singleAssetIdOf,\n} from '../assets/asset-selection'\n\n/**\n * Pure selection/filter core for the asset picker (EC-432). DOM-free and\n * side-effect-free so the media browser's \"what shows\", \"what an upload may send\" and\n * \"what a pick/upload commits\" are unit-testable without a browser — the picker\n * component is just the stage shell around these.\n *\n * EC-588: the picker takes the field's STRUCTURED `accept` list — the one shape the\n * schema declares — and derives everything from it here or in core: the grid filter\n * ({@link visiblePickerAssets}), the drop/upload split ({@link splitByAccept}), the\n * file-input attribute (`assetAcceptAttr`, core), and the commit filter\n * ({@link uploadBatchProgress}). An absent `accept` is unrestricted everywhere; there is\n * no hidden `image/*` default.\n */\n\n/**\n * The stage width preset the media browser mounts with (EC-432 / EC-431 AD-A):\n * the roomy `grid` measure (`min(1240px, 94vw)`) — browsing assets is spatial.\n * Exported so the geometry contract is pinnable without rendering the portal\n * (which is `null` in the node test env).\n */\nexport const ASSET_PICKER_STAGE_PRESET = 'grid' as const\n\n/**\n * The assets shown in the picker grid: the shipped search filter (EC-152) then\n * the `accept` filter (EC-432) — an image field's picker browses only images,\n * never the project's PDFs. Order follows the library.\n */\nexport function visiblePickerAssets(\n assets: readonly AssetRecord[],\n query: string,\n accept: readonly string[] | undefined,\n): AssetRecord[] {\n return filterAssets(assets, query).filter((asset) => assetMatchesAccept(asset, accept))\n}\n\n/**\n * THE drop rule (EC-301 → EC-588): split files about to be uploaded into the ones the\n * field's `accept` admits and the NAMES of the ones it refuses. A drag-drop bypasses\n * the file dialog's soft filter, so every upload path — the field's drop zone, a drop\n * into the open picker, the picker's Upload dialog — passes through this, and the\n * caller names the refused files in a visible notice instead of uploading them.\n */\nexport function splitByAccept<F extends { name: string; type: string }>(\n files: readonly F[],\n accept: readonly string[] | undefined,\n): { accepted: F[]; rejected: string[] } {\n const accepted: F[] = []\n const rejected: string[] = []\n for (const file of files) {\n if (assetMatchesAccept({ contentType: file.type, filename: file.name }, accept)) accepted.push(file)\n else rejected.push(file.name)\n }\n return { accepted, rejected }\n}\n\n/**\n * One upload BATCH (EC-460): the upload-queue ids THIS interaction started.\n *\n * EC-588 A7: it used to be a pre-upload library snapshot, and whatever the library\n * gained since was committed — so an upload from another tab (re-listed with ours) or\n * from a sibling field's drop could land in this field. A batch now names exactly its\n * own uploads, and the store reports which asset each one landed as.\n */\nexport interface UploadBatch {\n uploadIds: readonly string[]\n}\n\n/** What batch progress reads from the assets store (`AssetsStore` satisfies it). */\nexport interface UploadSource {\n assets: readonly AssetRecord[]\n uploads: readonly { id: string; state: { phase: string } }[]\n /** The asset id an upload landed as, once its record is confirmed. */\n landedAssetId(uploadId: string): string | undefined\n}\n\nexport interface UploadBatchProgress {\n /** The batch's landed asset ids that the field accepts, in upload order. */\n added: string[]\n /**\n * The batch is finished: none of its uploads is still running, and every one that\n * landed is in the library. A failed (or dismissed) upload is terminal — it stays in\n * the queue for retry and is never awaited forever. Commit/attach decisions happen\n * only once settled for `many` targets; a single-select target commits on the first\n * arrival.\n */\n settled: boolean\n}\n\n/**\n * Where a batch stands. An upload is in one of four states: running (in the queue,\n * not failed), landed-but-not-listed (the store confirmed it, the library re-list has\n * not arrived), landed (attachable), or terminal (failed / dismissed). Only landed\n * uploads whose RECORD satisfies `accept` are `added` (A7: the commit is filtered,\n * not just the dialog). Pure — the picker and the asset field both drive their\n * effects with this.\n */\nexport function uploadBatchProgress(\n batch: UploadBatch,\n source: UploadSource,\n accept: readonly string[] | undefined,\n): UploadBatchProgress {\n const running = new Set(source.uploads.filter((u) => u.state.phase !== 'failed').map((u) => u.id))\n const library = new Map(source.assets.map((asset) => [asset.id, asset]))\n const added: string[] = []\n let pending = false\n for (const uploadId of batch.uploadIds) {\n if (running.has(uploadId)) {\n pending = true\n continue\n }\n const assetId = source.landedAssetId(uploadId)\n if (assetId === undefined) continue // failed or dismissed — terminal\n const record = library.get(assetId)\n if (!record) {\n pending = true // confirmed, but the library re-list has not landed yet\n continue\n }\n if (assetMatchesAccept(record, accept)) added.push(assetId)\n }\n return { added, settled: !pending }\n}\n\n/**\n * THE batch-attach rule (EC-588, ratified 2i: N files → N uploads → N placements\n * APPENDED, in the order the files were given), for every surface that uploads into a\n * field — the field's own drop zone and the picker's Upload / drop. What to attach now,\n * and whether the batch is done (disarm):\n * - a `many` target attaches nothing until the batch settles, then every landed upload\n * the field accepts, in upload order ({@link uploadBatchProgress}'s `added`). The\n * field's drop used to attach each arrival as it landed, so a second file landing\n * before the first was appended first (measured on the base: `[a2, a1]`);\n * - a single target takes the first accepted arrival at once (the rest stay in the\n * library, where they are visible), and disarms when the batch ends with none.\n */\nexport function batchAttachment(\n progress: UploadBatchProgress,\n many: boolean,\n): { attach: string[]; disarm: boolean } {\n if (many) return progress.settled ? { attach: progress.added, disarm: true } : { attach: [], disarm: false }\n const first = progress.added[0]\n if (first !== undefined) return { attach: [first], disarm: true }\n return { attach: [], disarm: progress.settled }\n}\n\n/** The picker's commit callbacks — a pick fires `onChange` then closes the stage. */\nexport interface PickerCommit {\n onChange: (assetId: string | null) => void\n onOpenChange: (open: boolean) => void\n}\n\n/**\n * Commit a picked asset (or a clear, `assetId === null`) and close the picker —\n * the selection semantic (click = pick, fire `onChange`, close). Both the grid\n * card and the Clear affordance route through this.\n */\nexport function commitPickerSelection(assetId: string | null, { onChange, onOpenChange }: PickerCommit): void {\n onChange(assetId)\n onOpenChange(false)\n}\n\n/** The multi-commit callbacks (EC-460): a settled upload batch on a `many` field. */\nexport interface PickerCommitMany {\n onChangeMany: (assetIds: string[]) => void\n onOpenChange: (open: boolean) => void\n}\n\n/**\n * Commit EVERY asset a settled upload batch added (EC-460) and close the picker —\n * the `many`-field counterpart of {@link commitPickerSelection}, so selecting N\n * files in one picker interaction attaches all N, not just the first to land.\n */\nexport function commitPickerSelectionMany(\n assetIds: string[],\n { onChangeMany, onOpenChange }: PickerCommitMany,\n): void {\n onChangeMany(assetIds)\n onOpenChange(false)\n}\n\n/**\n * The picker props a field's value position needs, derived from the field alone\n * (EC-570 #1 → EC-588): the selected id, the field's structured `accept`, whether it\n * holds many, and the commits — a pick through the carry rule (EC-558), a settled\n * upload batch appended for a `many` field (EC-460).\n */\nexport interface AssetPickerBinding {\n value: string | null\n accept?: readonly string[]\n multiple: boolean\n onChange: (assetId: string | null) => void\n onChangeMany?: (assetIds: string[]) => void\n}\n\n/**\n * ONE picker configuration for every surface that edits an asset field — the detail\n * editor and the table cell — so the two cannot drift. Before, the cell opened the\n * picker with no `accept`, no `multiple` and no batch commit: its grid offered PDFs to\n * an image field and an N-file upload attached one (EC-570 #1). `write` receives the\n * next stored value; the caller decides how to land it (form state, cell commit).\n */\nexport function assetPickerBinding(\n field: Pick<AssetFieldDef, 'multiple' | 'accept' | 'fields'>,\n value: unknown,\n write: (next: unknown) => void,\n): AssetPickerBinding {\n const multiple = field.multiple\n return {\n value: multiple ? null : singleAssetIdOf(value),\n ...(field.accept ? { accept: field.accept } : {}),\n multiple,\n onChange: (picked) => write(applyAssetSelection(value, picked, multiple)),\n ...(multiple ? { onChangeMany: (picked: string[]) => write(appendAssetSelections(value, picked)) } : {}),\n }\n}\n"]}
1
+ {"version":3,"file":"asset-picker-core.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker-core.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssetFieldDef } from '@elytracms/core/cms-core'\nimport type { AssetReadLocale, AssetRecord } from '@elytracms/core/persistence'\nimport { assetMatchesAccept } from '@elytracms/core/cms-core'\nimport { filterAssets } from '../assets/asset-search'\nimport {\n appendAssetSelections,\n applyAssetSelection,\n singleAssetIdOf,\n} from '../assets/asset-selection'\n\n/**\n * Pure selection/filter core for the asset picker (EC-432). DOM-free and\n * side-effect-free so the media browser's \"what shows\", \"what an upload may send\" and\n * \"what a pick/upload commits\" are unit-testable without a browser — the picker\n * component is just the stage shell around these.\n *\n * EC-588: the picker takes the field's STRUCTURED `accept` list — the one shape the\n * schema declares — and derives everything from it here or in core: the grid filter\n * ({@link visiblePickerAssets}), the drop/upload split ({@link splitByAccept}), the\n * file-input attribute (`assetAcceptAttr`, core), and the commit filter\n * ({@link uploadBatchProgress}). An absent `accept` is unrestricted everywhere; there is\n * no hidden `image/*` default.\n */\n\n/**\n * The stage width preset the media browser mounts with (EC-432 / EC-431 AD-A):\n * the roomy `grid` measure (`min(1240px, 94vw)`) — browsing assets is spatial.\n * Exported so the geometry contract is pinnable without rendering the portal\n * (which is `null` in the node test env).\n */\nexport const ASSET_PICKER_STAGE_PRESET = 'grid' as const\n\n/**\n * The assets shown in the picker grid: the shipped search filter (EC-152) then\n * the `accept` filter (EC-432) — an image field's picker browses only images,\n * never the project's PDFs. Order follows the library.\n */\nexport function visiblePickerAssets(\n assets: readonly AssetRecord[],\n query: string,\n accept: readonly string[] | undefined,\n at: AssetReadLocale | null,\n): AssetRecord[] {\n return filterAssets(assets, query, at).filter((asset) => assetMatchesAccept(asset, accept))\n}\n\n/**\n * THE drop rule (EC-301 → EC-588): split files about to be uploaded into the ones the\n * field's `accept` admits and the NAMES of the ones it refuses. A drag-drop bypasses\n * the file dialog's soft filter, so every upload path — the field's drop zone, a drop\n * into the open picker, the picker's Upload dialog — passes through this, and the\n * caller names the refused files in a visible notice instead of uploading them.\n */\nexport function splitByAccept<F extends { name: string; type: string }>(\n files: readonly F[],\n accept: readonly string[] | undefined,\n): { accepted: F[]; rejected: string[] } {\n const accepted: F[] = []\n const rejected: string[] = []\n for (const file of files) {\n if (assetMatchesAccept({ contentType: file.type, filename: file.name }, accept)) accepted.push(file)\n else rejected.push(file.name)\n }\n return { accepted, rejected }\n}\n\n/**\n * One upload BATCH (EC-460): the upload-queue ids THIS interaction started.\n *\n * EC-588 A7: it used to be a pre-upload library snapshot, and whatever the library\n * gained since was committed — so an upload from another tab (re-listed with ours) or\n * from a sibling field's drop could land in this field. A batch now names exactly its\n * own uploads, and the store reports which asset each one landed as.\n */\nexport interface UploadBatch {\n uploadIds: readonly string[]\n}\n\n/** What batch progress reads from the assets store (`AssetsStore` satisfies it). */\nexport interface UploadSource {\n assets: readonly AssetRecord[]\n uploads: readonly { id: string; state: { phase: string } }[]\n /** The asset id an upload landed as, once its record is confirmed. */\n landedAssetId(uploadId: string): string | undefined\n}\n\nexport interface UploadBatchProgress {\n /** The batch's landed asset ids that the field accepts, in upload order. */\n added: string[]\n /**\n * The batch is finished: none of its uploads is still running, and every one that\n * landed is in the library. A failed (or dismissed) upload is terminal — it stays in\n * the queue for retry and is never awaited forever. Commit/attach decisions happen\n * only once settled for `many` targets; a single-select target commits on the first\n * arrival.\n */\n settled: boolean\n}\n\n/**\n * Where a batch stands. An upload is in one of four states: running (in the queue,\n * not failed), landed-but-not-listed (the store confirmed it, the library re-list has\n * not arrived), landed (attachable), or terminal (failed / dismissed). Only landed\n * uploads whose RECORD satisfies `accept` are `added` (A7: the commit is filtered,\n * not just the dialog). Pure — the picker and the asset field both drive their\n * effects with this.\n */\nexport function uploadBatchProgress(\n batch: UploadBatch,\n source: UploadSource,\n accept: readonly string[] | undefined,\n): UploadBatchProgress {\n const running = new Set(source.uploads.filter((u) => u.state.phase !== 'failed').map((u) => u.id))\n const library = new Map(source.assets.map((asset) => [asset.id, asset]))\n const added: string[] = []\n let pending = false\n for (const uploadId of batch.uploadIds) {\n if (running.has(uploadId)) {\n pending = true\n continue\n }\n const assetId = source.landedAssetId(uploadId)\n if (assetId === undefined) continue // failed or dismissed — terminal\n const record = library.get(assetId)\n if (!record) {\n pending = true // confirmed, but the library re-list has not landed yet\n continue\n }\n if (assetMatchesAccept(record, accept)) added.push(assetId)\n }\n return { added, settled: !pending }\n}\n\n/**\n * THE batch-attach rule (EC-588, ratified 2i: N files → N uploads → N placements\n * APPENDED, in the order the files were given), for every surface that uploads into a\n * field — the field's own drop zone and the picker's Upload / drop. What to attach now,\n * and whether the batch is done (disarm):\n * - a `many` target attaches nothing until the batch settles, then every landed upload\n * the field accepts, in upload order ({@link uploadBatchProgress}'s `added`). The\n * field's drop used to attach each arrival as it landed, so a second file landing\n * before the first was appended first (measured on the base: `[a2, a1]`);\n * - a single target takes the first accepted arrival at once (the rest stay in the\n * library, where they are visible), and disarms when the batch ends with none.\n */\nexport function batchAttachment(\n progress: UploadBatchProgress,\n many: boolean,\n): { attach: string[]; disarm: boolean } {\n if (many) return progress.settled ? { attach: progress.added, disarm: true } : { attach: [], disarm: false }\n const first = progress.added[0]\n if (first !== undefined) return { attach: [first], disarm: true }\n return { attach: [], disarm: progress.settled }\n}\n\n/** The picker's commit callbacks — a pick fires `onChange` then closes the stage. */\nexport interface PickerCommit {\n onChange: (assetId: string | null) => void\n onOpenChange: (open: boolean) => void\n}\n\n/**\n * Commit a picked asset (or a clear, `assetId === null`) and close the picker —\n * the selection semantic (click = pick, fire `onChange`, close). Both the grid\n * card and the Clear affordance route through this.\n */\nexport function commitPickerSelection(assetId: string | null, { onChange, onOpenChange }: PickerCommit): void {\n onChange(assetId)\n onOpenChange(false)\n}\n\n/** The multi-commit callbacks (EC-460): a settled upload batch on a `many` field. */\nexport interface PickerCommitMany {\n onChangeMany: (assetIds: string[]) => void\n onOpenChange: (open: boolean) => void\n}\n\n/**\n * Commit EVERY asset a settled upload batch added (EC-460) and close the picker —\n * the `many`-field counterpart of {@link commitPickerSelection}, so selecting N\n * files in one picker interaction attaches all N, not just the first to land.\n */\nexport function commitPickerSelectionMany(\n assetIds: string[],\n { onChangeMany, onOpenChange }: PickerCommitMany,\n): void {\n onChangeMany(assetIds)\n onOpenChange(false)\n}\n\n/**\n * The picker props a field's value position needs, derived from the field alone\n * (EC-570 #1 → EC-588): the selected id, the field's structured `accept`, whether it\n * holds many, and the commits — a pick through the carry rule (EC-558), a settled\n * upload batch appended for a `many` field (EC-460).\n */\nexport interface AssetPickerBinding {\n value: string | null\n accept?: readonly string[]\n multiple: boolean\n onChange: (assetId: string | null) => void\n onChangeMany?: (assetIds: string[]) => void\n}\n\n/**\n * ONE picker configuration for every surface that edits an asset field — the detail\n * editor and the table cell — so the two cannot drift. Before, the cell opened the\n * picker with no `accept`, no `multiple` and no batch commit: its grid offered PDFs to\n * an image field and an N-file upload attached one (EC-570 #1). `write` receives the\n * next stored value; the caller decides how to land it (form state, cell commit).\n */\nexport function assetPickerBinding(\n field: Pick<AssetFieldDef, 'multiple' | 'accept' | 'fields'>,\n value: unknown,\n write: (next: unknown) => void,\n): AssetPickerBinding {\n const multiple = field.multiple\n return {\n value: multiple ? null : singleAssetIdOf(value),\n ...(field.accept ? { accept: field.accept } : {}),\n multiple,\n onChange: (picked) => write(applyAssetSelection(value, picked, multiple)),\n ...(multiple ? { onChangeMany: (picked: string[]) => write(appendAssetSelections(value, picked)) } : {}),\n }\n}\n"]}
@@ -85,7 +85,9 @@ export function AssetPickerBrowser({ store, value, onOpenChange, onChange, onCha
85
85
  // child boundaries — the same per-element dragenter/dragleave flicker the field
86
86
  // fixes. Only hide when the count returns to zero.
87
87
  const dragDepth = useRef(0);
88
- const assets = visiblePickerAssets(store.assets, query, accept);
88
+ // EC-659: the search reads the alts the picker shows — through the studio's asset lens.
89
+ const searchLocale = useAssetReadLocale();
90
+ const assets = visiblePickerAssets(store.assets, query, accept, searchLocale);
89
91
  // Auto-select what an in-picker upload added, then close. A `many` consumer
90
92
  // (multiple + onChangeMany) waits for the batch to SETTLE and commits every
91
93
  // added id — uploading 2 files attaches 2 (EC-460); a single consumer keeps