@elytracms/studio 0.0.97 → 0.0.98

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 (38) hide show
  1. package/convex/assets.ts +16 -2
  2. package/convex/ingest-attempt.test.ts +161 -0
  3. package/convex/ingest.test.ts +185 -9
  4. package/convex/ingest.ts +136 -24
  5. package/convex/prototype-key.test.ts +92 -0
  6. package/convex/records-zod-refinements.convex.test.ts +94 -0
  7. package/dist/components/cms/cms-store.d.ts +2 -4
  8. package/dist/components/cms/cms-store.js +4 -22
  9. package/dist/components/cms/cms-store.js.map +1 -1
  10. package/dist/components/cms/field-control.d.ts +12 -1
  11. package/dist/components/cms/field-control.js +16 -1
  12. package/dist/components/cms/field-control.js.map +1 -1
  13. package/dist/components/cms/import-batch.d.ts +23 -0
  14. package/dist/components/cms/import-batch.js +43 -0
  15. package/dist/components/cms/import-batch.js.map +1 -0
  16. package/dist/components/cms/rich-text/editor.js +13 -10
  17. package/dist/components/cms/rich-text/editor.js.map +1 -1
  18. package/dist/components/cms/rich-text/value.d.ts +9 -0
  19. package/dist/components/cms/rich-text/value.js +13 -0
  20. package/dist/components/cms/rich-text/value.js.map +1 -1
  21. package/dist/components/cms/use-document-draft.d.ts +35 -2
  22. package/dist/components/cms/use-document-draft.js +42 -15
  23. package/dist/components/cms/use-document-draft.js.map +1 -1
  24. package/dist/components/records/record-lens.js.map +1 -1
  25. package/dist/lib/engine-version.d.ts +1 -1
  26. package/dist/lib/engine-version.js +1 -1
  27. package/dist/lib/engine-version.js.map +1 -1
  28. package/dist/lib/i18n/messages.de.js +3 -0
  29. package/dist/lib/i18n/messages.de.js.map +1 -1
  30. package/dist/lib/i18n/messages.en.d.ts +3 -0
  31. package/dist/lib/i18n/messages.en.js +3 -0
  32. package/dist/lib/i18n/messages.en.js.map +1 -1
  33. package/dist/lib/operations/cms-persistence.d.ts +9 -4
  34. package/dist/lib/operations/cms-persistence.js +132 -29
  35. package/dist/lib/operations/cms-persistence.js.map +1 -1
  36. package/dist/lib/persona/labels.js +1 -0
  37. package/dist/lib/persona/labels.js.map +1 -1
  38. package/package.json +4 -3
@@ -1 +1 @@
1
- {"version":3,"file":"value.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/value.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createBlockSequenceValue,\n isBlockSequenceValueShape,\n componentBlockNode,\n richTextDocsEquivalent,\n} from '@elytracms/core/rich-text'\nimport type {\n BlockNode,\n BlockSequenceValue,\n RichTextDoc,\n RichTextNode,\n TypographyNode,\n} from '@elytracms/core/rich-text'\nimport { isComponentBlockNode, isComponentNode } from '@elytracms/core/project-graph'\nimport type { JsonValue } from '@elytracms/core/project-graph'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport { UNKNOWN_NODE_CARRIER_TYPE } from './unknown-node'\n\n/**\n * Storage ⟷ editor projection for the CMS TipTap flow editor (EC-314, converged\n * by EC-547 per the EC-500 design).\n *\n * Storage is the converged block sequence `{ version: 2, content: BlockNode[] }`\n * — typography nodes byte-identical to the historical rich-text shape plus\n * first-class `elytra/component` nodes. TipTap requires ITS grammar (a\n * `{ type: 'doc', content }` document over a CLOSED schema), so this module is\n * the just-in-time projection between the two:\n *\n * - **Way in** ({@link toEditorDoc}): typography passes near-identically; an\n * `elytra/component` node becomes the existing ComponentEmbed atom (the\n * original node riding verbatim in `attrs.node`); any node type the editor\n * does not natively edit is wrapped into the opaque\n * {@link UNKNOWN_NODE_CARRIER_TYPE} atom so the closed ProseMirror schema\n * cannot drop it.\n * - **Way out** ({@link blockSequenceValueFromDoc}): embed atoms convert back to\n * tagged `elytra/component` nodes, carriers restore their carried JSON\n * byte-for-byte, typography is re-emitted with {@link normalizeEditorDoc}'s\n * lean-attrs canonicalisation applied.\n *\n * The round-trip law (EC-500 D3): the projection may only rewrite nodes the flow\n * editor natively edits; every other node survives byte-for-byte.\n *\n * Two editor-ergonomics concerns carry over unchanged from EC-314:\n *\n * 1. **Lean canonicalisation.** ProseMirror stamps every declared attribute onto a\n * node/mark, so TipTap emits `null` defaults the lean storage convention omits\n * (`link` → `ref: null`, `codeBlock` → `language: null`, `image` → `title: null`).\n * {@link normalizeEditorDoc} prunes those so an untouched document round-trips to\n * the same shape the extractor/renderer already read.\n *\n * 2. **No false dirty.** Opening a field must not mark it changed, and typing then\n * undoing back to the original must clear dirty. {@link isEquivalentToStored}\n * compares semantically in editor-projection space so the editor can re-emit the\n * *exact stored value* when nothing really changed.\n */\n\n/**\n * The storage node types the flow editor natively edits — the projection's ONE\n * wrap/pass decision: anything outside this set rides through the editor inside\n * the opaque carrier atom. Everything listed here MUST be declared by\n * `buildRichTextExtensions`' ProseMirror schema (an undeclared type would be\n * silently dropped); `extensions.test.ts` pins the two sets equal, so drift\n * between this declaration and the schema is loud.\n *\n * The ProseMirror schema declares TWO more types than this set: the\n * ComponentEmbed atom and the carrier — both editor-only projection vehicles\n * that are never valid STORAGE vocabulary (EC-548 retired `componentEmbed` at\n * rest). Excluding them here is deliberate and load-bearing: a STORED node\n * reusing either name is a foreigner and rides the carrier byte-for-byte,\n * instead of being misread as an editor atom (which for a stored\n * `componentEmbed` would silently rewrite it to `elytra/component` on save —\n * a compat migration the owner ruled out).\n */\nexport const EDITOR_NATIVE_NODE_TYPES: ReadonlySet<string> = new Set([\n 'doc',\n 'paragraph',\n 'text',\n 'heading',\n 'bulletList',\n 'orderedList',\n 'listItem',\n 'blockquote',\n 'codeBlock',\n 'hardBreak',\n 'horizontalRule',\n 'image',\n])\n\n/** ProseMirror node types that carry no reference/atom payload and render as prose. */\nconst EMPTY_TOLERANT_BLOCKS = new Set(['paragraph', 'heading'])\n\n/**\n * Project whatever a body-capable field currently holds into an editor document.\n *\n * Accepts the canonical v2 `{ version: 2, content }` sequence (object or JSON\n * string) and projects it per the module contract. Anything unparseable becomes\n * an empty document (never throws; validation is first-class elsewhere). The\n * projection is deliberately TOTAL and shape-driven, not schema-validating: a\n * v2 value with one malformed node must still open (the bad node rides in a\n * carrier, visibly labelled) rather than blanking the whole field — the editor\n * is not the validator.\n */\nexport function toEditorDoc(value: unknown): RichTextDoc {\n const sequence = asBlockSequenceShape(value)\n if (sequence) {\n return { type: 'doc', content: sequence.content.map(projectStoredNode) }\n }\n // Anything that is not the v2 sequence (a pre-convergence envelope, a bare\n // doc, legacy plaintext, junk) opens EMPTY — deliberately not projected:\n // validation names those shapes with the explicit \"re-seed\" issue (EC-548\n // deleted the staging acceptance), and quietly editing over them here would\n // hide it.\n return { type: 'doc', content: [] }\n}\n\n/** Structural v2 test, tolerating a JSON-string form of the same value. */\nfunction asBlockSequenceShape(value: unknown): { version: 2; content: unknown[] } | undefined {\n if (isBlockSequenceValueShape(value)) return value\n if (typeof value === 'string') {\n try {\n const parsed: unknown = JSON.parse(value)\n if (isBlockSequenceValueShape(parsed)) return parsed\n } catch {\n /* not JSON — fall through */\n }\n }\n return undefined\n}\n\n/**\n * One stored node → its editor projection. Total over arbitrary JSON:\n *\n * - a tagged component node → the ComponentEmbed atom, the node itself riding\n * verbatim (tag included) in `attrs.node`;\n * - a node type the editor natively edits → passed through, `content` recursed\n * (so a component nested in a blockquote still projects to an embed);\n * - EVERYTHING else — foreign types, a tagged-but-malformed component node,\n * even non-node JSON in the content array — → the opaque carrier atom, the\n * original payload riding verbatim in `attrs.node`.\n */\nfunction projectStoredNode(node: unknown): RichTextNode {\n if (isComponentBlockNode(node)) {\n return { type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: node as unknown as JsonValue } }\n }\n if (isEditorNativeNode(node)) {\n const out: RichTextNode = { ...node }\n if (node.content) out.content = node.content.map(projectStoredNode)\n return out\n }\n return { type: UNKNOWN_NODE_CARRIER_TYPE, attrs: { node: node as JsonValue } }\n}\n\nfunction isEditorNativeNode(node: unknown): node is RichTextNode {\n return (\n typeof node === 'object' &&\n node !== null &&\n !Array.isArray(node) &&\n typeof (node as { type?: unknown }).type === 'string' &&\n EDITOR_NATIVE_NODE_TYPES.has((node as { type: string }).type)\n )\n}\n\n/**\n * Project a (normalized) editor document back into the stored v2 sequence —\n * the way-out half of the module contract: embed atoms → tagged\n * `elytra/component` nodes, carriers → their carried JSON byte-for-byte,\n * typography re-emitted as-is (lean canonicalisation is\n * {@link normalizeEditorDoc}'s job, upstream).\n */\nexport function blockSequenceValueFromDoc(doc: RichTextDoc): BlockSequenceValue {\n return createBlockSequenceValue((doc.content ?? []).map(restoreStoredNode))\n}\n\nfunction restoreStoredNode(node: RichTextNode): BlockNode {\n if (node.type === COMPONENT_EMBED_NODE_TYPE) {\n const embedded: unknown = node.attrs?.node\n if (isComponentNode(embedded)) {\n // Re-tag through the one constructor (identity on already-tagged nodes),\n // so an atom payload the node factory built untagged leaves the editor\n // as a first-class tagged citizen — \"write current\".\n const { type: _tag, ...rest } = embedded\n return componentBlockNode(rest)\n }\n // An embed whose payload is missing/malformed: keep the node verbatim as\n // typography — never a silent drop; validation reports it elsewhere.\n return node as TypographyNode\n }\n if (node.type === UNKNOWN_NODE_CARRIER_TYPE) {\n const carried: unknown = node.attrs?.node\n // Byte-for-byte restore of the carried original (the D3 round-trip law).\n // An empty carrier (nothing to restore — not constructible via the\n // projection) degrades to the carrier node itself staying visible.\n if (carried !== null && carried !== undefined) return carried as BlockNode\n return node as TypographyNode\n }\n const out: TypographyNode = { ...(node as TypographyNode) }\n if (node.content) out.content = node.content.map(restoreStoredNode)\n return out\n}\n\n/**\n * Prune the `null` attribute values ProseMirror stamps from declared-but-unset\n * attributes, and drop the resulting empty `attrs` bags, so the editor's output\n * matches the lean storage convention. Recurses `content`; leaves `marks` attrs\n * pruned the same way. Pure; returns a fresh tree. (Embed and carrier payloads\n * are single non-null `attrs` entries, so pruning passes them through by\n * reference — their bytes are never rewritten here.)\n */\nexport function normalizeEditorDoc(doc: RichTextDoc): RichTextDoc {\n return { type: 'doc', content: (doc.content ?? []).map(pruneNode) }\n}\n\nfunction pruneNode(node: RichTextNode): RichTextNode {\n const out: RichTextNode = { type: node.type }\n const attrs = pruneAttrs(node.attrs)\n if (attrs) out.attrs = attrs\n if (typeof node.text === 'string') out.text = node.text\n if (node.marks && node.marks.length > 0) {\n out.marks = node.marks.map((mark) => {\n const markAttrs = pruneAttrs(mark.attrs)\n return markAttrs ? { type: mark.type, attrs: markAttrs } : { type: mark.type }\n })\n }\n if (node.content) out.content = node.content.map(pruneNode)\n return out\n}\n\nfunction pruneAttrs(\n attrs: Record<string, unknown> | undefined,\n): Record<string, never> | undefined {\n if (!attrs) return undefined\n const entries = Object.entries(attrs).filter(([, v]) => v !== null && v !== undefined)\n return entries.length > 0 ? (Object.fromEntries(entries) as Record<string, never>) : undefined\n}\n\n/**\n * Whether a document carries no author-visible content — only empty paragraphs or\n * headings (TipTap keeps a mandatory trailing block even when the field is blank).\n * Any text, atom (image/embed/carrier/hardBreak/hr), list, or blockquote makes it\n * non-empty.\n */\nexport function isEffectivelyEmpty(doc: RichTextDoc): boolean {\n return (doc.content ?? []).every(isEmptyBlock)\n}\n\nfunction isEmptyBlock(node: RichTextNode): boolean {\n if (!EMPTY_TOLERANT_BLOCKS.has(node.type)) return false\n return (node.content ?? []).every(\n (child) => child.type === 'text' && (child.text ?? '').length === 0,\n )\n}\n\n/**\n * Whether the (already normalized) editor document is semantically the stored one —\n * so the editor can re-emit the exact stored value instead of a re-serialization,\n * keeping the field un-dirtied. Both sides are compared in editor-projection space\n * ({@link toEditorDoc} is deterministic), so a stored v2 component/carrier and its\n * on-screen atom compare equal. Two empty documents are always equivalent (the\n * trailing-paragraph difference between `content: []` and `[{ paragraph }]` is not\n * a change); otherwise defers to the shared structural comparison.\n */\nexport function isEquivalentToStored(current: RichTextDoc, storedValue: unknown): boolean {\n const stored = toEditorDoc(storedValue)\n if (isEffectivelyEmpty(current) && isEffectivelyEmpty(stored)) return true\n return richTextDocsEquivalent(current, stored)\n}\n"]}
1
+ {"version":3,"file":"value.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/value.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createBlockSequenceValue,\n isBlockSequenceValueShape,\n componentBlockNode,\n richTextDocsEquivalent,\n} from '@elytracms/core/rich-text'\nimport type {\n BlockNode,\n BlockSequenceValue,\n RichTextDoc,\n RichTextNode,\n TypographyNode,\n} from '@elytracms/core/rich-text'\nimport { isComponentBlockNode, isComponentNode } from '@elytracms/core/project-graph'\nimport type { JsonValue } from '@elytracms/core/project-graph'\nimport { COMPONENT_EMBED_NODE_TYPE } from './component-embed'\nimport { UNKNOWN_NODE_CARRIER_TYPE } from './unknown-node'\n\n/**\n * Storage ⟷ editor projection for the CMS TipTap flow editor (EC-314, converged\n * by EC-547 per the EC-500 design).\n *\n * Storage is the converged block sequence `{ version: 2, content: BlockNode[] }`\n * — typography nodes byte-identical to the historical rich-text shape plus\n * first-class `elytra/component` nodes. TipTap requires ITS grammar (a\n * `{ type: 'doc', content }` document over a CLOSED schema), so this module is\n * the just-in-time projection between the two:\n *\n * - **Way in** ({@link toEditorDoc}): typography passes near-identically; an\n * `elytra/component` node becomes the existing ComponentEmbed atom (the\n * original node riding verbatim in `attrs.node`); any node type the editor\n * does not natively edit is wrapped into the opaque\n * {@link UNKNOWN_NODE_CARRIER_TYPE} atom so the closed ProseMirror schema\n * cannot drop it.\n * - **Way out** ({@link blockSequenceValueFromDoc}): embed atoms convert back to\n * tagged `elytra/component` nodes, carriers restore their carried JSON\n * byte-for-byte, typography is re-emitted with {@link normalizeEditorDoc}'s\n * lean-attrs canonicalisation applied.\n *\n * The round-trip law (EC-500 D3): the projection may only rewrite nodes the flow\n * editor natively edits; every other node survives byte-for-byte.\n *\n * Two editor-ergonomics concerns carry over unchanged from EC-314:\n *\n * 1. **Lean canonicalisation.** ProseMirror stamps every declared attribute onto a\n * node/mark, so TipTap emits `null` defaults the lean storage convention omits\n * (`link` → `ref: null`, `codeBlock` → `language: null`, `image` → `title: null`).\n * {@link normalizeEditorDoc} prunes those so an untouched document round-trips to\n * the same shape the extractor/renderer already read.\n *\n * 2. **No false dirty.** Opening a field must not mark it changed, and typing then\n * undoing back to the original must clear dirty. {@link isEquivalentToStored}\n * compares semantically in editor-projection space so the editor can re-emit the\n * *exact stored value* when nothing really changed.\n */\n\n/**\n * The storage node types the flow editor natively edits — the projection's ONE\n * wrap/pass decision: anything outside this set rides through the editor inside\n * the opaque carrier atom. Everything listed here MUST be declared by\n * `buildRichTextExtensions`' ProseMirror schema (an undeclared type would be\n * silently dropped); `extensions.test.ts` pins the two sets equal, so drift\n * between this declaration and the schema is loud.\n *\n * The ProseMirror schema declares TWO more types than this set: the\n * ComponentEmbed atom and the carrier — both editor-only projection vehicles\n * that are never valid STORAGE vocabulary (EC-548 retired `componentEmbed` at\n * rest). Excluding them here is deliberate and load-bearing: a STORED node\n * reusing either name is a foreigner and rides the carrier byte-for-byte,\n * instead of being misread as an editor atom (which for a stored\n * `componentEmbed` would silently rewrite it to `elytra/component` on save —\n * a compat migration the owner ruled out).\n */\nexport const EDITOR_NATIVE_NODE_TYPES: ReadonlySet<string> = new Set([\n 'doc',\n 'paragraph',\n 'text',\n 'heading',\n 'bulletList',\n 'orderedList',\n 'listItem',\n 'blockquote',\n 'codeBlock',\n 'hardBreak',\n 'horizontalRule',\n 'image',\n])\n\n/** ProseMirror node types that carry no reference/atom payload and render as prose. */\nconst EMPTY_TOLERANT_BLOCKS = new Set(['paragraph', 'heading'])\n\n/**\n * Project whatever a body-capable field currently holds into an editor document.\n *\n * Accepts the canonical v2 `{ version: 2, content }` sequence (object or JSON\n * string) and projects it per the module contract. Anything unparseable becomes\n * an empty document (never throws; validation is first-class elsewhere). The\n * projection is deliberately TOTAL and shape-driven, not schema-validating: a\n * v2 value with one malformed node must still open (the bad node rides in a\n * carrier, visibly labelled) rather than blanking the whole field — the editor\n * is not the validator.\n */\nexport function toEditorDoc(value: unknown): RichTextDoc {\n const sequence = asBlockSequenceShape(value)\n if (sequence) {\n return { type: 'doc', content: sequence.content.map(projectStoredNode) }\n }\n // Anything that is not the v2 sequence (a pre-convergence envelope, a bare\n // doc, legacy plaintext, junk) opens EMPTY — deliberately not projected:\n // validation names those shapes with the explicit \"re-seed\" issue (EC-548\n // deleted the staging acceptance), and quietly editing over them here would\n // hide it.\n return { type: 'doc', content: [] }\n}\n\n/** Structural v2 test, tolerating a JSON-string form of the same value. */\nfunction asBlockSequenceShape(value: unknown): { version: 2; content: unknown[] } | undefined {\n if (isBlockSequenceValueShape(value)) return value\n if (typeof value === 'string') {\n try {\n const parsed: unknown = JSON.parse(value)\n if (isBlockSequenceValueShape(parsed)) return parsed\n } catch {\n /* not JSON — fall through */\n }\n }\n return undefined\n}\n\n/**\n * One stored node → its editor projection. Total over arbitrary JSON:\n *\n * - a tagged component node → the ComponentEmbed atom, the node itself riding\n * verbatim (tag included) in `attrs.node`;\n * - a node type the editor natively edits → passed through, `content` recursed\n * (so a component nested in a blockquote still projects to an embed);\n * - EVERYTHING else — foreign types, a tagged-but-malformed component node,\n * even non-node JSON in the content array — → the opaque carrier atom, the\n * original payload riding verbatim in `attrs.node`.\n */\nfunction projectStoredNode(node: unknown): RichTextNode {\n if (isComponentBlockNode(node)) {\n return { type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: node as unknown as JsonValue } }\n }\n if (isEditorNativeNode(node)) {\n const out: RichTextNode = { ...node }\n if (node.content) out.content = node.content.map(projectStoredNode)\n return out\n }\n return { type: UNKNOWN_NODE_CARRIER_TYPE, attrs: { node: node as JsonValue } }\n}\n\nfunction isEditorNativeNode(node: unknown): node is RichTextNode {\n return (\n typeof node === 'object' &&\n node !== null &&\n !Array.isArray(node) &&\n typeof (node as { type?: unknown }).type === 'string' &&\n EDITOR_NATIVE_NODE_TYPES.has((node as { type: string }).type)\n )\n}\n\n/**\n * Project a (normalized) editor document back into the stored v2 sequence —\n * the way-out half of the module contract: embed atoms → tagged\n * `elytra/component` nodes, carriers → their carried JSON byte-for-byte,\n * typography re-emitted as-is (lean canonicalisation is\n * {@link normalizeEditorDoc}'s job, upstream).\n */\nexport function blockSequenceValueFromDoc(doc: RichTextDoc): BlockSequenceValue {\n return createBlockSequenceValue((doc.content ?? []).map(restoreStoredNode))\n}\n\nfunction restoreStoredNode(node: RichTextNode): BlockNode {\n if (node.type === COMPONENT_EMBED_NODE_TYPE) {\n const embedded: unknown = node.attrs?.node\n if (isComponentNode(embedded)) {\n // Re-tag through the one constructor (identity on already-tagged nodes),\n // so an atom payload the node factory built untagged leaves the editor\n // as a first-class tagged citizen — \"write current\".\n const { type: _tag, ...rest } = embedded\n return componentBlockNode(rest)\n }\n // An embed whose payload is missing/malformed: keep the node verbatim as\n // typography — never a silent drop; validation reports it elsewhere.\n return node as TypographyNode\n }\n if (node.type === UNKNOWN_NODE_CARRIER_TYPE) {\n const carried: unknown = node.attrs?.node\n // Byte-for-byte restore of the carried original (the D3 round-trip law).\n // An empty carrier (nothing to restore — not constructible via the\n // projection) degrades to the carrier node itself staying visible.\n if (carried !== null && carried !== undefined) return carried as BlockNode\n return node as TypographyNode\n }\n const out: TypographyNode = { ...(node as TypographyNode) }\n if (node.content) out.content = node.content.map(restoreStoredNode)\n return out\n}\n\n/**\n * Prune the `null` attribute values ProseMirror stamps from declared-but-unset\n * attributes, and drop the resulting empty `attrs` bags, so the editor's output\n * matches the lean storage convention. Recurses `content`; leaves `marks` attrs\n * pruned the same way. Pure; returns a fresh tree. (Embed and carrier payloads\n * are single non-null `attrs` entries, so pruning passes them through by\n * reference — their bytes are never rewritten here.)\n */\nexport function normalizeEditorDoc(doc: RichTextDoc): RichTextDoc {\n return { type: 'doc', content: (doc.content ?? []).map(pruneNode) }\n}\n\nfunction pruneNode(node: RichTextNode): RichTextNode {\n const out: RichTextNode = { type: node.type }\n const attrs = pruneAttrs(node.attrs)\n if (attrs) out.attrs = attrs\n if (typeof node.text === 'string') out.text = node.text\n if (node.marks && node.marks.length > 0) {\n out.marks = node.marks.map((mark) => {\n const markAttrs = pruneAttrs(mark.attrs)\n return markAttrs ? { type: mark.type, attrs: markAttrs } : { type: mark.type }\n })\n }\n if (node.content) out.content = node.content.map(pruneNode)\n return out\n}\n\nfunction pruneAttrs(\n attrs: Record<string, unknown> | undefined,\n): Record<string, never> | undefined {\n if (!attrs) return undefined\n const entries = Object.entries(attrs).filter(([, v]) => v !== null && v !== undefined)\n return entries.length > 0 ? (Object.fromEntries(entries) as Record<string, never>) : undefined\n}\n\n/**\n * Whether a document carries no author-visible content — only empty paragraphs or\n * headings (TipTap keeps a mandatory trailing block even when the field is blank).\n * Any text, atom (image/embed/carrier/hardBreak/hr), list, or blockquote makes it\n * non-empty.\n */\nexport function isEffectivelyEmpty(doc: RichTextDoc): boolean {\n return (doc.content ?? []).every(isEmptyBlock)\n}\n\nfunction isEmptyBlock(node: RichTextNode): boolean {\n if (!EMPTY_TOLERANT_BLOCKS.has(node.type)) return false\n return (node.content ?? []).every(\n (child) => child.type === 'text' && (child.text ?? '').length === 0,\n )\n}\n\n/**\n * Whether the (already normalized) editor document is semantically the stored one —\n * so the editor can re-emit the exact stored value instead of a re-serialization,\n * keeping the field un-dirtied. Both sides are compared in editor-projection space\n * ({@link toEditorDoc} is deterministic), so a stored v2 component/carrier and its\n * on-screen atom compare equal. Two empty documents are always equivalent (the\n * trailing-paragraph difference between `content: []` and `[{ paragraph }]` is not\n * a change); otherwise defers to the shared structural comparison.\n */\nexport function isEquivalentToStored(current: RichTextDoc, storedValue: unknown): boolean {\n const stored = toEditorDoc(storedValue)\n if (isEffectivelyEmpty(current) && isEffectivelyEmpty(stored)) return true\n return richTextDocsEquivalent(current, stored)\n}\n\n/**\n * EC-679 — the value the editor REPORTS for the (normalized) document on screen (pure;\n * the editor's `onUpdate` reports exactly this). Three outcomes: unchanged → the stored\n * value verbatim (never a re-serialization, so the field is not dirtied), emptied →\n * `undefined` (the field is cleared), otherwise the new stamped value. An empty editor\n * over an ABSENT body therefore reports `undefined` — the value the draft already holds,\n * which the form's writer judges as no edit ({@link fieldEdit} in `use-document-draft.ts`).\n */\nexport function reportedValue(current: RichTextDoc, stored: unknown): unknown {\n if (isEquivalentToStored(current, stored)) return stored\n return isEffectivelyEmpty(current) ? undefined : blockSequenceValueFromDoc(current)\n}\n"]}
@@ -100,6 +100,36 @@ export declare function positionPatch(doc: CmsDocument, path: ValuePath): Docume
100
100
  * over it, values from the editor's current draft.
101
101
  */
102
102
  export declare function rebaseDraft(sessionDoc: CmsDocument, draft: CmsDocument, uncommitted: readonly UncommittedPosition[]): CmsDocument;
103
+ /**
104
+ * THE form writers' core (pure; what {@link DocumentDraft.setFieldValue} and {@link
105
+ * DocumentDraft.editField} run): `update` transforms the draft at ONE field position;
106
+ * the draft takes the result (`apply`) and, with a revisioned write model, the position
107
+ * is the committer's, authored at its token read BEFORE the draft moves (EC-667).
108
+ *
109
+ * **EC-679 — an editor that opens a document never writes.** The position is EDITED only
110
+ * when the value AT THE POSITION differs from what the DRAFT held there before the call,
111
+ * by the write path's own comparison: {@link semanticValueEqual} over the value the
112
+ * update stores (`writeField` canonicalises, so an emptied value that stores as absent
113
+ * equals absent). A control that re-reports what the draft already holds — the rich-text
114
+ * editor on mount, on a remount (a tab or Draft/Published switch) or on a read-only flip,
115
+ * measured on the base as an `unset` of an absent body or a same-value `set` per open —
116
+ * moves nothing and commits nothing (`undefined`). Compared against the draft, never the
117
+ * session's base: typing a value and typing it away again are two edits, and the revert
118
+ * writes too. Every field type is covered here by construction; the canvas writes through
119
+ * {@link canvasCommit}, not this.
120
+ */
121
+ export declare function fieldEdit(input: {
122
+ collection: CollectionDef;
123
+ draft: DraftView;
124
+ field: string;
125
+ locale: Locale;
126
+ update: (current: CmsDocument) => CmsDocument;
127
+ /** The write model's view tokens (absent: no revisioned write model). */
128
+ tokens: ViewTokens | undefined;
129
+ /** Absent: no revisioned write model (the draft moves, nothing is committed). */
130
+ committer: Pick<DraftCommitter, 'edited'> | undefined;
131
+ apply: (next: CmsDocument) => void;
132
+ }): 'edited' | 'draft' | undefined;
103
133
  /**
104
134
  * EC-630 — the canvas's write into the draft (pure). The canvas writer's node patches,
105
135
  * keyed by node id, are the canvas's OWN delta: from `base`, the value it last agreed on
@@ -186,9 +216,12 @@ export interface DocumentDraft {
186
216
  dirty: boolean;
187
217
  /** The record model: the editor renders a Save button + navigation guard. */
188
218
  explicitSave: boolean;
189
- /** A field's control wrote `value` (the active locale for a localized field). */
219
+ /**
220
+ * A field's control wrote `value` (the active locale for a localized field). EC-679: a
221
+ * value the draft already holds at the position is no edit ({@link fieldEdit}).
222
+ */
190
223
  setFieldValue(field: FieldDef, value: unknown): void;
191
- /** A writer that transforms ONE field position (fill / reset from base). */
224
+ /** A writer that transforms ONE field position (fill / reset from base) — the same no-op rule. */
192
225
  editField(field: FieldDef, update: (draft: CmsDocument) => CmsDocument): void;
193
226
  /**
194
227
  * The canvas emitted a new value for its field (node patches, committed at once). The
@@ -107,6 +107,37 @@ export function rebaseDraft(sessionDoc, draft, uncommitted) {
107
107
  const patches = uncommitted.map((position) => positionPatch(draft, position.path));
108
108
  return applyDocumentPatches({ document: sessionDoc, patches, since: [] }).document;
109
109
  }
110
+ /**
111
+ * THE form writers' core (pure; what {@link DocumentDraft.setFieldValue} and {@link
112
+ * DocumentDraft.editField} run): `update` transforms the draft at ONE field position;
113
+ * the draft takes the result (`apply`) and, with a revisioned write model, the position
114
+ * is the committer's, authored at its token read BEFORE the draft moves (EC-667).
115
+ *
116
+ * **EC-679 — an editor that opens a document never writes.** The position is EDITED only
117
+ * when the value AT THE POSITION differs from what the DRAFT held there before the call,
118
+ * by the write path's own comparison: {@link semanticValueEqual} over the value the
119
+ * update stores (`writeField` canonicalises, so an emptied value that stores as absent
120
+ * equals absent). A control that re-reports what the draft already holds — the rich-text
121
+ * editor on mount, on a remount (a tab or Draft/Published switch) or on a read-only flip,
122
+ * measured on the base as an `unset` of an absent body or a same-value `set` per open —
123
+ * moves nothing and commits nothing (`undefined`). Compared against the draft, never the
124
+ * session's base: typing a value and typing it away again are two edits, and the revert
125
+ * writes too. Every field type is covered here by construction; the canvas writes through
126
+ * {@link canvasCommit}, not this.
127
+ */
128
+ export function fieldEdit(input) {
129
+ const { draft, field, committer } = input;
130
+ const path = fieldPositionPath(input.collection, field, input.locale);
131
+ const authored = { base: positionBase(draft, path, input.tokens) };
132
+ const next = input.update(draft.document);
133
+ if (semanticValueEqual(readValueAt(draft.document, path), readValueAt(next, path)))
134
+ return undefined;
135
+ input.apply(next);
136
+ if (!committer)
137
+ return 'draft';
138
+ committer.edited(field, path, authored);
139
+ return 'edited';
140
+ }
110
141
  /**
111
142
  * EC-630 — the canvas's write into the draft (pure). The canvas writer's node patches,
112
143
  * keyed by node id, are the canvas's OWN delta: from `base`, the value it last agreed on
@@ -298,23 +329,19 @@ export function useDocumentDraft(input) {
298
329
  setCanvasChange((last) => canvasFieldChange(previous, next.document, canvasPath, last, canvasRevision) ?? last);
299
330
  }, [doc]);
300
331
  // --- Writers ----------------------------------------------------------------------------
301
- const setFieldValue = useCallback((field, value) => {
302
- // EC-667: the position's token is read BEFORE the draft moves — from the view this
303
- // keystroke was made on (a re-edit keeps its first keystroke's either way).
304
- const path = fieldPositionPath(collection, field.name, locale);
305
- const authored = { base: positionBase(draftRef.current, path, documentModel) };
306
- const next = writeField(collection, draftRef.current.document, field.name, locale, value, { lookupManifest });
307
- setDraft(next);
308
- if (documentModel)
309
- committer.edited(field.name, path, authored);
310
- }, [collection, locale, lookupManifest, setDraft, documentModel, committer]);
311
332
  const editField = useCallback((field, update) => {
312
- const path = fieldPositionPath(collection, field.name, locale);
313
- const authored = { base: positionBase(draftRef.current, path, documentModel) };
314
- setDraft(update(draftRef.current.document));
315
- if (documentModel)
316
- committer.edited(field.name, path, authored);
333
+ fieldEdit({
334
+ collection,
335
+ draft: draftRef.current,
336
+ field: field.name,
337
+ locale,
338
+ update,
339
+ tokens: documentModel,
340
+ committer: documentModel ? committer : undefined,
341
+ apply: setDraft,
342
+ });
317
343
  }, [collection, locale, setDraft, documentModel, committer]);
344
+ const setFieldValue = useCallback((field, value) => editField(field, (current) => writeField(collection, current, field.name, locale, value, { lookupManifest })), [collection, locale, lookupManifest, editField]);
318
345
  const setCanvasValue = useCallback((field, lift) => {
319
346
  const focus = committer.focusedField();
320
347
  canvasCommit({
@@ -1 +1 @@
1
- {"version":3,"file":"use-document-draft.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-draft.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n PRESENCE_TTL_MS,\n livePresence,\n} from '@elytracms/core/persistence'\nimport type { PresenceEntry } from '@elytracms/core/persistence'\nimport {\n applyDocumentPatches,\n formatValuePath,\n readValueAt,\n semanticValueEqual,\n writeField,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentPatch,\n DocumentRef,\n FieldDef,\n Locale,\n PropFieldLookup,\n ValuePath,\n} from '@elytracms/core/cms-core'\nimport type { CollectionViewSource, DocumentWrites } from '../../lib/collection-view/collection-view-source'\nimport { fieldPositionPath } from '../../lib/cms/field-patch'\nimport { compositionPatches } from '../builder/composition-patches'\nimport type { CanvasFieldChange, CanvasLift } from '../builder/canvas-rebase'\nimport { commitPolicyOf } from './commit-policy'\nimport type { CommitPolicy } from './commit-policy'\nimport { canvasControl, routeCanvasWrite } from './canvas-commit'\nimport type { CanvasControl, CanvasControlPosition } from './canvas-commit'\nimport { browserScheduler, createDraftCommitter } from './draft-commit'\nimport type { DraftCommitter, Scheduler, UncommittedPosition } from './draft-commit'\n\n/**\n * EC-595 A2 — the detail editor's draft over the revisioned document (and, for the\n * explicit-save rails, over a record).\n *\n * **Document write model** (the CMS): the draft changes per keystroke (live validation\n * reads it); a field's edit is COMMITTED per its control kind's policy\n * ({@link commitPolicyOf} / {@link createDraftCommitter}) as the patch of the position it\n * wrote — never a document. The canvas commits each action's node patches at once\n * (`compositionPatches`); a burst of typing in one of its inspector controls is that\n * control's uncommitted position, committed on its kind's policy like a form field\n * (EC-630, `canvas-commit.ts`). The draft REBASES whenever the session's document changes\n * (a write result, a colleague's change by subscription, a resolved conflict): it is the\n * session's document with this editor's uncommitted positions laid over it — local wins\n * until committed, then the per-path check decides (EC-595 point 7).\n *\n * **Forced flushes** (the ruling: before unmount, tab hide, publish, preview, an AI\n * proposal, duplicate, transfer): {@link DocumentDraft.flush} commits every uncommitted\n * position and awaits the persister. Unmount and tab hide run it here; the editor's\n * publish, duplicate and the AI tap call it.\n *\n * **Thin presence** (Stage A): a heartbeat naming the focused field's path on focus\n * change and every {@link PRESENCE_HEARTBEAT_MS} (under the 30 s TTL), cleared on\n * unmount; the other sessions' live heartbeats arrive with the subscription.\n *\n * **Record write model** (users, redirects, assets, the account): the draft is edited\n * in place and written whole on the editor's Save button (EC-240) — unchanged.\n */\n\n/** The presence heartbeat interval — under {@link PRESENCE_TTL_MS} so a present editor never lapses. */\nexport const PRESENCE_HEARTBEAT_MS = 10_000\n\n/** An injectable visibility source (the tab-hide flush; tests drive it). */\nexport interface VisibilitySource {\n /** Call `onHidden` whenever the tab becomes hidden; returns the unsubscribe. */\n onHidden(onHidden: () => void): () => void\n}\n\nexport const documentVisibility: VisibilitySource = {\n onHidden(onHidden) {\n if (typeof document === 'undefined') return () => {}\n const listener = () => {\n if (document.visibilityState === 'hidden') onHidden()\n }\n document.addEventListener('visibilitychange', listener)\n return () => document.removeEventListener('visibilitychange', listener)\n },\n}\n\n/**\n * EC-647 — the editor's draft and the revision of the session view it was last rebased\n * onto, as ONE value: every edit made on the draft was authored against that revision (an\n * uncommitted position keeps the revision its first keystroke saw). `undefined`: the view\n * had no revision (a fixture session, a record rail, a document still being created).\n *\n * EC-667 — that session view is a COMPOSITE while it shows a pending write (Codex F18):\n * `revision` is its whole-document token (the EARLIEST authoring revision of any position\n * — what a whole-document writer, the slot alias or the translation, writes at), and\n * `shown` is the view itself, the token a position's edit reads ITS provenance from at\n * the first keystroke ({@link positionBase}).\n */\nexport interface DraftView {\n readonly document: CmsDocument\n readonly revision: number | undefined\n /** EC-667 — the session view the draft was last rebased onto (absent: none — a fixture, a record rail). */\n readonly shown?: CmsDocument\n}\n\n/** What a draft reads its tokens from: the write model's view registry (EC-647 / EC-667). */\nexport type ViewTokens = Pick<DocumentWrites, 'revisionOfView'>\n\n/** EC-667 — the draft of a session view as it is shown (pure): its value, its whole-document token, the view itself. */\nexport function sessionDraftView(doc: CmsDocument, tokens: ViewTokens | undefined): DraftView {\n return { document: doc, revision: tokens?.revisionOfView(doc), shown: doc }\n}\n\n/**\n * EC-667 — the base an edit of `path` that STARTS now on `draft` is authored against\n * (pure; the committer captures it at the first keystroke): the revision the content at\n * that position of the shown session view was authored against — under a pending\n * whole-document snapshot the snapshot's revision, under a pending own write that write's\n * base — never the revision of a row adopted underneath it. Captured at the start, it\n * survives the pending write's refusal or failure (looking it up when the commit arrives\n * would be too late). Without a view token: the draft's whole-document revision.\n */\nexport function positionBase(draft: DraftView, path: ValuePath, tokens: ViewTokens | undefined): number | undefined {\n return tokens && draft.shown ? tokens.revisionOfView(draft.shown, path) : draft.revision\n}\n\n/**\n * THE draft rebase as one value (pure — what the editor runs when the session's document\n * changes): the session view with the uncommitted positions laid over it ({@link\n * rebaseDraft}), its whole-document token, the view itself (EC-647 → EC-667: the rebase\n * moves the value AND its tokens), and — when the screen has a canvas — the token of the\n * canvas field's position, which the canvas's next edit is authored at.\n */\nexport function rebaseDraftView(\n doc: CmsDocument,\n previous: DraftView,\n uncommitted: readonly UncommittedPosition[],\n tokens: ViewTokens,\n canvasPath?: ValuePath,\n): { view: DraftView; canvasRevision: number | undefined } {\n return {\n view: { document: rebaseDraft(doc, previous.document, uncommitted), revision: tokens.revisionOfView(doc), shown: doc },\n canvasRevision: canvasPath ? tokens.revisionOfView(doc, canvasPath) : undefined,\n }\n}\n\n/**\n * EC-647 — the slot alias write (pure): the draft with `alias` set (or removed), written\n * whole AT THE DRAFT'S REVISION — never the session's tracked one, which may already hold\n * a colleague's change the draft has not absorbed (Codex F10). There is no identity-\n * metadata operation for the alias (the value-position grammar has no position for it,\n * measured on the base), so it stays a whole-document `replace` with its precondition.\n */\nexport function aliasWrite(\n draft: DraftView,\n alias: string | undefined,\n writes: { replace(doc: CmsDocument, revision: number | undefined): void },\n): CmsDocument {\n const next = { ...draft.document }\n if (alias === undefined) delete next.alias\n else next.alias = alias\n writes.replace(next, draft.revision)\n return next\n}\n\n/** The patch that writes the value `doc` holds at `path` (unset when absent). */\nexport function positionPatch(doc: CmsDocument, path: ValuePath): DocumentPatch {\n const value = readValueAt(doc, path)\n return value === undefined ? { op: 'unset', path } : { op: 'set', path, value }\n}\n\n/**\n * THE rebase (pure): the session's document with the editor's uncommitted positions laid\n * over it, values from the editor's current draft.\n */\nexport function rebaseDraft(\n sessionDoc: CmsDocument,\n draft: CmsDocument,\n uncommitted: readonly UncommittedPosition[],\n): CmsDocument {\n if (uncommitted.length === 0) return sessionDoc\n const patches = uncommitted.map((position) => positionPatch(draft, position.path))\n return applyDocumentPatches({ document: sessionDoc, patches, since: [] }).document\n}\n\n/**\n * EC-630 — the canvas's write into the draft (pure). The canvas writer's node patches,\n * keyed by node id, are the canvas's OWN delta: from `base`, the value it last agreed on\n * with the draft, to `value` — never a document diff, and never measured against the\n * draft's CURRENT value (a colleague's change that reached the draft a moment before the\n * edit lifted would read as part of the edit, and its patches would revert it). The draft\n * takes the delta over whatever it holds now (normally `base` itself) and canonicalises the\n * field through the write lens. `undefined` when the canvas changed nothing.\n */\nexport function canvasWrite(input: {\n collection: CollectionDef\n draft: CmsDocument\n field: string\n locale: Locale\n value: unknown\n base: unknown\n lookupManifest: PropFieldLookup\n}): { next: CmsDocument; patches: DocumentPatch[] } | undefined {\n const { collection, draft, field, locale, lookupManifest } = input\n const path = fieldPositionPath(collection, field, locale)\n const localized = path.length > 1 ? locale : undefined\n const patches = compositionPatches(input.base, input.value, field, localized)\n if (patches.length === 0) return undefined\n const applied = applyDocumentPatches({ document: draft, patches, since: [] }).document\n return { next: writeField(collection, draft, field, locale, readValueAt(applied, path), { lookupManifest }), patches }\n}\n\n/**\n * EC-630 — the change of the canvas field made UNDERNEATH the canvas when the draft\n * rebased from `previous` to `next` (pure), numbered after `last`; `undefined` when the\n * field did not move. The canvas's own edits are in both values (the session's view holds\n * every patch it committed, the draft overlays every position it has not), so the change\n * is someone else's: a colleague's patch, a write's canonical row, a resolved conflict.\n *\n * EC-647: it carries `revision`, the revision the draft is at after the rebase, and is\n * reported when only THAT moved too (`from` ≡ `to`): the canvas's agreed value is valid\n * at the newer revision, so its next edit is authored there — the revision follows the\n * value, never runs ahead of it.\n *\n * EC-667: `revision` is the token of the canvas FIELD's position in the shown view\n * ({@link rebaseDraftView}) — under a pending snapshot the snapshot's revision. The canvas\n * mounts at the draft's whole-document token (the earliest, possibly earlier than its\n * field's), so the first rebase with a token reports it even when nothing moved.\n */\nexport function canvasFieldChange(\n previous: CmsDocument,\n next: CmsDocument,\n path: ValuePath,\n last: CanvasFieldChange | undefined,\n revision: number | undefined,\n): CanvasFieldChange | undefined {\n const from = readValueAt(previous, path)\n const to = readValueAt(next, path)\n const tokenHeld = last === undefined ? revision === undefined : last.revision === revision\n if (semanticValueEqual(from, to) && tokenHeld) return undefined\n return { from, to, seq: (last?.seq ?? 0) + 1, revision }\n}\n\n/**\n * EC-647 — the canvas's write path (pure; what {@link DocumentDraft.setCanvasValue} runs):\n * the canvas writer's delta into the draft ({@link canvasWrite}, handed to `apply`), then\n * routed ({@link routeCanvasWrite}) — committed at once, or held as a focused inspector\n * control's uncommitted position — and in both cases paired with the revision of the\n * canvas value the edit was authored on (`lift.revision`), never the draft's: the draft\n * may already hold a colleague's change the canvas has not been rebased onto (Codex F12),\n * and the store must check the edit from the canvas's revision to refuse it where it\n * overlaps that change. `commit` absent → a draft without a revisioned write model (the\n * draft moves, nothing is committed). `undefined` when the canvas changed nothing.\n */\nexport function canvasCommit(input: {\n collection: CollectionDef\n draft: CmsDocument\n field: string\n locale: Locale\n lift: CanvasLift\n lookupManifest: PropFieldLookup\n /** The focused canvas inspector control, if focus is on one. */\n focused: CanvasControl | undefined\n committer: DraftCommitter\n /** The draft takes the canvas's write (before it is routed — a forced commit reads it). */\n apply: (next: CmsDocument) => void\n commit?: (patches: readonly DocumentPatch[], bases: readonly (number | undefined)[]) => void\n}): 'deferred' | 'committed' | 'draft' | undefined {\n const { lift, commit } = input\n const write = canvasWrite({\n collection: input.collection,\n draft: input.draft,\n field: input.field,\n locale: input.locale,\n value: lift.value,\n base: lift.base,\n lookupManifest: input.lookupManifest,\n })\n if (!write) return undefined\n input.apply(write.next)\n if (!commit) return 'draft'\n // EC-630: a burst of typing in a focused inspector control is that control's\n // uncommitted position (its policy commits it); a canvas action commits now.\n return routeCanvasWrite({\n patches: write.patches,\n focused: input.focused,\n committer: input.committer,\n revision: lift.revision,\n commitNow: (patches) => commit(patches, patches.map(() => lift.revision)),\n })\n}\n\n/**\n * The reader's \"now\" for presence liveness: THIS session's own latest heartbeat `at`\n * (written by the same store clock as everyone else's — the playground store runs a\n * deterministic sequential clock, Convex the writers' wall clocks), falling back to the\n * injected clock before the first heartbeat lands. Comparing others' `at` against a\n * different time base would call every colleague stale (measured live in the\n * playground: the store's clock is months behind the browser's).\n */\nexport function presenceReaderNow(\n entries: readonly PresenceEntry[],\n sessionId: string,\n fallback: string,\n): string {\n const own = entries.filter((entry) => entry.sessionId === sessionId).map((entry) => entry.at)\n return own.length > 0 ? own.sort().at(-1)! : fallback\n}\n\nexport interface DocumentDraft {\n draft: CmsDocument\n /**\n * EC-647 — the revision of the session view the draft was last rebased onto (see\n * {@link DraftView}): a writer that authors from {@link draft} (the translation) writes\n * at this revision. EC-667: the view's WHOLE-DOCUMENT token — the earliest authoring\n * revision of any position it shows (a field's own edit reads its position's token).\n */\n revision: number | undefined\n /** Uncommitted (document model) / unsaved (record model) edits exist. */\n dirty: boolean\n /** The record model: the editor renders a Save button + navigation guard. */\n explicitSave: boolean\n /** A field's control wrote `value` (the active locale for a localized field). */\n setFieldValue(field: FieldDef, value: unknown): void\n /** A writer that transforms ONE field position (fill / reset from base). */\n editField(field: FieldDef, update: (draft: CmsDocument) => CmsDocument): void\n /**\n * The canvas emitted a new value for its field (node patches, committed at once). The\n * lift's `base` is the value the canvas last agreed on with the draft: the canvas\n * writer's delta is `base` → `value`, applied over whatever the draft holds now (EC-630 —\n * a change that landed underneath meanwhile is never reverted by the canvas's own\n * write); its `revision` is the revision that value was agreed at — the revision the\n * edit is committed at (EC-647, {@link canvasCommit}).\n */\n setCanvasValue(field: FieldDef, lift: CanvasLift): void\n /** The slot alias (a document-level key: a whole-document write at the held revision). */\n setAlias(alias: string | undefined): void\n /** Field boundary events (focus / blur / Enter) — the commit policy's triggers. */\n focused(field: FieldDef): void\n blurred(field: FieldDef): void\n entered(field: FieldDef): void\n /**\n * EC-630 — the canvas inspector's boundary events: a control writing `position` of the\n * canvas field `field`, committing on `policy` (THE declaration for its kind). A burst of\n * typing in it is its uncommitted position until blur / Enter / the idle pause.\n */\n canvasFocused(field: FieldDef, position: CanvasControlPosition, policy: CommitPolicy): void\n canvasBlurred(field: FieldDef, position: CanvasControlPosition): void\n canvasEntered(field: FieldDef, position: CanvasControlPosition): void\n /** The forced commit (sync; the persister serialises what follows) / the record model's Save. */\n save(): void\n /** The FORCED FLUSH: commit everything uncommitted and await persistence. */\n flush(): Promise<void>\n /** Adopt a draft from outside (a restored version). */\n replaceDraft(next: CmsDocument): void\n /**\n * Bumped when the canvas must remount from the draft: a restored version replaced the\n * document, or a locale switch pointed the canvas at another bucket. (A change made\n * underneath the canvas is NOT a remount — {@link canvasChange}.)\n */\n canvasNonce: number\n /**\n * EC-630 — the latest change of the canvas field's value made UNDERNEATH the canvas (a\n * colleague's patch by subscription, a write's canonical row, a resolved conflict): the\n * draft's value before and after it. The canvas REBASES onto it — the change lands, its\n * undo history and selection survive — where it used to remount and lose both.\n */\n canvasChange: CanvasFieldChange | undefined\n /** The OTHER sessions present on this document, live by the reader's clock. */\n presence: readonly PresenceEntry[]\n}\n\nexport interface UseDocumentDraftInput {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n lookupManifest: PropFieldLookup\n /** The composition field the canvas edits on this screen, if any. */\n canvasField?: FieldDef\n scheduler?: Scheduler\n visibility?: VisibilitySource\n /** The reader's clock for presence liveness (ISO); `new Date().toISOString()` by default. */\n now?: () => string\n}\n\n/**\n * EC-616 — the base a record save is diffed against: the source's value whenever the draft\n * equals it (the record as opened, or as a save came back), else the previous base (the\n * editor has unsaved changes; a change from another writer meanwhile is not theirs). Pure.\n */\nexport function recordSaveBase(previous: CmsDocument, draft: CmsDocument, doc: CmsDocument): CmsDocument {\n return semanticValueEqual(draft, doc) ? doc : previous\n}\n\nexport function useDocumentDraft(input: UseDocumentDraftInput): DocumentDraft {\n const { source, collection, doc, locale, lookupManifest, canvasField } = input\n const writes = source.writes\n const documentModel = writes.kind === 'document' ? writes : undefined\n const ref: DocumentRef = useMemo(() => ({ collection: doc.collection, id: doc.id }), [doc.collection, doc.id])\n // EC-647: the draft and the revision of the view it rebased onto travel as ONE value\n // (EC-667: with the view itself — the per-position token).\n const [view, setViewState] = useState<DraftView>(() => sessionDraftView(doc, documentModel))\n const draft = view.document\n const draftRef = useRef(view)\n // EC-616: the record as the editor opened it (or as of their last successful save) — the\n // BASE a record rail diffs a save against, so it writes only the editor's changes. It moves\n // only while the draft equals the source's value (at open, and once a save is reflected\n // back): another writer's change arriving mid-edit never becomes the editor's base.\n const baseRef = useRef<CmsDocument>(doc)\n baseRef.current = recordSaveBase(baseRef.current, draftRef.current.document, doc)\n const setView = useCallback((next: DraftView) => {\n draftRef.current = next\n setViewState(next)\n }, [])\n /** An edit of the draft: its value moves, the tokens it is authored against do not. */\n const setDraft = useCallback(\n (next: CmsDocument) => setView({ ...draftRef.current, document: next }),\n [setView],\n )\n const scheduler = input.scheduler ?? browserScheduler\n const now = input.now ?? (() => new Date().toISOString())\n const fieldsByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection])\n\n // --- Committing --------------------------------------------------------------------\n const writesRef = useRef(documentModel)\n writesRef.current = documentModel\n /** EC-630 — the canvas inspector controls that took focus, by committer key (their position + policy). */\n const canvasControls = useRef(new Map<string, CanvasControl>())\n const committer = useMemo<DraftCommitter>(\n () =>\n createDraftCommitter({\n policyOf: (name) => {\n const field = fieldsByName.get(name)\n if (field) return commitPolicyOf(field)\n return canvasControls.current.get(name)?.policy ?? 'blur-or-idle'\n },\n commit: (positions) => {\n const target = writesRef.current\n if (!target) return\n void target.commit(\n ref,\n positions.map((position) => positionPatch(draftRef.current.document, position.path)),\n positions.map((position) => position.base),\n )\n },\n // EC-647: a position's base is the revision of the DRAFT its first keystroke was\n // made on — never the session's tracked revision, which may already hold a\n // colleague's change the draft has not rebased onto yet. EC-667: the form's\n // writers name the POSITION's token ({@link positionBase}, passed to `edited`);\n // this whole-document token (the earliest) is the fallback.\n baseOf: () => draftRef.current.revision,\n scheduler,\n }),\n // One committer per open document (the editor is keyed by it).\n [ref],\n )\n\n // --- The canvas's value: its remount and its rebase ------------------------------------\n const canvasPath = canvasField ? fieldPositionPath(collection, canvasField.name, locale) : undefined\n const canvasPathKey = canvasPath ? formatValuePath(canvasPath) : ''\n const [canvasNonce, setCanvasNonce] = useState(0)\n const [canvasChange, setCanvasChange] = useState<CanvasFieldChange | undefined>(undefined)\n // A locale switch points the canvas at another bucket: remount from it.\n const lastCanvasPathKey = useRef(canvasPathKey)\n useEffect(() => {\n if (lastCanvasPathKey.current === canvasPathKey) return\n lastCanvasPathKey.current = canvasPathKey\n setCanvasNonce((n) => n + 1)\n }, [canvasPathKey])\n\n // --- The rebase (document model) -------------------------------------------------------\n const firstDoc = useRef(true)\n useEffect(() => {\n if (firstDoc.current) {\n firstDoc.current = false\n return\n }\n if (!documentModel) return\n const previous = draftRef.current.document\n // EC-647: the rebase moves the value AND the revision — the session view's own\n // (EC-667: its tokens, whole-document and per position).\n const { view: next, canvasRevision } = rebaseDraftView(doc, draftRef.current, committer.uncommitted(), documentModel, canvasPath)\n setView(next)\n // EC-630: the canvas field moved underneath the canvas — the canvas rebases onto the\n // change (its own edits are in both values: the session's view holds every patch it\n // committed, and the draft overlays every position it has not). EC-647: the change\n // carries the revision — EC-667: the canvas FIELD's token — so the canvas's next edit\n // is authored where its value is.\n if (!canvasPath) return\n setCanvasChange((last) => canvasFieldChange(previous, next.document, canvasPath, last, canvasRevision) ?? last)\n }, [doc])\n\n // --- Writers ----------------------------------------------------------------------------\n const setFieldValue = useCallback(\n (field: FieldDef, value: unknown) => {\n // EC-667: the position's token is read BEFORE the draft moves — from the view this\n // keystroke was made on (a re-edit keeps its first keystroke's either way).\n const path = fieldPositionPath(collection, field.name, locale)\n const authored = { base: positionBase(draftRef.current, path, documentModel) }\n const next = writeField(collection, draftRef.current.document, field.name, locale, value, { lookupManifest })\n setDraft(next)\n if (documentModel) committer.edited(field.name, path, authored)\n },\n [collection, locale, lookupManifest, setDraft, documentModel, committer],\n )\n const editField = useCallback(\n (field: FieldDef, update: (current: CmsDocument) => CmsDocument) => {\n const path = fieldPositionPath(collection, field.name, locale)\n const authored = { base: positionBase(draftRef.current, path, documentModel) }\n setDraft(update(draftRef.current.document))\n if (documentModel) committer.edited(field.name, path, authored)\n },\n [collection, locale, setDraft, documentModel, committer],\n )\n const setCanvasValue = useCallback(\n (field: FieldDef, lift: CanvasLift) => {\n const focus = committer.focusedField()\n canvasCommit({\n collection,\n draft: draftRef.current.document,\n field: field.name,\n locale,\n lift,\n lookupManifest,\n focused: focus === undefined ? undefined : canvasControls.current.get(focus),\n committer,\n apply: setDraft,\n ...(documentModel\n ? { commit: (patches: readonly DocumentPatch[], bases: readonly (number | undefined)[]) => void documentModel.commit(ref, patches, bases) }\n : {}),\n })\n },\n [collection, locale, lookupManifest, setDraft, documentModel, ref, committer],\n )\n const setAlias = useCallback(\n (alias: string | undefined) => {\n if (!documentModel) {\n aliasWrite(draftRef.current, alias, { replace: (next) => setDraft(next) })\n return\n }\n // The uncommitted positions first (the snapshot holds them), then the snapshot AT\n // THE DRAFT'S REVISION (EC-647 — Codex F10).\n committer.commitAll()\n setDraft(aliasWrite(draftRef.current, alias, documentModel))\n },\n [setDraft, documentModel, committer],\n )\n\n // --- Forced flush ----------------------------------------------------------------------\n const save = useCallback(() => {\n if (documentModel) committer.commitAll()\n else if (writes.kind === 'record') writes.save(draftRef.current.document, baseRef.current)\n }, [documentModel, committer, writes])\n const flush = useCallback(async () => {\n save()\n if (documentModel) await documentModel.flush(ref)\n }, [save, documentModel, ref])\n\n // Tab hide: commit and flush. Unmount: the same, and leave the document.\n const visibility = input.visibility ?? documentVisibility\n useEffect(() => {\n if (!documentModel) return\n return visibility.onHidden(() => {\n committer.commitAll()\n void documentModel.flush(ref)\n })\n }, [ref])\n useEffect(\n () => () => {\n const target = writesRef.current\n if (!target) return\n committer.commitAll()\n committer.dispose()\n void target.flush(ref)\n target.clearPresence(ref)\n },\n [ref],\n )\n\n // --- Subscription + presence ------------------------------------------------------------\n const [presenceEntries, setPresenceEntries] = useState<readonly PresenceEntry[]>([])\n const [clock, setClock] = useState(now)\n useEffect(() => {\n if (!documentModel) return\n return documentModel.subscribe(ref, setPresenceEntries)\n }, [ref])\n const focusedPathRef = useRef<ValuePath | undefined>(undefined)\n const heartbeat = useCallback(() => {\n writesRef.current?.reportPresence(ref, focusedPathRef.current)\n setClock(now())\n }, [ref])\n useEffect(() => {\n if (!documentModel) return\n heartbeat()\n let handle: unknown\n const tick = () => {\n heartbeat()\n handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS)\n }\n handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS)\n return () => scheduler.clearTimeout(handle)\n }, [ref])\n const presence = useMemo(\n () =>\n documentModel\n ? livePresence(\n presenceEntries,\n presenceReaderNow(presenceEntries, documentModel.presenceSessionId, clock),\n PRESENCE_TTL_MS,\n ).filter((entry) => entry.sessionId !== documentModel.presenceSessionId)\n : [],\n [presenceEntries, clock, documentModel],\n )\n\n const focused = useCallback(\n (field: FieldDef) => {\n if (!documentModel) return\n committer.focused(field.name)\n focusedPathRef.current = fieldPositionPath(collection, field.name, locale)\n heartbeat()\n },\n [documentModel, committer, collection, locale, heartbeat],\n )\n const blurred = useCallback(\n (field: FieldDef) => {\n if (!documentModel) return\n committer.blurred(field.name)\n focusedPathRef.current = undefined\n heartbeat()\n },\n [documentModel, committer, heartbeat],\n )\n const entered = useCallback(\n (field: FieldDef) => {\n if (documentModel) committer.entered(field.name)\n },\n [documentModel, committer],\n )\n\n // EC-630 — the canvas inspector's commit boundary: a control at a position of the canvas\n // field gained focus, lost it, or saw Enter. Its policy is THE declaration's for its kind.\n const canvasControlOf = useCallback(\n (field: FieldDef, position: CanvasControlPosition, policy: CommitPolicy) =>\n canvasControl(fieldPositionPath(collection, field.name, locale), position, policy),\n [collection, locale],\n )\n const canvasFocused = useCallback(\n (field: FieldDef, position: CanvasControlPosition, policy: CommitPolicy) => {\n if (!documentModel) return\n const control = canvasControlOf(field, position, policy)\n canvasControls.current.set(control.key, control)\n committer.focused(control.key)\n focusedPathRef.current = control.path\n heartbeat()\n },\n [documentModel, committer, canvasControlOf, heartbeat],\n )\n const canvasBlurred = useCallback(\n (field: FieldDef, position: CanvasControlPosition) => {\n if (!documentModel) return\n const key = canvasControlOf(field, position, 'change').key\n committer.blurred(key)\n canvasControls.current.delete(key)\n focusedPathRef.current = undefined\n heartbeat()\n },\n [documentModel, committer, canvasControlOf, heartbeat],\n )\n const canvasEntered = useCallback(\n (field: FieldDef, position: CanvasControlPosition) => {\n if (documentModel) committer.entered(canvasControlOf(field, position, 'change').key)\n },\n [documentModel, committer, canvasControlOf],\n )\n\n const replaceDraft = useCallback(\n (next: CmsDocument) => {\n // A restored version replaces the document: when it moved the canvas field, the\n // canvas remounts from it (its history belonged to the replaced value).\n const moved = canvasPath && !semanticValueEqual(readValueAt(draftRef.current.document, canvasPath), readValueAt(next, canvasPath))\n // EC-647: the restored value comes with the revision the session showed it at.\n setView(sessionDraftView(next, documentModel))\n if (moved) setCanvasNonce((n) => n + 1)\n },\n // The path is keyed by its string form.\n [setView, documentModel, canvasPathKey],\n )\n\n const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc])\n\n return {\n draft,\n revision: view.revision,\n dirty,\n explicitSave: writes.kind === 'record',\n setFieldValue,\n editField,\n setCanvasValue,\n setAlias,\n focused,\n blurred,\n entered,\n canvasFocused,\n canvasBlurred,\n canvasEntered,\n save,\n flush,\n replaceDraft,\n canvasNonce,\n canvasChange,\n presence,\n }\n}\n"]}
1
+ {"version":3,"file":"use-document-draft.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-draft.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n PRESENCE_TTL_MS,\n livePresence,\n} from '@elytracms/core/persistence'\nimport type { PresenceEntry } from '@elytracms/core/persistence'\nimport {\n applyDocumentPatches,\n formatValuePath,\n readValueAt,\n semanticValueEqual,\n writeField,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentPatch,\n DocumentRef,\n FieldDef,\n Locale,\n PropFieldLookup,\n ValuePath,\n} from '@elytracms/core/cms-core'\nimport type { CollectionViewSource, DocumentWrites } from '../../lib/collection-view/collection-view-source'\nimport { fieldPositionPath } from '../../lib/cms/field-patch'\nimport { compositionPatches } from '../builder/composition-patches'\nimport type { CanvasFieldChange, CanvasLift } from '../builder/canvas-rebase'\nimport { commitPolicyOf } from './commit-policy'\nimport type { CommitPolicy } from './commit-policy'\nimport { canvasControl, routeCanvasWrite } from './canvas-commit'\nimport type { CanvasControl, CanvasControlPosition } from './canvas-commit'\nimport { browserScheduler, createDraftCommitter } from './draft-commit'\nimport type { DraftCommitter, Scheduler, UncommittedPosition } from './draft-commit'\n\n/**\n * EC-595 A2 — the detail editor's draft over the revisioned document (and, for the\n * explicit-save rails, over a record).\n *\n * **Document write model** (the CMS): the draft changes per keystroke (live validation\n * reads it); a field's edit is COMMITTED per its control kind's policy\n * ({@link commitPolicyOf} / {@link createDraftCommitter}) as the patch of the position it\n * wrote — never a document. The canvas commits each action's node patches at once\n * (`compositionPatches`); a burst of typing in one of its inspector controls is that\n * control's uncommitted position, committed on its kind's policy like a form field\n * (EC-630, `canvas-commit.ts`). The draft REBASES whenever the session's document changes\n * (a write result, a colleague's change by subscription, a resolved conflict): it is the\n * session's document with this editor's uncommitted positions laid over it — local wins\n * until committed, then the per-path check decides (EC-595 point 7).\n *\n * **Forced flushes** (the ruling: before unmount, tab hide, publish, preview, an AI\n * proposal, duplicate, transfer): {@link DocumentDraft.flush} commits every uncommitted\n * position and awaits the persister. Unmount and tab hide run it here; the editor's\n * publish, duplicate and the AI tap call it.\n *\n * **Thin presence** (Stage A): a heartbeat naming the focused field's path on focus\n * change and every {@link PRESENCE_HEARTBEAT_MS} (under the 30 s TTL), cleared on\n * unmount; the other sessions' live heartbeats arrive with the subscription.\n *\n * **Record write model** (users, redirects, assets, the account): the draft is edited\n * in place and written whole on the editor's Save button (EC-240) — unchanged.\n */\n\n/** The presence heartbeat interval — under {@link PRESENCE_TTL_MS} so a present editor never lapses. */\nexport const PRESENCE_HEARTBEAT_MS = 10_000\n\n/** An injectable visibility source (the tab-hide flush; tests drive it). */\nexport interface VisibilitySource {\n /** Call `onHidden` whenever the tab becomes hidden; returns the unsubscribe. */\n onHidden(onHidden: () => void): () => void\n}\n\nexport const documentVisibility: VisibilitySource = {\n onHidden(onHidden) {\n if (typeof document === 'undefined') return () => {}\n const listener = () => {\n if (document.visibilityState === 'hidden') onHidden()\n }\n document.addEventListener('visibilitychange', listener)\n return () => document.removeEventListener('visibilitychange', listener)\n },\n}\n\n/**\n * EC-647 — the editor's draft and the revision of the session view it was last rebased\n * onto, as ONE value: every edit made on the draft was authored against that revision (an\n * uncommitted position keeps the revision its first keystroke saw). `undefined`: the view\n * had no revision (a fixture session, a record rail, a document still being created).\n *\n * EC-667 — that session view is a COMPOSITE while it shows a pending write (Codex F18):\n * `revision` is its whole-document token (the EARLIEST authoring revision of any position\n * — what a whole-document writer, the slot alias or the translation, writes at), and\n * `shown` is the view itself, the token a position's edit reads ITS provenance from at\n * the first keystroke ({@link positionBase}).\n */\nexport interface DraftView {\n readonly document: CmsDocument\n readonly revision: number | undefined\n /** EC-667 — the session view the draft was last rebased onto (absent: none — a fixture, a record rail). */\n readonly shown?: CmsDocument\n}\n\n/** What a draft reads its tokens from: the write model's view registry (EC-647 / EC-667). */\nexport type ViewTokens = Pick<DocumentWrites, 'revisionOfView'>\n\n/** EC-667 — the draft of a session view as it is shown (pure): its value, its whole-document token, the view itself. */\nexport function sessionDraftView(doc: CmsDocument, tokens: ViewTokens | undefined): DraftView {\n return { document: doc, revision: tokens?.revisionOfView(doc), shown: doc }\n}\n\n/**\n * EC-667 — the base an edit of `path` that STARTS now on `draft` is authored against\n * (pure; the committer captures it at the first keystroke): the revision the content at\n * that position of the shown session view was authored against — under a pending\n * whole-document snapshot the snapshot's revision, under a pending own write that write's\n * base — never the revision of a row adopted underneath it. Captured at the start, it\n * survives the pending write's refusal or failure (looking it up when the commit arrives\n * would be too late). Without a view token: the draft's whole-document revision.\n */\nexport function positionBase(draft: DraftView, path: ValuePath, tokens: ViewTokens | undefined): number | undefined {\n return tokens && draft.shown ? tokens.revisionOfView(draft.shown, path) : draft.revision\n}\n\n/**\n * THE draft rebase as one value (pure — what the editor runs when the session's document\n * changes): the session view with the uncommitted positions laid over it ({@link\n * rebaseDraft}), its whole-document token, the view itself (EC-647 → EC-667: the rebase\n * moves the value AND its tokens), and — when the screen has a canvas — the token of the\n * canvas field's position, which the canvas's next edit is authored at.\n */\nexport function rebaseDraftView(\n doc: CmsDocument,\n previous: DraftView,\n uncommitted: readonly UncommittedPosition[],\n tokens: ViewTokens,\n canvasPath?: ValuePath,\n): { view: DraftView; canvasRevision: number | undefined } {\n return {\n view: { document: rebaseDraft(doc, previous.document, uncommitted), revision: tokens.revisionOfView(doc), shown: doc },\n canvasRevision: canvasPath ? tokens.revisionOfView(doc, canvasPath) : undefined,\n }\n}\n\n/**\n * EC-647 — the slot alias write (pure): the draft with `alias` set (or removed), written\n * whole AT THE DRAFT'S REVISION — never the session's tracked one, which may already hold\n * a colleague's change the draft has not absorbed (Codex F10). There is no identity-\n * metadata operation for the alias (the value-position grammar has no position for it,\n * measured on the base), so it stays a whole-document `replace` with its precondition.\n */\nexport function aliasWrite(\n draft: DraftView,\n alias: string | undefined,\n writes: { replace(doc: CmsDocument, revision: number | undefined): void },\n): CmsDocument {\n const next = { ...draft.document }\n if (alias === undefined) delete next.alias\n else next.alias = alias\n writes.replace(next, draft.revision)\n return next\n}\n\n/** The patch that writes the value `doc` holds at `path` (unset when absent). */\nexport function positionPatch(doc: CmsDocument, path: ValuePath): DocumentPatch {\n const value = readValueAt(doc, path)\n return value === undefined ? { op: 'unset', path } : { op: 'set', path, value }\n}\n\n/**\n * THE rebase (pure): the session's document with the editor's uncommitted positions laid\n * over it, values from the editor's current draft.\n */\nexport function rebaseDraft(\n sessionDoc: CmsDocument,\n draft: CmsDocument,\n uncommitted: readonly UncommittedPosition[],\n): CmsDocument {\n if (uncommitted.length === 0) return sessionDoc\n const patches = uncommitted.map((position) => positionPatch(draft, position.path))\n return applyDocumentPatches({ document: sessionDoc, patches, since: [] }).document\n}\n\n/**\n * THE form writers' core (pure; what {@link DocumentDraft.setFieldValue} and {@link\n * DocumentDraft.editField} run): `update` transforms the draft at ONE field position;\n * the draft takes the result (`apply`) and, with a revisioned write model, the position\n * is the committer's, authored at its token read BEFORE the draft moves (EC-667).\n *\n * **EC-679 — an editor that opens a document never writes.** The position is EDITED only\n * when the value AT THE POSITION differs from what the DRAFT held there before the call,\n * by the write path's own comparison: {@link semanticValueEqual} over the value the\n * update stores (`writeField` canonicalises, so an emptied value that stores as absent\n * equals absent). A control that re-reports what the draft already holds — the rich-text\n * editor on mount, on a remount (a tab or Draft/Published switch) or on a read-only flip,\n * measured on the base as an `unset` of an absent body or a same-value `set` per open —\n * moves nothing and commits nothing (`undefined`). Compared against the draft, never the\n * session's base: typing a value and typing it away again are two edits, and the revert\n * writes too. Every field type is covered here by construction; the canvas writes through\n * {@link canvasCommit}, not this.\n */\nexport function fieldEdit(input: {\n collection: CollectionDef\n draft: DraftView\n field: string\n locale: Locale\n update: (current: CmsDocument) => CmsDocument\n /** The write model's view tokens (absent: no revisioned write model). */\n tokens: ViewTokens | undefined\n /** Absent: no revisioned write model (the draft moves, nothing is committed). */\n committer: Pick<DraftCommitter, 'edited'> | undefined\n apply: (next: CmsDocument) => void\n}): 'edited' | 'draft' | undefined {\n const { draft, field, committer } = input\n const path = fieldPositionPath(input.collection, field, input.locale)\n const authored = { base: positionBase(draft, path, input.tokens) }\n const next = input.update(draft.document)\n if (semanticValueEqual(readValueAt(draft.document, path), readValueAt(next, path))) return undefined\n input.apply(next)\n if (!committer) return 'draft'\n committer.edited(field, path, authored)\n return 'edited'\n}\n\n/**\n * EC-630 — the canvas's write into the draft (pure). The canvas writer's node patches,\n * keyed by node id, are the canvas's OWN delta: from `base`, the value it last agreed on\n * with the draft, to `value` — never a document diff, and never measured against the\n * draft's CURRENT value (a colleague's change that reached the draft a moment before the\n * edit lifted would read as part of the edit, and its patches would revert it). The draft\n * takes the delta over whatever it holds now (normally `base` itself) and canonicalises the\n * field through the write lens. `undefined` when the canvas changed nothing.\n */\nexport function canvasWrite(input: {\n collection: CollectionDef\n draft: CmsDocument\n field: string\n locale: Locale\n value: unknown\n base: unknown\n lookupManifest: PropFieldLookup\n}): { next: CmsDocument; patches: DocumentPatch[] } | undefined {\n const { collection, draft, field, locale, lookupManifest } = input\n const path = fieldPositionPath(collection, field, locale)\n const localized = path.length > 1 ? locale : undefined\n const patches = compositionPatches(input.base, input.value, field, localized)\n if (patches.length === 0) return undefined\n const applied = applyDocumentPatches({ document: draft, patches, since: [] }).document\n return { next: writeField(collection, draft, field, locale, readValueAt(applied, path), { lookupManifest }), patches }\n}\n\n/**\n * EC-630 — the change of the canvas field made UNDERNEATH the canvas when the draft\n * rebased from `previous` to `next` (pure), numbered after `last`; `undefined` when the\n * field did not move. The canvas's own edits are in both values (the session's view holds\n * every patch it committed, the draft overlays every position it has not), so the change\n * is someone else's: a colleague's patch, a write's canonical row, a resolved conflict.\n *\n * EC-647: it carries `revision`, the revision the draft is at after the rebase, and is\n * reported when only THAT moved too (`from` ≡ `to`): the canvas's agreed value is valid\n * at the newer revision, so its next edit is authored there — the revision follows the\n * value, never runs ahead of it.\n *\n * EC-667: `revision` is the token of the canvas FIELD's position in the shown view\n * ({@link rebaseDraftView}) — under a pending snapshot the snapshot's revision. The canvas\n * mounts at the draft's whole-document token (the earliest, possibly earlier than its\n * field's), so the first rebase with a token reports it even when nothing moved.\n */\nexport function canvasFieldChange(\n previous: CmsDocument,\n next: CmsDocument,\n path: ValuePath,\n last: CanvasFieldChange | undefined,\n revision: number | undefined,\n): CanvasFieldChange | undefined {\n const from = readValueAt(previous, path)\n const to = readValueAt(next, path)\n const tokenHeld = last === undefined ? revision === undefined : last.revision === revision\n if (semanticValueEqual(from, to) && tokenHeld) return undefined\n return { from, to, seq: (last?.seq ?? 0) + 1, revision }\n}\n\n/**\n * EC-647 — the canvas's write path (pure; what {@link DocumentDraft.setCanvasValue} runs):\n * the canvas writer's delta into the draft ({@link canvasWrite}, handed to `apply`), then\n * routed ({@link routeCanvasWrite}) — committed at once, or held as a focused inspector\n * control's uncommitted position — and in both cases paired with the revision of the\n * canvas value the edit was authored on (`lift.revision`), never the draft's: the draft\n * may already hold a colleague's change the canvas has not been rebased onto (Codex F12),\n * and the store must check the edit from the canvas's revision to refuse it where it\n * overlaps that change. `commit` absent → a draft without a revisioned write model (the\n * draft moves, nothing is committed). `undefined` when the canvas changed nothing.\n */\nexport function canvasCommit(input: {\n collection: CollectionDef\n draft: CmsDocument\n field: string\n locale: Locale\n lift: CanvasLift\n lookupManifest: PropFieldLookup\n /** The focused canvas inspector control, if focus is on one. */\n focused: CanvasControl | undefined\n committer: DraftCommitter\n /** The draft takes the canvas's write (before it is routed — a forced commit reads it). */\n apply: (next: CmsDocument) => void\n commit?: (patches: readonly DocumentPatch[], bases: readonly (number | undefined)[]) => void\n}): 'deferred' | 'committed' | 'draft' | undefined {\n const { lift, commit } = input\n const write = canvasWrite({\n collection: input.collection,\n draft: input.draft,\n field: input.field,\n locale: input.locale,\n value: lift.value,\n base: lift.base,\n lookupManifest: input.lookupManifest,\n })\n if (!write) return undefined\n input.apply(write.next)\n if (!commit) return 'draft'\n // EC-630: a burst of typing in a focused inspector control is that control's\n // uncommitted position (its policy commits it); a canvas action commits now.\n return routeCanvasWrite({\n patches: write.patches,\n focused: input.focused,\n committer: input.committer,\n revision: lift.revision,\n commitNow: (patches) => commit(patches, patches.map(() => lift.revision)),\n })\n}\n\n/**\n * The reader's \"now\" for presence liveness: THIS session's own latest heartbeat `at`\n * (written by the same store clock as everyone else's — the playground store runs a\n * deterministic sequential clock, Convex the writers' wall clocks), falling back to the\n * injected clock before the first heartbeat lands. Comparing others' `at` against a\n * different time base would call every colleague stale (measured live in the\n * playground: the store's clock is months behind the browser's).\n */\nexport function presenceReaderNow(\n entries: readonly PresenceEntry[],\n sessionId: string,\n fallback: string,\n): string {\n const own = entries.filter((entry) => entry.sessionId === sessionId).map((entry) => entry.at)\n return own.length > 0 ? own.sort().at(-1)! : fallback\n}\n\nexport interface DocumentDraft {\n draft: CmsDocument\n /**\n * EC-647 — the revision of the session view the draft was last rebased onto (see\n * {@link DraftView}): a writer that authors from {@link draft} (the translation) writes\n * at this revision. EC-667: the view's WHOLE-DOCUMENT token — the earliest authoring\n * revision of any position it shows (a field's own edit reads its position's token).\n */\n revision: number | undefined\n /** Uncommitted (document model) / unsaved (record model) edits exist. */\n dirty: boolean\n /** The record model: the editor renders a Save button + navigation guard. */\n explicitSave: boolean\n /**\n * A field's control wrote `value` (the active locale for a localized field). EC-679: a\n * value the draft already holds at the position is no edit ({@link fieldEdit}).\n */\n setFieldValue(field: FieldDef, value: unknown): void\n /** A writer that transforms ONE field position (fill / reset from base) — the same no-op rule. */\n editField(field: FieldDef, update: (draft: CmsDocument) => CmsDocument): void\n /**\n * The canvas emitted a new value for its field (node patches, committed at once). The\n * lift's `base` is the value the canvas last agreed on with the draft: the canvas\n * writer's delta is `base` → `value`, applied over whatever the draft holds now (EC-630 —\n * a change that landed underneath meanwhile is never reverted by the canvas's own\n * write); its `revision` is the revision that value was agreed at — the revision the\n * edit is committed at (EC-647, {@link canvasCommit}).\n */\n setCanvasValue(field: FieldDef, lift: CanvasLift): void\n /** The slot alias (a document-level key: a whole-document write at the held revision). */\n setAlias(alias: string | undefined): void\n /** Field boundary events (focus / blur / Enter) — the commit policy's triggers. */\n focused(field: FieldDef): void\n blurred(field: FieldDef): void\n entered(field: FieldDef): void\n /**\n * EC-630 — the canvas inspector's boundary events: a control writing `position` of the\n * canvas field `field`, committing on `policy` (THE declaration for its kind). A burst of\n * typing in it is its uncommitted position until blur / Enter / the idle pause.\n */\n canvasFocused(field: FieldDef, position: CanvasControlPosition, policy: CommitPolicy): void\n canvasBlurred(field: FieldDef, position: CanvasControlPosition): void\n canvasEntered(field: FieldDef, position: CanvasControlPosition): void\n /** The forced commit (sync; the persister serialises what follows) / the record model's Save. */\n save(): void\n /** The FORCED FLUSH: commit everything uncommitted and await persistence. */\n flush(): Promise<void>\n /** Adopt a draft from outside (a restored version). */\n replaceDraft(next: CmsDocument): void\n /**\n * Bumped when the canvas must remount from the draft: a restored version replaced the\n * document, or a locale switch pointed the canvas at another bucket. (A change made\n * underneath the canvas is NOT a remount — {@link canvasChange}.)\n */\n canvasNonce: number\n /**\n * EC-630 — the latest change of the canvas field's value made UNDERNEATH the canvas (a\n * colleague's patch by subscription, a write's canonical row, a resolved conflict): the\n * draft's value before and after it. The canvas REBASES onto it — the change lands, its\n * undo history and selection survive — where it used to remount and lose both.\n */\n canvasChange: CanvasFieldChange | undefined\n /** The OTHER sessions present on this document, live by the reader's clock. */\n presence: readonly PresenceEntry[]\n}\n\nexport interface UseDocumentDraftInput {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n lookupManifest: PropFieldLookup\n /** The composition field the canvas edits on this screen, if any. */\n canvasField?: FieldDef\n scheduler?: Scheduler\n visibility?: VisibilitySource\n /** The reader's clock for presence liveness (ISO); `new Date().toISOString()` by default. */\n now?: () => string\n}\n\n/**\n * EC-616 — the base a record save is diffed against: the source's value whenever the draft\n * equals it (the record as opened, or as a save came back), else the previous base (the\n * editor has unsaved changes; a change from another writer meanwhile is not theirs). Pure.\n */\nexport function recordSaveBase(previous: CmsDocument, draft: CmsDocument, doc: CmsDocument): CmsDocument {\n return semanticValueEqual(draft, doc) ? doc : previous\n}\n\nexport function useDocumentDraft(input: UseDocumentDraftInput): DocumentDraft {\n const { source, collection, doc, locale, lookupManifest, canvasField } = input\n const writes = source.writes\n const documentModel = writes.kind === 'document' ? writes : undefined\n const ref: DocumentRef = useMemo(() => ({ collection: doc.collection, id: doc.id }), [doc.collection, doc.id])\n // EC-647: the draft and the revision of the view it rebased onto travel as ONE value\n // (EC-667: with the view itself — the per-position token).\n const [view, setViewState] = useState<DraftView>(() => sessionDraftView(doc, documentModel))\n const draft = view.document\n const draftRef = useRef(view)\n // EC-616: the record as the editor opened it (or as of their last successful save) — the\n // BASE a record rail diffs a save against, so it writes only the editor's changes. It moves\n // only while the draft equals the source's value (at open, and once a save is reflected\n // back): another writer's change arriving mid-edit never becomes the editor's base.\n const baseRef = useRef<CmsDocument>(doc)\n baseRef.current = recordSaveBase(baseRef.current, draftRef.current.document, doc)\n const setView = useCallback((next: DraftView) => {\n draftRef.current = next\n setViewState(next)\n }, [])\n /** An edit of the draft: its value moves, the tokens it is authored against do not. */\n const setDraft = useCallback(\n (next: CmsDocument) => setView({ ...draftRef.current, document: next }),\n [setView],\n )\n const scheduler = input.scheduler ?? browserScheduler\n const now = input.now ?? (() => new Date().toISOString())\n const fieldsByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection])\n\n // --- Committing --------------------------------------------------------------------\n const writesRef = useRef(documentModel)\n writesRef.current = documentModel\n /** EC-630 — the canvas inspector controls that took focus, by committer key (their position + policy). */\n const canvasControls = useRef(new Map<string, CanvasControl>())\n const committer = useMemo<DraftCommitter>(\n () =>\n createDraftCommitter({\n policyOf: (name) => {\n const field = fieldsByName.get(name)\n if (field) return commitPolicyOf(field)\n return canvasControls.current.get(name)?.policy ?? 'blur-or-idle'\n },\n commit: (positions) => {\n const target = writesRef.current\n if (!target) return\n void target.commit(\n ref,\n positions.map((position) => positionPatch(draftRef.current.document, position.path)),\n positions.map((position) => position.base),\n )\n },\n // EC-647: a position's base is the revision of the DRAFT its first keystroke was\n // made on — never the session's tracked revision, which may already hold a\n // colleague's change the draft has not rebased onto yet. EC-667: the form's\n // writers name the POSITION's token ({@link positionBase}, passed to `edited`);\n // this whole-document token (the earliest) is the fallback.\n baseOf: () => draftRef.current.revision,\n scheduler,\n }),\n // One committer per open document (the editor is keyed by it).\n [ref],\n )\n\n // --- The canvas's value: its remount and its rebase ------------------------------------\n const canvasPath = canvasField ? fieldPositionPath(collection, canvasField.name, locale) : undefined\n const canvasPathKey = canvasPath ? formatValuePath(canvasPath) : ''\n const [canvasNonce, setCanvasNonce] = useState(0)\n const [canvasChange, setCanvasChange] = useState<CanvasFieldChange | undefined>(undefined)\n // A locale switch points the canvas at another bucket: remount from it.\n const lastCanvasPathKey = useRef(canvasPathKey)\n useEffect(() => {\n if (lastCanvasPathKey.current === canvasPathKey) return\n lastCanvasPathKey.current = canvasPathKey\n setCanvasNonce((n) => n + 1)\n }, [canvasPathKey])\n\n // --- The rebase (document model) -------------------------------------------------------\n const firstDoc = useRef(true)\n useEffect(() => {\n if (firstDoc.current) {\n firstDoc.current = false\n return\n }\n if (!documentModel) return\n const previous = draftRef.current.document\n // EC-647: the rebase moves the value AND the revision — the session view's own\n // (EC-667: its tokens, whole-document and per position).\n const { view: next, canvasRevision } = rebaseDraftView(doc, draftRef.current, committer.uncommitted(), documentModel, canvasPath)\n setView(next)\n // EC-630: the canvas field moved underneath the canvas — the canvas rebases onto the\n // change (its own edits are in both values: the session's view holds every patch it\n // committed, and the draft overlays every position it has not). EC-647: the change\n // carries the revision — EC-667: the canvas FIELD's token — so the canvas's next edit\n // is authored where its value is.\n if (!canvasPath) return\n setCanvasChange((last) => canvasFieldChange(previous, next.document, canvasPath, last, canvasRevision) ?? last)\n }, [doc])\n\n // --- Writers ----------------------------------------------------------------------------\n const editField = useCallback(\n (field: FieldDef, update: (current: CmsDocument) => CmsDocument) => {\n fieldEdit({\n collection,\n draft: draftRef.current,\n field: field.name,\n locale,\n update,\n tokens: documentModel,\n committer: documentModel ? committer : undefined,\n apply: setDraft,\n })\n },\n [collection, locale, setDraft, documentModel, committer],\n )\n const setFieldValue = useCallback(\n (field: FieldDef, value: unknown) =>\n editField(field, (current) => writeField(collection, current, field.name, locale, value, { lookupManifest })),\n [collection, locale, lookupManifest, editField],\n )\n const setCanvasValue = useCallback(\n (field: FieldDef, lift: CanvasLift) => {\n const focus = committer.focusedField()\n canvasCommit({\n collection,\n draft: draftRef.current.document,\n field: field.name,\n locale,\n lift,\n lookupManifest,\n focused: focus === undefined ? undefined : canvasControls.current.get(focus),\n committer,\n apply: setDraft,\n ...(documentModel\n ? { commit: (patches: readonly DocumentPatch[], bases: readonly (number | undefined)[]) => void documentModel.commit(ref, patches, bases) }\n : {}),\n })\n },\n [collection, locale, lookupManifest, setDraft, documentModel, ref, committer],\n )\n const setAlias = useCallback(\n (alias: string | undefined) => {\n if (!documentModel) {\n aliasWrite(draftRef.current, alias, { replace: (next) => setDraft(next) })\n return\n }\n // The uncommitted positions first (the snapshot holds them), then the snapshot AT\n // THE DRAFT'S REVISION (EC-647 — Codex F10).\n committer.commitAll()\n setDraft(aliasWrite(draftRef.current, alias, documentModel))\n },\n [setDraft, documentModel, committer],\n )\n\n // --- Forced flush ----------------------------------------------------------------------\n const save = useCallback(() => {\n if (documentModel) committer.commitAll()\n else if (writes.kind === 'record') writes.save(draftRef.current.document, baseRef.current)\n }, [documentModel, committer, writes])\n const flush = useCallback(async () => {\n save()\n if (documentModel) await documentModel.flush(ref)\n }, [save, documentModel, ref])\n\n // Tab hide: commit and flush. Unmount: the same, and leave the document.\n const visibility = input.visibility ?? documentVisibility\n useEffect(() => {\n if (!documentModel) return\n return visibility.onHidden(() => {\n committer.commitAll()\n void documentModel.flush(ref)\n })\n }, [ref])\n useEffect(\n () => () => {\n const target = writesRef.current\n if (!target) return\n committer.commitAll()\n committer.dispose()\n void target.flush(ref)\n target.clearPresence(ref)\n },\n [ref],\n )\n\n // --- Subscription + presence ------------------------------------------------------------\n const [presenceEntries, setPresenceEntries] = useState<readonly PresenceEntry[]>([])\n const [clock, setClock] = useState(now)\n useEffect(() => {\n if (!documentModel) return\n return documentModel.subscribe(ref, setPresenceEntries)\n }, [ref])\n const focusedPathRef = useRef<ValuePath | undefined>(undefined)\n const heartbeat = useCallback(() => {\n writesRef.current?.reportPresence(ref, focusedPathRef.current)\n setClock(now())\n }, [ref])\n useEffect(() => {\n if (!documentModel) return\n heartbeat()\n let handle: unknown\n const tick = () => {\n heartbeat()\n handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS)\n }\n handle = scheduler.setTimeout(tick, PRESENCE_HEARTBEAT_MS)\n return () => scheduler.clearTimeout(handle)\n }, [ref])\n const presence = useMemo(\n () =>\n documentModel\n ? livePresence(\n presenceEntries,\n presenceReaderNow(presenceEntries, documentModel.presenceSessionId, clock),\n PRESENCE_TTL_MS,\n ).filter((entry) => entry.sessionId !== documentModel.presenceSessionId)\n : [],\n [presenceEntries, clock, documentModel],\n )\n\n const focused = useCallback(\n (field: FieldDef) => {\n if (!documentModel) return\n committer.focused(field.name)\n focusedPathRef.current = fieldPositionPath(collection, field.name, locale)\n heartbeat()\n },\n [documentModel, committer, collection, locale, heartbeat],\n )\n const blurred = useCallback(\n (field: FieldDef) => {\n if (!documentModel) return\n committer.blurred(field.name)\n focusedPathRef.current = undefined\n heartbeat()\n },\n [documentModel, committer, heartbeat],\n )\n const entered = useCallback(\n (field: FieldDef) => {\n if (documentModel) committer.entered(field.name)\n },\n [documentModel, committer],\n )\n\n // EC-630 — the canvas inspector's commit boundary: a control at a position of the canvas\n // field gained focus, lost it, or saw Enter. Its policy is THE declaration's for its kind.\n const canvasControlOf = useCallback(\n (field: FieldDef, position: CanvasControlPosition, policy: CommitPolicy) =>\n canvasControl(fieldPositionPath(collection, field.name, locale), position, policy),\n [collection, locale],\n )\n const canvasFocused = useCallback(\n (field: FieldDef, position: CanvasControlPosition, policy: CommitPolicy) => {\n if (!documentModel) return\n const control = canvasControlOf(field, position, policy)\n canvasControls.current.set(control.key, control)\n committer.focused(control.key)\n focusedPathRef.current = control.path\n heartbeat()\n },\n [documentModel, committer, canvasControlOf, heartbeat],\n )\n const canvasBlurred = useCallback(\n (field: FieldDef, position: CanvasControlPosition) => {\n if (!documentModel) return\n const key = canvasControlOf(field, position, 'change').key\n committer.blurred(key)\n canvasControls.current.delete(key)\n focusedPathRef.current = undefined\n heartbeat()\n },\n [documentModel, committer, canvasControlOf, heartbeat],\n )\n const canvasEntered = useCallback(\n (field: FieldDef, position: CanvasControlPosition) => {\n if (documentModel) committer.entered(canvasControlOf(field, position, 'change').key)\n },\n [documentModel, committer, canvasControlOf],\n )\n\n const replaceDraft = useCallback(\n (next: CmsDocument) => {\n // A restored version replaces the document: when it moved the canvas field, the\n // canvas remounts from it (its history belonged to the replaced value).\n const moved = canvasPath && !semanticValueEqual(readValueAt(draftRef.current.document, canvasPath), readValueAt(next, canvasPath))\n // EC-647: the restored value comes with the revision the session showed it at.\n setView(sessionDraftView(next, documentModel))\n if (moved) setCanvasNonce((n) => n + 1)\n },\n // The path is keyed by its string form.\n [setView, documentModel, canvasPathKey],\n )\n\n const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc])\n\n return {\n draft,\n revision: view.revision,\n dirty,\n explicitSave: writes.kind === 'record',\n setFieldValue,\n editField,\n setCanvasValue,\n setAlias,\n focused,\n blurred,\n entered,\n canvasFocused,\n canvasBlurred,\n canvasEntered,\n save,\n flush,\n replaceDraft,\n canvasNonce,\n canvasChange,\n presence,\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"record-lens.js","sourceRoot":"","sources":["../../../src/components/records/record-lens.ts"],"names":[],"mappings":"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","sourcesContent":["import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { metaPatchBetween } from '@elytracms/core/persistence'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence'\nimport { metaBagProjection } from '../rail-view-source'\n\n/**\n * EC-616 — the projections between a record kind and the portable list/edit surfaces\n * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)\n * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save\n * writes is unit-testable on its own (it is the one place the write is derived from the\n * edited lens; a slip here silently corrupts a record).\n *\n * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then\n * the kind's columns — a canonical column always wins over a same-named meta key\n * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a\n * re-save wrote it into the canonical `to`).\n * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is\n * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2\n * — a mount's additive field). A column the document does not carry is left out, so the\n * stored value is kept (EC-577: a create default applies once, in the adapter's\n * transaction — never restated here). A REQUIRED text column is always sent (`''` when\n * blank): the editor owns it, and an empty one is refused visibly by the write's\n * validation rather than kept silently. `meta` is omitted when no additive value exists,\n * so a rail with no additive fields never touches the bag.\n */\n\n/** The kind's columns (its declared fields; `meta` is the bag, not a column). */\nexport function recordColumnNames(kind: AnyRecordKind): ReadonlySet<string> {\n return new Set(Object.keys(kind.fields))\n}\n\n/** A stored record as the lens document the portable surfaces edit. */\nexport function recordToLensDocument<K extends AnyRecordKind>(kind: K, entry: RecordEntryOf<K>): CmsDocument {\n const record = entry as Record<string, unknown>\n const columns: Record<string, unknown> = {}\n for (const column of recordColumnNames(kind)) {\n if (record[column] !== undefined) columns[column] = record[column]\n }\n return {\n collection: kind.kind,\n id: String(record.id),\n values: { ...((record.meta as Record<string, unknown> | undefined) ?? {}), ...columns },\n }\n}\n\n/**\n * A column's zod definition, read through `_zod.def` rather than `instanceof`: a kind is\n * declared in whichever package owns it (an addon), whose `zod` may be a different copy\n * than this one — `instanceof` across copies is silently false.\n */\ninterface ZodDefView {\n type: string\n innerType?: { _zod: { def: ZodDefView } }\n entries?: Record<string, unknown>\n}\nfunction defOf(field: unknown): ZodDefView {\n return (field as { _zod: { def: ZodDefView } })._zod.def\n}\n\n/** Is this column a required text column (no default, not optional)? */\nfunction isRequiredText(field: unknown): boolean {\n return defOf(field).type === 'string'\n}\n\n/** The column's own definition, below any optional/default/nullable wrapper. */\nfunction unwrapped(field: unknown): ZodDefView {\n let def = defOf(field)\n while ((def.type === 'optional' || def.type === 'default' || def.type === 'nullable') && def.innerType) {\n def = def.innerType._zod.def\n }\n return def\n}\n\n/** A lens value as the write sends it for one column (a required text column is always a string). */\nfunction columnWriteValue(field: unknown, value: unknown): unknown {\n return isRequiredText(field) ? (typeof value === 'string' ? value : '') : value\n}\n\n/**\n * An edited lens document as the record write (see the module header).\n *\n * **With `base`** — the lens document of the record as the editor OPENED it — the write\n * carries only what the editor changed (EC-616, the multi-session baseline): a column whose\n * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the\n * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored\n * value — a column another writer changed after the editor opened the record survives the\n * editor's save. With nothing changed the write is just the id (a no-op edit).\n *\n * **Without `base`** (a record the editor is creating) the write is whole, as it always was.\n */\nexport function lensDocumentToRecordWrite<K extends AnyRecordKind>(\n kind: K,\n doc: CmsDocument,\n base?: CmsDocument,\n): RecordWriteOf<K> {\n const values = doc.values ?? {}\n const write: Record<string, unknown> = { id: doc.id }\n const columns = recordColumnNames(kind)\n if (!base) {\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = columnWriteValue(field, values[column])\n if (value !== undefined) write[column] = value\n }\n const meta = metaBagProjection(values, columns)\n if (Object.keys(meta).length > 0) write.meta = meta\n return write as RecordWriteOf<K>\n }\n const before = base.values ?? {}\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = values[column]\n if (semanticValueEqual(value, before[column])) continue\n // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.\n write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value)\n }\n // The one per-key diff (core `metaPatchBetween`), shared with the asset editors (EC-634).\n const metaPatch = metaPatchBetween(metaBagProjection(before, columns), metaBagProjection(values, columns))\n if (Object.keys(metaPatch).length > 0) write.metaPatch = metaPatch\n return write as RecordWriteOf<K>\n}\n\n/** The lens field type a column edits as (unwrapping optional/default); `undefined` = not lens-editable. */\nfunction lensFieldType(field: unknown): FieldDef['type'] | undefined {\n switch (unwrapped(field).type) {\n case 'string':\n return 'text'\n case 'boolean':\n return 'boolean'\n case 'number':\n return 'number'\n case 'enum':\n return 'select'\n default:\n return undefined\n }\n}\n\nfunction humanize(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n\n/**\n * The DEFAULT lens of a kind: one field per column whose type the portable form edits\n * (text, boolean, number, select — derived from the column's zod type), labelled from its\n * name. A column of another shape (an object, a list) is not in the lens: the rail never\n * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens\n * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the\n * built-in rails use), or passes its own `schema`.\n */\nexport function recordLensDef(kind: AnyRecordKind, options: { label?: string; titleField?: string } = {}): CollectionDef {\n const fields: Record<string, unknown>[] = []\n for (const [name, field] of Object.entries(kind.fields)) {\n const type = lensFieldType(field)\n if (!type) continue\n const entry: Record<string, unknown> = { name, type, form: { label: humanize(name) } }\n if (type === 'select') {\n entry.options = Object.values(unwrapped(field).entries ?? {}).map((value) => ({ value: String(value), label: String(value) }))\n }\n fields.push(entry)\n }\n const titleField = options.titleField ?? fields.find((field) => field.type === 'text')?.name\n return collectionDefSchema.parse({\n id: kind.kind,\n // A record kind is not a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n ...(titleField ? { titleField } : {}),\n form: { label: options.label ?? humanize(kind.module) },\n fields,\n })\n}\n"]}
1
+ {"version":3,"file":"record-lens.js","sourceRoot":"","sources":["../../../src/components/records/record-lens.ts"],"names":[],"mappings":"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","sourcesContent":["import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport { metaPatchBetween } from '@elytracms/core/persistence'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence'\nimport { metaBagProjection } from '../rail-view-source'\n\n/**\n * EC-616 — the projections between a record kind and the portable list/edit surfaces\n * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)\n * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save\n * writes is unit-testable on its own (it is the one place the write is derived from the\n * edited lens; a slip here silently corrupts a record).\n *\n * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then\n * the kind's columns — a canonical column always wins over a same-named meta key\n * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a\n * re-save wrote it into the canonical `to`).\n * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is\n * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2\n * — a mount's additive field). A column the document does not carry is left out, so the\n * stored value is kept (EC-577: a create default applies once, in the adapter's\n * transaction — never restated here). A REQUIRED text column is always sent (`''` when\n * blank): the editor owns it, and an empty one is refused visibly by the write's\n * validation rather than kept silently. `meta` is omitted when no additive value exists,\n * so a rail with no additive fields never touches the bag.\n */\n\n/** The kind's columns (its declared fields; `meta` is the bag, not a column). */\nexport function recordColumnNames(kind: AnyRecordKind): ReadonlySet<string> {\n return new Set(Object.keys(kind.fields))\n}\n\n/** A stored record as the lens document the portable surfaces edit. */\nexport function recordToLensDocument<K extends AnyRecordKind>(kind: K, entry: RecordEntryOf<K>): CmsDocument {\n const record = entry as Record<string, unknown>\n const columns: Record<string, unknown> = {}\n for (const column of recordColumnNames(kind)) {\n if (record[column] !== undefined) columns[column] = record[column]\n }\n return {\n collection: kind.kind,\n id: String(record.id),\n values: { ...((record.meta as Record<string, unknown> | undefined) ?? {}), ...columns },\n }\n}\n\n/**\n * A column's zod definition, read through `_zod.def`: a kind is declared in whichever\n * package owns it (an addon), whose `zod` may be a different copy than this one, and the\n * def is the one field every zod 4 copy shares. (EC-678 correction, as EC-639 corrected\n * `persistence/record-write.ts`: zod 4's `instanceof` is trait-based and answers TRUE across\n * copies — measured 2026-10-28 with the repo's `zod-copy-older` / `zod-copy-newer` against\n * its own `zod`, control `ZodString` vs `ZodNumber` false — so it was never \"silently false\";\n * it simply cannot tell the copies apart. Reading the def is the shape this lens needs.)\n */\ninterface ZodDefView {\n type: string\n innerType?: { _zod: { def: ZodDefView } }\n entries?: Record<string, unknown>\n}\nfunction defOf(field: unknown): ZodDefView {\n return (field as { _zod: { def: ZodDefView } })._zod.def\n}\n\n/** Is this column a required text column (no default, not optional)? */\nfunction isRequiredText(field: unknown): boolean {\n return defOf(field).type === 'string'\n}\n\n/** The column's own definition, below any optional/default/nullable wrapper. */\nfunction unwrapped(field: unknown): ZodDefView {\n let def = defOf(field)\n while ((def.type === 'optional' || def.type === 'default' || def.type === 'nullable') && def.innerType) {\n def = def.innerType._zod.def\n }\n return def\n}\n\n/** A lens value as the write sends it for one column (a required text column is always a string). */\nfunction columnWriteValue(field: unknown, value: unknown): unknown {\n return isRequiredText(field) ? (typeof value === 'string' ? value : '') : value\n}\n\n/**\n * An edited lens document as the record write (see the module header).\n *\n * **With `base`** — the lens document of the record as the editor OPENED it — the write\n * carries only what the editor changed (EC-616, the multi-session baseline): a column whose\n * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the\n * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored\n * value — a column another writer changed after the editor opened the record survives the\n * editor's save. With nothing changed the write is just the id (a no-op edit).\n *\n * **Without `base`** (a record the editor is creating) the write is whole, as it always was.\n */\nexport function lensDocumentToRecordWrite<K extends AnyRecordKind>(\n kind: K,\n doc: CmsDocument,\n base?: CmsDocument,\n): RecordWriteOf<K> {\n const values = doc.values ?? {}\n const write: Record<string, unknown> = { id: doc.id }\n const columns = recordColumnNames(kind)\n if (!base) {\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = columnWriteValue(field, values[column])\n if (value !== undefined) write[column] = value\n }\n const meta = metaBagProjection(values, columns)\n if (Object.keys(meta).length > 0) write.meta = meta\n return write as RecordWriteOf<K>\n }\n const before = base.values ?? {}\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = values[column]\n if (semanticValueEqual(value, before[column])) continue\n // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.\n write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value)\n }\n // The one per-key diff (core `metaPatchBetween`), shared with the asset editors (EC-634).\n const metaPatch = metaPatchBetween(metaBagProjection(before, columns), metaBagProjection(values, columns))\n if (Object.keys(metaPatch).length > 0) write.metaPatch = metaPatch\n return write as RecordWriteOf<K>\n}\n\n/** The lens field type a column edits as (unwrapping optional/default); `undefined` = not lens-editable. */\nfunction lensFieldType(field: unknown): FieldDef['type'] | undefined {\n switch (unwrapped(field).type) {\n case 'string':\n return 'text'\n case 'boolean':\n return 'boolean'\n case 'number':\n return 'number'\n case 'enum':\n return 'select'\n default:\n return undefined\n }\n}\n\nfunction humanize(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n\n/**\n * The DEFAULT lens of a kind: one field per column whose type the portable form edits\n * (text, boolean, number, select — derived from the column's zod type), labelled from its\n * name. A column of another shape (an object, a list) is not in the lens: the rail never\n * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens\n * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the\n * built-in rails use), or passes its own `schema`.\n */\nexport function recordLensDef(kind: AnyRecordKind, options: { label?: string; titleField?: string } = {}): CollectionDef {\n const fields: Record<string, unknown>[] = []\n for (const [name, field] of Object.entries(kind.fields)) {\n const type = lensFieldType(field)\n if (!type) continue\n const entry: Record<string, unknown> = { name, type, form: { label: humanize(name) } }\n if (type === 'select') {\n entry.options = Object.values(unwrapped(field).entries ?? {}).map((value) => ({ value: String(value), label: String(value) }))\n }\n fields.push(entry)\n }\n const titleField = options.titleField ?? fields.find((field) => field.type === 'text')?.name\n return collectionDefSchema.parse({\n id: kind.kind,\n // A record kind is not a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n ...(titleField ? { titleField } : {}),\n form: { label: options.label ?? humanize(kind.module) },\n fields,\n })\n}\n"]}
@@ -13,4 +13,4 @@
13
13
  * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
14
14
  * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
15
15
  */
16
- export declare const ENGINE_VERSION = "0.0.97";
16
+ export declare const ENGINE_VERSION = "0.0.98";
@@ -13,5 +13,5 @@
13
13
  * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
14
14
  * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
15
15
  */
16
- export const ENGINE_VERSION = '0.0.97';
16
+ export const ENGINE_VERSION = '0.0.98';
17
17
  //# sourceMappingURL=engine-version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. `pnpm release:bump` writes this\n * alongside the five `package.json` versions (EC-608).\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.97'\n"]}
1
+ {"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. `pnpm release:bump` writes this\n * alongside the five `package.json` versions (EC-608).\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.98'\n"]}
@@ -479,6 +479,7 @@ export const deMessages = {
479
479
  'cms.issue.invalid-collection-config': 'Ungültige Sammlungskonfiguration',
480
480
  'cms.issue.invalid-field-config': 'Ungültige Feldkonfiguration',
481
481
  'cms.issue.reserved-field-name': 'Reservierter Feldname',
482
+ 'cms.issue.prototype-key': 'Schlüssel nicht speicherbar',
482
483
  'cms.issue.missing-required-field': 'Pflichtfeld fehlt',
483
484
  'cms.issue.invalid-field-value': 'Ungültiger Wert',
484
485
  'cms.issue.invalid-field-value.warning': 'Wert prüfen',
@@ -593,6 +594,8 @@ export const deMessages = {
593
594
  'cms.issueMessage.unfilledSlot': 'Der Slot „{slot}“ von „{collection}“ hat noch kein Dokument. Weisen Sie eines zu, damit Code, der „{slot}“ auflöst, es findet.',
594
595
  'cms.issueMessage.reservedFieldName': 'Der Feldname „{field}“ ist reserviert (der Diskriminator selbstbeschreibender Knoten): Ein Feld namens „{field}“ darf nur ein Auswahlfeld sein, dessen Optionen keinen Knotentyp der Engine enthalten (reference, asset, block, elytra/*) — dieses ist ein Feld vom Typ „{kind}“.',
595
596
  'cms.issueMessage.reservedFieldTypeOption': 'Das Auswahlfeld „{field}“ führt die Option „{option}“, einen Knotentyp der Engine — ein „{field}“ mit dem Wert „{option}“ würde als Engine-Knoten gelesen (reference, asset, block, elytra/*). Benennen Sie die Option um.',
597
+ 'cms.issueMessage.reservedPrototypeFieldName': 'Der Feldname „{field}“ ist reserviert: Unter ihm lässt sich kein Wert speichern (weder ein JavaScript-Objekt noch der Convex-Speicher behält den Schlüssel, und jeder Schreibvorgang lehnt ihn ab). Benennen Sie das Feld um.',
598
+ 'cms.issueMessage.prototypeKey': '„{key}“ kann nicht als Schlüssel verwendet werden: Weder ein JavaScript-Objekt noch der Convex-Speicher behält ihn. Bitte umbenennen.',
596
599
  'cms.issueMessage.duplicateSelectOption': 'Doppelte Auswahloption „{option}“ im Feld „{field}“.',
597
600
  'cms.issueMessage.duplicateObjectField': 'Doppeltes Feld „{field}“ im Objektfeld „{parent}“.',
598
601
  'cms.issueMessage.assetCollectionSingleton': 'Die Mediensammlung „{collection}“ kann kein Einzelstück sein.',