@elytracms/studio 0.0.83 → 0.0.85

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 (44) hide show
  1. package/dist/components/builder/blocks-model.js +5 -2
  2. package/dist/components/builder/blocks-model.js.map +1 -1
  3. package/dist/components/builder/canvas-inline-editing.js +1 -1
  4. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  5. package/dist/components/builder/composition-field-editor.d.ts +48 -0
  6. package/dist/components/builder/composition-field-editor.js +94 -22
  7. package/dist/components/builder/composition-field-editor.js.map +1 -1
  8. package/dist/components/builder/inline-edit.js +3 -2
  9. package/dist/components/builder/inline-edit.js.map +1 -1
  10. package/dist/components/builder/node-factory.d.ts +4 -0
  11. package/dist/components/builder/node-factory.js +14 -9
  12. package/dist/components/builder/node-factory.js.map +1 -1
  13. package/dist/components/cms/cms-composition-validators.d.ts +7 -16
  14. package/dist/components/cms/cms-composition-validators.js +101 -52
  15. package/dist/components/cms/cms-composition-validators.js.map +1 -1
  16. package/dist/components/cms/gate-validation.js +3 -1
  17. package/dist/components/cms/gate-validation.js.map +1 -1
  18. package/dist/components/cms/rich-text/component-embed.d.ts +13 -7
  19. package/dist/components/cms/rich-text/component-embed.js +21 -13
  20. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  21. package/dist/components/cms/rich-text/editor.d.ts +1 -1
  22. package/dist/components/cms/rich-text/editor.js +2 -2
  23. package/dist/components/cms/rich-text/editor.js.map +1 -1
  24. package/dist/components/cms/rich-text/extensions.d.ts +4 -0
  25. package/dist/components/cms/rich-text/extensions.js +6 -0
  26. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  27. package/dist/components/cms/rich-text/insert.js +1 -1
  28. package/dist/components/cms/rich-text/insert.js.map +1 -1
  29. package/dist/components/cms/rich-text/unknown-node.d.ts +66 -0
  30. package/dist/components/cms/rich-text/unknown-node.js +101 -0
  31. package/dist/components/cms/rich-text/unknown-node.js.map +1 -0
  32. package/dist/components/cms/rich-text/value.d.ts +82 -13
  33. package/dist/components/cms/rich-text/value.js +169 -38
  34. package/dist/components/cms/rich-text/value.js.map +1 -1
  35. package/dist/components/cms/rich-text-preview.js +1 -1
  36. package/dist/components/cms/rich-text-preview.js.map +1 -1
  37. package/dist/lib/ai/rich-text-translate.js +3 -3
  38. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  39. package/dist/lib/i18n/messages.de.js +7 -3
  40. package/dist/lib/i18n/messages.de.js.map +1 -1
  41. package/dist/lib/i18n/messages.en.d.ts +4 -0
  42. package/dist/lib/i18n/messages.en.js +7 -3
  43. package/dist/lib/i18n/messages.en.js.map +1 -1
  44. package/package.json +2 -2
@@ -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 { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, blockRootsOf } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport { Button, Panel, Row, Spacer, Text, Toolbar, ToolbarGroup, ToolbarSeparator } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\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'\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()`). 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/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\n\n/**\n * Wrap the field value's blocks into a synthetic root container (EC-287). The\n * builder store edits this single root-bearing composition directly; its\n * children are the field's blocks, lifted back out via `onChange`.\n */\nfunction buildSyntheticRoot(value: unknown): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: blockRootsOf(value) },\n }\n}\n\n/** Unwrap the synthetic root's children back to a field value per cardinality. */\nfunction unwrapValue(blocks: readonly ComponentNode[], multiple: boolean): unknown {\n return multiple ? [...blocks] : blocks[0]\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\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 // Build the synthetic root ONCE from the value at mount. The editor is\n // uncontrolled thereafter — it is the source of truth while open and lifts\n // changes out via onChange; re-opening the tab remounts from the current draft.\n // (BuilderProvider reads `composition` only in its lazy initial state, so later\n // prop changes are ignored regardless.)\n const initialRoot = useMemo(() => buildSyntheticRoot(value), [])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift multiple={field.multiple} onChange={onChange} readOnly={readOnly} />\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 readOnly={readOnly} fieldAllow={field.allow} trailingPanes={trailingPanes} />\n </PropSyncProvider>\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the unwrapped value.\n */\nfunction CompositionLift({\n multiple,\n onChange,\n readOnly,\n}: {\n multiple: boolean\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\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 if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(unwrapValue(blocks, multiple))\n }\n }, [blocks, multiple, 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 fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\n /** The field's top-level component vocabulary (EC-188) — the leash applied to\n * inserts at the composition root so forbidden components aren't even offered. */\n fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\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 const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\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, fieldAllow),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, fieldAllow, 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 {/* 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'} />\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 { AlertTriangle, Plus, Redo2, SlidersHorizontal, Undo2 } from 'lucide-react'\nimport { DEFAULT_SLOT, isComponentBlockNode, isComponentNode } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport type { BlocksFieldDef } from '@elytracms/core/cms-core'\nimport { componentBlockNode, createBlockSequenceValue, isBlockSequenceValueShape } from '@elytracms/core/rich-text'\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 '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { BuilderProvider, useBuilder } from './builder-store'\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'\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/** The implicit root container the field's blocks compose inside (like a page root). */\nconst SYNTH_ROOT_COMPONENT = 'base.Stack'\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 interface PreservedBlockNode {\n node: BlockNode\n componentPrefix: number\n}\n\n/** A field value split for canvas editing: component roots + everything else. */\nexport interface SplitCompositionValue {\n roots: ComponentNode[]\n preserved: PreservedBlockNode[]\n}\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 */\nexport function splitCompositionValue(value: unknown): SplitCompositionValue {\n if (isBlockSequenceValueShape(value)) {\n const roots: ComponentNode[] = []\n const preserved: PreservedBlockNode[] = []\n for (const item of value.content) {\n if (isComponentBlockNode(item)) roots.push(item)\n // Preservation over parsing: an item the schema would reject still rides\n // along untouched (validation reports it; the canvas must not destroy it).\n else preserved.push({ node: item as BlockNode, componentPrefix: roots.length })\n }\n return { roots, preserved }\n }\n // EC-547 staging: dies at the tightening commit — legacy bare values (a\n // single ComponentNode for cardinality one, an array for many) are still\n // accepted on READ because fixtures haven't flipped; write-back is v2 always.\n if (Array.isArray(value)) return { roots: value.filter(isComponentNode), preserved: [] }\n return { roots: isComponentNode(value) ? [value] : [], preserved: [] }\n}\n\n/**\n * Merge canvas-edited component roots back into the stored sequence (EC-547).\n *\n * The merge rule, exactly: each preserved item re-enters the sequence after\n * `min(componentPrefix, roots.length)` component roots — it stays anchored to\n * its ORDINAL position between components, not to any component's identity.\n * Consequences, all deliberate:\n * - no canvas edit ⇒ the original sequence is reproduced exactly;\n * - reordering components on canvas reorders them WITHIN the component\n * positions while preserved items stay put between those positions;\n * - deleting components clamps now-out-of-range items to the sequence end;\n * - a component appended on canvas takes the NEXT component position; a\n * trailing typography item is anchored after all prior components, so the\n * appended component lands after it — anchors hold, positions fill.\n * Preserved items keep their relative order (their prefixes are non-decreasing\n * by construction in {@link splitCompositionValue}).\n *\n * Every component root is passed through `componentBlockNode`, so the write is\n * v2-native even when the roots were read from a legacy untagged value.\n */\nexport function mergeCompositionValue(\n roots: readonly ComponentNode[],\n preserved: readonly PreservedBlockNode[],\n): BlockSequenceValue {\n const content: BlockNode[] = []\n let next = 0\n for (let position = 0; position <= roots.length; position++) {\n while (\n next < preserved.length &&\n Math.min(preserved[next]!.componentPrefix, roots.length) === position\n ) {\n content.push(preserved[next]!.node)\n next += 1\n }\n if (position < roots.length) content.push(componentBlockNode(roots[position]!))\n }\n return createBlockSequenceValue(content)\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 */\nfunction buildSyntheticRoot(roots: readonly ComponentNode[]): ComponentNode {\n return {\n id: SYNTH_ROOT_ID,\n componentId: SYNTH_ROOT_COMPONENT,\n slots: { [DEFAULT_SLOT]: [...roots] },\n }\n}\n\nexport function CompositionFieldEditor({\n field,\n value,\n onChange,\n readOnly,\n trailingPanes,\n}: {\n field: BlocksFieldDef\n value: unknown\n onChange: (value: unknown) => void\n readOnly: boolean\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\n // editor is uncontrolled thereafter — it is the source of truth while open and\n // lifts changes out via onChange; re-opening the tab remounts from the current\n // draft. (BuilderProvider reads `composition` only in its lazy initial state,\n // so later prop changes are ignored regardless.)\n const initial = useMemo(() => splitCompositionValue(value), [])\n const initialRoot = useMemo(() => buildSyntheticRoot(initial.roots), [initial])\n\n return (\n <BuilderProvider composition={initialRoot}>\n <CompositionLift preserved={initial.preserved} onChange={onChange} readOnly={readOnly} />\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 readOnly={readOnly} fieldAllow={field.allow} trailingPanes={trailingPanes} />\n </PropSyncProvider>\n </ReuseProvider>\n </BuilderProvider>\n )\n}\n\n/**\n * Lifts committed edits out of the scoped store into the CMS draft. The first\n * render establishes the (container-normalized) baseline so simply opening the\n * tab never marks the document dirty; every subsequent change to the block list\n * emits the merged v2 sequence value (EC-547) — the canvas roots spliced back\n * between the preserved typography items. Cardinality (`multiple`) no longer\n * shapes the stored value: it is a validation constraint over the one envelope\n * (EC-500 D2), so the bare-object special case for `multiple: false` is gone.\n */\nfunction CompositionLift({\n preserved,\n onChange,\n readOnly,\n}: {\n preserved: readonly PreservedBlockNode[]\n onChange: (value: unknown) => void\n readOnly: boolean\n}) {\n const builder = useBuilder()\n const blocks = builder.present.slots?.[DEFAULT_SLOT] ?? EMPTY_BLOCKS\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n const baselineRef = useRef<readonly ComponentNode[] | null>(null)\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 if (baselineRef.current === null) {\n baselineRef.current = blocks\n return\n }\n if (blocks !== baselineRef.current) {\n baselineRef.current = blocks\n onChangeRef.current(mergeCompositionValue(blocks, preserved))\n }\n }, [blocks, preserved, 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 fieldAllow,\n trailingPanes,\n}: {\n readOnly: boolean\n /** The field's top-level component vocabulary (EC-188) — the leash applied to\n * inserts at the composition root so forbidden components aren't even offered. */\n fieldAllow?: readonly string[]\n /** Document-level panes appended after `Properties` (EC-224). */\n trailingPanes?: readonly InspectorPane[]\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 const ctx = resolveInsertContext(builder, nodeId, fieldAllow)\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, fieldAllow),\n openAt: (nodeId: string, position: InsertPosition) => {\n const ctx = resolveInsertContextAt(builder, nodeId, position, fieldAllow)\n if (!ctx) return\n if (nodeId !== builder.selectedId) dispatch({ type: 'select', id: nodeId })\n setInsertContext(ctx)\n setInsertOpen(true)\n },\n }),\n [builder, fieldAllow, 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 {/* 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'} />\n </div>\n <InspectorSidebar panes={panes} />\n </div>\n\n <InsertMenu context={insertContext} open={insertOpen} onOpenChange={setInsertOpen} />\n </div>\n </InsertAffordanceProvider>\n )\n}\n"]}
@@ -94,13 +94,14 @@ export function createInlineRichTextSession(args) {
94
94
  if (!active)
95
95
  return null;
96
96
  active = false;
97
- if (richTextDocsEquivalent(doc, args.original.doc))
97
+ if (richTextDocsEquivalent(doc, { type: 'doc', content: (args.original.content ?? []) }))
98
98
  return null;
99
99
  return {
100
100
  kind: 'commit',
101
101
  nodeId: args.nodeId,
102
102
  propName: args.propName,
103
- value: { version: args.original.version, doc },
103
+ // EC-547: the committed doc is render/edit-time; storage is the v2 sequence.
104
+ value: { version: args.original.version, content: (doc.content ?? []) },
104
105
  };
105
106
  },
106
107
  cancel() {
@@ -1 +1 @@
1
- {"version":3,"file":"inline-edit.js","sourceRoot":"","sources":["../../../src/components/builder/inline-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { inlineEditablePropNames } from '@elytracms/core/component-registry'\nimport { isStaticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { parseRichTextValue, richTextDocsEquivalent } from '@elytracms/core/rich-text'\nimport type { RichTextDoc, RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * Inline text editing on the canvas (EC-158) — framework-free core.\n *\n * The canvas wires DOM events (dblclick/keydown/blur) to this module; the\n * module owns the editing-session state machine and the\n * one-gesture-one-command guarantee. A session finishes exactly once: the\n * first `commit()`/`cancel()` wins and every later call returns `null`, so\n * overlapping DOM signals (Enter immediately followed by blur, blur racing a\n * teardown) can never dispatch a second operations command or stack a second\n * undo step.\n *\n * Locale semantics (EC-176): inline editing targets STATIC graph props only,\n * and static graph props are locale-independent by design — the canonical\n * graph stores one value per prop, not per-locale variants. Bound props are\n * never inline-editable (below), so fallback-shown BOUND text on the canvas\n * cannot be edited in place either: translating localized content happens in\n * the CMS surfaces, where the active-locale variant is created explicitly.\n * Nothing here needs the ambient locale.\n */\n\n/** The single graph mutation a finished session may produce. */\nexport interface InlineEditCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited text to write as the prop's static value. */\n value: string\n}\n\n/** A cancelled session: restore this text on the DOM, dispatch nothing. */\nexport interface InlineEditCancel {\n kind: 'cancel'\n /** The original rendered value to restore. */\n value: string\n}\n\nexport interface InlineEditSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: string\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing `text`. Returns the one commit outcome, or `null`\n * when the session already finished or the text is unchanged (an unchanged\n * commit finishes the session but dispatches nothing — graph untouched).\n */\n commit(text: string): InlineEditCommit | null\n /** Finish by cancelling. Returns the restore outcome once, then `null`. */\n cancel(): InlineEditCancel | null\n}\n\nexport function createInlineEditSession(args: {\n nodeId: string\n propName: string\n original: string\n}): InlineEditSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(text: string): InlineEditCommit | null {\n if (!active) return null\n active = false\n if (text === args.original) return null\n return { kind: 'commit', nodeId: args.nodeId, propName: args.propName, value: text }\n },\n cancel(): InlineEditCancel | null {\n if (!active) return null\n active = false\n return { kind: 'cancel', value: args.original }\n },\n }\n}\n\n/** A node prop that can be edited in place right now. */\nexport interface InlineEditableProp {\n propName: string\n /** The current static string value (or string default when unset). */\n value: string\n}\n\n/**\n * Resolve which prop of `node` an inline-edit gesture should target: the\n * first manifest prop flagged `inlineEditable` whose *current* value is a\n * plain static string (or unset with a string default). Bound props and\n * non-string values are never inline-editable — they stay inspector-only.\n */\nexport function resolveInlineEditableProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineEditableProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n const current = node.props?.[propName]\n if (current === undefined) {\n const fallback = manifest.props[propName]?.default\n if (typeof fallback === 'string') return { propName, value: fallback }\n continue\n }\n if (isStaticProp(current) && typeof current.value === 'string') {\n return { propName, value: current.value }\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text inline editing (EC-159) — the same session contract over the\n// `@elytracms/core/rich-text` storage format.\n// ---------------------------------------------------------------------------\n\n/** A node prop carrying rich-text content that can be edited in place now. */\nexport interface InlineRichTextProp {\n propName: string\n /** The current stored value (or the manifest default when unset). */\n value: RichTextValue\n}\n\n/**\n * Resolve which rich-text prop of `node` an inline-edit gesture should\n * target: the first manifest prop flagged `inlineEditable` whose inspector\n * control is `richtext` and whose *current* value is a static, schema-valid\n * rich-text value (or unset with a valid rich-text default). Bound props are\n * never inline-editable — translating/bound content stays inspector- and\n * CMS-owned, exactly like EC-158's plain-text rule.\n */\nexport function resolveInlineRichTextProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineRichTextProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n // EC-190: a rich-text prop is the `richText` field-def type (was control.kind 'richtext').\n if (manifest.props[propName]?.type !== 'richText') continue\n const current = node.props?.[propName]\n const raw = current === undefined ? manifest.props[propName]?.default : undefined\n if (current !== undefined && !isStaticProp(current)) continue\n const parsed = parseRichTextValue(isStaticProp(current) ? current.value : raw)\n if (parsed.ok) return { propName, value: parsed.value }\n }\n return null\n}\n\n/** The single graph mutation a finished rich-text session may produce. */\nexport interface InlineRichTextCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited content to write as the prop's static value. */\n value: RichTextValue\n}\n\nexport interface InlineRichTextSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: RichTextValue\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing the parsed-back document. Returns the one commit\n * outcome, or `null` when the session already finished or the document is\n * equivalent to the original. An equivalent commit finishes the session and\n * dispatches nothing — the stored value is never rewritten, so untouched\n * content round-trips byte-identical.\n */\n commit(doc: RichTextDoc): InlineRichTextCommit | null\n /** Finish by cancelling. Returns `true` exactly once (restore the DOM). */\n cancel(): boolean\n}\n\n/**\n * One rich-text editing session (EC-159), with EC-158's one-finish guarantee:\n * the first `commit()`/`cancel()` wins and every later call is inert, so\n * overlapping DOM signals can never dispatch a second operations command or\n * stack a second undo step. The committed value keeps the original's stored\n * schema `version` stamp — only the document tree changes.\n */\nexport function createInlineRichTextSession(args: {\n nodeId: string\n propName: string\n original: RichTextValue\n}): InlineRichTextSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(doc: RichTextDoc): InlineRichTextCommit | null {\n if (!active) return null\n active = false\n if (richTextDocsEquivalent(doc, args.original.doc)) return null\n return {\n kind: 'commit',\n nodeId: args.nodeId,\n propName: args.propName,\n value: { version: args.original.version, doc },\n }\n },\n cancel(): boolean {\n if (!active) return false\n active = false\n return true\n },\n }\n}\n\n/**\n * The structural slice of `Element` the host finder needs — kept minimal so\n * the descent logic is unit-testable without a DOM.\n */\nexport interface InlineHostElement {\n readonly childElementCount: number\n readonly firstElementChild: InlineHostElement | null\n readonly textContent: string | null\n getAttribute(name: string): string | null\n}\n\n/**\n * Find the rendered element inside a node's `.ec-editable` wrapper that hosts\n * the editable text. Descends single-child chains as long as no text is lost,\n * never crossing into another node's wrapper, and only accepts a host whose\n * text content is *exactly* the stored prop value — if the component renders\n * the prop transformed (or alongside other text), inline editing is skipped\n * and the inspector remains the editing surface. That exactness is what makes\n * edit-mode typography pixel-identical to render state: we edit the rendered\n * element itself.\n */\nexport function findInlineTextHost(\n wrapper: InlineHostElement,\n expected: string,\n): InlineHostElement | null {\n let el = wrapper.firstElementChild\n if (!el) return null\n if (el.getAttribute('data-ec-node-id') !== null) return null\n while (\n el.childElementCount === 1 &&\n el.firstElementChild!.getAttribute('data-ec-node-id') === null &&\n el.firstElementChild!.textContent === el.textContent\n ) {\n el = el.firstElementChild!\n }\n return (el.textContent ?? '') === expected ? el : null\n}\n"]}
1
+ {"version":3,"file":"inline-edit.js","sourceRoot":"","sources":["../../../src/components/builder/inline-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { inlineEditablePropNames } from '@elytracms/core/component-registry'\nimport { isStaticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { parseRichTextValue, richTextDocsEquivalent } from '@elytracms/core/rich-text'\nimport type { BlockNode, RichTextDoc, RichTextNode, RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * Inline text editing on the canvas (EC-158) — framework-free core.\n *\n * The canvas wires DOM events (dblclick/keydown/blur) to this module; the\n * module owns the editing-session state machine and the\n * one-gesture-one-command guarantee. A session finishes exactly once: the\n * first `commit()`/`cancel()` wins and every later call returns `null`, so\n * overlapping DOM signals (Enter immediately followed by blur, blur racing a\n * teardown) can never dispatch a second operations command or stack a second\n * undo step.\n *\n * Locale semantics (EC-176): inline editing targets STATIC graph props only,\n * and static graph props are locale-independent by design — the canonical\n * graph stores one value per prop, not per-locale variants. Bound props are\n * never inline-editable (below), so fallback-shown BOUND text on the canvas\n * cannot be edited in place either: translating localized content happens in\n * the CMS surfaces, where the active-locale variant is created explicitly.\n * Nothing here needs the ambient locale.\n */\n\n/** The single graph mutation a finished session may produce. */\nexport interface InlineEditCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited text to write as the prop's static value. */\n value: string\n}\n\n/** A cancelled session: restore this text on the DOM, dispatch nothing. */\nexport interface InlineEditCancel {\n kind: 'cancel'\n /** The original rendered value to restore. */\n value: string\n}\n\nexport interface InlineEditSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: string\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing `text`. Returns the one commit outcome, or `null`\n * when the session already finished or the text is unchanged (an unchanged\n * commit finishes the session but dispatches nothing — graph untouched).\n */\n commit(text: string): InlineEditCommit | null\n /** Finish by cancelling. Returns the restore outcome once, then `null`. */\n cancel(): InlineEditCancel | null\n}\n\nexport function createInlineEditSession(args: {\n nodeId: string\n propName: string\n original: string\n}): InlineEditSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(text: string): InlineEditCommit | null {\n if (!active) return null\n active = false\n if (text === args.original) return null\n return { kind: 'commit', nodeId: args.nodeId, propName: args.propName, value: text }\n },\n cancel(): InlineEditCancel | null {\n if (!active) return null\n active = false\n return { kind: 'cancel', value: args.original }\n },\n }\n}\n\n/** A node prop that can be edited in place right now. */\nexport interface InlineEditableProp {\n propName: string\n /** The current static string value (or string default when unset). */\n value: string\n}\n\n/**\n * Resolve which prop of `node` an inline-edit gesture should target: the\n * first manifest prop flagged `inlineEditable` whose *current* value is a\n * plain static string (or unset with a string default). Bound props and\n * non-string values are never inline-editable — they stay inspector-only.\n */\nexport function resolveInlineEditableProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineEditableProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n const current = node.props?.[propName]\n if (current === undefined) {\n const fallback = manifest.props[propName]?.default\n if (typeof fallback === 'string') return { propName, value: fallback }\n continue\n }\n if (isStaticProp(current) && typeof current.value === 'string') {\n return { propName, value: current.value }\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text inline editing (EC-159) — the same session contract over the\n// `@elytracms/core/rich-text` storage format.\n// ---------------------------------------------------------------------------\n\n/** A node prop carrying rich-text content that can be edited in place now. */\nexport interface InlineRichTextProp {\n propName: string\n /** The current stored value (or the manifest default when unset). */\n value: RichTextValue\n}\n\n/**\n * Resolve which rich-text prop of `node` an inline-edit gesture should\n * target: the first manifest prop flagged `inlineEditable` whose inspector\n * control is `richtext` and whose *current* value is a static, schema-valid\n * rich-text value (or unset with a valid rich-text default). Bound props are\n * never inline-editable — translating/bound content stays inspector- and\n * CMS-owned, exactly like EC-158's plain-text rule.\n */\nexport function resolveInlineRichTextProp(\n manifest: ComponentManifest | undefined,\n node: ComponentNode,\n): InlineRichTextProp | null {\n if (!manifest) return null\n for (const propName of inlineEditablePropNames(manifest)) {\n // EC-190: a rich-text prop is the `richText` field-def type (was control.kind 'richtext').\n if (manifest.props[propName]?.type !== 'richText') continue\n const current = node.props?.[propName]\n const raw = current === undefined ? manifest.props[propName]?.default : undefined\n if (current !== undefined && !isStaticProp(current)) continue\n const parsed = parseRichTextValue(isStaticProp(current) ? current.value : raw)\n if (parsed.ok) return { propName, value: parsed.value }\n }\n return null\n}\n\n/** The single graph mutation a finished rich-text session may produce. */\nexport interface InlineRichTextCommit {\n kind: 'commit'\n nodeId: string\n propName: string\n /** The edited content to write as the prop's static value. */\n value: RichTextValue\n}\n\nexport interface InlineRichTextSession {\n readonly nodeId: string\n readonly propName: string\n readonly original: RichTextValue\n /** False once the session committed or cancelled. */\n readonly active: boolean\n /**\n * Finish by committing the parsed-back document. Returns the one commit\n * outcome, or `null` when the session already finished or the document is\n * equivalent to the original. An equivalent commit finishes the session and\n * dispatches nothing — the stored value is never rewritten, so untouched\n * content round-trips byte-identical.\n */\n commit(doc: RichTextDoc): InlineRichTextCommit | null\n /** Finish by cancelling. Returns `true` exactly once (restore the DOM). */\n cancel(): boolean\n}\n\n/**\n * One rich-text editing session (EC-159), with EC-158's one-finish guarantee:\n * the first `commit()`/`cancel()` wins and every later call is inert, so\n * overlapping DOM signals can never dispatch a second operations command or\n * stack a second undo step. The committed value keeps the original's stored\n * schema `version` stamp — only the document tree changes.\n */\nexport function createInlineRichTextSession(args: {\n nodeId: string\n propName: string\n original: RichTextValue\n}): InlineRichTextSession {\n let active = true\n return {\n nodeId: args.nodeId,\n propName: args.propName,\n original: args.original,\n get active() {\n return active\n },\n commit(doc: RichTextDoc): InlineRichTextCommit | null {\n if (!active) return null\n active = false\n if (richTextDocsEquivalent(doc, { type: 'doc', content: (args.original.content ?? []) as RichTextNode[] })) return null\n return {\n kind: 'commit',\n nodeId: args.nodeId,\n propName: args.propName,\n // EC-547: the committed doc is render/edit-time; storage is the v2 sequence.\n value: { version: args.original.version, content: (doc.content ?? []) as BlockNode[] },\n }\n },\n cancel(): boolean {\n if (!active) return false\n active = false\n return true\n },\n }\n}\n\n/**\n * The structural slice of `Element` the host finder needs — kept minimal so\n * the descent logic is unit-testable without a DOM.\n */\nexport interface InlineHostElement {\n readonly childElementCount: number\n readonly firstElementChild: InlineHostElement | null\n readonly textContent: string | null\n getAttribute(name: string): string | null\n}\n\n/**\n * Find the rendered element inside a node's `.ec-editable` wrapper that hosts\n * the editable text. Descends single-child chains as long as no text is lost,\n * never crossing into another node's wrapper, and only accepts a host whose\n * text content is *exactly* the stored prop value — if the component renders\n * the prop transformed (or alongside other text), inline editing is skipped\n * and the inspector remains the editing surface. That exactness is what makes\n * edit-mode typography pixel-identical to render state: we edit the rendered\n * element itself.\n */\nexport function findInlineTextHost(\n wrapper: InlineHostElement,\n expected: string,\n): InlineHostElement | null {\n let el = wrapper.firstElementChild\n if (!el) return null\n if (el.getAttribute('data-ec-node-id') !== null) return null\n while (\n el.childElementCount === 1 &&\n el.firstElementChild!.getAttribute('data-ec-node-id') === null &&\n el.firstElementChild!.textContent === el.textContent\n ) {\n el = el.firstElementChild!\n }\n return (el.textContent ?? '') === expected ? el : null\n}\n"]}
@@ -5,6 +5,10 @@ import type { ComponentManifest } from '@elytracms/core/component-registry';
5
5
  * EC-066). The placeholder id is reassigned by the Builder store on insert.
6
6
  * Required slots are left empty so the renderer shows a missing-slot placeholder
7
7
  * (EC-063) prompting the author to fill them.
8
+ *
9
+ * EC-547 (EC-500 D1): every factory node goes through `componentBlockNode`, so
10
+ * it carries the `elytra/component` discriminator from birth — a newly created
11
+ * canvas node is a v2 block-sequence citizen without any tagging on write.
8
12
  */
9
13
  export declare function nodeFromManifest(manifest: ComponentManifest): ComponentNode;
10
14
  /** Layout helper presets (brief §8.2, EC-067) — grid / flex / stack / repeater / switch. */
@@ -1,9 +1,14 @@
1
1
  import { isLayoutContainerId, staticProp } from '@elytracms/core/project-graph';
2
+ import { componentBlockNode } from '@elytracms/core/rich-text';
2
3
  /**
3
4
  * Build a fresh node for a component from its manifest defaults (brief §8.2,
4
5
  * EC-066). The placeholder id is reassigned by the Builder store on insert.
5
6
  * Required slots are left empty so the renderer shows a missing-slot placeholder
6
7
  * (EC-063) prompting the author to fill them.
8
+ *
9
+ * EC-547 (EC-500 D1): every factory node goes through `componentBlockNode`, so
10
+ * it carries the `elytra/component` discriminator from birth — a newly created
11
+ * canvas node is a v2 block-sequence citizen without any tagging on write.
7
12
  */
8
13
  export function nodeFromManifest(manifest) {
9
14
  const props = {};
@@ -11,17 +16,17 @@ export function nodeFromManifest(manifest) {
11
16
  if (spec.default !== undefined)
12
17
  props[name] = staticProp(spec.default);
13
18
  }
14
- return { id: 'tmp', componentId: manifest.id, props };
19
+ return componentBlockNode({ id: 'tmp', componentId: manifest.id, props });
15
20
  }
16
21
  function text(value) {
17
- return { id: 'tmp', componentId: 'base.Text', props: { value: staticProp(value) } };
22
+ return componentBlockNode({ id: 'tmp', componentId: 'base.Text', props: { value: staticProp(value) } });
18
23
  }
19
24
  export const LAYOUT_PRESETS = [
20
25
  {
21
26
  key: 'stack',
22
27
  label: 'Stack',
23
28
  description: 'Vertical flex layout',
24
- build: () => ({
29
+ build: () => componentBlockNode({
25
30
  id: 'tmp',
26
31
  componentId: 'base.Stack',
27
32
  props: { gap: staticProp(4) },
@@ -32,7 +37,7 @@ export const LAYOUT_PRESETS = [
32
37
  key: 'grid',
33
38
  label: 'Grid',
34
39
  description: 'Multi-column grid',
35
- build: () => ({
40
+ build: () => componentBlockNode({
36
41
  id: 'tmp',
37
42
  componentId: 'base.Grid',
38
43
  props: { columns: staticProp(2), gap: staticProp(4) },
@@ -43,7 +48,7 @@ export const LAYOUT_PRESETS = [
43
48
  key: 'container',
44
49
  label: 'Container',
45
50
  description: 'Generic block wrapper',
46
- build: () => ({
51
+ build: () => componentBlockNode({
47
52
  id: 'tmp',
48
53
  componentId: 'base.Container',
49
54
  slots: { children: [text('Container content')] },
@@ -53,7 +58,7 @@ export const LAYOUT_PRESETS = [
53
58
  key: 'repeater',
54
59
  label: 'Repeater',
55
60
  description: 'Repeat a template over a bound list',
56
- build: () => ({
61
+ build: () => componentBlockNode({
57
62
  id: 'tmp',
58
63
  componentId: 'base.Repeater',
59
64
  // Inserts with an empty list and an unbound template — the studio never
@@ -63,12 +68,12 @@ export const LAYOUT_PRESETS = [
63
68
  props: { items: staticProp([]) },
64
69
  slots: {
65
70
  item: [
66
- {
71
+ componentBlockNode({
67
72
  id: 'tmp',
68
73
  componentId: 'base.Stack',
69
74
  props: { gap: staticProp(1) },
70
75
  slots: { children: [text('Item title'), text('Item body')] },
71
- },
76
+ }),
72
77
  ],
73
78
  },
74
79
  }),
@@ -77,7 +82,7 @@ export const LAYOUT_PRESETS = [
77
82
  key: 'switch',
78
83
  label: 'Switch',
79
84
  description: 'Render one case by its key',
80
- build: () => ({
85
+ build: () => componentBlockNode({
81
86
  id: 'tmp',
82
87
  componentId: 'base.Switch',
83
88
  // Start with the declared `fallback` slot (renders until a `case` matches).
@@ -1 +1 @@
1
- {"version":3,"file":"node-factory.js","sourceRoot":"","sources":["../../../src/components/builder/node-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { isLayoutContainerId, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\n\n/**\n * Build a fresh node for a component from its manifest defaults (brief §8.2,\n * EC-066). The placeholder id is reassigned by the Builder store on insert.\n * Required slots are left empty so the renderer shows a missing-slot placeholder\n * (EC-063) prompting the author to fill them.\n */\nexport function nodeFromManifest(manifest: ComponentManifest): ComponentNode {\n const props: ComponentNode['props'] = {}\n for (const [name, spec] of Object.entries(manifest.props)) {\n if (spec.default !== undefined) props[name] = staticProp(spec.default as JsonValue)\n }\n return { id: 'tmp', componentId: manifest.id, props }\n}\n\nfunction text(value: string): ComponentNode {\n return { id: 'tmp', componentId: 'base.Text', props: { value: staticProp(value) } }\n}\n\n\n/** Layout helper presets (brief §8.2, EC-067) — grid / flex / stack / repeater / switch. */\nexport interface LayoutPreset {\n key: string\n label: string\n description: string\n build: () => ComponentNode\n}\n\nexport const LAYOUT_PRESETS: readonly LayoutPreset[] = [\n {\n key: 'stack',\n label: 'Stack',\n description: 'Vertical flex layout',\n build: () => ({\n id: 'tmp',\n componentId: 'base.Stack',\n props: { gap: staticProp(4) },\n slots: { children: [text('First item'), text('Second item')] },\n }),\n },\n {\n key: 'grid',\n label: 'Grid',\n description: 'Multi-column grid',\n build: () => ({\n id: 'tmp',\n componentId: 'base.Grid',\n props: { columns: staticProp(2), gap: staticProp(4) },\n slots: { children: [text('Cell one'), text('Cell two')] },\n }),\n },\n {\n key: 'container',\n label: 'Container',\n description: 'Generic block wrapper',\n build: () => ({\n id: 'tmp',\n componentId: 'base.Container',\n slots: { children: [text('Container content')] },\n }),\n },\n {\n key: 'repeater',\n label: 'Repeater',\n description: 'Repeat a template over a bound list',\n build: () => ({\n id: 'tmp',\n componentId: 'base.Repeater',\n // Inserts with an empty list and an unbound template — the studio never\n // fabricates data (Oli, 2026-06-19). The author binds `items` to a real list\n // source in the inspector; until then it renders nothing. (Repeater's binding\n // story is parked anyway — see RepeaterManifest \"slice 5 decides its fate\".)\n props: { items: staticProp([]) },\n slots: {\n item: [\n {\n id: 'tmp',\n componentId: 'base.Stack',\n props: { gap: staticProp(1) },\n slots: { children: [text('Item title'), text('Item body')] },\n },\n ],\n },\n }),\n },\n {\n key: 'switch',\n label: 'Switch',\n description: 'Render one case by its key',\n build: () => ({\n id: 'tmp',\n componentId: 'base.Switch',\n // Start with the declared `fallback` slot (renders until a `case` matches).\n // `case` + `case:<key>` slots are authored via the inspector. Keeping only\n // declared slots here means the inserted node validates cleanly.\n slots: { fallback: [text('Fallback')] },\n }),\n },\n]\n\n/**\n * Build a fresh node for insertion, seeding layout containers so they survive\n * (EC-383). A bare `Stack`/`Grid` inserts empty, and EC-161 normalization\n * dissolves BOTH empty and single-child containers on commit — so a bare\n * manifest-default container insert vanishes instantly and reads as a broken\n * insert. Seeding a layout container exactly like its `LAYOUT_PRESETS` sibling\n * (≥2 children) gives it visible, survivable content, so the registry \"Default\"\n * tile behaves like the preset instead of disappearing. `base.Container` is NOT\n * a normalized layout container, so its bare default survives untouched; every\n * non-container manifest is returned unchanged (bare defaults, EC-066).\n */\nexport function seededNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n if (!isLayoutContainerId(manifest.id)) return nodeFromManifest(manifest)\n const preset = LAYOUT_PRESETS.find((p) => p.build().componentId === manifest.id)\n return preset ? preset.build() : nodeFromManifest(manifest)\n}\n"]}
1
+ {"version":3,"file":"node-factory.js","sourceRoot":"","sources":["../../../src/components/builder/node-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { isLayoutContainerId, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { componentBlockNode } from '@elytracms/core/rich-text'\n\n/**\n * Build a fresh node for a component from its manifest defaults (brief §8.2,\n * EC-066). The placeholder id is reassigned by the Builder store on insert.\n * Required slots are left empty so the renderer shows a missing-slot placeholder\n * (EC-063) prompting the author to fill them.\n *\n * EC-547 (EC-500 D1): every factory node goes through `componentBlockNode`, so\n * it carries the `elytra/component` discriminator from birth — a newly created\n * canvas node is a v2 block-sequence citizen without any tagging on write.\n */\nexport function nodeFromManifest(manifest: ComponentManifest): ComponentNode {\n const props: ComponentNode['props'] = {}\n for (const [name, spec] of Object.entries(manifest.props)) {\n if (spec.default !== undefined) props[name] = staticProp(spec.default as JsonValue)\n }\n return componentBlockNode({ id: 'tmp', componentId: manifest.id, props })\n}\n\nfunction text(value: string): ComponentNode {\n return componentBlockNode({ id: 'tmp', componentId: 'base.Text', props: { value: staticProp(value) } })\n}\n\n\n/** Layout helper presets (brief §8.2, EC-067) — grid / flex / stack / repeater / switch. */\nexport interface LayoutPreset {\n key: string\n label: string\n description: string\n build: () => ComponentNode\n}\n\nexport const LAYOUT_PRESETS: readonly LayoutPreset[] = [\n {\n key: 'stack',\n label: 'Stack',\n description: 'Vertical flex layout',\n build: () =>\n componentBlockNode({\n id: 'tmp',\n componentId: 'base.Stack',\n props: { gap: staticProp(4) },\n slots: { children: [text('First item'), text('Second item')] },\n }),\n },\n {\n key: 'grid',\n label: 'Grid',\n description: 'Multi-column grid',\n build: () =>\n componentBlockNode({\n id: 'tmp',\n componentId: 'base.Grid',\n props: { columns: staticProp(2), gap: staticProp(4) },\n slots: { children: [text('Cell one'), text('Cell two')] },\n }),\n },\n {\n key: 'container',\n label: 'Container',\n description: 'Generic block wrapper',\n build: () =>\n componentBlockNode({\n id: 'tmp',\n componentId: 'base.Container',\n slots: { children: [text('Container content')] },\n }),\n },\n {\n key: 'repeater',\n label: 'Repeater',\n description: 'Repeat a template over a bound list',\n build: () =>\n componentBlockNode({\n id: 'tmp',\n componentId: 'base.Repeater',\n // Inserts with an empty list and an unbound template — the studio never\n // fabricates data (Oli, 2026-06-19). The author binds `items` to a real list\n // source in the inspector; until then it renders nothing. (Repeater's binding\n // story is parked anyway — see RepeaterManifest \"slice 5 decides its fate\".)\n props: { items: staticProp([]) },\n slots: {\n item: [\n componentBlockNode({\n id: 'tmp',\n componentId: 'base.Stack',\n props: { gap: staticProp(1) },\n slots: { children: [text('Item title'), text('Item body')] },\n }),\n ],\n },\n }),\n },\n {\n key: 'switch',\n label: 'Switch',\n description: 'Render one case by its key',\n build: () =>\n componentBlockNode({\n id: 'tmp',\n componentId: 'base.Switch',\n // Start with the declared `fallback` slot (renders until a `case` matches).\n // `case` + `case:<key>` slots are authored via the inspector. Keeping only\n // declared slots here means the inserted node validates cleanly.\n slots: { fallback: [text('Fallback')] },\n }),\n },\n]\n\n/**\n * Build a fresh node for insertion, seeding layout containers so they survive\n * (EC-383). A bare `Stack`/`Grid` inserts empty, and EC-161 normalization\n * dissolves BOTH empty and single-child containers on commit — so a bare\n * manifest-default container insert vanishes instantly and reads as a broken\n * insert. Seeding a layout container exactly like its `LAYOUT_PRESETS` sibling\n * (≥2 children) gives it visible, survivable content, so the registry \"Default\"\n * tile behaves like the preset instead of disappearing. `base.Container` is NOT\n * a normalized layout container, so its bare default survives untouched; every\n * non-container manifest is returned unchanged (bare defaults, EC-066).\n */\nexport function seededNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n if (!isLayoutContainerId(manifest.id)) return nodeFromManifest(manifest)\n const preset = LAYOUT_PRESETS.find((p) => p.build().componentId === manifest.id)\n return preset ? preset.build() : nodeFromManifest(manifest)\n}\n"]}
@@ -1,25 +1,16 @@
1
1
  import type { BlocksFieldDef, CompositionIssue, RichTextFieldDef } from '@elytracms/core/cms-core';
2
- /**
3
- * Composition validator adapters (EC-326, extracted from `cms-store.tsx`) —
4
- * injected into `validateDocument`'s `validateComposition`/`validateRichTextEmbeds`
5
- * options so `@elytracms/core/cms-core` stays free of `@elytracms/core/project-graph`
6
- * and `@elytracms/core/rich-text`.
7
- */
8
2
  /**
9
3
  * EC-188: validate a `blocks` composition field value against the field's `allow`
10
4
  * vocabulary + the code-owned component registry (the leash), mapping graph
11
- * issues into plain CMS findings. Never throws — a malformed (non-node)
12
- * value degrades to a single finding, matching the "validation is first-class,
13
- * never a crash" rule.
5
+ * issues into plain CMS findings. Never throws — a malformed value degrades to
6
+ * a single finding, matching the "validation is first-class, never a crash"
7
+ * rule.
14
8
  */
15
9
  export declare function validateComposition(value: unknown, field: BlocksFieldDef): CompositionIssue[];
16
10
  /**
17
- * EC-189: validate the composition embeds inside a `richText` field value. Each
18
- * `componentEmbed` holds a single `ComponentNode` constrained by the field's
19
- * `allow` vocabulary (the SAME leash a `blocks` field uses). Parse the value, walk
20
- * its embeds at any depth, and validate each embedded node via
21
- * `validateCompositionValue`; findings carry the rich-text node path joined with
22
- * the in-embed graph path. Never throws — a malformed embed degrades to a
23
- * single finding.
11
+ * EC-189: validate the composition nodes inside a `richText` field value with
12
+ * the SAME leash a `blocks` field uses — since EC-547 literally the same walk
13
+ * over the same shape: the value's `elytra/component` nodes ARE sequence
14
+ * citizens, at the top level and nested in typography content.
24
15
  */
25
16
  export declare function validateRichTextEmbeds(value: unknown, field: RichTextFieldDef): CompositionIssue[];
@@ -1,75 +1,124 @@
1
1
  import { validateCompositionValue } from '@elytracms/core/project-graph';
2
- import { collectComponentEmbeds, parseRichTextValue } from '@elytracms/core/rich-text';
2
+ import { BLOCK_SEQUENCE_VERSION, classifyBlockNode, isBlockSequenceValueShape, parseBlockSequenceValue, } from '@elytracms/core/rich-text';
3
3
  import { getStudioRegistry } from '@elytracms/studio/lib/registry';
4
4
  /**
5
5
  * Composition validator adapters (EC-326, extracted from `cms-store.tsx`) —
6
6
  * injected into `validateDocument`'s `validateComposition`/`validateRichTextEmbeds`
7
7
  * options so `@elytracms/core/cms-core` stays free of `@elytracms/core/project-graph`
8
8
  * and `@elytracms/core/rich-text`.
9
+ *
10
+ * EC-547 (EC-500 D1/D4): both field types now share ONE stored shape — the v2
11
+ * block sequence `{ version: 2, content: BlockNode[] }` — and therefore ONE
12
+ * validation walk: unwrap the envelope, validate the sequence's component nodes
13
+ * (top-level roots AND nodes nested inside typography content) against the
14
+ * field's `allow` vocabulary + the code-owned component registry. "`allow`
15
+ * means component ids insertable at this position" is now literally the same
16
+ * check on the same shape for `blocks` and `richText`.
17
+ *
18
+ * Pre-convergence shapes (the v1 `{ version, doc }` envelope, bare node lists)
19
+ * never reach these adapters: `validateDocument`'s shape pass reports them as
20
+ * the explicit pre-convergence issue and skips the deeper validators (EC-548
21
+ * removed the legacy walks). A retired `componentEmbed` node riding in v2
22
+ * content is a foreign typography node — not rendered, not validated, surfaced
23
+ * by the renderer's unknown-node fallback (render/validate parity, EC-497).
9
24
  */
25
+ /**
26
+ * Validate the component nodes of a v2 block sequence against the field leash.
27
+ *
28
+ * - Top-level component roots validate in ONE `validateCompositionValue` call,
29
+ * so cross-root checks (duplicate node ids) see the whole sequence. Their
30
+ * `root/<k>` issue paths are re-keyed to the node's actual sequence index
31
+ * (`content/<i>/…`).
32
+ * - A component nested in typography content validates per-node with the SAME
33
+ * leash, its findings prefixed with the typography path
34
+ * (`content/<i>/content/<j>/…`).
35
+ * - A malformed value degrades to a single finding, never a crash.
36
+ */
37
+ function validateBlockSequence(value, allow) {
38
+ // Structural gate FIRST: only the stored `{ version: 2, content }` object is a
39
+ // sequence here. The parser alone would also accept a JSON-string encoding of
40
+ // one — which no delivery reader accepts — and validating that clean would be
41
+ // the invisible divergence EC-390 exists to prevent.
42
+ if (!isBlockSequenceValueShape(value)) {
43
+ return [
44
+ {
45
+ message: `is not a valid block sequence (version ${BLOCK_SEQUENCE_VERSION})`,
46
+ severity: 'error',
47
+ },
48
+ ];
49
+ }
50
+ const parsed = parseBlockSequenceValue(value);
51
+ if (!parsed.ok) {
52
+ return [
53
+ {
54
+ message: `is not a valid block sequence (version ${BLOCK_SEQUENCE_VERSION})`,
55
+ severity: 'error',
56
+ },
57
+ ];
58
+ }
59
+ const components = getStudioRegistry().registry.lookup;
60
+ const opts = { ...(allow ? { allow } : {}), components };
61
+ const out = [];
62
+ /** `root/0/<rest>` from a single-node validation call → `<base>/<rest>`. */
63
+ const nestedPath = (base, path) => {
64
+ const rest = path.slice(2);
65
+ return rest.length > 0 ? `${base}/${rest.join('/')}` : base;
66
+ };
67
+ const walkTypography = (nodes, base) => {
68
+ nodes?.forEach((child, i) => {
69
+ const here = `${base}/content/${i}`;
70
+ const kind = classifyBlockNode(child);
71
+ if (kind.kind === 'component') {
72
+ for (const issue of validateCompositionValue(kind.node, opts)) {
73
+ out.push({ message: issue.message, severity: issue.severity, nodePath: nestedPath(here, issue.path) });
74
+ }
75
+ }
76
+ else {
77
+ walkTypography(kind.node.content, here);
78
+ }
79
+ });
80
+ };
81
+ const roots = [];
82
+ parsed.value.content.forEach((node, index) => {
83
+ const kind = classifyBlockNode(node);
84
+ if (kind.kind === 'component')
85
+ roots.push({ node: kind.node, index });
86
+ else
87
+ walkTypography(kind.node.content, `content/${index}`);
88
+ });
89
+ for (const issue of validateCompositionValue(roots.map((r) => r.node), opts)) {
90
+ // path is `['root', <k>, ...rest]` — re-key <k> to the sequence index.
91
+ const [, k, ...rest] = issue.path;
92
+ const index = typeof k === 'number' ? (roots[k]?.index ?? k) : k;
93
+ out.push({
94
+ message: issue.message,
95
+ severity: issue.severity,
96
+ nodePath: ['content', ...(index === undefined ? [] : [index]), ...rest].join('/'),
97
+ });
98
+ }
99
+ return out;
100
+ }
10
101
  /**
11
102
  * EC-188: validate a `blocks` composition field value against the field's `allow`
12
103
  * vocabulary + the code-owned component registry (the leash), mapping graph
13
- * issues into plain CMS findings. Never throws — a malformed (non-node)
14
- * value degrades to a single finding, matching the "validation is first-class,
15
- * never a crash" rule.
104
+ * issues into plain CMS findings. Never throws — a malformed value degrades to
105
+ * a single finding, matching the "validation is first-class, never a crash"
106
+ * rule.
16
107
  */
17
108
  export function validateComposition(value, field) {
18
109
  if (value == null)
19
110
  return [];
20
- const roots = Array.isArray(value) ? value : [value];
21
- if (!roots.every((r) => r != null && typeof r === 'object' && 'componentId' in r)) {
22
- return [{ message: 'is not a valid composition (expected component nodes)', severity: 'error' }];
23
- }
24
- const components = getStudioRegistry().registry.lookup;
25
- return validateCompositionValue(value, {
26
- allow: field.allow,
27
- components,
28
- }).map((issue) => ({
29
- message: issue.message,
30
- severity: issue.severity,
31
- nodePath: issue.path.join('/'),
32
- }));
111
+ return validateBlockSequence(value, field.allow);
33
112
  }
34
113
  /**
35
- * EC-189: validate the composition embeds inside a `richText` field value. Each
36
- * `componentEmbed` holds a single `ComponentNode` constrained by the field's
37
- * `allow` vocabulary (the SAME leash a `blocks` field uses). Parse the value, walk
38
- * its embeds at any depth, and validate each embedded node via
39
- * `validateCompositionValue`; findings carry the rich-text node path joined with
40
- * the in-embed graph path. Never throws — a malformed embed degrades to a
41
- * single finding.
114
+ * EC-189: validate the composition nodes inside a `richText` field value with
115
+ * the SAME leash a `blocks` field uses — since EC-547 literally the same walk
116
+ * over the same shape: the value's `elytra/component` nodes ARE sequence
117
+ * citizens, at the top level and nested in typography content.
42
118
  */
43
119
  export function validateRichTextEmbeds(value, field) {
44
120
  if (value == null)
45
121
  return [];
46
- const parsed = parseRichTextValue(value);
47
- // A plain-string / schema-invalid rich-text value carries no embeds to check
48
- // here (scalar type validation owns reporting a malformed value).
49
- if (!parsed.ok)
50
- return [];
51
- const components = getStudioRegistry().registry.lookup;
52
- const out = [];
53
- for (const occurrence of collectComponentEmbeds(parsed.value.doc)) {
54
- if (!occurrence.embedded) {
55
- out.push({
56
- message: `embed at ${occurrence.nodePath} is missing a valid embedded component`,
57
- severity: 'error',
58
- nodePath: occurrence.nodePath,
59
- });
60
- continue;
61
- }
62
- for (const issue of validateCompositionValue(occurrence.embedded, {
63
- allow: field.allow,
64
- components,
65
- })) {
66
- out.push({
67
- message: issue.message,
68
- severity: issue.severity,
69
- nodePath: `${occurrence.nodePath}/attrs/node/${issue.path.join('/')}`,
70
- });
71
- }
72
- }
73
- return out;
122
+ return validateBlockSequence(value, field.allow);
74
123
  }
75
124
  //# sourceMappingURL=cms-composition-validators.js.map