@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.
- package/dist/components/builder/blocks-model.js +5 -2
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.js +1 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +48 -0
- package/dist/components/builder/composition-field-editor.js +94 -22
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.js +3 -2
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +4 -0
- package/dist/components/builder/node-factory.js +14 -9
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/cms-composition-validators.d.ts +7 -16
- package/dist/components/cms/cms-composition-validators.js +101 -52
- package/dist/components/cms/cms-composition-validators.js.map +1 -1
- package/dist/components/cms/gate-validation.js +3 -1
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.d.ts +13 -7
- package/dist/components/cms/rich-text/component-embed.js +21 -13
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +1 -1
- package/dist/components/cms/rich-text/editor.js +2 -2
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +4 -0
- package/dist/components/cms/rich-text/extensions.js +6 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/insert.js +1 -1
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/rich-text/unknown-node.d.ts +66 -0
- package/dist/components/cms/rich-text/unknown-node.js +101 -0
- package/dist/components/cms/rich-text/unknown-node.js.map +1 -0
- package/dist/components/cms/rich-text/value.d.ts +82 -13
- package/dist/components/cms/rich-text/value.js +169 -38
- package/dist/components/cms/rich-text/value.js.map +1 -1
- package/dist/components/cms/rich-text-preview.js +1 -1
- package/dist/components/cms/rich-text-preview.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.js +3 -3
- package/dist/lib/ai/rich-text-translate.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +7 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -0
- package/dist/lib/i18n/messages.en.js +7 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-composition-validators.js","sourceRoot":"","sources":["../../../src/components/cms/cms-composition-validators.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAExE,OAAO,
|
|
1
|
+
{"version":3,"file":"cms-composition-validators.js","sourceRoot":"","sources":["../../../src/components/cms/cms-composition-validators.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAExE,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAElE;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;;;;;GAWG;AACH,SAAS,qBAAqB,CAC5B,KAAc,EACd,KAAoC;IAEpC,+EAA+E;IAC/E,8EAA8E;IAC9E,8EAA8E;IAC9E,qDAAqD;IACrD,IAAI,CAAC,yBAAyB,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO;YACL;gBACE,OAAO,EAAE,0CAA0C,sBAAsB,GAAG;gBAC5E,QAAQ,EAAE,OAAO;aAClB;SACF,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAA;IAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO;YACL;gBACE,OAAO,EAAE,0CAA0C,sBAAsB,GAAG;gBAC5E,QAAQ,EAAE,OAAO;aAClB;SACF,CAAA;IACH,CAAC;IACD,MAAM,UAAU,GAAG,iBAAiB,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAA;IACtD,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,CAAA;IACxD,MAAM,GAAG,GAAuB,EAAE,CAAA;IAElC,4EAA4E;IAC5E,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,IAAoC,EAAU,EAAE;QAChF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAC1B,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7D,CAAC,CAAA;IAED,MAAM,cAAc,GAAG,CAAC,KAAuC,EAAE,IAAY,EAAQ,EAAE;QACrF,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;YAC1B,MAAM,IAAI,GAAG,GAAG,IAAI,YAAY,CAAC,EAAE,CAAA;YACnC,MAAM,IAAI,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAA;YACrC,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC9B,KAAK,MAAM,KAAK,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;oBAC9D,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBACxG,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;YACzC,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;IAED,MAAM,KAAK,GAA6C,EAAE,CAAA;IAC1D,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC3C,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;;YAChE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,KAAK,EAAE,CAAC,CAAA;IAC5D,CAAC,CAAC,CAAA;IAEF,KAAK,MAAM,KAAK,IAAI,wBAAwB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;QAC7E,uEAAuE;QACvE,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAA;QACjC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAChE,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;SAClF,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAc,EAAE,KAAqB;IACvE,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAA;IAC5B,OAAO,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAE,KAAuB;IAC5E,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAA;IAC5B,OAAO,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;AAClD,CAAC","sourcesContent":["import type { BlocksFieldDef, CompositionIssue, RichTextFieldDef } from '@elytracms/core/cms-core'\nimport { validateCompositionValue } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport {\n BLOCK_SEQUENCE_VERSION,\n classifyBlockNode,\n isBlockSequenceValueShape,\n parseBlockSequenceValue,\n} from '@elytracms/core/rich-text'\nimport type { BlockNode } from '@elytracms/core/rich-text'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\n\n/**\n * Composition validator adapters (EC-326, extracted from `cms-store.tsx`) —\n * injected into `validateDocument`'s `validateComposition`/`validateRichTextEmbeds`\n * options so `@elytracms/core/cms-core` stays free of `@elytracms/core/project-graph`\n * and `@elytracms/core/rich-text`.\n *\n * EC-547 (EC-500 D1/D4): both field types now share ONE stored shape — the v2\n * block sequence `{ version: 2, content: BlockNode[] }` — and therefore ONE\n * validation walk: unwrap the envelope, validate the sequence's component nodes\n * (top-level roots AND nodes nested inside typography content) against the\n * field's `allow` vocabulary + the code-owned component registry. \"`allow`\n * means component ids insertable at this position\" is now literally the same\n * check on the same shape for `blocks` and `richText`.\n *\n * Pre-convergence shapes (the v1 `{ version, doc }` envelope, bare node lists)\n * never reach these adapters: `validateDocument`'s shape pass reports them as\n * the explicit pre-convergence issue and skips the deeper validators (EC-548\n * removed the legacy walks). A retired `componentEmbed` node riding in v2\n * content is a foreign typography node — not rendered, not validated, surfaced\n * by the renderer's unknown-node fallback (render/validate parity, EC-497).\n */\n\n/**\n * Validate the component nodes of a v2 block sequence against the field leash.\n *\n * - Top-level component roots validate in ONE `validateCompositionValue` call,\n * so cross-root checks (duplicate node ids) see the whole sequence. Their\n * `root/<k>` issue paths are re-keyed to the node's actual sequence index\n * (`content/<i>/…`).\n * - A component nested in typography content validates per-node with the SAME\n * leash, its findings prefixed with the typography path\n * (`content/<i>/content/<j>/…`).\n * - A malformed value degrades to a single finding, never a crash.\n */\nfunction validateBlockSequence(\n value: unknown,\n allow: readonly string[] | undefined,\n): CompositionIssue[] {\n // Structural gate FIRST: only the stored `{ version: 2, content }` object is a\n // sequence here. The parser alone would also accept a JSON-string encoding of\n // one — which no delivery reader accepts — and validating that clean would be\n // the invisible divergence EC-390 exists to prevent.\n if (!isBlockSequenceValueShape(value)) {\n return [\n {\n message: `is not a valid block sequence (version ${BLOCK_SEQUENCE_VERSION})`,\n severity: 'error',\n },\n ]\n }\n const parsed = parseBlockSequenceValue(value)\n if (!parsed.ok) {\n return [\n {\n message: `is not a valid block sequence (version ${BLOCK_SEQUENCE_VERSION})`,\n severity: 'error',\n },\n ]\n }\n const components = getStudioRegistry().registry.lookup\n const opts = { ...(allow ? { allow } : {}), components }\n const out: CompositionIssue[] = []\n\n /** `root/0/<rest>` from a single-node validation call → `<base>/<rest>`. */\n const nestedPath = (base: string, path: ReadonlyArray<string | number>): string => {\n const rest = path.slice(2)\n return rest.length > 0 ? `${base}/${rest.join('/')}` : base\n }\n\n const walkTypography = (nodes: readonly BlockNode[] | undefined, base: string): void => {\n nodes?.forEach((child, i) => {\n const here = `${base}/content/${i}`\n const kind = classifyBlockNode(child)\n if (kind.kind === 'component') {\n for (const issue of validateCompositionValue(kind.node, opts)) {\n out.push({ message: issue.message, severity: issue.severity, nodePath: nestedPath(here, issue.path) })\n }\n } else {\n walkTypography(kind.node.content, here)\n }\n })\n }\n\n const roots: { node: ComponentNode; index: number }[] = []\n parsed.value.content.forEach((node, index) => {\n const kind = classifyBlockNode(node)\n if (kind.kind === 'component') roots.push({ node: kind.node, index })\n else walkTypography(kind.node.content, `content/${index}`)\n })\n\n for (const issue of validateCompositionValue(roots.map((r) => r.node), opts)) {\n // path is `['root', <k>, ...rest]` — re-key <k> to the sequence index.\n const [, k, ...rest] = issue.path\n const index = typeof k === 'number' ? (roots[k]?.index ?? k) : k\n out.push({\n message: issue.message,\n severity: issue.severity,\n nodePath: ['content', ...(index === undefined ? [] : [index]), ...rest].join('/'),\n })\n }\n return out\n}\n\n/**\n * EC-188: validate a `blocks` composition field value against the field's `allow`\n * vocabulary + the code-owned component registry (the leash), mapping graph\n * issues into plain CMS findings. Never throws — a malformed value degrades to\n * a single finding, matching the \"validation is first-class, never a crash\"\n * rule.\n */\nexport function validateComposition(value: unknown, field: BlocksFieldDef): CompositionIssue[] {\n if (value == null) return []\n return validateBlockSequence(value, field.allow)\n}\n\n/**\n * EC-189: validate the composition nodes inside a `richText` field value with\n * the SAME leash a `blocks` field uses — since EC-547 literally the same walk\n * over the same shape: the value's `elytra/component` nodes ARE sequence\n * citizens, at the top level and nested in typography content.\n */\nexport function validateRichTextEmbeds(value: unknown, field: RichTextFieldDef): CompositionIssue[] {\n if (value == null) return []\n return validateBlockSequence(value, field.allow)\n}\n"]}
|
|
@@ -7,7 +7,9 @@ export function gateValidateOptions(input) {
|
|
|
7
7
|
// `cms-core` stays free of `project-graph` and `rich-text`. These are what the
|
|
8
8
|
// operations layer could never supply on its own — it has no registry — and
|
|
9
9
|
// the reason the canonical validator is injected downward rather than rebuilt
|
|
10
|
-
// there.
|
|
10
|
+
// there. Since EC-547 both injected validators unwrap the converged v2 block
|
|
11
|
+
// sequence (`{ version: 2, content }`) and validate its component nodes —
|
|
12
|
+
// top-level and nested in typography — with the same field `allow` leash.
|
|
11
13
|
validateComposition,
|
|
12
14
|
validateRichTextEmbeds,
|
|
13
15
|
isPublished: input.isPublished,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-validation.js","sourceRoot":"","sources":["../../../src/components/cms/gate-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AA2C1F,MAAM,UAAU,mBAAmB,CAAC,KAA0B;IAC5D,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,wEAAwE;QACxE,+EAA+E;QAC/E,4EAA4E;QAC5E,8EAA8E;QAC9E,
|
|
1
|
+
{"version":3,"file":"gate-validation.js","sourceRoot":"","sources":["../../../src/components/cms/gate-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AA2C1F,MAAM,UAAU,mBAAmB,CAAC,KAA0B;IAC5D,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,wEAAwE;QACxE,+EAA+E;QAC/E,4EAA4E;QAC5E,8EAA8E;QAC9E,6EAA6E;QAC7E,0EAA0E;QAC1E,0EAA0E;QAC1E,mBAAmB;QACnB,sBAAsB;QACtB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjE,CAAA;AACH,CAAC","sourcesContent":["import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators'\nimport type {\n DocumentRef,\n DocumentStore,\n LocaleConfig,\n ValidateDocumentOptions,\n} from '@elytracms/core/cms-core'\n\n/**\n * THE option bag (EC-497, AD-497a).\n *\n * Before this module there were seven `validateDocument` call sites, each\n * hand-assembling its own subset of `ValidateDocumentOptions` — and the weakest of\n * the seven was the publish gate, the last thing between a draft and the live\n * site. One document therefore earned a different verdict depending on which\n * surface asked, which is a validation system that cannot be reasoned about.\n *\n * The contract, in one line: **surfaces differ in what they DO with the issues,\n * never in what they validated with.** Publish filters by severity, cell edit\n * filters by field + locale, the AI paths take `introducedErrors` (after − before)\n * — all three start from the identical list. There are deliberately NO named\n * \"weaker subsets\": a subset is how this defect happened, and the moment one\n * exists someone has to decide per call site which one applies.\n *\n * The one conditionality that remains is the engine's own and is documented on\n * `ValidateDocumentOptions`: `store`-dependent rules (relation/asset existence,\n * hierarchy cycles, `validation.unique`) fire exactly when a store is supplied.\n *\n * **Out of contract by declaration:** the assets and redirects rails. They\n * validate synthetic, non-`cmsDocuments` collections against their own registries\n * with no session store, and their writes go through `operations.assets.*` /\n * `operations.redirects.*` — a different entity, not a weaker check of this one.\n */\nexport interface GateValidationInput {\n /** The session document store, with the document under judgement swapped in. */\n store: DocumentStore\n localeConfig: LocaleConfig\n /** Published-ness from the version POINTER (EC-224) — never a flat state flag. */\n isPublished: (ref: DocumentRef) => boolean\n /** The real asset library oracle; absent where no library is mounted. */\n assetExists?: (id: string) => boolean\n}\n\nexport function gateValidateOptions(input: GateValidationInput): ValidateDocumentOptions {\n return {\n store: input.store,\n localeConfig: input.localeConfig,\n // EC-188/EC-189: the composition + rich-text-embed leashes, injected so\n // `cms-core` stays free of `project-graph` and `rich-text`. These are what the\n // operations layer could never supply on its own — it has no registry — and\n // the reason the canonical validator is injected downward rather than rebuilt\n // there. Since EC-547 both injected validators unwrap the converged v2 block\n // sequence (`{ version: 2, content }`) and validate its component nodes —\n // top-level and nested in typography — with the same field `allow` leash.\n validateComposition,\n validateRichTextEmbeds,\n isPublished: input.isPublished,\n ...(input.assetExists ? { assetExists: input.assetExists } : {}),\n }\n}\n"]}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Node } from '@tiptap/react';
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
* composition value at `attrs.node
|
|
5
|
-
*
|
|
6
|
-
* embedded
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* The flow editor's component atom (EC-189/EC-314, re-scoped by EC-547/EC-548): a
|
|
4
|
+
* block atom holding a single composition value at `attrs.node` — how a registry
|
|
5
|
+
* component appears inside editable prose. The node carries no ProseMirror content
|
|
6
|
+
* (the embedded component owns its own children through composition slots).
|
|
7
|
+
*
|
|
8
|
+
* EDITOR-ONLY vocabulary since EC-548: at rest a component is a first-class
|
|
9
|
+
* `elytra/component` sequence node (EC-500); the storage⇄editor projection
|
|
10
|
+
* (`./value`) creates this atom on the way in and converts it back to the tagged
|
|
11
|
+
* sequence node on the way out, so the `componentEmbed` type never reaches
|
|
12
|
+
* storage. A LEGACY stored `componentEmbed` (pre-convergence content) is a
|
|
13
|
+
* foreign node everywhere — carrier in the editor, visible fallback on delivery.
|
|
10
14
|
*
|
|
11
15
|
* The editor shows a placeholder card naming the embedded component (a live in-editor
|
|
12
16
|
* preview would need the runtime renderer + resolvers; deferred). Insertion is wired
|
|
@@ -21,6 +25,8 @@ import { Node } from '@tiptap/react';
|
|
|
21
25
|
* a proper ProseMirror transaction (undoable), immediately per prop change.
|
|
22
26
|
* `readOnly` renders an inert card (no edit/delete).
|
|
23
27
|
*/
|
|
28
|
+
/** The editor-only atom type for a component riding in prose. Never a storage type (EC-548). */
|
|
29
|
+
export declare const COMPONENT_EMBED_NODE_TYPE = "componentEmbed";
|
|
24
30
|
export declare const ComponentEmbed: Node<any, any>;
|
|
25
31
|
/**
|
|
26
32
|
* The presentational embed card — component identity, an optional one-line prop
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react';
|
|
4
4
|
import { Blocks, Pencil, Trash2 } from 'lucide-react';
|
|
5
|
-
import {
|
|
5
|
+
import { componentNodeSchema, safeParseTotal } from '@elytracms/core/project-graph';
|
|
6
6
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
7
7
|
import { Button } from '@elytracms/studio/ui';
|
|
8
8
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
@@ -10,13 +10,17 @@ import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
|
|
|
10
10
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
11
11
|
import { EmbedPropsStage, embedPropsSummary } from './embed-props.js';
|
|
12
12
|
/**
|
|
13
|
-
* The
|
|
14
|
-
* composition value at `attrs.node
|
|
15
|
-
*
|
|
16
|
-
* embedded
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* The flow editor's component atom (EC-189/EC-314, re-scoped by EC-547/EC-548): a
|
|
14
|
+
* block atom holding a single composition value at `attrs.node` — how a registry
|
|
15
|
+
* component appears inside editable prose. The node carries no ProseMirror content
|
|
16
|
+
* (the embedded component owns its own children through composition slots).
|
|
17
|
+
*
|
|
18
|
+
* EDITOR-ONLY vocabulary since EC-548: at rest a component is a first-class
|
|
19
|
+
* `elytra/component` sequence node (EC-500); the storage⇄editor projection
|
|
20
|
+
* (`./value`) creates this atom on the way in and converts it back to the tagged
|
|
21
|
+
* sequence node on the way out, so the `componentEmbed` type never reaches
|
|
22
|
+
* storage. A LEGACY stored `componentEmbed` (pre-convergence content) is a
|
|
23
|
+
* foreign node everywhere — carrier in the editor, visible fallback on delivery.
|
|
20
24
|
*
|
|
21
25
|
* The editor shows a placeholder card naming the embedded component (a live in-editor
|
|
22
26
|
* preview would need the runtime renderer + resolvers; deferred). Insertion is wired
|
|
@@ -31,6 +35,8 @@ import { EmbedPropsStage, embedPropsSummary } from './embed-props.js';
|
|
|
31
35
|
* a proper ProseMirror transaction (undoable), immediately per prop change.
|
|
32
36
|
* `readOnly` renders an inert card (no edit/delete).
|
|
33
37
|
*/
|
|
38
|
+
/** The editor-only atom type for a component riding in prose. Never a storage type (EC-548). */
|
|
39
|
+
export const COMPONENT_EMBED_NODE_TYPE = 'componentEmbed';
|
|
34
40
|
export const ComponentEmbed = Node.create({
|
|
35
41
|
name: COMPONENT_EMBED_NODE_TYPE,
|
|
36
42
|
group: 'block',
|
|
@@ -51,12 +57,14 @@ export const ComponentEmbed = Node.create({
|
|
|
51
57
|
return ReactNodeViewRenderer(ComponentEmbedView);
|
|
52
58
|
},
|
|
53
59
|
});
|
|
54
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Read the embedded ComponentNode from a ProseMirror node's attrs, if valid.
|
|
62
|
+
* `safeParseTotal` (EC-304 B-1): a hostile deeply-nested payload degrades to
|
|
63
|
+
* `undefined` (the unknown-embed card), never a throw.
|
|
64
|
+
*/
|
|
55
65
|
function readEmbedded(attrNode) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
attrs: { node: attrNode },
|
|
59
|
-
});
|
|
66
|
+
const parsed = safeParseTotal(componentNodeSchema, attrNode);
|
|
67
|
+
return parsed.success ? parsed.data : undefined;
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
62
70
|
* The presentational embed card — component identity, an optional one-line prop
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-embed.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/component-embed.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react'\nimport type { ReactNodeViewProps } from '@tiptap/react'\nimport { Blocks, Pencil, Trash2 } from 'lucide-react'\nimport { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { EmbedPropsStage, embedPropsSummary } from './embed-props'\n\n/**\n * The rich-text component-embed node (EC-189/EC-314): a block atom holding a single\n * composition value at `attrs.node`. It is the third place constrained composition\n * lives (after the page collection and `blocks` fields) — a registry component\n * embedded in prose. The node carries no ProseMirror content (the embedded component\n * owns its own children through composition slots); `attrs.node` round-trips verbatim\n * so the extractor still indexes the embedded component's references, and the delivery\n * renderer renders it exactly as a `blocks` value.\n *\n * The editor shows a placeholder card naming the embedded component (a live in-editor\n * preview would need the runtime renderer + resolvers; deferred). Insertion is wired\n * from the toolbar / slash menu.\n *\n * Prop editing (EC-430/AD-D): \"Edit settings\" on the card opens the\n * {@link EmbedPropsStage} — the ONE prop form, identical wherever the rich-text\n * editor is mounted (a document field in the form view, a richText PROP in the\n * canvas-tab property inspector, or the expanded rich-text stage). It replaced the\n * 0.0.41 host-sidebar/modal fork, whose sidebar half only rendered in the form view\n * and left the canvas tab's edit affordance dead. Edits rewrite `attrs.node` through\n * a proper ProseMirror transaction (undoable), immediately per prop change.\n * `readOnly` renders an inert card (no edit/delete).\n */\nexport const ComponentEmbed = Node.create({\n name: COMPONENT_EMBED_NODE_TYPE,\n group: 'block',\n atom: true,\n selectable: true,\n draggable: true,\n\n addAttributes() {\n // `node` holds the embedded ComponentNode JSON; storage/JSON only, not markup.\n return { node: { default: null, rendered: false } }\n },\n\n parseHTML() {\n return [{ tag: `div[data-${COMPONENT_EMBED_NODE_TYPE}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['div', mergeAttributes(HTMLAttributes, { [`data-${COMPONENT_EMBED_NODE_TYPE}`]: '' })]\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(ComponentEmbedView)\n },\n})\n\n/** Read the embedded ComponentNode from a ProseMirror node's attrs, if valid. */\nfunction readEmbedded(attrNode: unknown): ComponentNode | undefined {\n return embeddedComponentNode({\n type: COMPONENT_EMBED_NODE_TYPE,\n attrs: { node: attrNode as JsonValue },\n })\n}\n\n/**\n * The presentational embed card — component identity, an optional one-line prop\n * summary, and (only when a handler is supplied) edit / delete affordances. It is\n * kept free of TipTap and registry context so it renders + asserts standalone: an\n * inert (read-only) card is simply the card with no `onEdit`/`onDelete`.\n */\nexport function EmbedCard({\n displayName,\n summary,\n onEdit,\n onDelete,\n}: {\n displayName: string\n summary?: string | undefined\n onEdit?: (() => void) | undefined\n onDelete?: (() => void) | undefined\n}) {\n return (\n <div className=\"flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm\">\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate font-medium text-foreground\">{displayName}</span>\n {summary ? <span className=\"truncate text-xs text-muted-foreground\">{summary}</span> : null}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {translate('richText.embed.badge')}\n </span>\n {onEdit ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('richText.embed.editProps')}\n // EC-430: opening the stage must not hand this click to ProseMirror's\n // selection/focus handling — the stage takes focus, and the editor\n // re-grabbing it would swallow Escape (prosemirror-view preventDefaults\n // Escape whenever its editable is focused).\n onMouseDown={(e) => e.preventDefault()}\n onClick={onEdit}\n >\n <Pencil className=\"size-3.5\" />\n </Button>\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('common.remove')}\n onClick={onDelete}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n ) : null}\n </div>\n )\n}\n\nfunction ComponentEmbedView({\n node,\n updateAttributes,\n deleteNode,\n selected,\n editor,\n}: ReactNodeViewProps) {\n const embedded = readEmbedded(node.attrs.node)\n const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined\n const editable = editor.isEditable\n const [open, setOpen] = useState(false)\n\n const displayName = embedded\n ? componentDisplayName(embedded.componentId, manifest)\n : translate('richText.embed.unknown')\n const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined\n // Editing needs a manifest to know which prop fields to render. A read-only editor\n // never offers the affordance, so the stage is unreachable read-only by\n // construction (and unmounts here if editability flips away while it is open).\n const canEdit = editable && embedded != null && manifest != null\n\n return (\n <NodeViewWrapper data-drag-handle className={cn('my-2', selected && 'rounded-md ring-2 ring-ring/50')}>\n <EmbedCard\n displayName={displayName}\n summary={summary}\n onEdit={canEdit ? () => setOpen(true) : undefined}\n onDelete={editable ? () => deleteNode() : undefined}\n />\n {/* EC-430/AD-D: the ONE embed prop form, on the centered stage. It portals to\n the studio body, so mounting it from inside the node view works identically\n on every rich-text surface (form view, canvas-tab prop inspector, expanded\n rich-text stage — where it rides the layer stack ABOVE the prose stage).\n Each prop edit commits immediately through one updateAttributes transaction;\n the node view re-renders with the fresh attrs, so the open form always\n reflects the committed node. */}\n {open && canEdit && embedded && manifest ? (\n <EmbedPropsStage\n node={embedded}\n manifest={manifest}\n onChange={(next) => updateAttributes({ node: next })}\n onDismiss={() => setOpen(false)}\n />\n ) : null}\n </NodeViewWrapper>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"component-embed.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/component-embed.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAEnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAA;AAC3E,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,gGAAgG;AAChG,MAAM,CAAC,MAAM,yBAAyB,GAAG,gBAAgB,CAAA;AAEzD,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACxC,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,SAAS,EAAE,IAAI;IAEf,aAAa;QACX,+EAA+E;QAC/E,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAA;IACrD,CAAC;IAED,SAAS;QACP,OAAO,CAAC,EAAE,GAAG,EAAE,YAAY,yBAAyB,GAAG,EAAE,CAAC,CAAA;IAC5D,CAAC;IAED,UAAU,CAAC,EAAE,cAAc,EAAE;QAC3B,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,cAAc,EAAE,EAAE,CAAC,QAAQ,yBAAyB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IAChG,CAAC;IAED,WAAW;QACT,OAAO,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IAClD,CAAC;CACF,CAAC,CAAA;AAEF;;;;GAIG;AACH,SAAS,YAAY,CAAC,QAAiB;IACrC,MAAM,MAAM,GAAG,cAAc,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAA;IAC5D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,WAAW,EACX,OAAO,EACP,MAAM,EACN,QAAQ,GAMT;IACC,OAAO,CACL,eAAK,SAAS,EAAC,2FAA2F,aACxG,KAAC,MAAM,IAAC,SAAS,EAAC,uCAAuC,GAAG,EAC5D,gBAAM,SAAS,EAAC,8BAA8B,aAC5C,eAAM,SAAS,EAAC,sCAAsC,YAAE,WAAW,GAAQ,EAC1E,OAAO,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,wCAAwC,YAAE,OAAO,GAAQ,CAAC,CAAC,CAAC,IAAI,IACtF,EACP,eAAM,SAAS,EAAC,wCAAwC,YACrD,SAAS,CAAC,sBAAsB,CAAC,GAC7B,EACN,MAAM,CAAC,CAAC,CAAC,CACR,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,EACd,eAAe,EAAE,KAAK,gBACV,SAAS,CAAC,0BAA0B,CAAC;gBACjD,sEAAsE;gBACtE,mEAAmE;gBACnE,wEAAwE;gBACxE,4CAA4C;gBAC5C,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,EAAE,EACtC,OAAO,EAAE,MAAM,YAEf,KAAC,MAAM,IAAC,SAAS,EAAC,UAAU,GAAG,GACxB,CACV,CAAC,CAAC,CAAC,IAAI,EACP,QAAQ,CAAC,CAAC,CAAC,CACV,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,EACd,eAAe,EAAE,KAAK,gBACV,SAAS,CAAC,eAAe,CAAC,EACtC,OAAO,EAAE,QAAQ,YAEjB,KAAC,MAAM,IAAC,SAAS,EAAC,UAAU,GAAG,GACxB,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,EAC1B,IAAI,EACJ,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,MAAM,GACa;IACnB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACtG,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAA;IAClC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEvC,MAAM,WAAW,GAAG,QAAQ;QAC1B,CAAC,CAAC,oBAAoB,CAAC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;QACtD,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAA;IACvC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC5E,mFAAmF;IACnF,wEAAwE;IACxE,+EAA+E;IAC/E,MAAM,OAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,QAAQ,IAAI,IAAI,CAAA;IAEhE,OAAO,CACL,MAAC,eAAe,8BAAkB,SAAS,EAAE,EAAE,CAAC,MAAM,EAAE,QAAQ,IAAI,gCAAgC,CAAC,aACnG,KAAC,SAAS,IACR,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,EACjD,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,GACnD,EAQD,IAAI,IAAI,OAAO,IAAI,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,CACzC,KAAC,eAAe,IACd,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EACpD,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAC/B,CACH,CAAC,CAAC,CAAC,IAAI,IACQ,CACnB,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react'\nimport type { ReactNodeViewProps } from '@tiptap/react'\nimport { Blocks, Pencil, Trash2 } from 'lucide-react'\nimport { componentNodeSchema, safeParseTotal } from '@elytracms/core/project-graph'\nimport type { ComponentNode } from '@elytracms/core/project-graph'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { EmbedPropsStage, embedPropsSummary } from './embed-props'\n\n/**\n * The flow editor's component atom (EC-189/EC-314, re-scoped by EC-547/EC-548): a\n * block atom holding a single composition value at `attrs.node` — how a registry\n * component appears inside editable prose. The node carries no ProseMirror content\n * (the embedded component owns its own children through composition slots).\n *\n * EDITOR-ONLY vocabulary since EC-548: at rest a component is a first-class\n * `elytra/component` sequence node (EC-500); the storage⇄editor projection\n * (`./value`) creates this atom on the way in and converts it back to the tagged\n * sequence node on the way out, so the `componentEmbed` type never reaches\n * storage. A LEGACY stored `componentEmbed` (pre-convergence content) is a\n * foreign node everywhere — carrier in the editor, visible fallback on delivery.\n *\n * The editor shows a placeholder card naming the embedded component (a live in-editor\n * preview would need the runtime renderer + resolvers; deferred). Insertion is wired\n * from the toolbar / slash menu.\n *\n * Prop editing (EC-430/AD-D): \"Edit settings\" on the card opens the\n * {@link EmbedPropsStage} — the ONE prop form, identical wherever the rich-text\n * editor is mounted (a document field in the form view, a richText PROP in the\n * canvas-tab property inspector, or the expanded rich-text stage). It replaced the\n * 0.0.41 host-sidebar/modal fork, whose sidebar half only rendered in the form view\n * and left the canvas tab's edit affordance dead. Edits rewrite `attrs.node` through\n * a proper ProseMirror transaction (undoable), immediately per prop change.\n * `readOnly` renders an inert card (no edit/delete).\n */\n\n/** The editor-only atom type for a component riding in prose. Never a storage type (EC-548). */\nexport const COMPONENT_EMBED_NODE_TYPE = 'componentEmbed'\n\nexport const ComponentEmbed = Node.create({\n name: COMPONENT_EMBED_NODE_TYPE,\n group: 'block',\n atom: true,\n selectable: true,\n draggable: true,\n\n addAttributes() {\n // `node` holds the embedded ComponentNode JSON; storage/JSON only, not markup.\n return { node: { default: null, rendered: false } }\n },\n\n parseHTML() {\n return [{ tag: `div[data-${COMPONENT_EMBED_NODE_TYPE}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['div', mergeAttributes(HTMLAttributes, { [`data-${COMPONENT_EMBED_NODE_TYPE}`]: '' })]\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(ComponentEmbedView)\n },\n})\n\n/**\n * Read the embedded ComponentNode from a ProseMirror node's attrs, if valid.\n * `safeParseTotal` (EC-304 B-1): a hostile deeply-nested payload degrades to\n * `undefined` (the unknown-embed card), never a throw.\n */\nfunction readEmbedded(attrNode: unknown): ComponentNode | undefined {\n const parsed = safeParseTotal(componentNodeSchema, attrNode)\n return parsed.success ? parsed.data : undefined\n}\n\n/**\n * The presentational embed card — component identity, an optional one-line prop\n * summary, and (only when a handler is supplied) edit / delete affordances. It is\n * kept free of TipTap and registry context so it renders + asserts standalone: an\n * inert (read-only) card is simply the card with no `onEdit`/`onDelete`.\n */\nexport function EmbedCard({\n displayName,\n summary,\n onEdit,\n onDelete,\n}: {\n displayName: string\n summary?: string | undefined\n onEdit?: (() => void) | undefined\n onDelete?: (() => void) | undefined\n}) {\n return (\n <div className=\"flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm\">\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate font-medium text-foreground\">{displayName}</span>\n {summary ? <span className=\"truncate text-xs text-muted-foreground\">{summary}</span> : null}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {translate('richText.embed.badge')}\n </span>\n {onEdit ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('richText.embed.editProps')}\n // EC-430: opening the stage must not hand this click to ProseMirror's\n // selection/focus handling — the stage takes focus, and the editor\n // re-grabbing it would swallow Escape (prosemirror-view preventDefaults\n // Escape whenever its editable is focused).\n onMouseDown={(e) => e.preventDefault()}\n onClick={onEdit}\n >\n <Pencil className=\"size-3.5\" />\n </Button>\n ) : null}\n {onDelete ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('common.remove')}\n onClick={onDelete}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n ) : null}\n </div>\n )\n}\n\nfunction ComponentEmbedView({\n node,\n updateAttributes,\n deleteNode,\n selected,\n editor,\n}: ReactNodeViewProps) {\n const embedded = readEmbedded(node.attrs.node)\n const manifest = embedded ? getStudioRegistry().registry.getManifest(embedded.componentId) : undefined\n const editable = editor.isEditable\n const [open, setOpen] = useState(false)\n\n const displayName = embedded\n ? componentDisplayName(embedded.componentId, manifest)\n : translate('richText.embed.unknown')\n const summary = embedded ? embedPropsSummary(embedded, manifest) : undefined\n // Editing needs a manifest to know which prop fields to render. A read-only editor\n // never offers the affordance, so the stage is unreachable read-only by\n // construction (and unmounts here if editability flips away while it is open).\n const canEdit = editable && embedded != null && manifest != null\n\n return (\n <NodeViewWrapper data-drag-handle className={cn('my-2', selected && 'rounded-md ring-2 ring-ring/50')}>\n <EmbedCard\n displayName={displayName}\n summary={summary}\n onEdit={canEdit ? () => setOpen(true) : undefined}\n onDelete={editable ? () => deleteNode() : undefined}\n />\n {/* EC-430/AD-D: the ONE embed prop form, on the centered stage. It portals to\n the studio body, so mounting it from inside the node view works identically\n on every rich-text surface (form view, canvas-tab prop inspector, expanded\n rich-text stage — where it rides the layer stack ABOVE the prose stage).\n Each prop edit commits immediately through one updateAttributes transaction;\n the node view re-renders with the fresh attrs, so the open form always\n reflects the committed node. */}\n {open && canEdit && embedded && manifest ? (\n <EmbedPropsStage\n node={embedded}\n manifest={manifest}\n onChange={(next) => updateAttributes({ node: next })}\n onDismiss={() => setOpen(false)}\n />\n ) : null}\n </NodeViewWrapper>\n )\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface RichTextEditorProps {
|
|
2
|
-
/** The stored
|
|
2
|
+
/** The stored value (v2 `{ version: 2, content }`; staging: v1/legacy forms). */
|
|
3
3
|
value: unknown;
|
|
4
4
|
onChange: (value: unknown) => void;
|
|
5
5
|
readOnly?: boolean;
|
|
@@ -9,7 +9,7 @@ import { useOptionalCollectionView } from '../../collection-view/collection-view
|
|
|
9
9
|
import { useOptionalAssets } from '../../assets/assets-store.js';
|
|
10
10
|
import { AssetPicker } from '../asset-picker.js';
|
|
11
11
|
import { buildRichTextExtensions } from './extensions.js';
|
|
12
|
-
import { isEffectivelyEmpty, isEquivalentToStored, normalizeEditorDoc,
|
|
12
|
+
import { blockSequenceValueFromDoc, isEffectivelyEmpty, isEquivalentToStored, normalizeEditorDoc, toEditorDoc, } from './value.js';
|
|
13
13
|
import { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert.js';
|
|
14
14
|
import { EmbedPicker } from './embed-picker.js';
|
|
15
15
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
@@ -103,7 +103,7 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow, l
|
|
|
103
103
|
? stored
|
|
104
104
|
: isEffectivelyEmpty(current)
|
|
105
105
|
? undefined
|
|
106
|
-
:
|
|
106
|
+
: blockSequenceValueFromDoc(current);
|
|
107
107
|
lastEmitted.current = next;
|
|
108
108
|
onChangeRef.current(next);
|
|
109
109
|
},
|
|
@@ -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 '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/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 {\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n richTextValueFromDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */\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 const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : richTextValueFromDoc(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 useEffect(() => {\n editor?.setEditable(!readOnly)\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=\"image/*\"\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":";AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE5D,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,+CAA+C,CAAA;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EACL,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,GACZ,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAqB3C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,GAAG,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAuB;IACzG,kFAAkF;IAClF,mFAAmF;IACnF,MAAM,MAAM,GAAG,yBAAyB,EAAE,CAAA;IAC1C,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAElC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjD,gFAAgF;IAChF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE/C,MAAM,QAAQ,GAAG,MAAM,IAAI,IAAI,CAAA;IAC/B,8EAA8E;IAC9E,mEAAmE;IACnE,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,iBAAiB,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAChE,CAAC,KAAK,CAAC,CACR,CAAA;IACD,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAA;IAEtC,oFAAoF;IACpF,0EAA0E;IAC1E,MAAM,WAAW,GAAG,OAAO,CAAe,GAAG,EAAE;QAC7C,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAA;QACtB,MAAM,GAAG,GAAiB,EAAE,CAAA;QAC5B,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,UAAU,CAAC,EAAE,KAAK,MAAM,CAAC,iBAAiB;gBAAE,SAAQ;YACxD,MAAM,eAAe,GAAG,cAAc,CAAC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;YACrF,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5D,MAAM,GAAG,GAAG,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAA;gBACrD,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,eAAe,EAAE,CAAC,CAAA;YAChE,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IAEZ,kEAAkE;IAClE,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;IACxB,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IACpC,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAA;IAC9B,4EAA4E;IAC5E,MAAM,WAAW,GAAG,MAAM,CAAU,KAAK,CAAC,CAAA;IAE1C,gFAAgF;IAChF,qEAAqE;IACrE,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACjE,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAEjE,MAAM,MAAM,GAAG,SAAS,CAAC;QACvB,UAAU,EAAE,uBAAuB,CAAC;YAClC,KAAK,EAAE;gBACL,OAAO,EAAE,SAAS;gBAClB,4EAA4E;gBAC5E,sEAAsE;gBACtE,KAAK,EAAE,QAAQ;oBACb,CAAC,CAAC;wBACE,UAAU,EAAE,UAAU;wBACtB,MAAM,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;4BAC9B,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAA;4BAC3C,oBAAoB,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;wBACtD,CAAC;qBACF;oBACH,CAAC,CAAC,SAAS;aACd;SACF,CAAC;QACF,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC;QAC3B,QAAQ,EAAE,CAAC,QAAQ;QACnB,WAAW,EAAE;YACX,UAAU,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;SAC/D;QACD,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;YACvB,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAA4B,CAAC,CAAA;YAC9E,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAA;YAC/B,4EAA4E;YAC5E,4EAA4E;YAC5E,MAAM,IAAI,GAAG,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC;gBAChD,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC;oBAC3B,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,yBAAyB,CAAC,OAAO,CAAC,CAAA;YACxC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAA;YAC1B,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;KACF,CAAC,CAAA;IAEF,+EAA+E;IAC/E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,IAAI,KAAK,KAAK,WAAW,CAAC,OAAO;YAAE,OAAM;QACzC,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAA4B,CAAC,CAAA;QAC9E,IAAI,oBAAoB,CAAC,OAAO,EAAE,KAAK,CAAC;YAAE,OAAM;QAChD,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IACvE,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;IAEnB,kFAAkF;IAClF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAA;IAChC,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEtB,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,4EAA4E;IAC5E,kFAAkF;IAClF,iFAAiF;IACjF,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,kDAAkD;IAClD,MAAM,OAAO,GAAG,CACd,8BACG,CAAC,QAAQ,CAAC,CAAC,CAAC,CACX,KAAC,eAAe,IACd,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,WAAW,EACpB,WAAW,EAAE,SAAS,EACtB,WAAW,EAAE,SAAS,EACtB,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAC5C,CACH,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,aAAa,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC,QAAQ,IAAI,6BAA6B,CAAC,GAAI,IAC1F,CACJ,CAAA;IAED,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CACX,+EAA+E,EAC/E,wEAAwE,EACxE,QAAQ,IAAI,YAAY,CACzB,aAEA,QAAQ,CAAC,CAAC,CAAC,CACV,8BACE,KAAC,yBAAyB,IAAC,UAAU,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,GAAI,EACnE,KAAC,mBAAmB,IAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,YACpE,OAAO,GACY,IACrB,CACJ,CAAC,CAAC,CAAC,CACF,OAAO,CACR,EACA,QAAQ,CAAC,CAAC,CAAC,CACV,KAAC,WAAW,IACV,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,IAAI,EACX,MAAM,EAAC,SAAS,EAChB,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE;oBACpB,YAAY,CAAC,KAAK,CAAC,CAAA;oBACnB,IAAI,OAAO;wBAAE,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;gBACxD,CAAC,GACD,CACH,CAAC,CAAC,CAAC,IAAI,EACP,QAAQ,CAAC,CAAC,CAAC,CACV,KAAC,WAAW,IACV,IAAI,EAAE,SAAS,EACf,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,oBAAoB,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAChF,CACH,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GACxC,wHAAwH,CAAA;AAE1H;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,EAAE,UAAU,EAA8B;IAClF,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,UAAU,EACnB,SAAS,EAAC,wIAAwI,aAElJ,KAAC,SAAS,IAAC,SAAS,EAAC,QAAQ,GAAG,EAChC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,gCAAgC,CAAC,GACvC,IACA,CACV,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAC/C,CAAC,CAAC,SAAS,CAAC,gCAAgC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC/D,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,mBAAmB,CAAC,EAC3B,QAAQ,EACR,UAAU,EACV,KAAK,GAMN;IACC,OAAO,CACL,KAAC,iBAAiB,IAChB,KAAK,EAAE,kBAAkB,CAAC,KAAK,CAAC,EAChC,UAAU,EAAE,SAAS,CAAC,0BAA0B,CAAC,EACjD,OAAO,EAAC,OAAO,EACf,MAAM,EAAC,OAAO,EACd,SAAS,EAAE,UAAU,YAErB,cAAK,SAAS,EAAC,mEAAmE,YAC/E,QAAQ,GACL,GACY,CACrB,CAAA;AACH,CAAC","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 '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/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 {\n blockSequenceValueFromDoc,\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/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 const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : blockSequenceValueFromDoc(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 useEffect(() => {\n editor?.setEditable(!readOnly)\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=\"image/*\"\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"]}
|
|
@@ -17,6 +17,10 @@ import type { SlashCommandOptions } from './slash-command.js';
|
|
|
17
17
|
* - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
|
|
18
18
|
* atoms (asset image, embedded registry component) so existing content of those
|
|
19
19
|
* kinds survives a round-trip verbatim rather than being dropped by the schema.
|
|
20
|
+
* - {@link UnknownNodeCarrier} (EC-547) is the editor-only opaque atom the
|
|
21
|
+
* storage⇄editor projection wraps any OTHER stored node type into, so the
|
|
22
|
+
* closed ProseMirror schema can never drop a node the flow editor does not
|
|
23
|
+
* natively edit — the EC-500 D3 round-trip law.
|
|
20
24
|
* - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run
|
|
21
25
|
* round-trips (an undeclared mark would be silently dropped on load).
|
|
22
26
|
*
|
|
@@ -5,6 +5,7 @@ import { RefLink } from './ref-link.js';
|
|
|
5
5
|
import { FontSize } from './font-size.js';
|
|
6
6
|
import { AssetImage } from './asset-image.js';
|
|
7
7
|
import { ComponentEmbed } from './component-embed.js';
|
|
8
|
+
import { UnknownNodeCarrier } from './unknown-node.js';
|
|
8
9
|
import { createSlashCommand } from './slash-command.js';
|
|
9
10
|
/**
|
|
10
11
|
* The TipTap extension set for the CMS rich-text editor (EC-314).
|
|
@@ -23,6 +24,10 @@ import { createSlashCommand } from './slash-command.js';
|
|
|
23
24
|
* - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
|
|
24
25
|
* atoms (asset image, embedded registry component) so existing content of those
|
|
25
26
|
* kinds survives a round-trip verbatim rather than being dropped by the schema.
|
|
27
|
+
* - {@link UnknownNodeCarrier} (EC-547) is the editor-only opaque atom the
|
|
28
|
+
* storage⇄editor projection wraps any OTHER stored node type into, so the
|
|
29
|
+
* closed ProseMirror schema can never drop a node the flow editor does not
|
|
30
|
+
* natively edit — the EC-500 D3 round-trip law.
|
|
26
31
|
* - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run
|
|
27
32
|
* round-trips (an undeclared mark would be silently dropped on load).
|
|
28
33
|
*
|
|
@@ -42,6 +47,7 @@ export function buildRichTextExtensions(options = {}) {
|
|
|
42
47
|
FontSize,
|
|
43
48
|
AssetImage,
|
|
44
49
|
ComponentEmbed,
|
|
50
|
+
UnknownNodeCarrier,
|
|
45
51
|
Placeholder.configure({ placeholder: translate('richText.placeholder') }),
|
|
46
52
|
createSlashCommand(options.slash ?? {}),
|
|
47
53
|
];
|
|
@@ -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,4BAA4B,CAAA;AACtD,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,iBAAiB,CAAA;AAGpD
|
|
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,4BAA4B,CAAA;AACtD,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 '@elytracms/studio/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,7 +1,7 @@
|
|
|
1
1
|
import { assetNode } from '@elytracms/core/cms-core';
|
|
2
|
-
import { COMPONENT_EMBED_NODE_TYPE } from '@elytracms/core/rich-text';
|
|
3
2
|
import { staticProp } from '@elytracms/core/project-graph';
|
|
4
3
|
import { isPrimitive } from '@elytracms/core/component-registry';
|
|
4
|
+
import { COMPONENT_EMBED_NODE_TYPE } from './component-embed.js';
|
|
5
5
|
/**
|
|
6
6
|
* Reference-carrying node insertions shared by the toolbar controls and the slash
|
|
7
7
|
* menu (EC-314). Centralised so both entry points produce byte-identical envelopes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/insert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,
|
|
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 { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { isPrimitive } 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/** 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 const record = assets?.assets.find((a) => a.id === assetId)\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'image',\n attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },\n })\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/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */\nexport function embedNodeFromManifest(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: newEmbedId(), componentId: manifest.id, props }\n}\n\n/**\n * The components embeddable in a rich-text field: the field's `allow` list, or every\n * non-primitive component when open — matching the `blocks` field and the\n * `validateRichTextEmbeds` leash.\n */\nexport function listEmbeddable(\n allow: string[] | undefined,\n registered: ComponentManifest[],\n): ComponentManifest[] {\n const permitted =\n allow && allow.length > 0\n ? registered.filter((m) => allow.includes(m.id))\n : registered.filter((m) => !isPrimitive(m.id))\n return permitted.filter((m) => m.clientRenderable !== false)\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"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Node } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* The generic unknown-node carrier (EC-547, per EC-500 D3).
|
|
4
|
+
*
|
|
5
|
+
* A ProseMirror schema is CLOSED: any node type it does not declare is silently
|
|
6
|
+
* dropped on load, and the first keystroke persists the loss. That makes the
|
|
7
|
+
* flow editor a data shredder for any stored node it does not natively edit —
|
|
8
|
+
* exactly what the converged block sequence must survive (the anti-Gutenberg
|
|
9
|
+
* round-trip law: a projection may only rewrite nodes it natively edits; every
|
|
10
|
+
* other node survives byte-for-byte).
|
|
11
|
+
*
|
|
12
|
+
* So the storage⇄editor projection ({@link ../value}) wraps every node whose
|
|
13
|
+
* type is outside the editor's native vocabulary into this block atom on the
|
|
14
|
+
* way IN — the ORIGINAL node rides verbatim in the single JSON `node` attr
|
|
15
|
+
* (the `componentEmbed` pattern; in-repo precedent #2 is the dom-editor's
|
|
16
|
+
* `PRESERVE_NODE_ATTR` verbatim-JSON carry) — and unwraps it byte-for-byte on
|
|
17
|
+
* the way OUT. The NodeView renders a visibly-labelled read-only block naming
|
|
18
|
+
* the carried type, matching the server renderer's unknown fallback
|
|
19
|
+
* (`rich-text/render.tsx`: "Unknown content block: <type>") so both surfaces
|
|
20
|
+
* speak the same vocabulary. Never a silent drop, never a crash.
|
|
21
|
+
*
|
|
22
|
+
* The carrier exists ONLY in the editor document — it is never written to
|
|
23
|
+
* storage (the projection-out replaces it with its carried payload), so a
|
|
24
|
+
* stored node that happened to reuse this type name would itself be wrapped on
|
|
25
|
+
* the way in and restored untouched on the way out: misinterpretation is
|
|
26
|
+
* structurally impossible, not policed.
|
|
27
|
+
*
|
|
28
|
+
* **Two declared, deliberate limits (EC-550, measured and DECLINED for now):**
|
|
29
|
+
*
|
|
30
|
+
* 1. *Unknown MARKS still shred.* A foreign mark type on a text run inside a
|
|
31
|
+
* natively-edited node is dropped by ProseMirror's closed mark schema on
|
|
32
|
+
* load, and the first keystroke persists the loss (foreign marks inside a
|
|
33
|
+
* CARRIED node survive in its payload). Declined because the failure is
|
|
34
|
+
* bounded — the text itself survives, only the annotation drops — and the
|
|
35
|
+
* mark vocabulary is fixed and global today, so no writer can produce a
|
|
36
|
+
* foreign mark short of hand-written JSON. A mark-preserving projection
|
|
37
|
+
* (marks copied per-run to a carrier attr) is the design if a real producer
|
|
38
|
+
* ever lands (registry-extensible marks, external/AI writers).
|
|
39
|
+
* 2. *Inline-position unknowns relocate.* This carrier is a BLOCK atom; a
|
|
40
|
+
* foreign node inside a paragraph's inline content is wrapped as a block and
|
|
41
|
+
* ProseMirror's fitting moves it out of the inline position — it survives
|
|
42
|
+
* byte-for-byte but changes position on save. Declined for the same reason:
|
|
43
|
+
* there is no inline foreign-node producer today (the engine's own inline
|
|
44
|
+
* vocabulary is closed), and the fix needs a second inline carrier plus a
|
|
45
|
+
* position-aware projection. Revisit alongside (1).
|
|
46
|
+
*/
|
|
47
|
+
/** The editor-only node type of the opaque carrier. Never a storage type. */
|
|
48
|
+
export declare const UNKNOWN_NODE_CARRIER_TYPE = "unknownCarrier";
|
|
49
|
+
export declare const UnknownNodeCarrier: Node<any, any>;
|
|
50
|
+
/**
|
|
51
|
+
* The carried node's `type` for the visible label; `'unknown'` when the payload
|
|
52
|
+
* is not even node-shaped (the carrier is total — it can hold any JSON).
|
|
53
|
+
*/
|
|
54
|
+
export declare function carriedNodeType(carried: unknown): string;
|
|
55
|
+
/**
|
|
56
|
+
* The presentational carrier block — a labelled, read-only card naming the
|
|
57
|
+
* carried node type. Kept free of TipTap context so it renders + asserts
|
|
58
|
+
* standalone (the house `renderToStaticMarkup` smoke pattern). The label rides
|
|
59
|
+
* the studio message catalog (`richText.carrier.unknown`, EC-550) and matches
|
|
60
|
+
* the server renderer's unknown-node fallback vocabulary (`data-unknown-node` /
|
|
61
|
+
* "Unknown content block: <type>" — deliberately unlocalized there: delivery
|
|
62
|
+
* has no viewer-locale catalog).
|
|
63
|
+
*/
|
|
64
|
+
export declare function UnknownNodeCard({ nodeType }: {
|
|
65
|
+
nodeType: string;
|
|
66
|
+
}): import("react").JSX.Element;
|