@elytracms/studio 0.0.95 → 0.0.96

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 (92) hide show
  1. package/convex/cms.ts +124 -8
  2. package/convex/strict-reference-race.test.ts +139 -0
  3. package/dist/components/ai/ai-chat-panel.js +15 -2
  4. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  5. package/dist/components/ai/translate-document-action.d.ts +6 -0
  6. package/dist/components/ai/translate-document-action.js.map +1 -1
  7. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  8. package/dist/components/ai/translate-document-dialog.js +5 -5
  9. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  10. package/dist/components/assets/asset-detail.js +5 -2
  11. package/dist/components/assets/asset-detail.js.map +1 -1
  12. package/dist/components/assets/asset-locale.d.ts +7 -0
  13. package/dist/components/assets/asset-locale.js +16 -2
  14. package/dist/components/assets/asset-locale.js.map +1 -1
  15. package/dist/components/assets/asset-meta-drift-notice.d.ts +13 -0
  16. package/dist/components/assets/asset-meta-drift-notice.js +22 -0
  17. package/dist/components/assets/asset-meta-drift-notice.js.map +1 -0
  18. package/dist/components/assets/asset-meta-label.d.ts +2 -1
  19. package/dist/components/assets/asset-meta-label.js +4 -2
  20. package/dist/components/assets/asset-meta-label.js.map +1 -1
  21. package/dist/components/assets/asset-metadata.d.ts +33 -13
  22. package/dist/components/assets/asset-metadata.js +46 -22
  23. package/dist/components/assets/asset-metadata.js.map +1 -1
  24. package/dist/components/assets/asset-search.d.ts +6 -2
  25. package/dist/components/assets/asset-search.js +15 -5
  26. package/dist/components/assets/asset-search.js.map +1 -1
  27. package/dist/components/assets/assets-collection-view-source.d.ts +5 -3
  28. package/dist/components/assets/assets-collection-view-source.js +7 -5
  29. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  30. package/dist/components/builder/canvas-rebase.d.ts +36 -12
  31. package/dist/components/builder/canvas-rebase.js +13 -3
  32. package/dist/components/builder/canvas-rebase.js.map +1 -1
  33. package/dist/components/builder/canvas-render-context.js +5 -4
  34. package/dist/components/builder/canvas-render-context.js.map +1 -1
  35. package/dist/components/builder/composition-field-editor.d.ts +12 -6
  36. package/dist/components/builder/composition-field-editor.js +6 -5
  37. package/dist/components/builder/composition-field-editor.js.map +1 -1
  38. package/dist/components/cms/asset-picker-core.d.ts +2 -2
  39. package/dist/components/cms/asset-picker-core.js +2 -2
  40. package/dist/components/cms/asset-picker-core.js.map +1 -1
  41. package/dist/components/cms/asset-picker.js +3 -1
  42. package/dist/components/cms/asset-picker.js.map +1 -1
  43. package/dist/components/cms/canvas-commit.d.ts +6 -0
  44. package/dist/components/cms/canvas-commit.js +5 -1
  45. package/dist/components/cms/canvas-commit.js.map +1 -1
  46. package/dist/components/cms/cms-store.d.ts +21 -8
  47. package/dist/components/cms/cms-store.js +19 -10
  48. package/dist/components/cms/cms-store.js.map +1 -1
  49. package/dist/components/cms/collection-table.js +4 -11
  50. package/dist/components/cms/collection-table.js.map +1 -1
  51. package/dist/components/cms/detail-editor.js +2 -2
  52. package/dist/components/cms/detail-editor.js.map +1 -1
  53. package/dist/components/cms/draft-commit.d.ts +16 -3
  54. package/dist/components/cms/draft-commit.js +3 -2
  55. package/dist/components/cms/draft-commit.js.map +1 -1
  56. package/dist/components/cms/use-document-draft.d.ts +67 -9
  57. package/dist/components/cms/use-document-draft.js +112 -39
  58. package/dist/components/cms/use-document-draft.js.map +1 -1
  59. package/dist/components/collection-view/cms-collection-view-source.js +6 -4
  60. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  61. package/dist/components/components/transfer-dialog.js +6 -4
  62. package/dist/components/components/transfer-dialog.js.map +1 -1
  63. package/dist/lib/ai/chat-state.d.ts +7 -0
  64. package/dist/lib/ai/chat-state.js.map +1 -1
  65. package/dist/lib/ai/tools.d.ts +8 -0
  66. package/dist/lib/ai/tools.js +4 -0
  67. package/dist/lib/ai/tools.js.map +1 -1
  68. package/dist/lib/cms-views/view-model.d.ts +15 -8
  69. package/dist/lib/cms-views/view-model.js +19 -5
  70. package/dist/lib/cms-views/view-model.js.map +1 -1
  71. package/dist/lib/collection-view/__fixtures__/document-writes.js +1 -0
  72. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -1
  73. package/dist/lib/collection-view/collection-view-source.d.ts +15 -7
  74. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  75. package/dist/lib/data/collection-content.js +5 -0
  76. package/dist/lib/data/collection-content.js.map +1 -1
  77. package/dist/lib/engine-version.d.ts +1 -1
  78. package/dist/lib/engine-version.js +1 -1
  79. package/dist/lib/engine-version.js.map +1 -1
  80. package/dist/lib/i18n/messages.de.js +13 -1
  81. package/dist/lib/i18n/messages.de.js.map +1 -1
  82. package/dist/lib/i18n/messages.en.d.ts +11 -0
  83. package/dist/lib/i18n/messages.en.js +14 -1
  84. package/dist/lib/i18n/messages.en.js.map +1 -1
  85. package/dist/lib/operations/cms-persistence.d.ts +80 -35
  86. package/dist/lib/operations/cms-persistence.js +348 -85
  87. package/dist/lib/operations/cms-persistence.js.map +1 -1
  88. package/dist/lib/persistence/convex-adapter.js +54 -8
  89. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  90. package/dist/lib/persona/labels.js +1 -0
  91. package/dist/lib/persona/labels.js.map +1 -1
  92. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"draft-commit.js","sourceRoot":"","sources":["../../../src/components/cms/draft-commit.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatValuePath } from '@elytracms/core/cms-core'\nimport type { ValuePath } from '@elytracms/core/cms-core'\nimport type { CommitPolicy } from './commit-policy'\nimport { IDLE_COMMIT_MS } from './commit-policy'\n\n/**\n * EC-595 A2 — the editor's COMMIT-ON-QUIESCENCE core (pure, clock injected; the house\n * pattern: no DOM, no React).\n *\n * The editor's draft changes per keystroke (live validation reads it); what is\n * COMMITTED — handed to the persister as patches — follows each field's\n * {@link CommitPolicy}. This core tracks the uncommitted positions and decides when\n * each is committed:\n *\n * - an edit of a `change`-policy field commits at once;\n * - an edit of the FOCUSED field waits for its blur, Enter (`blur-or-enter`) or idle\n * pause (`blur-or-idle`, re-armed on every edit — {@link Scheduler});\n * - an edit of a field that does NOT have focus (a derived value — the auto-slug\n * following the title, fill-from-base) rides along with the focused field's next\n * commit, or commits at once when no field has focus;\n * - {@link DraftCommitter.commitAll} is the FORCED commit (publish, unmount, tab hide,\n * duplicate, an AI proposal, transfer) — everything uncommitted, now.\n *\n * `commit` receives the positions; the caller builds the patches from its latest draft\n * (the value AT commit time, so a committed value is never a stale keystroke).\n */\n\n/** The injected clock: timers for the idle pause. */\nexport interface Scheduler {\n setTimeout(callback: () => void, ms: number): unknown\n clearTimeout(handle: unknown): void\n}\n\n/** The real clock (the editor's default). */\nexport const browserScheduler: Scheduler = {\n setTimeout: (callback, ms) => globalThis.setTimeout(callback, ms),\n clearTimeout: (handle) => globalThis.clearTimeout(handle as ReturnType<typeof setTimeout>),\n}\n\n/** One uncommitted position and the field (by name) whose control wrote it. */\nexport interface UncommittedPosition {\n readonly field: string\n readonly path: ValuePath\n /**\n * The revision the user's view was at when they STARTED editing this position — the\n * patch's base. A colleague's change of the same position that arrives (by\n * subscription) while the user is still typing is therefore refused on that field at\n * commit, never overwritten (EC-595 point 7: \"local wins until flushed, then the\n * per-path check decides\").\n */\n readonly base: number | undefined\n}\n\nexport interface DraftCommitter {\n /** A field's control wrote `path`. */\n edited(field: string, path: ValuePath): void\n /** Focus entered a field's control. */\n focused(field: string): void\n /** Focus left a field's control (commits it and what rode along). */\n blurred(field: string): void\n /** Enter in a single-line control (commits it). */\n entered(field: string): void\n /** The forced commit: everything uncommitted, now. */\n commitAll(): void\n /** The uncommitted positions (the editor overlays them on a rebased draft). */\n uncommitted(): readonly UncommittedPosition[]\n /** The field that has focus, if any. */\n focusedField(): string | undefined\n /** Cancel the idle timers (unmount). */\n dispose(): void\n}\n\nexport interface DraftCommitterOptions {\n policyOf: (field: string) => CommitPolicy\n /** The revision the session's view is at now (captured as a position's base on its first edit). */\n baseOf?: () => number | undefined\n commit: (positions: readonly UncommittedPosition[]) => void\n scheduler?: Scheduler\n idleMs?: number\n}\n\nexport function createDraftCommitter(options: DraftCommitterOptions): DraftCommitter {\n const scheduler = options.scheduler ?? browserScheduler\n const idleMs = options.idleMs ?? IDLE_COMMIT_MS\n /** Uncommitted positions by path key (one per position — a re-edit keeps one entry). */\n const pending = new Map<string, UncommittedPosition>()\n const idleTimers = new Map<string, unknown>()\n let focus: string | undefined\n\n const cancelIdle = (field: string) => {\n const handle = idleTimers.get(field)\n if (handle !== undefined) scheduler.clearTimeout(handle)\n idleTimers.delete(field)\n }\n\n /** Commit `field`'s positions plus every position of a field that does not have focus. */\n const commitField = (field: string) => {\n cancelIdle(field)\n const batch = [...pending.entries()].filter(\n ([, position]) => position.field === field || position.field !== focus,\n )\n if (batch.length === 0) return\n for (const [key] of batch) pending.delete(key)\n options.commit(batch.map(([, position]) => position))\n }\n\n return {\n edited(field, path) {\n const key = formatValuePath(path)\n const previous = pending.get(key)\n pending.set(key, { field, path, base: previous ? previous.base : options.baseOf?.() })\n const policy = options.policyOf(field)\n if (policy === 'change' || focus === undefined) {\n commitField(field)\n return\n }\n if (focus !== field) return // rides along with the focused field's commit\n if (policy === 'blur-or-idle') {\n cancelIdle(field)\n idleTimers.set(\n field,\n scheduler.setTimeout(() => {\n idleTimers.delete(field)\n commitField(field)\n }, idleMs),\n )\n }\n },\n focused(field) {\n if (focus !== undefined && focus !== field) commitField(focus)\n focus = field\n },\n blurred(field) {\n if (focus === field) focus = undefined\n commitField(field)\n },\n entered(field) {\n if (options.policyOf(field) === 'blur-or-enter') commitField(field)\n },\n commitAll() {\n for (const field of [...idleTimers.keys()]) cancelIdle(field)\n if (pending.size === 0) return\n const batch = [...pending.values()]\n pending.clear()\n options.commit(batch)\n },\n uncommitted: () => [...pending.values()],\n focusedField: () => focus,\n dispose() {\n for (const field of [...idleTimers.keys()]) cancelIdle(field)\n },\n }\n}\n"]}
1
+ {"version":3,"file":"draft-commit.js","sourceRoot":"","sources":["../../../src/components/cms/draft-commit.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatValuePath } from '@elytracms/core/cms-core'\nimport type { ValuePath } from '@elytracms/core/cms-core'\nimport type { CommitPolicy } from './commit-policy'\nimport { IDLE_COMMIT_MS } from './commit-policy'\n\n/**\n * EC-595 A2 — the editor's COMMIT-ON-QUIESCENCE core (pure, clock injected; the house\n * pattern: no DOM, no React).\n *\n * The editor's draft changes per keystroke (live validation reads it); what is\n * COMMITTED — handed to the persister as patches — follows each field's\n * {@link CommitPolicy}. This core tracks the uncommitted positions and decides when\n * each is committed:\n *\n * - an edit of a `change`-policy field commits at once;\n * - an edit of the FOCUSED field waits for its blur, Enter (`blur-or-enter`) or idle\n * pause (`blur-or-idle`, re-armed on every edit — {@link Scheduler});\n * - an edit of a field that does NOT have focus (a derived value — the auto-slug\n * following the title, fill-from-base) rides along with the focused field's next\n * commit, or commits at once when no field has focus;\n * - {@link DraftCommitter.commitAll} is the FORCED commit (publish, unmount, tab hide,\n * duplicate, an AI proposal, transfer) — everything uncommitted, now.\n *\n * `commit` receives the positions; the caller builds the patches from its latest draft\n * (the value AT commit time, so a committed value is never a stale keystroke).\n */\n\n/** The injected clock: timers for the idle pause. */\nexport interface Scheduler {\n setTimeout(callback: () => void, ms: number): unknown\n clearTimeout(handle: unknown): void\n}\n\n/** The real clock (the editor's default). */\nexport const browserScheduler: Scheduler = {\n setTimeout: (callback, ms) => globalThis.setTimeout(callback, ms),\n clearTimeout: (handle) => globalThis.clearTimeout(handle as ReturnType<typeof setTimeout>),\n}\n\n/** One uncommitted position and the field (by name) whose control wrote it. */\nexport interface UncommittedPosition {\n readonly field: string\n readonly path: ValuePath\n /**\n * The revision the user's view was at when they STARTED editing this position — the\n * patch's base. A colleague's change of the same position that arrives (by\n * subscription) while the user is still typing is therefore refused on that field at\n * commit, never overwritten (EC-595 point 7: \"local wins until flushed, then the\n * per-path check decides\").\n */\n readonly base: number | undefined\n}\n\nexport interface DraftCommitter {\n /**\n * A field's control wrote `path`. `authored` — the revision the edit was authored\n * against when the writer knows it better than the draft does (EC-647: a canvas\n * inspector control's burst is authored on the CANVAS's value, whose revision can trail\n * the draft's); absent → {@link DraftCommitterOptions.baseOf}. A re-edit of an\n * uncommitted position keeps the base of its first edit either way.\n */\n edited(field: string, path: ValuePath, authored?: { readonly base: number | undefined }): void\n /** Focus entered a field's control. */\n focused(field: string): void\n /** Focus left a field's control (commits it and what rode along). */\n blurred(field: string): void\n /** Enter in a single-line control (commits it). */\n entered(field: string): void\n /** The forced commit: everything uncommitted, now. */\n commitAll(): void\n /** The uncommitted positions (the editor overlays them on a rebased draft). */\n uncommitted(): readonly UncommittedPosition[]\n /** The field that has focus, if any. */\n focusedField(): string | undefined\n /** Cancel the idle timers (unmount). */\n dispose(): void\n}\n\nexport interface DraftCommitterOptions {\n policyOf: (field: string) => CommitPolicy\n /**\n * The revision the editor's draft is at now — the revision of the view a field's edit\n * is authored on (captured as a position's base on its first edit). EC-647: the\n * draft's, never the session's tracked one (which may hold a change the draft has not\n * rebased onto yet).\n */\n baseOf?: () => number | undefined\n commit: (positions: readonly UncommittedPosition[]) => void\n scheduler?: Scheduler\n idleMs?: number\n}\n\nexport function createDraftCommitter(options: DraftCommitterOptions): DraftCommitter {\n const scheduler = options.scheduler ?? browserScheduler\n const idleMs = options.idleMs ?? IDLE_COMMIT_MS\n /** Uncommitted positions by path key (one per position — a re-edit keeps one entry). */\n const pending = new Map<string, UncommittedPosition>()\n const idleTimers = new Map<string, unknown>()\n let focus: string | undefined\n\n const cancelIdle = (field: string) => {\n const handle = idleTimers.get(field)\n if (handle !== undefined) scheduler.clearTimeout(handle)\n idleTimers.delete(field)\n }\n\n /** Commit `field`'s positions plus every position of a field that does not have focus. */\n const commitField = (field: string) => {\n cancelIdle(field)\n const batch = [...pending.entries()].filter(\n ([, position]) => position.field === field || position.field !== focus,\n )\n if (batch.length === 0) return\n for (const [key] of batch) pending.delete(key)\n options.commit(batch.map(([, position]) => position))\n }\n\n return {\n edited(field, path, authored) {\n const key = formatValuePath(path)\n const previous = pending.get(key)\n const base = previous ? previous.base : authored ? authored.base : options.baseOf?.()\n pending.set(key, { field, path, base })\n const policy = options.policyOf(field)\n if (policy === 'change' || focus === undefined) {\n commitField(field)\n return\n }\n if (focus !== field) return // rides along with the focused field's commit\n if (policy === 'blur-or-idle') {\n cancelIdle(field)\n idleTimers.set(\n field,\n scheduler.setTimeout(() => {\n idleTimers.delete(field)\n commitField(field)\n }, idleMs),\n )\n }\n },\n focused(field) {\n if (focus !== undefined && focus !== field) commitField(focus)\n focus = field\n },\n blurred(field) {\n if (focus === field) focus = undefined\n commitField(field)\n },\n entered(field) {\n if (options.policyOf(field) === 'blur-or-enter') commitField(field)\n },\n commitAll() {\n for (const field of [...idleTimers.keys()]) cancelIdle(field)\n if (pending.size === 0) return\n const batch = [...pending.values()]\n pending.clear()\n options.commit(batch)\n },\n uncommitted: () => [...pending.values()],\n focusedField: () => focus,\n dispose() {\n for (const field of [...idleTimers.keys()]) cancelIdle(field)\n },\n }\n}\n"]}
@@ -1,10 +1,10 @@
1
1
  import type { PresenceEntry } from '@elytracms/core/persistence';
2
2
  import type { CmsDocument, CollectionDef, DocumentPatch, FieldDef, Locale, PropFieldLookup, ValuePath } from '@elytracms/core/cms-core';
3
3
  import type { CollectionViewSource } from '../../lib/collection-view/collection-view-source.js';
4
- import type { CanvasFieldChange } from '../builder/canvas-rebase.js';
4
+ import type { CanvasFieldChange, CanvasLift } from '../builder/canvas-rebase.js';
5
5
  import type { CommitPolicy } from './commit-policy.js';
6
- import type { CanvasControlPosition } from './canvas-commit.js';
7
- import type { Scheduler, UncommittedPosition } from './draft-commit.js';
6
+ import type { CanvasControl, CanvasControlPosition } from './canvas-commit.js';
7
+ import type { DraftCommitter, Scheduler, UncommittedPosition } from './draft-commit.js';
8
8
  /**
9
9
  * EC-595 A2 — the detail editor's draft over the revisioned document (and, for the
10
10
  * explicit-save rails, over a record).
@@ -40,6 +40,26 @@ export interface VisibilitySource {
40
40
  onHidden(onHidden: () => void): () => void;
41
41
  }
42
42
  export declare const documentVisibility: VisibilitySource;
43
+ /**
44
+ * EC-647 — the editor's draft and the revision of the session view it was last rebased
45
+ * onto, as ONE value: every edit made on the draft was authored against that revision (an
46
+ * uncommitted position keeps the revision its first keystroke saw). `undefined`: the view
47
+ * had no revision (a fixture session, a record rail, a document still being created).
48
+ */
49
+ export interface DraftView {
50
+ readonly document: CmsDocument;
51
+ readonly revision: number | undefined;
52
+ }
53
+ /**
54
+ * EC-647 — the slot alias write (pure): the draft with `alias` set (or removed), written
55
+ * whole AT THE DRAFT'S REVISION — never the session's tracked one, which may already hold
56
+ * a colleague's change the draft has not absorbed (Codex F10). There is no identity-
57
+ * metadata operation for the alias (the value-position grammar has no position for it,
58
+ * measured on the base), so it stays a whole-document `replace` with its precondition.
59
+ */
60
+ export declare function aliasWrite(draft: DraftView, alias: string | undefined, writes: {
61
+ replace(doc: CmsDocument, revision: number | undefined): void;
62
+ }): CmsDocument;
43
63
  /** The patch that writes the value `doc` holds at `path` (unset when absent). */
44
64
  export declare function positionPatch(doc: CmsDocument, path: ValuePath): DocumentPatch;
45
65
  /**
@@ -74,8 +94,38 @@ export declare function canvasWrite(input: {
74
94
  * field did not move. The canvas's own edits are in both values (the session's view holds
75
95
  * every patch it committed, the draft overlays every position it has not), so the change
76
96
  * is someone else's: a colleague's patch, a write's canonical row, a resolved conflict.
97
+ *
98
+ * EC-647: it carries `revision`, the revision the draft is at after the rebase, and is
99
+ * reported when only THAT moved too (`from` ≡ `to`): the canvas's agreed value is valid
100
+ * at the newer revision, so its next edit is authored there — the revision follows the
101
+ * value, never runs ahead of it.
77
102
  */
78
- export declare function canvasFieldChange(previous: CmsDocument, next: CmsDocument, path: ValuePath, last: CanvasFieldChange | undefined): CanvasFieldChange | undefined;
103
+ export declare function canvasFieldChange(previous: CmsDocument, next: CmsDocument, path: ValuePath, last: CanvasFieldChange | undefined, revision: number | undefined): CanvasFieldChange | undefined;
104
+ /**
105
+ * EC-647 — the canvas's write path (pure; what {@link DocumentDraft.setCanvasValue} runs):
106
+ * the canvas writer's delta into the draft ({@link canvasWrite}, handed to `apply`), then
107
+ * routed ({@link routeCanvasWrite}) — committed at once, or held as a focused inspector
108
+ * control's uncommitted position — and in both cases paired with the revision of the
109
+ * canvas value the edit was authored on (`lift.revision`), never the draft's: the draft
110
+ * may already hold a colleague's change the canvas has not been rebased onto (Codex F12),
111
+ * and the store must check the edit from the canvas's revision to refuse it where it
112
+ * overlaps that change. `commit` absent → a draft without a revisioned write model (the
113
+ * draft moves, nothing is committed). `undefined` when the canvas changed nothing.
114
+ */
115
+ export declare function canvasCommit(input: {
116
+ collection: CollectionDef;
117
+ draft: CmsDocument;
118
+ field: string;
119
+ locale: Locale;
120
+ lift: CanvasLift;
121
+ lookupManifest: PropFieldLookup;
122
+ /** The focused canvas inspector control, if focus is on one. */
123
+ focused: CanvasControl | undefined;
124
+ committer: DraftCommitter;
125
+ /** The draft takes the canvas's write (before it is routed — a forced commit reads it). */
126
+ apply: (next: CmsDocument) => void;
127
+ commit?: (patches: readonly DocumentPatch[], bases: readonly (number | undefined)[]) => void;
128
+ }): 'deferred' | 'committed' | 'draft' | undefined;
79
129
  /**
80
130
  * The reader's "now" for presence liveness: THIS session's own latest heartbeat `at`
81
131
  * (written by the same store clock as everyone else's — the playground store runs a
@@ -87,6 +137,12 @@ export declare function canvasFieldChange(previous: CmsDocument, next: CmsDocume
87
137
  export declare function presenceReaderNow(entries: readonly PresenceEntry[], sessionId: string, fallback: string): string;
88
138
  export interface DocumentDraft {
89
139
  draft: CmsDocument;
140
+ /**
141
+ * EC-647 — the revision of the session view the draft was last rebased onto (see
142
+ * {@link DraftView}): a writer that authors from {@link draft} (the translation) writes
143
+ * at this revision.
144
+ */
145
+ revision: number | undefined;
90
146
  /** Uncommitted (document model) / unsaved (record model) edits exist. */
91
147
  dirty: boolean;
92
148
  /** The record model: the editor renders a Save button + navigation guard. */
@@ -96,12 +152,14 @@ export interface DocumentDraft {
96
152
  /** A writer that transforms ONE field position (fill / reset from base). */
97
153
  editField(field: FieldDef, update: (draft: CmsDocument) => CmsDocument): void;
98
154
  /**
99
- * The canvas emitted a new value for its field (node patches, committed at once). `base`
100
- * is the value the canvas last agreed on with the draft: the canvas writer's delta is
101
- * `base` → `value`, applied over whatever the draft holds now (EC-630 — a change that
102
- * landed underneath meanwhile is never reverted by the canvas's own write).
155
+ * The canvas emitted a new value for its field (node patches, committed at once). The
156
+ * lift's `base` is the value the canvas last agreed on with the draft: the canvas
157
+ * writer's delta is `base` → `value`, applied over whatever the draft holds now (EC-630 —
158
+ * a change that landed underneath meanwhile is never reverted by the canvas's own
159
+ * write); its `revision` is the revision that value was agreed at — the revision the
160
+ * edit is committed at (EC-647, {@link canvasCommit}).
103
161
  */
104
- setCanvasValue(field: FieldDef, value: unknown, base: unknown): void;
162
+ setCanvasValue(field: FieldDef, lift: CanvasLift): void;
105
163
  /** The slot alias (a document-level key: a whole-document write at the held revision). */
106
164
  setAlias(alias: string | undefined): void;
107
165
  /** Field boundary events (focus / blur / Enter) — the commit policy's triggers. */
@@ -47,6 +47,22 @@ export const documentVisibility = {
47
47
  return () => document.removeEventListener('visibilitychange', listener);
48
48
  },
49
49
  };
50
+ /**
51
+ * EC-647 — the slot alias write (pure): the draft with `alias` set (or removed), written
52
+ * whole AT THE DRAFT'S REVISION — never the session's tracked one, which may already hold
53
+ * a colleague's change the draft has not absorbed (Codex F10). There is no identity-
54
+ * metadata operation for the alias (the value-position grammar has no position for it,
55
+ * measured on the base), so it stays a whole-document `replace` with its precondition.
56
+ */
57
+ export function aliasWrite(draft, alias, writes) {
58
+ const next = { ...draft.document };
59
+ if (alias === undefined)
60
+ delete next.alias;
61
+ else
62
+ next.alias = alias;
63
+ writes.replace(next, draft.revision);
64
+ return next;
65
+ }
50
66
  /** The patch that writes the value `doc` holds at `path` (unset when absent). */
51
67
  export function positionPatch(doc, path) {
52
68
  const value = readValueAt(doc, path);
@@ -87,11 +103,55 @@ export function canvasWrite(input) {
87
103
  * field did not move. The canvas's own edits are in both values (the session's view holds
88
104
  * every patch it committed, the draft overlays every position it has not), so the change
89
105
  * is someone else's: a colleague's patch, a write's canonical row, a resolved conflict.
106
+ *
107
+ * EC-647: it carries `revision`, the revision the draft is at after the rebase, and is
108
+ * reported when only THAT moved too (`from` ≡ `to`): the canvas's agreed value is valid
109
+ * at the newer revision, so its next edit is authored there — the revision follows the
110
+ * value, never runs ahead of it.
90
111
  */
91
- export function canvasFieldChange(previous, next, path, last) {
112
+ export function canvasFieldChange(previous, next, path, last, revision) {
92
113
  const from = readValueAt(previous, path);
93
114
  const to = readValueAt(next, path);
94
- return semanticValueEqual(from, to) ? undefined : { from, to, seq: (last?.seq ?? 0) + 1 };
115
+ if (semanticValueEqual(from, to) && (last === undefined || last.revision === revision))
116
+ return undefined;
117
+ return { from, to, seq: (last?.seq ?? 0) + 1, revision };
118
+ }
119
+ /**
120
+ * EC-647 — the canvas's write path (pure; what {@link DocumentDraft.setCanvasValue} runs):
121
+ * the canvas writer's delta into the draft ({@link canvasWrite}, handed to `apply`), then
122
+ * routed ({@link routeCanvasWrite}) — committed at once, or held as a focused inspector
123
+ * control's uncommitted position — and in both cases paired with the revision of the
124
+ * canvas value the edit was authored on (`lift.revision`), never the draft's: the draft
125
+ * may already hold a colleague's change the canvas has not been rebased onto (Codex F12),
126
+ * and the store must check the edit from the canvas's revision to refuse it where it
127
+ * overlaps that change. `commit` absent → a draft without a revisioned write model (the
128
+ * draft moves, nothing is committed). `undefined` when the canvas changed nothing.
129
+ */
130
+ export function canvasCommit(input) {
131
+ const { lift, commit } = input;
132
+ const write = canvasWrite({
133
+ collection: input.collection,
134
+ draft: input.draft,
135
+ field: input.field,
136
+ locale: input.locale,
137
+ value: lift.value,
138
+ base: lift.base,
139
+ lookupManifest: input.lookupManifest,
140
+ });
141
+ if (!write)
142
+ return undefined;
143
+ input.apply(write.next);
144
+ if (!commit)
145
+ return 'draft';
146
+ // EC-630: a burst of typing in a focused inspector control is that control's
147
+ // uncommitted position (its policy commits it); a canvas action commits now.
148
+ return routeCanvasWrite({
149
+ patches: write.patches,
150
+ focused: input.focused,
151
+ committer: input.committer,
152
+ revision: lift.revision,
153
+ commitNow: (patches) => commit(patches, patches.map(() => lift.revision)),
154
+ });
95
155
  }
96
156
  /**
97
157
  * The reader's "now" for presence liveness: THIS session's own latest heartbeat `at`
@@ -118,18 +178,25 @@ export function useDocumentDraft(input) {
118
178
  const writes = source.writes;
119
179
  const documentModel = writes.kind === 'document' ? writes : undefined;
120
180
  const ref = useMemo(() => ({ collection: doc.collection, id: doc.id }), [doc.collection, doc.id]);
121
- const [draft, setDraftState] = useState(doc);
122
- const draftRef = useRef(draft);
181
+ // EC-647: the draft and the revision of the view it rebased onto travel as ONE value.
182
+ const [view, setViewState] = useState(() => ({
183
+ document: doc,
184
+ revision: documentModel?.revisionOfView(doc),
185
+ }));
186
+ const draft = view.document;
187
+ const draftRef = useRef(view);
123
188
  // EC-616: the record as the editor opened it (or as of their last successful save) — the
124
189
  // BASE a record rail diffs a save against, so it writes only the editor's changes. It moves
125
190
  // only while the draft equals the source's value (at open, and once a save is reflected
126
191
  // back): another writer's change arriving mid-edit never becomes the editor's base.
127
192
  const baseRef = useRef(doc);
128
- baseRef.current = recordSaveBase(baseRef.current, draftRef.current, doc);
129
- const setDraft = useCallback((next) => {
193
+ baseRef.current = recordSaveBase(baseRef.current, draftRef.current.document, doc);
194
+ const setView = useCallback((next) => {
130
195
  draftRef.current = next;
131
- setDraftState(next);
196
+ setViewState(next);
132
197
  }, []);
198
+ /** An edit of the draft: its value moves, the revision it is authored against does not. */
199
+ const setDraft = useCallback((next) => setView({ document: next, revision: draftRef.current.revision }), [setView]);
133
200
  const scheduler = input.scheduler ?? browserScheduler;
134
201
  const now = input.now ?? (() => new Date().toISOString());
135
202
  const fieldsByName = useMemo(() => new Map(collection.fields.map((f) => [f.name, f])), [collection]);
@@ -149,9 +216,12 @@ export function useDocumentDraft(input) {
149
216
  const target = writesRef.current;
150
217
  if (!target)
151
218
  return;
152
- void target.commit(ref, positions.map((position) => positionPatch(draftRef.current, position.path)), positions.map((position) => position.base));
219
+ void target.commit(ref, positions.map((position) => positionPatch(draftRef.current.document, position.path)), positions.map((position) => position.base));
153
220
  },
154
- baseOf: () => writesRef.current?.revisionOf(ref),
221
+ // EC-647: a position's base is the revision of the DRAFT its first keystroke was
222
+ // made on — never the session's tracked revision, which may already hold a
223
+ // colleague's change the draft has not rebased onto yet.
224
+ baseOf: () => draftRef.current.revision,
155
225
  scheduler,
156
226
  }),
157
227
  // One committer per open document (the editor is keyed by it).
@@ -178,63 +248,64 @@ export function useDocumentDraft(input) {
178
248
  }
179
249
  if (!documentModel)
180
250
  return;
181
- const previous = draftRef.current;
251
+ const previous = draftRef.current.document;
182
252
  const next = rebaseDraft(doc, previous, committer.uncommitted());
183
- setDraft(next);
253
+ // EC-647: the rebase moves the value AND the revision — the session view's own.
254
+ const revision = documentModel.revisionOfView(doc);
255
+ setView({ document: next, revision });
184
256
  // EC-630: the canvas field moved underneath the canvas — the canvas rebases onto the
185
257
  // change (its own edits are in both values: the session's view holds every patch it
186
- // committed, and the draft overlays every position it has not).
258
+ // committed, and the draft overlays every position it has not). EC-647: the change
259
+ // carries the revision, so the canvas's next edit is authored where its value is.
187
260
  if (!canvasPath)
188
261
  return;
189
- setCanvasChange((last) => canvasFieldChange(previous, next, canvasPath, last) ?? last);
262
+ setCanvasChange((last) => canvasFieldChange(previous, next, canvasPath, last, revision) ?? last);
190
263
  }, [doc]);
191
264
  // --- Writers ----------------------------------------------------------------------------
192
265
  const setFieldValue = useCallback((field, value) => {
193
- const next = writeField(collection, draftRef.current, field.name, locale, value, { lookupManifest });
266
+ const next = writeField(collection, draftRef.current.document, field.name, locale, value, { lookupManifest });
194
267
  setDraft(next);
195
268
  if (documentModel)
196
269
  committer.edited(field.name, fieldPositionPath(collection, field.name, locale));
197
270
  }, [collection, locale, lookupManifest, setDraft, documentModel, committer]);
198
271
  const editField = useCallback((field, update) => {
199
- setDraft(update(draftRef.current));
272
+ setDraft(update(draftRef.current.document));
200
273
  if (documentModel)
201
274
  committer.edited(field.name, fieldPositionPath(collection, field.name, locale));
202
275
  }, [collection, locale, setDraft, documentModel, committer]);
203
- const setCanvasValue = useCallback((field, value, base) => {
204
- const write = canvasWrite({ collection, draft: draftRef.current, field: field.name, locale, value, base, lookupManifest });
205
- if (!write)
206
- return;
207
- setDraft(write.next);
208
- if (!documentModel)
209
- return;
210
- // EC-630: a burst of typing in a focused inspector control is that control's
211
- // uncommitted position (its policy commits it); a canvas action commits now.
276
+ const setCanvasValue = useCallback((field, lift) => {
212
277
  const focus = committer.focusedField();
213
- routeCanvasWrite({
214
- patches: write.patches,
278
+ canvasCommit({
279
+ collection,
280
+ draft: draftRef.current.document,
281
+ field: field.name,
282
+ locale,
283
+ lift,
284
+ lookupManifest,
215
285
  focused: focus === undefined ? undefined : canvasControls.current.get(focus),
216
286
  committer,
217
- commitNow: (patches) => void documentModel.commit(ref, patches),
287
+ apply: setDraft,
288
+ ...(documentModel
289
+ ? { commit: (patches, bases) => void documentModel.commit(ref, patches, bases) }
290
+ : {}),
218
291
  });
219
292
  }, [collection, locale, lookupManifest, setDraft, documentModel, ref, committer]);
220
293
  const setAlias = useCallback((alias) => {
221
- const next = { ...draftRef.current };
222
- if (alias === undefined)
223
- delete next.alias;
224
- else
225
- next.alias = alias;
226
- setDraft(next);
227
- if (!documentModel)
294
+ if (!documentModel) {
295
+ aliasWrite(draftRef.current, alias, { replace: (next) => setDraft(next) });
228
296
  return;
297
+ }
298
+ // The uncommitted positions first (the snapshot holds them), then the snapshot AT
299
+ // THE DRAFT'S REVISION (EC-647 — Codex F10).
229
300
  committer.commitAll();
230
- documentModel.replace(next);
301
+ setDraft(aliasWrite(draftRef.current, alias, documentModel));
231
302
  }, [setDraft, documentModel, committer]);
232
303
  // --- Forced flush ----------------------------------------------------------------------
233
304
  const save = useCallback(() => {
234
305
  if (documentModel)
235
306
  committer.commitAll();
236
307
  else if (writes.kind === 'record')
237
- writes.save(draftRef.current, baseRef.current);
308
+ writes.save(draftRef.current.document, baseRef.current);
238
309
  }, [documentModel, committer, writes]);
239
310
  const flush = useCallback(async () => {
240
311
  save();
@@ -334,16 +405,18 @@ export function useDocumentDraft(input) {
334
405
  const replaceDraft = useCallback((next) => {
335
406
  // A restored version replaces the document: when it moved the canvas field, the
336
407
  // canvas remounts from it (its history belonged to the replaced value).
337
- const moved = canvasPath && !semanticValueEqual(readValueAt(draftRef.current, canvasPath), readValueAt(next, canvasPath));
338
- setDraft(next);
408
+ const moved = canvasPath && !semanticValueEqual(readValueAt(draftRef.current.document, canvasPath), readValueAt(next, canvasPath));
409
+ // EC-647: the restored value comes with the revision the session showed it at.
410
+ setView({ document: next, revision: documentModel?.revisionOfView(next) });
339
411
  if (moved)
340
412
  setCanvasNonce((n) => n + 1);
341
413
  },
342
414
  // The path is keyed by its string form.
343
- [setDraft, canvasPathKey]);
415
+ [setView, documentModel, canvasPathKey]);
344
416
  const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc]);
345
417
  return {
346
418
  draft,
419
+ revision: view.revision,
347
420
  dirty,
348
421
  explicitSave: writes.kind === 'record',
349
422
  setFieldValue,
@@ -1 +1 @@
1
- {"version":3,"file":"use-document-draft.js","sourceRoot":"","sources":["../../../src/components/cms/use-document-draft.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzE,OAAO,EACL,eAAe,EACf,YAAY,GACb,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,UAAU,GACX,MAAM,0BAA0B,CAAA;AAYjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAEnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEjE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAGvE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,wGAAwG;AACxG,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAA;AAQ3C,MAAM,CAAC,MAAM,kBAAkB,GAAqB;IAClD,QAAQ,CAAC,QAAQ;QACf,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;QACpD,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,IAAI,QAAQ,CAAC,eAAe,KAAK,QAAQ;gBAAE,QAAQ,EAAE,CAAA;QACvD,CAAC,CAAA;QACD,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;QACvD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;IACzE,CAAC;CACF,CAAA;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,GAAgB,EAAE,IAAe;IAC7D,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACpC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,UAAuB,EACvB,KAAkB,EAClB,WAA2C;IAE3C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,UAAU,CAAA;IAC/C,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;IAClF,OAAO,oBAAoB,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAA;AACpF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAQ3B;IACC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,KAAK,CAAA;IAClE,MAAM,IAAI,GAAG,iBAAiB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IACzD,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IACtD,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;IAC7E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC1C,MAAM,OAAO,GAAG,oBAAoB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAA;IACtF,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,cAAc,EAAE,CAAC,EAAE,OAAO,EAAE,CAAA;AACxH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAqB,EACrB,IAAiB,EACjB,IAAe,EACf,IAAmC;IAEnC,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACxC,MAAM,EAAE,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAClC,OAAO,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAA;AAC3F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAiC,EACjC,SAAiB,EACjB,QAAgB;IAEhB,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAC7F,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvD,CAAC;AAsED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAAqB,EAAE,KAAkB,EAAE,GAAgB;IACxF,OAAO,kBAAkB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC3D,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,KAAK,CAAA;IAC9E,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;IAC5B,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IACrE,MAAM,GAAG,GAAgB,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAC9G,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAc,GAAG,CAAC,CAAA;IACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC9B,yFAAyF;IACzF,4FAA4F;IAC5F,wFAAwF;IACxF,oFAAoF;IACpF,MAAM,OAAO,GAAG,MAAM,CAAc,GAAG,CAAC,CAAA;IACxC,OAAO,CAAC,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACxE,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAAiB,EAAE,EAAE;QACjD,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAA;QACvB,aAAa,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,gBAAgB,CAAA;IACrD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;IACzD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAEpG,sFAAsF;IACtF,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,CAAA;IACvC,SAAS,CAAC,OAAO,GAAG,aAAa,CAAA;IACjC,0GAA0G;IAC1G,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,GAAG,EAAyB,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAG,OAAO,CACvB,GAAG,EAAE,CACH,oBAAoB,CAAC;QACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;YACjB,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACpC,IAAI,KAAK;gBAAE,OAAO,cAAc,CAAC,KAAK,CAAC,CAAA;YACvC,OAAO,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,IAAI,cAAc,CAAA;QACnE,CAAC;QACD,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE;YACpB,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAA;YAChC,IAAI,CAAC,MAAM;gBAAE,OAAM;YACnB,KAAK,MAAM,CAAC,MAAM,CAChB,GAAG,EACH,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EAC3E,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAC3C,CAAA;QACH,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC;QAChD,SAAS;KACV,CAAC;IACJ,+DAA+D;IAC/D,CAAC,GAAG,CAAC,CACN,CAAA;IAED,0FAA0F;IAC1F,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACpG,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACnE,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IACjD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAgC,SAAS,CAAC,CAAA;IAC1F,wEAAwE;IACxE,MAAM,iBAAiB,GAAG,MAAM,CAAC,aAAa,CAAC,CAAA;IAC/C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,iBAAiB,CAAC,OAAO,KAAK,aAAa;YAAE,OAAM;QACvD,iBAAiB,CAAC,OAAO,GAAG,aAAa,CAAA;QACzC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9B,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB,0FAA0F;IAC1F,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC7B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAA;YACxB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAA;QACjC,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC,CAAA;QAChE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,qFAAqF;QACrF,oFAAoF;QACpF,gEAAgE;QAChE,IAAI,CAAC,UAAU;YAAE,OAAM;QACvB,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,CAAA;IACxF,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IAET,2FAA2F;IAC3F,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,KAAe,EAAE,KAAc,EAAE,EAAE;QAClC,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,cAAc,EAAE,CAAC,CAAA;QACpG,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,aAAa;YAAE,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IACpG,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,CAAC,CACzE,CAAA;IACD,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,KAAe,EAAE,MAA6C,EAAE,EAAE;QACjE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QAClC,IAAI,aAAa;YAAE,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IACpG,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,CAAC,CACzD,CAAA;IACD,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,KAAe,EAAE,KAAc,EAAE,IAAa,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAA;QAC1H,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACpB,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,6EAA6E;QAC7E,6EAA6E;QAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,YAAY,EAAE,CAAA;QACtC,gBAAgB,CAAC;YACf,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAC5E,SAAS;YACT,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC;SAChE,CAAC,CAAA;IACJ,CAAC,EACD,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,CAAC,CAC9E,CAAA;IACD,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,KAAyB,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAA;QACpC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,KAAK,CAAA;;YACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QACvB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,CAAC,SAAS,EAAE,CAAA;QACrB,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,CAAC,EACD,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,CAAC,CACrC,CAAA;IAED,0FAA0F;IAC1F,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,IAAI,aAAa;YAAE,SAAS,CAAC,SAAS,EAAE,CAAA;aACnC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IACnF,CAAC,EAAE,CAAC,aAAa,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,IAAI,EAAE,CAAA;QACN,IAAI,aAAa;YAAE,MAAM,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACnD,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC,CAAA;IAE9B,yEAAyE;IACzE,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,kBAAkB,CAAA;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,OAAO,UAAU,CAAC,QAAQ,CAAC,GAAG,EAAE;YAC9B,SAAS,CAAC,SAAS,EAAE,CAAA;YACrB,KAAK,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC/B,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAA;QAChC,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,SAAS,CAAC,SAAS,EAAE,CAAA;QACrB,SAAS,CAAC,OAAO,EAAE,CAAA;QACnB,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACtB,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC,EACD,CAAC,GAAG,CAAC,CACN,CAAA;IAED,2FAA2F;IAC3F,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAA2B,EAAE,CAAC,CAAA;IACpF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IACvC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,OAAO,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAA;IACzD,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,MAAM,cAAc,GAAG,MAAM,CAAwB,SAAS,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;QACjC,SAAS,CAAC,OAAO,EAAE,cAAc,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,CAAA;QAC9D,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAA;IACjB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,EAAE,CAAA;QACX,IAAI,MAAe,CAAA;QACnB,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,SAAS,EAAE,CAAA;YACX,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAA;QAC5D,CAAC,CAAA;QACD,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAA;QAC1D,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;IAC7C,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IACT,MAAM,QAAQ,GAAG,OAAO,CACtB,GAAG,EAAE,CACH,aAAa;QACX,CAAC,CAAC,YAAY,CACV,eAAe,EACf,iBAAiB,CAAC,eAAe,EAAE,aAAa,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAC1E,eAAe,CAChB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,KAAK,aAAa,CAAC,iBAAiB,CAAC;QAC1E,CAAC,CAAC,EAAE,EACR,CAAC,eAAe,EAAE,KAAK,EAAE,aAAa,CAAC,CACxC,CAAA;IAED,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAe,EAAE,EAAE;QAClB,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7B,cAAc,CAAC,OAAO,GAAG,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC1E,SAAS,EAAE,CAAA;IACb,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAC1D,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAe,EAAE,EAAE;QAClB,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7B,cAAc,CAAC,OAAO,GAAG,SAAS,CAAA;QAClC,SAAS,EAAE,CAAA;IACb,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,SAAS,CAAC,CACtC,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAe,EAAE,EAAE;QAClB,IAAI,aAAa;YAAE,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAClD,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,CAAC,CAC3B,CAAA;IAED,yFAAyF;IACzF,2FAA2F;IAC3F,MAAM,eAAe,GAAG,WAAW,CACjC,CAAC,KAAe,EAAE,QAA+B,EAAE,MAAoB,EAAE,EAAE,CACzE,aAAa,CAAC,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EACpF,CAAC,UAAU,EAAE,MAAM,CAAC,CACrB,CAAA;IACD,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,KAAe,EAAE,QAA+B,EAAE,MAAoB,EAAE,EAAE;QACzE,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;QACxD,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAChD,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC9B,cAAc,CAAC,OAAO,GAAG,OAAO,CAAC,IAAI,CAAA;QACrC,SAAS,EAAE,CAAA;IACb,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,CAAC,CACvD,CAAA;IACD,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,KAAe,EAAE,QAA+B,EAAE,EAAE;QACnD,IAAI,CAAC,aAAa;YAAE,OAAM;QAC1B,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAA;QAC1D,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACtB,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAClC,cAAc,CAAC,OAAO,GAAG,SAAS,CAAA;QAClC,SAAS,EAAE,CAAA;IACb,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,CAAC,CACvD,CAAA;IACD,MAAM,aAAa,GAAG,WAAW,CAC/B,CAAC,KAAe,EAAE,QAA+B,EAAE,EAAE;QACnD,IAAI,aAAa;YAAE,SAAS,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAA;IACtF,CAAC,EACD,CAAC,aAAa,EAAE,SAAS,EAAE,eAAe,CAAC,CAC5C,CAAA;IAED,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,IAAiB,EAAE,EAAE;QACpB,gFAAgF;QAChF,wEAAwE;QACxE,MAAM,KAAK,GAAG,UAAU,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAA;QACzH,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,KAAK;YAAE,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACzC,CAAC;IACD,wCAAwC;IACxC,CAAC,QAAQ,EAAE,aAAa,CAAC,CAC1B,CAAA;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAA;IAE1E,OAAO;QACL,KAAK;QACL,KAAK;QACL,YAAY,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ;QACtC,aAAa;QACb,SAAS;QACT,cAAc;QACd,QAAQ;QACR,OAAO;QACP,OAAO;QACP,OAAO;QACP,aAAa;QACb,aAAa;QACb,aAAa;QACb,IAAI;QACJ,KAAK;QACL,YAAY;QACZ,WAAW;QACX,YAAY;QACZ,QAAQ;KACT,CAAA;AACH,CAAC","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 } from '../../lib/collection-view/collection-view-source'\nimport { fieldPositionPath } from '../../lib/cms/field-patch'\nimport { compositionPatches } from '../builder/composition-patches'\nimport type { CanvasFieldChange } 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/** 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 */\nexport function canvasFieldChange(\n previous: CmsDocument,\n next: CmsDocument,\n path: ValuePath,\n last: CanvasFieldChange | undefined,\n): CanvasFieldChange | undefined {\n const from = readValueAt(previous, path)\n const to = readValueAt(next, path)\n return semanticValueEqual(from, to) ? undefined : { from, to, seq: (last?.seq ?? 0) + 1 }\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 /** 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). `base`\n * is the value the canvas last agreed on with the draft: the canvas writer's delta is\n * `base` → `value`, applied over whatever the draft holds now (EC-630 — a change that\n * landed underneath meanwhile is never reverted by the canvas's own write).\n */\n setCanvasValue(field: FieldDef, value: unknown, base: unknown): 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 const [draft, setDraftState] = useState<CmsDocument>(doc)\n const draftRef = useRef(draft)\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, doc)\n const setDraft = useCallback((next: CmsDocument) => {\n draftRef.current = next\n setDraftState(next)\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, position.path)),\n positions.map((position) => position.base),\n )\n },\n baseOf: () => writesRef.current?.revisionOf(ref),\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\n const next = rebaseDraft(doc, previous, committer.uncommitted())\n setDraft(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).\n if (!canvasPath) return\n setCanvasChange((last) => canvasFieldChange(previous, next, canvasPath, last) ?? last)\n }, [doc])\n\n // --- Writers ----------------------------------------------------------------------------\n const setFieldValue = useCallback(\n (field: FieldDef, value: unknown) => {\n const next = writeField(collection, draftRef.current, field.name, locale, value, { lookupManifest })\n setDraft(next)\n if (documentModel) committer.edited(field.name, fieldPositionPath(collection, field.name, locale))\n },\n [collection, locale, lookupManifest, setDraft, documentModel, committer],\n )\n const editField = useCallback(\n (field: FieldDef, update: (current: CmsDocument) => CmsDocument) => {\n setDraft(update(draftRef.current))\n if (documentModel) committer.edited(field.name, fieldPositionPath(collection, field.name, locale))\n },\n [collection, locale, setDraft, documentModel, committer],\n )\n const setCanvasValue = useCallback(\n (field: FieldDef, value: unknown, base: unknown) => {\n const write = canvasWrite({ collection, draft: draftRef.current, field: field.name, locale, value, base, lookupManifest })\n if (!write) return\n setDraft(write.next)\n if (!documentModel) return\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 const focus = committer.focusedField()\n routeCanvasWrite({\n patches: write.patches,\n focused: focus === undefined ? undefined : canvasControls.current.get(focus),\n committer,\n commitNow: (patches) => void documentModel.commit(ref, patches),\n })\n },\n [collection, locale, lookupManifest, setDraft, documentModel, ref, committer],\n )\n const setAlias = useCallback(\n (alias: string | undefined) => {\n const next = { ...draftRef.current }\n if (alias === undefined) delete next.alias\n else next.alias = alias\n setDraft(next)\n if (!documentModel) return\n committer.commitAll()\n documentModel.replace(next)\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, 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, canvasPath), readValueAt(next, canvasPath))\n setDraft(next)\n if (moved) setCanvasNonce((n) => n + 1)\n },\n // The path is keyed by its string form.\n [setDraft, canvasPathKey],\n )\n\n const dirty = useMemo(() => !semanticValueEqual(draft, doc), [draft, doc])\n\n return {\n draft,\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 } 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 */\nexport interface DraftView {\n readonly document: CmsDocument\n readonly revision: number | undefined\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 */\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 if (semanticValueEqual(from, to) && (last === undefined || last.revision === revision)) 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.\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 const [view, setViewState] = useState<DraftView>(() => ({\n document: doc,\n revision: documentModel?.revisionOfView(doc),\n }))\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 revision it is authored against does not. */\n const setDraft = useCallback(\n (next: CmsDocument) => setView({ document: next, revision: draftRef.current.revision }),\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.\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 const next = rebaseDraft(doc, previous, committer.uncommitted())\n // EC-647: the rebase moves the value AND the revision — the session view's own.\n const revision = documentModel.revisionOfView(doc)\n setView({ document: next, revision })\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, so the canvas's next edit is authored where its value is.\n if (!canvasPath) return\n setCanvasChange((last) => canvasFieldChange(previous, next, canvasPath, last, revision) ?? last)\n }, [doc])\n\n // --- Writers ----------------------------------------------------------------------------\n const setFieldValue = useCallback(\n (field: FieldDef, value: unknown) => {\n const next = writeField(collection, draftRef.current.document, field.name, locale, value, { lookupManifest })\n setDraft(next)\n if (documentModel) committer.edited(field.name, fieldPositionPath(collection, field.name, locale))\n },\n [collection, locale, lookupManifest, setDraft, documentModel, committer],\n )\n const editField = useCallback(\n (field: FieldDef, update: (current: CmsDocument) => CmsDocument) => {\n setDraft(update(draftRef.current.document))\n if (documentModel) committer.edited(field.name, fieldPositionPath(collection, field.name, locale))\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({ document: next, revision: documentModel?.revisionOfView(next) })\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"]}