@elytracms/studio 0.0.104 → 0.0.106

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 (59) hide show
  1. package/dist/components/app-views/module.d.ts +4 -0
  2. package/dist/components/app-views/module.js.map +1 -1
  3. package/dist/components/builder/builder-store.d.ts +19 -3
  4. package/dist/components/builder/builder-store.js +24 -13
  5. package/dist/components/builder/builder-store.js.map +1 -1
  6. package/dist/components/builder/clipboard-actions.js +6 -5
  7. package/dist/components/builder/clipboard-actions.js.map +1 -1
  8. package/dist/components/builder/composition-field-editor.js +4 -2
  9. package/dist/components/builder/composition-field-editor.js.map +1 -1
  10. package/dist/components/cms/document-diff-view.js +43 -7
  11. package/dist/components/cms/document-diff-view.js.map +1 -1
  12. package/dist/components/cms/duplicate-document.js +5 -5
  13. package/dist/components/cms/duplicate-document.js.map +1 -1
  14. package/dist/components/cms/rich-text/editor.d.ts +7 -1
  15. package/dist/components/cms/rich-text/editor.js +9 -4
  16. package/dist/components/cms/rich-text/editor.js.map +1 -1
  17. package/dist/components/cms/rich-text/embed-identity.d.ts +36 -0
  18. package/dist/components/cms/rich-text/embed-identity.js +114 -0
  19. package/dist/components/cms/rich-text/embed-identity.js.map +1 -0
  20. package/dist/components/cms/rich-text/extensions.d.ts +7 -1
  21. package/dist/components/cms/rich-text/extensions.js +7 -1
  22. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  23. package/dist/components/cms/rich-text/insert.d.ts +10 -5
  24. package/dist/components/cms/rich-text/insert.js +10 -12
  25. package/dist/components/cms/rich-text/insert.js.map +1 -1
  26. package/dist/components/components/component-preview.d.ts +0 -2
  27. package/dist/components/components/component-preview.js +0 -4
  28. package/dist/components/components/component-preview.js.map +1 -1
  29. package/dist/components/components/transfer-dialog.js +3 -1
  30. package/dist/components/components/transfer-dialog.js.map +1 -1
  31. package/dist/components/components/transfer.d.ts +12 -8
  32. package/dist/components/components/transfer.js +16 -15
  33. package/dist/components/components/transfer.js.map +1 -1
  34. package/dist/components/redirects/redirects-schema.d.ts +31 -0
  35. package/dist/components/redirects/redirects-schema.js +12 -6
  36. package/dist/components/redirects/redirects-schema.js.map +1 -1
  37. package/dist/lib/ai/translate-document.d.ts +4 -1
  38. package/dist/lib/ai/translate-document.js +87 -47
  39. package/dist/lib/ai/translate-document.js.map +1 -1
  40. package/dist/lib/ai/writer-form.d.ts +48 -5
  41. package/dist/lib/ai/writer-form.js +119 -64
  42. package/dist/lib/ai/writer-form.js.map +1 -1
  43. package/dist/lib/engine-version.d.ts +1 -1
  44. package/dist/lib/engine-version.js +1 -1
  45. package/dist/lib/engine-version.js.map +1 -1
  46. package/dist/lib/i18n/messages.de.js +20 -1
  47. package/dist/lib/i18n/messages.de.js.map +1 -1
  48. package/dist/lib/i18n/messages.en.d.ts +20 -1
  49. package/dist/lib/i18n/messages.en.js +21 -2
  50. package/dist/lib/i18n/messages.en.js.map +1 -1
  51. package/dist/lib/persona/labels.js +2 -0
  52. package/dist/lib/persona/labels.js.map +1 -1
  53. package/dist/lib/registry/preview.d.ts +2 -1
  54. package/dist/lib/registry/preview.js +8 -3
  55. package/dist/lib/registry/preview.js.map +1 -1
  56. package/dist/lib/workspace/index.d.ts +9 -1
  57. package/dist/lib/workspace/index.js +13 -2
  58. package/dist/lib/workspace/index.js.map +1 -1
  59. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '../../../lib/utils'\nimport { localizedLabel, translate } from '../../../lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport { isEquivalentToStored, normalizeEditorDoc, reportedValue, toEditorDoc } from './value'\nimport { INLINE_IMAGE_ACCEPT, insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '../../../lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored value (v2 `{ version: 2, content }`; staging: v1/legacy forms). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n /**\n * EC-436 (AD-1) — the field's resolved localized display label, the SAME\n * string the field/prop row renders next to this editor. Used only to title\n * the expand stage (\"<label> — Rich text\" via the parameterized\n * `richText.expand.titleWithField` message, {@link richTextStageTitle});\n * absent → the generic `richText.expand.title` fallback.\n */\n label?: string\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow, label }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n // Three outcomes ({@link reportedValue}): unchanged → re-emit the stored value\n // verbatim (no dirty); emptied → clear the field (undefined); otherwise → the new\n // stamped value.\n const next = reportedValue(current, valueRef.current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n // EC-679: WITHOUT an update event (`emitUpdate: false`) — TipTap's default emits\n // `update`, so this effect reported the value on screen on every mount and every\n // read-only flip, a change the user never made. Measured on the base (playground):\n // opening a post with an absent body wrote `unset body.en` (a same-value `set` with a\n // body); a tab switch in the DE view of a body that follows EN wrote the EN text into\n // `body.de` (the field silently stopped following EN); and switching to the Published\n // view wrote the PUBLISHED body into the DRAFT over an unpublished edit.\n useEffect(() => {\n editor?.setEditable(!readOnly, false)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage label={label} onCollapse={() => setExpanded(false)}>\n {surface}\n </RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept={INLINE_IMAGE_ACCEPT}\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to a readable measure and\n * centered with `mx-auto`, which keeps the gutters symmetric at every stage\n * width. EC-436 widened the measure from the classic ~70ch to ~78ch in step\n * with the wider `prose` stage panel (owner feedback: \"the dialog + text looks\n * a bit narrow on a big screen\"). Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[78ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-436 (AD-1) — the expand stage's title. With a field label in reach it\n * composes \"<label> — <localized type>\" through the PARAMETERIZED\n * `richText.expand.titleWithField` message — never string concatenation in\n * code, because word order is a translation concern; without one (a custom\n * mount that passes no label) it falls back to the generic\n * `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.\n */\nexport function richTextStageTitle(label?: string): string {\n return label !== undefined && label.trim() !== ''\n ? translate('richText.expand.titleWithField', { field: label })\n : translate('richText.expand.title')\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n label,\n}: {\n children: ReactNode\n onCollapse: () => void\n /** The field's resolved display label — see {@link richTextStageTitle}. */\n label?: string\n}) {\n return (\n <ExpandableSurface\n title={richTextStageTitle(label)}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
1
+ {"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '../../../lib/utils'\nimport { localizedLabel, translate } from '../../../lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport { isEquivalentToStored, normalizeEditorDoc, reportedValue, toEditorDoc } from './value'\nimport { INLINE_IMAGE_ACCEPT, insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry, studioManifestLookup } from '../../../lib/registry'\nimport { createUuidIdFactory } from '@elytracms/core/determinism'\nimport type { IdFactory } from '@elytracms/core/determinism'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored value (v2 `{ version: 2, content }`; staging: v1/legacy forms). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n /**\n * EC-436 (AD-1) — the field's resolved localized display label, the SAME\n * string the field/prop row renders next to this editor. Used only to title\n * the expand stage (\"<label> — Rich text\" via the parameterized\n * `richText.expand.titleWithField` message, {@link richTextStageTitle});\n * absent → the generic `richText.expand.title` fallback.\n */\n label?: string\n /**\n * EC-578 / EC-776 — the node-id factory a new or copied embed is minted from. Defaults to\n * UUIDs (one per editor, like the canvas store's); fixtures inject a deterministic one.\n */\n idFactory?: IdFactory\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow, label, idFactory }: RichTextEditorProps) {\n // EC-776: identity is minted where an embed is created or copied — the factory and the\n // project's prop declarations, fixed for the editor's lifetime (its extensions are built once).\n const [identity] = useState(() => ({ newId: idFactory ?? createUuidIdFactory(), lookupManifest: studioManifestLookup() }))\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n identity,\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { newId: identity.newId, select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n // Three outcomes ({@link reportedValue}): unchanged → re-emit the stored value\n // verbatim (no dirty); emptied → clear the field (undefined); otherwise → the new\n // stamped value.\n const next = reportedValue(current, valueRef.current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n // EC-679: WITHOUT an update event (`emitUpdate: false`) — TipTap's default emits\n // `update`, so this effect reported the value on screen on every mount and every\n // read-only flip, a change the user never made. Measured on the base (playground):\n // opening a post with an absent body wrote `unset body.en` (a same-value `set` with a\n // body); a tab switch in the DE view of a body that follows EN wrote the EN text into\n // `body.de` (the field silently stopped following EN); and switching to the Published\n // view wrote the PUBLISHED body into the DRAFT over an unpublished edit.\n useEffect(() => {\n editor?.setEditable(!readOnly, false)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage label={label} onCollapse={() => setExpanded(false)}>\n {surface}\n </RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept={INLINE_IMAGE_ACCEPT}\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { newId: identity.newId, select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to a readable measure and\n * centered with `mx-auto`, which keeps the gutters symmetric at every stage\n * width. EC-436 widened the measure from the classic ~70ch to ~78ch in step\n * with the wider `prose` stage panel (owner feedback: \"the dialog + text looks\n * a bit narrow on a big screen\"). Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[78ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-436 (AD-1) — the expand stage's title. With a field label in reach it\n * composes \"<label> — <localized type>\" through the PARAMETERIZED\n * `richText.expand.titleWithField` message — never string concatenation in\n * code, because word order is a translation concern; without one (a custom\n * mount that passes no label) it falls back to the generic\n * `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.\n */\nexport function richTextStageTitle(label?: string): string {\n return label !== undefined && label.trim() !== ''\n ? translate('richText.expand.titleWithField', { field: label })\n : translate('richText.expand.title')\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n label,\n}: {\n children: ReactNode\n onCollapse: () => void\n /** The field's resolved display label — see {@link richTextStageTitle}. */\n label?: string\n}) {\n return (\n <ExpandableSurface\n title={richTextStageTitle(label)}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { Extension } from '@tiptap/react';
2
+ import { Plugin } from '@tiptap/pm/state';
3
+ import type { EditorState, Transaction } from '@tiptap/pm/state';
4
+ import type { PropFieldLookup } from '@elytracms/core/cms-core';
5
+ import type { IdFactory } from '@elytracms/core/determinism';
6
+ /**
7
+ * **Identity in the flow editor** (EC-776, owner ruling 2026-11-04: a component node id is
8
+ * unique within the whole document). The embed atom is draggable, and a drop with Alt (macOS) /
9
+ * Ctrl COPIES the slice instead of moving it — the copy carried the original's `attrs.node`, id
10
+ * and all. Identity is minted where content is copied, so this plugin re-mints, after every
11
+ * user transaction that changed the document, each embed occurrence the transaction ADDED whose id
12
+ * the document already holds: the whole embedded subtree, through `withFreshNodeIds` (the one copy).
13
+ *
14
+ * "Added" follows the transaction's own mapping, not document order: an occurrence that maps from
15
+ * an occurrence of the previous document keeps its id — a move (deleted at one place, inserted at
16
+ * another, one transaction) leaves one occurrence, nothing to re-mint; a copy dropped BEFORE its
17
+ * original is the one re-minted. Stored twins (drift the validator names as `duplicate-node-id`)
18
+ * both map from the previous document and are left as stored; so is an external re-push of the
19
+ * stored value (`setContent` with `emitUpdate: false` sets TipTap's `preventUpdate` meta) — the
20
+ * editor never rewrites storage on load.
21
+ *
22
+ * Pure ProseMirror (`appendTransaction`), pinned in node through prosemirror-state.
23
+ */
24
+ export declare const EMBED_IDENTITY_EXTENSION = "elytraEmbedIdentity";
25
+ export interface EmbedIdentityOptions {
26
+ /** THE id factory (EC-578): UUIDs in the studio, a sequential factory in fixtures. */
27
+ readonly newId: IdFactory;
28
+ /** The project's prop declarations — the copy re-mints the nodes the embed's props hold. */
29
+ readonly lookupManifest: PropFieldLookup;
30
+ }
31
+ /** The re-mint transaction for `transactions`, or `null` when no added embed repeats an id. */
32
+ export declare function remintCopiedEmbeds(transactions: readonly Transaction[], oldState: EditorState, newState: EditorState, options: EmbedIdentityOptions): Transaction | null;
33
+ /** The ProseMirror plugin ({@link remintCopiedEmbeds} after every transaction). */
34
+ export declare function embedIdentityPlugin(options: EmbedIdentityOptions): Plugin;
35
+ /** The TipTap extension the editor's set carries ({@link buildRichTextExtensions}). */
36
+ export declare function createEmbedIdentity(options: EmbedIdentityOptions): Extension<any, any>;
@@ -0,0 +1,114 @@
1
+ import { Extension } from '@tiptap/react';
2
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
3
+ import { isComponentNodeValue, withFreshNodeIds } from '@elytracms/core/cms-core';
4
+ import { COMPONENT_EMBED_NODE_TYPE } from './component-embed.js';
5
+ /**
6
+ * **Identity in the flow editor** (EC-776, owner ruling 2026-11-04: a component node id is
7
+ * unique within the whole document). The embed atom is draggable, and a drop with Alt (macOS) /
8
+ * Ctrl COPIES the slice instead of moving it — the copy carried the original's `attrs.node`, id
9
+ * and all. Identity is minted where content is copied, so this plugin re-mints, after every
10
+ * user transaction that changed the document, each embed occurrence the transaction ADDED whose id
11
+ * the document already holds: the whole embedded subtree, through `withFreshNodeIds` (the one copy).
12
+ *
13
+ * "Added" follows the transaction's own mapping, not document order: an occurrence that maps from
14
+ * an occurrence of the previous document keeps its id — a move (deleted at one place, inserted at
15
+ * another, one transaction) leaves one occurrence, nothing to re-mint; a copy dropped BEFORE its
16
+ * original is the one re-minted. Stored twins (drift the validator names as `duplicate-node-id`)
17
+ * both map from the previous document and are left as stored; so is an external re-push of the
18
+ * stored value (`setContent` with `emitUpdate: false` sets TipTap's `preventUpdate` meta) — the
19
+ * editor never rewrites storage on load.
20
+ *
21
+ * Pure ProseMirror (`appendTransaction`), pinned in node through prosemirror-state.
22
+ */
23
+ export const EMBED_IDENTITY_EXTENSION = 'elytraEmbedIdentity';
24
+ const KEY = new PluginKey(EMBED_IDENTITY_EXTENSION);
25
+ function embedIdOf(node) {
26
+ if (node.type.name !== COMPONENT_EMBED_NODE_TYPE)
27
+ return undefined;
28
+ const embedded = node.attrs.node;
29
+ return isComponentNodeValue(embedded) ? embedded.id : undefined;
30
+ }
31
+ /** Every embed with a readable node, in document order. */
32
+ function occurrencesIn(doc) {
33
+ const out = [];
34
+ doc.descendants((node, pos) => {
35
+ if (node.type.name !== COMPONENT_EMBED_NODE_TYPE)
36
+ return true;
37
+ const id = embedIdOf(node);
38
+ if (id !== undefined)
39
+ out.push({ pos, id });
40
+ return false; // an atom: nothing inside
41
+ });
42
+ return out;
43
+ }
44
+ /** Where `pos` of the previous document sits after `transactions` — `undefined` once deleted. */
45
+ function mapForward(pos, transactions) {
46
+ let at = pos;
47
+ for (const tr of transactions) {
48
+ const result = tr.mapping.mapResult(at, 1);
49
+ if (result.deleted)
50
+ return undefined;
51
+ at = result.pos;
52
+ }
53
+ return at;
54
+ }
55
+ /** The re-mint transaction for `transactions`, or `null` when no added embed repeats an id. */
56
+ export function remintCopiedEmbeds(transactions, oldState, newState, options) {
57
+ if (!transactions.some((tr) => tr.docChanged))
58
+ return null;
59
+ // An external re-push of the stored value: storage is the validator's to judge, never rewritten here.
60
+ if (transactions.some((tr) => tr.getMeta('preventUpdate') === true))
61
+ return null;
62
+ const after = occurrencesIn(newState.doc);
63
+ const counts = new Map();
64
+ for (const { id } of after)
65
+ counts.set(id, (counts.get(id) ?? 0) + 1);
66
+ if (![...counts.values()].some((count) => count > 1))
67
+ return null;
68
+ const carried = new Set();
69
+ for (const { pos } of occurrencesIn(oldState.doc)) {
70
+ const mapped = mapForward(pos, transactions);
71
+ if (mapped !== undefined)
72
+ carried.add(mapped);
73
+ }
74
+ // Per repeated id: the occurrences that map from the previous document keep it; with none, the
75
+ // first in document order does. Every other occurrence is a copy.
76
+ const keeps = new Set();
77
+ for (const [id, count] of counts) {
78
+ if (count < 2)
79
+ continue;
80
+ const of = after.filter((occurrence) => occurrence.id === id);
81
+ const kept = of.filter((occurrence) => carried.has(occurrence.pos));
82
+ for (const occurrence of kept.length > 0 ? kept : of.slice(0, 1))
83
+ keeps.add(occurrence.pos);
84
+ }
85
+ let tr = null;
86
+ for (const { pos, id } of after) {
87
+ if ((counts.get(id) ?? 0) < 2 || keeps.has(pos))
88
+ continue;
89
+ const node = newState.doc.nodeAt(pos);
90
+ const fresh = withFreshNodeIds(node.attrs.node, options.newId, {
91
+ lookupManifest: options.lookupManifest,
92
+ });
93
+ // An atom of the same size: re-marking it moves no other position.
94
+ tr = (tr ?? newState.tr).setNodeMarkup(pos, undefined, { ...node.attrs, node: fresh });
95
+ }
96
+ return tr;
97
+ }
98
+ /** The ProseMirror plugin ({@link remintCopiedEmbeds} after every transaction). */
99
+ export function embedIdentityPlugin(options) {
100
+ return new Plugin({
101
+ key: KEY,
102
+ appendTransaction: (transactions, oldState, newState) => remintCopiedEmbeds(transactions, oldState, newState, options),
103
+ });
104
+ }
105
+ /** The TipTap extension the editor's set carries ({@link buildRichTextExtensions}). */
106
+ export function createEmbedIdentity(options) {
107
+ return Extension.create({
108
+ name: EMBED_IDENTITY_EXTENSION,
109
+ addProseMirrorPlugins() {
110
+ return [embedIdentityPlugin(options)];
111
+ },
112
+ });
113
+ }
114
+ //# sourceMappingURL=embed-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed-identity.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { Extension } from '@tiptap/react'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport type { Node as PmNode } from '@tiptap/pm/model'\nimport { isComponentNodeValue, withFreshNodeIds } from '@elytracms/core/cms-core'\nimport type { PropFieldLookup } from '@elytracms/core/cms-core'\nimport type { IdFactory } from '@elytracms/core/determinism'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\n\n/**\n * **Identity in the flow editor** (EC-776, owner ruling 2026-11-04: a component node id is\n * unique within the whole document). The embed atom is draggable, and a drop with Alt (macOS) /\n * Ctrl COPIES the slice instead of moving it — the copy carried the original's `attrs.node`, id\n * and all. Identity is minted where content is copied, so this plugin re-mints, after every\n * user transaction that changed the document, each embed occurrence the transaction ADDED whose id\n * the document already holds: the whole embedded subtree, through `withFreshNodeIds` (the one copy).\n *\n * \"Added\" follows the transaction's own mapping, not document order: an occurrence that maps from\n * an occurrence of the previous document keeps its id — a move (deleted at one place, inserted at\n * another, one transaction) leaves one occurrence, nothing to re-mint; a copy dropped BEFORE its\n * original is the one re-minted. Stored twins (drift the validator names as `duplicate-node-id`)\n * both map from the previous document and are left as stored; so is an external re-push of the\n * stored value (`setContent` with `emitUpdate: false` sets TipTap's `preventUpdate` meta) — the\n * editor never rewrites storage on load.\n *\n * Pure ProseMirror (`appendTransaction`), pinned in node through prosemirror-state.\n */\n\nexport const EMBED_IDENTITY_EXTENSION = 'elytraEmbedIdentity'\n\nexport interface EmbedIdentityOptions {\n /** THE id factory (EC-578): UUIDs in the studio, a sequential factory in fixtures. */\n readonly newId: IdFactory\n /** The project's prop declarations — the copy re-mints the nodes the embed's props hold. */\n readonly lookupManifest: PropFieldLookup\n}\n\nconst KEY = new PluginKey(EMBED_IDENTITY_EXTENSION)\n\ninterface Occurrence {\n readonly pos: number\n readonly id: string\n}\n\nfunction embedIdOf(node: PmNode): string | undefined {\n if (node.type.name !== COMPONENT_EMBED_NODE_TYPE) return undefined\n const embedded: unknown = node.attrs.node\n return isComponentNodeValue(embedded) ? embedded.id : undefined\n}\n\n/** Every embed with a readable node, in document order. */\nfunction occurrencesIn(doc: PmNode): Occurrence[] {\n const out: Occurrence[] = []\n doc.descendants((node, pos) => {\n if (node.type.name !== COMPONENT_EMBED_NODE_TYPE) return true\n const id = embedIdOf(node)\n if (id !== undefined) out.push({ pos, id })\n return false // an atom: nothing inside\n })\n return out\n}\n\n/** Where `pos` of the previous document sits after `transactions` — `undefined` once deleted. */\nfunction mapForward(pos: number, transactions: readonly Transaction[]): number | undefined {\n let at = pos\n for (const tr of transactions) {\n const result = tr.mapping.mapResult(at, 1)\n if (result.deleted) return undefined\n at = result.pos\n }\n return at\n}\n\n/** The re-mint transaction for `transactions`, or `null` when no added embed repeats an id. */\nexport function remintCopiedEmbeds(\n transactions: readonly Transaction[],\n oldState: EditorState,\n newState: EditorState,\n options: EmbedIdentityOptions,\n): Transaction | null {\n if (!transactions.some((tr) => tr.docChanged)) return null\n // An external re-push of the stored value: storage is the validator's to judge, never rewritten here.\n if (transactions.some((tr) => tr.getMeta('preventUpdate') === true)) return null\n const after = occurrencesIn(newState.doc)\n const counts = new Map<string, number>()\n for (const { id } of after) counts.set(id, (counts.get(id) ?? 0) + 1)\n if (![...counts.values()].some((count) => count > 1)) return null\n\n const carried = new Set<number>()\n for (const { pos } of occurrencesIn(oldState.doc)) {\n const mapped = mapForward(pos, transactions)\n if (mapped !== undefined) carried.add(mapped)\n }\n // Per repeated id: the occurrences that map from the previous document keep it; with none, the\n // first in document order does. Every other occurrence is a copy.\n const keeps = new Set<number>()\n for (const [id, count] of counts) {\n if (count < 2) continue\n const of = after.filter((occurrence) => occurrence.id === id)\n const kept = of.filter((occurrence) => carried.has(occurrence.pos))\n for (const occurrence of kept.length > 0 ? kept : of.slice(0, 1)) keeps.add(occurrence.pos)\n }\n let tr: Transaction | null = null\n for (const { pos, id } of after) {\n if ((counts.get(id) ?? 0) < 2 || keeps.has(pos)) continue\n const node = newState.doc.nodeAt(pos)!\n const fresh = withFreshNodeIds(node.attrs.node as Parameters<typeof withFreshNodeIds>[0], options.newId, {\n lookupManifest: options.lookupManifest,\n })\n // An atom of the same size: re-marking it moves no other position.\n tr = (tr ?? newState.tr).setNodeMarkup(pos, undefined, { ...node.attrs, node: fresh })\n }\n return tr\n}\n\n/** The ProseMirror plugin ({@link remintCopiedEmbeds} after every transaction). */\nexport function embedIdentityPlugin(options: EmbedIdentityOptions): Plugin {\n return new Plugin({\n key: KEY,\n appendTransaction: (transactions, oldState, newState) => remintCopiedEmbeds(transactions, oldState, newState, options),\n })\n}\n\n/** The TipTap extension the editor's set carries ({@link buildRichTextExtensions}). */\nexport function createEmbedIdentity(options: EmbedIdentityOptions) {\n return Extension.create({\n name: EMBED_IDENTITY_EXTENSION,\n addProseMirrorPlugins() {\n return [embedIdentityPlugin(options)]\n },\n })\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { Extensions } from '@tiptap/react';
2
2
  import type { SlashCommandOptions } from './slash-command.js';
3
+ import type { EmbedIdentityOptions } from './embed-identity.js';
3
4
  /**
4
5
  * The TipTap extension set for the CMS rich-text editor (EC-314).
5
6
  *
@@ -29,7 +30,12 @@ import type { SlashCommandOptions } from './slash-command.js';
29
30
  *
30
31
  * `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers
31
32
  * (omit an action to hide that slash item).
33
+ *
34
+ * `identity` (EC-776, required) — the embed identity plugin's id factory and prop
35
+ * declarations: an embed COPIED inside the editor (an Alt/Ctrl drag) gets fresh ids for its
36
+ * whole subtree, so one value never holds one component id twice (`./embed-identity`).
32
37
  */
33
- export declare function buildRichTextExtensions(options?: {
38
+ export declare function buildRichTextExtensions(options: {
34
39
  slash?: SlashCommandOptions;
40
+ identity: EmbedIdentityOptions;
35
41
  }): Extensions;
@@ -7,6 +7,7 @@ import { AssetImage } from './asset-image.js';
7
7
  import { ComponentEmbed } from './component-embed.js';
8
8
  import { UnknownNodeCarrier } from './unknown-node.js';
9
9
  import { createSlashCommand } from './slash-command.js';
10
+ import { createEmbedIdentity } from './embed-identity.js';
10
11
  /**
11
12
  * The TipTap extension set for the CMS rich-text editor (EC-314).
12
13
  *
@@ -36,8 +37,12 @@ import { createSlashCommand } from './slash-command.js';
36
37
  *
37
38
  * `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers
38
39
  * (omit an action to hide that slash item).
40
+ *
41
+ * `identity` (EC-776, required) — the embed identity plugin's id factory and prop
42
+ * declarations: an embed COPIED inside the editor (an Alt/Ctrl drag) gets fresh ids for its
43
+ * whole subtree, so one value never holds one component id twice (`./embed-identity`).
39
44
  */
40
- export function buildRichTextExtensions(options = {}) {
45
+ export function buildRichTextExtensions(options) {
41
46
  return [
42
47
  StarterKit.configure({
43
48
  link: false,
@@ -50,6 +55,7 @@ export function buildRichTextExtensions(options = {}) {
50
55
  UnknownNodeCarrier,
51
56
  Placeholder.configure({ placeholder: translate('richText.placeholder') }),
52
57
  createSlashCommand(options.slash ?? {}),
58
+ createEmbedIdentity(options.identity),
53
59
  ];
54
60
  }
55
61
  //# sourceMappingURL=extensions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAA2C,EAAE;IACnF,OAAO;QACL,UAAU,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;SACxC,CAAC;QACF,OAAO,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC5E,QAAQ;QACR,UAAU;QACV,cAAc;QACd,kBAAkB;QAClB,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,sBAAsB,CAAC,EAAE,CAAC;QACzE,kBAAkB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;KACxC,CAAA;AACH,CAAC","sourcesContent":["import StarterKit from '@tiptap/starter-kit'\nimport { Placeholder } from '@tiptap/extensions'\nimport type { Extensions } from '@tiptap/react'\nimport { translate } from '../../../lib/i18n'\nimport { RefLink } from './ref-link'\nimport { FontSize } from './font-size'\nimport { AssetImage } from './asset-image'\nimport { ComponentEmbed } from './component-embed'\nimport { UnknownNodeCarrier } from './unknown-node'\nimport { createSlashCommand } from './slash-command'\nimport type { SlashCommandOptions } from './slash-command'\n\n/**\n * The TipTap extension set for the CMS rich-text editor (EC-314).\n *\n * Deliberately matches the storage vocabulary (`@elytracms/core/rich-text` schema)\n * one-for-one so every editor document round-trips through `parseRichTextValue` /\n * the delivery renderer with no format change:\n *\n * - **StarterKit** supplies `doc/paragraph/text/heading/bulletList/orderedList/\n * listItem/blockquote/codeBlock/hardBreak/horizontalRule` and the marks\n * `bold/italic/code/strike/underline` — all ProseMirror-standard names identical\n * to storage. Its bundled `link` is disabled and replaced by {@link RefLink} so\n * internal-document links can carry a structured `ref` (EC-290). Headings allow\n * all six levels so existing `h4`–`h6` content is preserved even though the\n * toolbar surfaces only 1–3.\n * - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying\n * atoms (asset image, embedded registry component) so existing content of those\n * kinds survives a round-trip verbatim rather than being dropped by the schema.\n * - {@link UnknownNodeCarrier} (EC-547) is the editor-only opaque atom the\n * storage⇄editor projection wraps any OTHER stored node type into, so the\n * closed ProseMirror schema can never drop a node the flow editor does not\n * natively edit — the EC-500 D3 round-trip law.\n * - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run\n * round-trips (an undeclared mark would be silently dropped on load).\n *\n * No `allow` gating lives here — that governs *insertion* (the embed picker) and\n * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.\n *\n * `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers\n * (omit an action to hide that slash item).\n */\nexport function buildRichTextExtensions(options: { slash?: SlashCommandOptions } = {}): Extensions {\n return [\n StarterKit.configure({\n link: false,\n heading: { levels: [1, 2, 3, 4, 5, 6] },\n }),\n RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),\n FontSize,\n AssetImage,\n ComponentEmbed,\n UnknownNodeCarrier,\n Placeholder.configure({ placeholder: translate('richText.placeholder') }),\n createSlashCommand(options.slash ?? {}),\n ]\n}\n"]}
1
+ {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAGtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAGvC;IACC,OAAO;QACL,UAAU,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;SACxC,CAAC;QACF,OAAO,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC5E,QAAQ;QACR,UAAU;QACV,cAAc;QACd,kBAAkB;QAClB,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,sBAAsB,CAAC,EAAE,CAAC;QACzE,kBAAkB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QACvC,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC;KACtC,CAAA;AACH,CAAC","sourcesContent":["import StarterKit from '@tiptap/starter-kit'\nimport { Placeholder } from '@tiptap/extensions'\nimport type { Extensions } from '@tiptap/react'\nimport { translate } from '../../../lib/i18n'\nimport { RefLink } from './ref-link'\nimport { FontSize } from './font-size'\nimport { AssetImage } from './asset-image'\nimport { ComponentEmbed } from './component-embed'\nimport { UnknownNodeCarrier } from './unknown-node'\nimport { createSlashCommand } from './slash-command'\nimport type { SlashCommandOptions } from './slash-command'\nimport { createEmbedIdentity } from './embed-identity'\nimport type { EmbedIdentityOptions } from './embed-identity'\n\n/**\n * The TipTap extension set for the CMS rich-text editor (EC-314).\n *\n * Deliberately matches the storage vocabulary (`@elytracms/core/rich-text` schema)\n * one-for-one so every editor document round-trips through `parseRichTextValue` /\n * the delivery renderer with no format change:\n *\n * - **StarterKit** supplies `doc/paragraph/text/heading/bulletList/orderedList/\n * listItem/blockquote/codeBlock/hardBreak/horizontalRule` and the marks\n * `bold/italic/code/strike/underline` — all ProseMirror-standard names identical\n * to storage. Its bundled `link` is disabled and replaced by {@link RefLink} so\n * internal-document links can carry a structured `ref` (EC-290). Headings allow\n * all six levels so existing `h4`–`h6` content is preserved even though the\n * toolbar surfaces only 1–3.\n * - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying\n * atoms (asset image, embedded registry component) so existing content of those\n * kinds survives a round-trip verbatim rather than being dropped by the schema.\n * - {@link UnknownNodeCarrier} (EC-547) is the editor-only opaque atom the\n * storage⇄editor projection wraps any OTHER stored node type into, so the\n * closed ProseMirror schema can never drop a node the flow editor does not\n * natively edit — the EC-500 D3 round-trip law.\n * - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run\n * round-trips (an undeclared mark would be silently dropped on load).\n *\n * No `allow` gating lives here — that governs *insertion* (the embed picker) and\n * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.\n *\n * `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers\n * (omit an action to hide that slash item).\n *\n * `identity` (EC-776, required) — the embed identity plugin's id factory and prop\n * declarations: an embed COPIED inside the editor (an Alt/Ctrl drag) gets fresh ids for its\n * whole subtree, so one value never holds one component id twice (`./embed-identity`).\n */\nexport function buildRichTextExtensions(options: {\n slash?: SlashCommandOptions\n identity: EmbedIdentityOptions\n}): Extensions {\n return [\n StarterKit.configure({\n link: false,\n heading: { levels: [1, 2, 3, 4, 5, 6] },\n }),\n RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),\n FontSize,\n AssetImage,\n ComponentEmbed,\n UnknownNodeCarrier,\n Placeholder.configure({ placeholder: translate('richText.placeholder') }),\n createSlashCommand(options.slash ?? {}),\n createEmbedIdentity(options.identity),\n ]\n}\n"]}
@@ -2,6 +2,7 @@ import type { Editor } from '@tiptap/react';
2
2
  import { assetNode } from '@elytracms/core/cms-core';
3
3
  import type { ComponentNode } from '@elytracms/core/project-graph';
4
4
  import type { ComponentManifest } from '@elytracms/core/component-registry';
5
+ import type { IdFactory } from '@elytracms/core/determinism';
5
6
  import type { AssetsStore } from '../../assets/assets-store.js';
6
7
  /**
7
8
  * Reference-carrying node insertions shared by the toolbar controls and the slash
@@ -29,18 +30,22 @@ export declare function assetImageAttrs(assetId: string, assets: AssetsStore | n
29
30
  export declare function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void;
30
31
  /**
31
32
  * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest
32
- * defaults. With `{ select: true }` the inserted node is then selected — so a host
33
- * embed inspector (0.0.41) opens straight onto its props for editing.
33
+ * defaults, its id minted by the editor's injected factory (`newId`, EC-578 / EC-776).
34
+ * With `select: true` the inserted node is then selected — so a host embed inspector
35
+ * (0.0.41) opens straight onto its props for editing.
34
36
  */
35
- export declare function insertComponentEmbed(editor: Editor, manifest: ComponentManifest, options?: {
37
+ export declare function insertComponentEmbed(editor: Editor, manifest: ComponentManifest, options: {
38
+ newId: IdFactory;
36
39
  select?: boolean;
37
40
  }): void;
38
41
  /**
39
42
  * A fresh embedded ComponentNode (mirrors builder node-factory). EC-591 / ratified
40
43
  * 2f: NO props — an absent prop is `undefined`, so the implementation's own default
41
- * applies; the manifest `default` is a hint, never baked into the stored node.
44
+ * applies; the manifest `default` is a hint, never baked into the stored node. Its id
45
+ * comes from the injected factory (EC-578 / EC-776 — it used `crypto.randomUUID` with a
46
+ * `Math.random` fallback, outside the factory).
42
47
  */
43
- export declare function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode;
48
+ export declare function embedNodeFromManifest(manifest: ComponentManifest, newId: IdFactory): ComponentNode;
44
49
  /**
45
50
  * The components embeddable in a rich-text field or prop (EC-584, ratified 4b): its
46
51
  * declared `allow` (a closed set — EC-544: an empty one offers nothing), else the
@@ -35,17 +35,18 @@ export function insertAssetImage(editor, assetId, assets) {
35
35
  }
36
36
  /**
37
37
  * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest
38
- * defaults. With `{ select: true }` the inserted node is then selected — so a host
39
- * embed inspector (0.0.41) opens straight onto its props for editing.
38
+ * defaults, its id minted by the editor's injected factory (`newId`, EC-578 / EC-776).
39
+ * With `select: true` the inserted node is then selected — so a host embed inspector
40
+ * (0.0.41) opens straight onto its props for editing.
40
41
  */
41
42
  export function insertComponentEmbed(editor, manifest, options) {
42
- const embed = embedNodeFromManifest(manifest);
43
+ const embed = embedNodeFromManifest(manifest, options.newId);
43
44
  editor
44
45
  .chain()
45
46
  .focus()
46
47
  .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embed } })
47
48
  .run();
48
- if (options?.select)
49
+ if (options.select)
49
50
  selectComponentEmbed(editor, embed.id);
50
51
  }
51
52
  /**
@@ -73,10 +74,12 @@ function selectComponentEmbed(editor, id) {
73
74
  /**
74
75
  * A fresh embedded ComponentNode (mirrors builder node-factory). EC-591 / ratified
75
76
  * 2f: NO props — an absent prop is `undefined`, so the implementation's own default
76
- * applies; the manifest `default` is a hint, never baked into the stored node.
77
+ * applies; the manifest `default` is a hint, never baked into the stored node. Its id
78
+ * comes from the injected factory (EC-578 / EC-776 — it used `crypto.randomUUID` with a
79
+ * `Math.random` fallback, outside the factory).
77
80
  */
78
- export function embedNodeFromManifest(manifest) {
79
- return { id: newEmbedId(), componentId: manifest.id, props: {} };
81
+ export function embedNodeFromManifest(manifest, newId) {
82
+ return { id: newId(), componentId: manifest.id, props: {} };
80
83
  }
81
84
  /**
82
85
  * The components embeddable in a rich-text field or prop (EC-584, ratified 4b): its
@@ -93,9 +96,4 @@ export function listEmbeddable(allow, registered) {
93
96
  const vocabulary = resolveAllow(allow, FIELD_TYPES.richText.defaultAllow);
94
97
  return registered.filter((manifest) => placementRefusal({ componentId: manifest.id, view: toManifestView(manifest), allow: vocabulary }) === null);
95
98
  }
96
- /** A unique id for an embedded node (studio runs in the browser; crypto is present). */
97
- function newEmbedId() {
98
- const uuid = globalThis.crypto?.randomUUID?.();
99
- return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`;
100
- }
101
99
  //# sourceMappingURL=insert.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport { FIELD_TYPES } from '@elytracms/core/cms-core'\nimport { placementRefusal, resolveAllow } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { toManifestView } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport type { AssetsStore } from '../../assets/assets-store'\n\n/**\n * Reference-carrying node insertions shared by the toolbar controls and the slash\n * menu (EC-314). Centralised so both entry points produce byte-identical envelopes\n * (the shapes `reference-extraction` indexes).\n */\n\n/**\n * What the rich-text image insert accepts (EC-588): images, in the structured `accept`\n * vocabulary every picker takes — stated once here rather than as a file-input literal\n * at the call site. The picker derives its grid filter, its Upload dialog and its drop\n * rule from it, like a field's own `accept`.\n */\nexport const INLINE_IMAGE_ACCEPT: readonly string[] = ['image']\n\n/**\n * The `image` node attrs for a project asset: the canonical `attrs.ref` plus an\n * editor-display `src` snapshot. NO `alt` (EC-587, ASSETS D19): an asset-pinned image's\n * alt is the asset's, read at render through the one reader — a snapshot here went\n * stale on the first alt edit (and read the legacy column only). Pure, for the test.\n */\nexport function assetImageAttrs(assetId: string, assets: AssetsStore | null): { src: string; ref: ReturnType<typeof assetNode> } {\n const record = assets?.assets.find((a) => a.id === assetId)\n return { src: record?.url ?? '', ref: assetNode(assetId) }\n}\n\n/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */\nexport function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void {\n editor\n .chain()\n .focus()\n .insertContent({ type: 'image', attrs: assetImageAttrs(assetId, assets) })\n .run()\n}\n\n/**\n * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest\n * defaults. With `{ select: true }` the inserted node is then selected — so a host\n * embed inspector (0.0.41) opens straight onto its props for editing.\n */\nexport function insertComponentEmbed(\n editor: Editor,\n manifest: ComponentManifest,\n options?: { select?: boolean },\n): void {\n const embed = embedNodeFromManifest(manifest)\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embed } })\n .run()\n if (options?.select) selectComponentEmbed(editor, embed.id)\n}\n\n/**\n * Select the just-inserted embed by its unique embedded-node id. Located by scan\n * (rather than fragile position arithmetic after `insertContent`, which may split a\n * paragraph or append a trailing block); the id is unique so the first match is it.\n */\nfunction selectComponentEmbed(editor: Editor, id: string): void {\n let pos = -1\n editor.state.doc.descendants((node, at) => {\n if (pos >= 0) return false\n if (node.type.name === COMPONENT_EMBED_NODE_TYPE) {\n const embedded = node.attrs.node as { id?: string } | null\n if (embedded?.id === id) {\n pos = at\n return false\n }\n }\n return true\n })\n if (pos >= 0) editor.commands.setNodeSelection(pos)\n}\n\n/**\n * A fresh embedded ComponentNode (mirrors builder node-factory). EC-591 / ratified\n * 2f: NO props — an absent prop is `undefined`, so the implementation's own default\n * applies; the manifest `default` is a hint, never baked into the stored node.\n */\nexport function embedNodeFromManifest(manifest: ComponentManifest): ComponentNode {\n return { id: newEmbedId(), componentId: manifest.id, props: {} }\n}\n\n/**\n * The components embeddable in a rich-text field or prop (EC-584, ratified 4b): its\n * declared `allow` (a closed set — EC-544: an empty one offers nothing), else the\n * `richText` TYPE's default, declared once in the per-FieldType definition\n * (`FIELD_TYPES.richText.defaultAllow`): any canvas-eligible component that fills no\n * LAYOUT role — a Stack or Grid mid-paragraph is a layout surface's job. Judged by the\n * ONE core placement judge (role exclusion + `isCanvasEligible`, carried on the\n * manifest view), the same verdict the composition validator reaches — so the embed\n * picker, the toolbar, the slash menu and the inspector's rich-text editor offer\n * exactly what publishes.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const vocabulary = resolveAllow(allow, FIELD_TYPES.richText.defaultAllow!)\n return registered.filter(\n (manifest) => placementRefusal({ componentId: manifest.id, view: toManifestView(manifest), allow: vocabulary }) === null,\n )\n}\n\n/** A unique id for an embedded node (studio runs in the browser; crypto is present). */\nfunction newEmbedId(): string {\n const uuid = globalThis.crypto?.randomUUID?.()\n return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`\n}\n"]}
1
+ {"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport { FIELD_TYPES } from '@elytracms/core/cms-core'\nimport { placementRefusal, resolveAllow } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { toManifestView } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport type { IdFactory } from '@elytracms/core/determinism'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport type { AssetsStore } from '../../assets/assets-store'\n\n/**\n * Reference-carrying node insertions shared by the toolbar controls and the slash\n * menu (EC-314). Centralised so both entry points produce byte-identical envelopes\n * (the shapes `reference-extraction` indexes).\n */\n\n/**\n * What the rich-text image insert accepts (EC-588): images, in the structured `accept`\n * vocabulary every picker takes — stated once here rather than as a file-input literal\n * at the call site. The picker derives its grid filter, its Upload dialog and its drop\n * rule from it, like a field's own `accept`.\n */\nexport const INLINE_IMAGE_ACCEPT: readonly string[] = ['image']\n\n/**\n * The `image` node attrs for a project asset: the canonical `attrs.ref` plus an\n * editor-display `src` snapshot. NO `alt` (EC-587, ASSETS D19): an asset-pinned image's\n * alt is the asset's, read at render through the one reader — a snapshot here went\n * stale on the first alt edit (and read the legacy column only). Pure, for the test.\n */\nexport function assetImageAttrs(assetId: string, assets: AssetsStore | null): { src: string; ref: ReturnType<typeof assetNode> } {\n const record = assets?.assets.find((a) => a.id === assetId)\n return { src: record?.url ?? '', ref: assetNode(assetId) }\n}\n\n/** Insert an `image` node for a project asset (canonical `attrs.ref`, snapshot `src`). */\nexport function insertAssetImage(editor: Editor, assetId: string, assets: AssetsStore | null): void {\n editor\n .chain()\n .focus()\n .insertContent({ type: 'image', attrs: assetImageAttrs(assetId, assets) })\n .run()\n}\n\n/**\n * Insert a `componentEmbed` wrapping a fresh ComponentNode built from manifest\n * defaults, its id minted by the editor's injected factory (`newId`, EC-578 / EC-776).\n * With `select: true` the inserted node is then selected — so a host embed inspector\n * (0.0.41) opens straight onto its props for editing.\n */\nexport function insertComponentEmbed(\n editor: Editor,\n manifest: ComponentManifest,\n options: { newId: IdFactory; select?: boolean },\n): void {\n const embed = embedNodeFromManifest(manifest, options.newId)\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embed } })\n .run()\n if (options.select) selectComponentEmbed(editor, embed.id)\n}\n\n/**\n * Select the just-inserted embed by its unique embedded-node id. Located by scan\n * (rather than fragile position arithmetic after `insertContent`, which may split a\n * paragraph or append a trailing block); the id is unique so the first match is it.\n */\nfunction selectComponentEmbed(editor: Editor, id: string): void {\n let pos = -1\n editor.state.doc.descendants((node, at) => {\n if (pos >= 0) return false\n if (node.type.name === COMPONENT_EMBED_NODE_TYPE) {\n const embedded = node.attrs.node as { id?: string } | null\n if (embedded?.id === id) {\n pos = at\n return false\n }\n }\n return true\n })\n if (pos >= 0) editor.commands.setNodeSelection(pos)\n}\n\n/**\n * A fresh embedded ComponentNode (mirrors builder node-factory). EC-591 / ratified\n * 2f: NO props — an absent prop is `undefined`, so the implementation's own default\n * applies; the manifest `default` is a hint, never baked into the stored node. Its id\n * comes from the injected factory (EC-578 / EC-776 — it used `crypto.randomUUID` with a\n * `Math.random` fallback, outside the factory).\n */\nexport function embedNodeFromManifest(manifest: ComponentManifest, newId: IdFactory): ComponentNode {\n return { id: newId(), componentId: manifest.id, props: {} }\n}\n\n/**\n * The components embeddable in a rich-text field or prop (EC-584, ratified 4b): its\n * declared `allow` (a closed set — EC-544: an empty one offers nothing), else the\n * `richText` TYPE's default, declared once in the per-FieldType definition\n * (`FIELD_TYPES.richText.defaultAllow`): any canvas-eligible component that fills no\n * LAYOUT role — a Stack or Grid mid-paragraph is a layout surface's job. Judged by the\n * ONE core placement judge (role exclusion + `isCanvasEligible`, carried on the\n * manifest view), the same verdict the composition validator reaches — so the embed\n * picker, the toolbar, the slash menu and the inspector's rich-text editor offer\n * exactly what publishes.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const vocabulary = resolveAllow(allow, FIELD_TYPES.richText.defaultAllow!)\n return registered.filter(\n (manifest) => placementRefusal({ componentId: manifest.id, view: toManifestView(manifest), allow: vocabulary }) === null,\n )\n}\n"]}
@@ -28,8 +28,6 @@ export interface PreviewState {
28
28
  }
29
29
  /** Preview state for a manifest: the fixture selection + the live prop overrides. */
30
30
  export declare function usePreviewState(manifest: ComponentManifest): PreviewState;
31
- /** Re-id a preview node into the canvas `node-<n>` namespace for embedding (transfer). */
32
- export declare function embedNodeFrom(node: ComponentNode, id: string): ComponentNode;
33
31
  /**
34
32
  * The live preview surface: the fixture-state selector (when a component declares
35
33
  * more than one) plus the rendered node. Server-only components (EC-191
@@ -53,10 +53,6 @@ export function usePreviewState(manifest) {
53
53
  node,
54
54
  };
55
55
  }
56
- /** Re-id a preview node into the canvas `node-<n>` namespace for embedding (transfer). */
57
- export function embedNodeFrom(node, id) {
58
- return { ...node, id };
59
- }
60
56
  /**
61
57
  * The live preview surface: the fixture-state selector (when a component declares
62
58
  * more than one) plus the rendered node. Server-only components (EC-191