smartrte-react 0.3.4 → 1.0.0-beta.1

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 (97) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +23 -0
  3. package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
  4. package/dist/adapters/domChecklistCommandBridge.js +33 -0
  5. package/dist/adapters/domCommandBridge.d.ts +19 -0
  6. package/dist/adapters/domCommandBridge.js +33 -0
  7. package/dist/adapters/domInlineAtomCommandBridge.d.ts +15 -0
  8. package/dist/adapters/domInlineAtomCommandBridge.js +205 -0
  9. package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
  10. package/dist/adapters/domInlineImageCommandBridge.js +6 -0
  11. package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
  12. package/dist/adapters/domMoveCommandBridge.js +54 -0
  13. package/dist/adapters/domSelectionBridge.d.ts +5 -3
  14. package/dist/adapters/domSelectionBridge.js +96 -44
  15. package/dist/adapters/domSmartDocument.d.ts +4 -4
  16. package/dist/adapters/domSmartDocument.js +159 -17
  17. package/dist/adapters/domTableCommandBridge.d.ts +61 -0
  18. package/dist/adapters/domTableCommandBridge.js +231 -0
  19. package/dist/adapters/internalFlags.d.ts +0 -8
  20. package/dist/adapters/internalFlags.js +0 -24
  21. package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
  22. package/dist/adapters/legacyListShadowComparator.js +316 -0
  23. package/dist/adapters/pdfPrint.d.ts +8 -0
  24. package/dist/adapters/pdfPrint.js +22 -0
  25. package/dist/adapters/shadowMode.d.ts +3 -3
  26. package/dist/adapters/shadowMode.js +1 -1
  27. package/dist/canonicalEditorRuntime.d.ts +129 -0
  28. package/dist/canonicalEditorRuntime.js +331 -0
  29. package/dist/capabilityPresets.d.ts +27 -0
  30. package/dist/capabilityPresets.js +21 -0
  31. package/dist/collabTransport.d.ts +66 -0
  32. package/dist/collabTransport.js +1 -0
  33. package/dist/commentProvider.d.ts +24 -0
  34. package/dist/commentProvider.js +1 -0
  35. package/dist/components/CanonicalAuthorityEditor.d.ts +59 -0
  36. package/dist/components/CanonicalAuthorityEditor.js +1492 -0
  37. package/dist/components/ClassicEditorAuthority.d.ts +109 -0
  38. package/dist/components/ClassicEditorAuthority.js +17 -0
  39. package/dist/components/ColorPickerPopover.d.ts +39 -0
  40. package/dist/components/ColorPickerPopover.js +285 -0
  41. package/dist/components/CommentMarkers.d.ts +25 -0
  42. package/dist/components/CommentMarkers.js +105 -0
  43. package/dist/components/CommentThreadPanel.d.ts +26 -0
  44. package/dist/components/CommentThreadPanel.js +47 -0
  45. package/dist/components/ContextMenu.d.ts +23 -0
  46. package/dist/components/ContextMenu.js +141 -0
  47. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  48. package/dist/components/FormulaLibraryPopover.js +107 -0
  49. package/dist/components/LinkEditorPopover.d.ts +3 -1
  50. package/dist/components/LinkEditorPopover.js +22 -6
  51. package/dist/components/MediaManager.d.ts +4 -20
  52. package/dist/components/MediaManager.js +35 -6
  53. package/dist/components/MediaOverlay.d.ts +36 -0
  54. package/dist/components/MediaOverlay.js +163 -0
  55. package/dist/components/MediaPicker.d.ts +10 -0
  56. package/dist/components/MediaPicker.js +16 -0
  57. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  58. package/dist/components/SpecialCharacterPopover.js +99 -0
  59. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  60. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  61. package/dist/components/SuggestionPanel.d.ts +26 -0
  62. package/dist/components/SuggestionPanel.js +33 -0
  63. package/dist/components/TableBorderPopover.d.ts +59 -0
  64. package/dist/components/TableBorderPopover.js +147 -0
  65. package/dist/components/TableResizeHandles.d.ts +25 -0
  66. package/dist/components/TableResizeHandles.js +301 -0
  67. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  68. package/dist/components/TableSizePickerPopover.js +126 -0
  69. package/dist/components/ToolbarPrimitives.d.ts +129 -0
  70. package/dist/components/ToolbarPrimitives.js +146 -0
  71. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  72. package/dist/components/VersionHistoryPanel.js +157 -0
  73. package/dist/formulaLibrary.d.ts +31 -0
  74. package/dist/formulaLibrary.js +90 -0
  75. package/dist/index.d.ts +23 -1
  76. package/dist/index.js +14 -1
  77. package/dist/mediaManagerAdapter.d.ts +12 -0
  78. package/dist/mediaManagerAdapter.js +34 -0
  79. package/dist/mediaProvider.d.ts +59 -0
  80. package/dist/mediaProvider.js +1 -0
  81. package/dist/specialCharacters.d.ts +22 -0
  82. package/dist/specialCharacters.js +86 -0
  83. package/dist/standalone/classic-editor-embed.js +19 -12
  84. package/dist/suggestionProvider.d.ts +17 -0
  85. package/dist/suggestionProvider.js +1 -0
  86. package/dist/theme.d.ts +1 -1
  87. package/dist/theme.js +161 -1
  88. package/dist/versionProvider.d.ts +29 -0
  89. package/dist/versionProvider.js +1 -0
  90. package/package.json +28 -14
  91. package/dist/adapters/coreBold.d.ts +0 -5
  92. package/dist/adapters/coreBold.js +0 -3
  93. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  94. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  95. package/dist/components/ClassicEditor.d.ts +0 -66
  96. package/dist/components/ClassicEditor.js +0 -6617
  97. package/dist/standalone/editor.js +0 -841
@@ -1,4 +1,4 @@
1
- import { type CommandContext, type SmartCommand, type SmartEditorState, type SmartTransaction } from "smartrte-core";
1
+ import { type LegacyCommandContext, type SmartCommand, type SmartEditorState, type LegacySmartTransaction } from "smartrte-core/legacy";
2
2
  export interface ShadowComparison {
3
3
  commandId: string;
4
4
  matches: boolean;
@@ -15,9 +15,9 @@ export declare const reportShadowDifference: (comparison: ShadowComparison) => v
15
15
  */
16
16
  export declare const runShadowCommand: <Input>(args: {
17
17
  command: SmartCommand<Input>;
18
- context: CommandContext;
18
+ context: LegacyCommandContext;
19
19
  input?: Input;
20
20
  state: SmartEditorState;
21
21
  legacyHtml: string;
22
22
  serialize: (state: SmartEditorState) => string;
23
- }) => SmartTransaction | null;
23
+ }) => LegacySmartTransaction | null;
@@ -1,4 +1,4 @@
1
- import { applyTransaction, normalizeCompatibilityHtml, } from "smartrte-core";
1
+ import { applyTransaction, normalizeCompatibilityHtml, } from "smartrte-core/legacy";
2
2
  import { isShadowModeFlagEnabled } from "./internalFlags.js";
3
3
  const semanticHtml = (html) => normalizeCompatibilityHtml(html)
4
4
  .replace(/<(\/?)b(?=[\s>])/g, "<$1strong")
@@ -0,0 +1,129 @@
1
+ import { type CanonicalInputPipeline, type ClipboardDiagnosticReport, type CanonicalSubtreeRenderer, type DocumentVersion, type FoundationEditor, type PersistedEditorDocument, type SmartMark, type SmartOperation, type SmartSelection, type SmartTransaction } from "smartrte-core/foundation";
2
+ import { type EditorCapabilityPreset } from "./capabilityPresets.js";
3
+ export interface SmartEditorCheckpoint {
4
+ envelope: PersistedEditorDocument;
5
+ selection: SmartSelection;
6
+ storedMarks?: SmartMark[];
7
+ savedRevision: number;
8
+ }
9
+ export interface SmartEditorChange {
10
+ revision: number;
11
+ documentChanged: boolean;
12
+ transaction: SmartTransaction;
13
+ }
14
+ export interface SmartEditorHandle {
15
+ getValue(): PersistedEditorDocument;
16
+ replaceValue(doc: PersistedEditorDocument, opts?: {
17
+ keepSelection?: boolean;
18
+ }): void;
19
+ isDirty(): boolean;
20
+ markSaved(revision: number): void;
21
+ getRevision(): number;
22
+ focus(): void;
23
+ executeOperations(operations: readonly SmartOperation[], opts?: ExecuteOperationsOptions): void;
24
+ createCheckpoint(): SmartEditorCheckpoint;
25
+ restoreCheckpoint(checkpoint: SmartEditorCheckpoint): void;
26
+ saveVersion(opts?: SaveVersionOptions): DocumentVersion;
27
+ restoreVersion(version: DocumentVersion, opts?: {
28
+ keepSelection?: boolean;
29
+ }): void;
30
+ }
31
+ export interface SaveVersionOptions {
32
+ label?: string;
33
+ authorId?: string;
34
+ }
35
+ export interface ExecuteOperationsOptions {
36
+ historyGroup?: string;
37
+ addToHistory?: boolean;
38
+ /** Keep structural commands anchored to the same stable inline-owner IDs. */
39
+ preserveSelectionById?: boolean;
40
+ /** Put the caret in a newly-created inline owner after the operation. */
41
+ selectionOwnerId?: string;
42
+ selectionOffset?: number;
43
+ }
44
+ export interface CanonicalEditorRuntimeOptions {
45
+ initialValue?: string | PersistedEditorDocument;
46
+ onChange?: (change: SmartEditorChange) => void;
47
+ onHtmlChange?: (html: string) => void;
48
+ onClipboardDiagnostic?: (report: ClipboardDiagnosticReport) => void;
49
+ /**
50
+ * Renderer-integrated content-visibility (Phase 11 Tier 3, opt-in,
51
+ * default off) - only applies content-visibility:auto to a top-level
52
+ * block the renderer's own diff already proved untouched this render
53
+ * pass and that isn't the actively-selected block. See
54
+ * surface/renderer.ts's syncContentVisibility for why this differs from
55
+ * the disproven naive per-block experiment.
56
+ */
57
+ contentVisibility?: boolean;
58
+ /** Which built-in plugins this instance is constructed with - see capabilityPresets.ts. Defaults to "full" (every plugin), matching this runtime's only behavior before this option existed. */
59
+ preset?: EditorCapabilityPreset;
60
+ }
61
+ /** Persistent, React-independent owner for one product editor instance. */
62
+ export declare class CanonicalEditorRuntime implements SmartEditorHandle {
63
+ readonly editor: FoundationEditor;
64
+ private root;
65
+ private renderer;
66
+ private pipeline;
67
+ private unsubscribe;
68
+ private savedRevision;
69
+ private onChange?;
70
+ private onHtmlChange?;
71
+ private readonly onClipboardDiagnostic?;
72
+ private readonly contentVisibility;
73
+ private htmlChangeTimer;
74
+ private pendingHtmlDocument;
75
+ constructor(options?: CanonicalEditorRuntimeOptions);
76
+ setCallbacks(onChange?: (change: SmartEditorChange) => void, onHtmlChange?: (html: string) => void): void;
77
+ private scheduleHtmlChange;
78
+ mount(root: HTMLElement): void;
79
+ unmount(): void;
80
+ getValue(): PersistedEditorDocument;
81
+ replaceValue(doc: PersistedEditorDocument, opts?: {
82
+ keepSelection?: boolean;
83
+ }): void;
84
+ isDirty(): boolean;
85
+ markSaved(revision: number): void;
86
+ getRevision(): number;
87
+ focus(): void;
88
+ executeOperations(operations: readonly SmartOperation[], opts?: ExecuteOperationsOptions): void;
89
+ createCheckpoint(): SmartEditorCheckpoint;
90
+ restoreCheckpoint(checkpoint: SmartEditorCheckpoint): void;
91
+ /**
92
+ * A pure read of current state, like `createCheckpoint` - never touches
93
+ * the undo stack, never a `transact()` call. The runtime doesn't call a
94
+ * `VersionProvider` itself (same division as `mediaProvider.upload`
95
+ * being called directly from the React component, not the runtime) -
96
+ * this just builds the payload a caller then hands to a provider.
97
+ */
98
+ saveVersion(opts?: SaveVersionOptions): DocumentVersion;
99
+ /**
100
+ * Reuses `replaceValue` (the same "load a full snapshot" mechanism
101
+ * `restoreCheckpoint` already uses) rather than replaying the version as
102
+ * a sequence of operations - a version can be arbitrarily old, and
103
+ * `replaceState` already validates/repairs an incoming document the same
104
+ * way loading any document does. Deliberately does NOT reuse the
105
+ * version's own stored `revision`: `replaceState` sets the live
106
+ * revision counter to whatever it's given verbatim, so restoring an old
107
+ * version's envelope as-is would roll a monotonic counter backward -
108
+ * colliding with `StaleTransactionError`'s bookkeeping and this
109
+ * runtime's own `isDirty()` comparison. Always re-stamps to
110
+ * `currentRevision + 1` instead. Not undoable, same as checkpoint
111
+ * restore today (`replaceState`'s emitted transaction always sets
112
+ * `addToHistory: false`). Per the Phase 12a spec's explicit
113
+ * recommendation, restore is non-destructive of version history at the
114
+ * UI layer: the caller (`VersionHistoryPanel`) saves a new version
115
+ * recording the restored state immediately after calling this, rather
116
+ * than this method silently discarding anything - the version list only
117
+ * ever grows, never rewrites.
118
+ */
119
+ restoreVersion(version: DocumentVersion, opts?: {
120
+ keepSelection?: boolean;
121
+ }): void;
122
+ /** Instrumentation for product-path composition and takeover tests. */
123
+ get surface(): {
124
+ root: HTMLElement;
125
+ renderer: CanonicalSubtreeRenderer;
126
+ pipeline: CanonicalInputPipeline;
127
+ };
128
+ }
129
+ export declare const createCanonicalEditorRuntime: (options?: CanonicalEditorRuntimeOptions) => CanonicalEditorRuntime;
@@ -0,0 +1,331 @@
1
+ import { createFoundationEditor, applyOperations, createInputPipeline, createNodeId, createSubtreeRenderer, createTransactionMap, foundationSchema, isTextNode, parseCanonicalListHtml, serializeCanonicalListHtml, } from "smartrte-core/foundation";
2
+ import { capabilityPresetRegistry } from "./capabilityPresets.js";
3
+ const nodeAtPath = (root, path) => {
4
+ let node = root;
5
+ for (const index of path) {
6
+ if (isTextNode(node) || !node.children?.[index])
7
+ return null;
8
+ node = node.children[index];
9
+ }
10
+ return node;
11
+ };
12
+ const pathOfNode = (root, nodeId, path = []) => {
13
+ if (!isTextNode(root) && root.id === nodeId)
14
+ return path;
15
+ if (isTextNode(root))
16
+ return null;
17
+ for (let index = 0; index < (root.children?.length || 0); index += 1) {
18
+ const found = pathOfNode(root.children[index], nodeId, [...path, index]);
19
+ if (found)
20
+ return found;
21
+ }
22
+ return null;
23
+ };
24
+ const inlineWidth = (node) => isTextNode(node)
25
+ ? node.text.length
26
+ : node.children?.reduce((width, child) => width + (isTextNode(child) ? child.text.length : 1), 0) ?? 0;
27
+ const firstTextSelection = (document) => {
28
+ const visit = (node, path) => {
29
+ if (isTextNode(node))
30
+ return null;
31
+ const spec = foundationSchema.nodes[node.type];
32
+ const children = node.children || [];
33
+ // Atomic nodes (e.g. block_image) never accept a text caret inside them,
34
+ // even when they report an empty children array - only a genuine
35
+ // text-content container (an empty or all-inline block) is a valid
36
+ // first-text-selection target.
37
+ if (spec?.group === "block" && !spec.atomic && (children.length === 0 || children.every((child) => isTextNode(child) || foundationSchema.nodes[child.type]?.group === "inline")))
38
+ return { path, offset: 0 };
39
+ for (let index = 0; index < children.length; index += 1) {
40
+ const found = visit(children[index], [...path, index]);
41
+ if (found)
42
+ return found;
43
+ }
44
+ return null;
45
+ };
46
+ const pos = visit(document, []) || { path: [], offset: 0 };
47
+ return { type: "text", anchor: pos, head: pos };
48
+ };
49
+ const envelopeFrom = (value) => {
50
+ if (typeof value === "object" && value)
51
+ return structuredClone(value);
52
+ const document = parseCanonicalListHtml(typeof value === "string" ? value : "<p></p>");
53
+ return { schemaVersion: foundationSchema.version, revision: 0, document };
54
+ };
55
+ const sameOperations = (transaction) => transaction.operations.length > 0;
56
+ const cellIdAt = (document, position) => {
57
+ const node = nodeAtPath(document, position.path);
58
+ return node && !isTextNode(node) && node.type === "table_cell" ? node.id : null;
59
+ };
60
+ const cellSelectionIn = (document, anchorId, headId) => {
61
+ const anchorPath = pathOfNode(document, anchorId) || pathOfNode(document, headId);
62
+ const headPath = pathOfNode(document, headId) || anchorPath;
63
+ if (!anchorPath || !headPath)
64
+ return null;
65
+ const anchor = nodeAtPath(document, anchorPath);
66
+ const head = nodeAtPath(document, headPath);
67
+ if (!anchor || isTextNode(anchor) || anchor.type !== "table_cell"
68
+ || !head || isTextNode(head) || head.type !== "table_cell")
69
+ return null;
70
+ return {
71
+ type: "cell",
72
+ anchor: { path: anchorPath, offset: 0 },
73
+ head: { path: headPath, offset: head.children?.length || 0 },
74
+ };
75
+ };
76
+ /** Persistent, React-independent owner for one product editor instance. */
77
+ export class CanonicalEditorRuntime {
78
+ constructor(options = {}) {
79
+ this.root = null;
80
+ this.renderer = null;
81
+ this.pipeline = null;
82
+ this.unsubscribe = null;
83
+ this.htmlChangeTimer = null;
84
+ this.pendingHtmlDocument = null;
85
+ const envelope = envelopeFrom(options.initialValue);
86
+ // "full" (the default) intentionally passes no schema/commands/etc., so
87
+ // FoundationEditor's own defaulting (`options.schema || foundationSchema`)
88
+ // applies - every existing consumer who never sets `preset` is
89
+ // byte-for-byte unaffected by this option existing.
90
+ const registry = options.preset && options.preset !== "full" ? capabilityPresetRegistry(options.preset) : null;
91
+ this.editor = createFoundationEditor({
92
+ document: envelope.document,
93
+ revision: envelope.revision,
94
+ selection: firstTextSelection(envelope.document),
95
+ ...(registry ? { schema: registry.schema, commands: registry.commands, keyboardShortcuts: registry.keyboardShortcuts, contextMenu: registry.contextMenu } : {}),
96
+ });
97
+ this.savedRevision = envelope.revision;
98
+ this.onChange = options.onChange;
99
+ this.onHtmlChange = options.onHtmlChange;
100
+ this.onClipboardDiagnostic = options.onClipboardDiagnostic;
101
+ this.contentVisibility = options.contentVisibility === true;
102
+ }
103
+ setCallbacks(onChange, onHtmlChange) {
104
+ this.onChange = onChange;
105
+ this.onHtmlChange = onHtmlChange;
106
+ }
107
+ scheduleHtmlChange(document) {
108
+ if (!this.onHtmlChange)
109
+ return;
110
+ this.pendingHtmlDocument = document;
111
+ const view = this.root?.ownerDocument.defaultView;
112
+ if (this.htmlChangeTimer !== null) {
113
+ if (view)
114
+ view.clearTimeout(this.htmlChangeTimer);
115
+ else
116
+ globalThis.clearTimeout(this.htmlChangeTimer);
117
+ }
118
+ const flush = () => {
119
+ this.htmlChangeTimer = null;
120
+ const pending = this.pendingHtmlDocument;
121
+ this.pendingHtmlDocument = null;
122
+ if (pending && this.onHtmlChange)
123
+ this.onHtmlChange(serializeCanonicalListHtml(pending, { clean: true }));
124
+ };
125
+ if (view) {
126
+ // Transitional HTML serialization is intentionally debounced outside the
127
+ // typing frame. The canonical onChange contract remains per transaction.
128
+ this.htmlChangeTimer = view.setTimeout(flush, 250);
129
+ }
130
+ else {
131
+ this.htmlChangeTimer = setTimeout(flush, 250);
132
+ }
133
+ }
134
+ mount(root) {
135
+ if (this.root === root && this.pipeline && this.renderer)
136
+ return;
137
+ this.unmount();
138
+ this.root = root;
139
+ this.renderer = createSubtreeRenderer(root, { contentVisibility: this.contentVisibility });
140
+ this.pipeline = createInputPipeline(this.editor, this.renderer, root, { onClipboardDiagnostic: this.onClipboardDiagnostic });
141
+ this.unsubscribe = this.editor.subscribe((transaction, state) => {
142
+ this.renderer?.render(this.editor.document, this.editor.selection);
143
+ this.onChange?.({ revision: state.revision, documentChanged: sameOperations(transaction), transaction });
144
+ if (sameOperations(transaction))
145
+ this.scheduleHtmlChange(state.document);
146
+ });
147
+ }
148
+ unmount() {
149
+ this.unsubscribe?.();
150
+ this.unsubscribe = null;
151
+ this.pipeline?.destroy();
152
+ this.pipeline = null;
153
+ this.renderer?.destroy();
154
+ this.renderer = null;
155
+ const view = this.root?.ownerDocument.defaultView;
156
+ if (this.htmlChangeTimer !== null) {
157
+ if (view)
158
+ view.clearTimeout(this.htmlChangeTimer);
159
+ else
160
+ globalThis.clearTimeout(this.htmlChangeTimer);
161
+ }
162
+ this.htmlChangeTimer = null;
163
+ this.pendingHtmlDocument = null;
164
+ this.root?.replaceChildren();
165
+ this.root = null;
166
+ }
167
+ getValue() {
168
+ const state = this.editor.state;
169
+ return { schemaVersion: state.schemaVersion, revision: state.revision, document: state.document };
170
+ }
171
+ replaceValue(doc, opts = {}) {
172
+ let selection = firstTextSelection(doc.document);
173
+ if (opts.keepSelection) {
174
+ const current = this.editor.selection;
175
+ const owner = nodeAtPath(this.editor.document, current.head.path);
176
+ if (owner && !isTextNode(owner)) {
177
+ const find = (node, path) => {
178
+ if (!isTextNode(node) && node.id === owner.id)
179
+ return path;
180
+ if (isTextNode(node))
181
+ return null;
182
+ for (let index = 0; index < (node.children?.length || 0); index += 1) {
183
+ const found = find(node.children[index], [...path, index]);
184
+ if (found)
185
+ return found;
186
+ }
187
+ return null;
188
+ };
189
+ const path = find(doc.document, []);
190
+ if (path) {
191
+ const replacementOwner = nodeAtPath(doc.document, path);
192
+ const offset = Math.min(current.head.offset, inlineWidth(replacementOwner));
193
+ const pos = { path, offset };
194
+ selection = { type: "text", anchor: pos, head: pos };
195
+ }
196
+ }
197
+ }
198
+ this.editor.replaceState(doc, { selection });
199
+ }
200
+ isDirty() { return this.editor.state.revision !== this.savedRevision; }
201
+ markSaved(revision) {
202
+ if (revision > this.editor.state.revision)
203
+ throw new Error("Cannot mark a future revision as saved.");
204
+ this.savedRevision = revision;
205
+ }
206
+ getRevision() { return this.editor.state.revision; }
207
+ focus() { this.root?.focus(); }
208
+ executeOperations(operations, opts = {}) {
209
+ if (!operations.length)
210
+ return;
211
+ const beforeSelection = this.editor.selection;
212
+ let selection = createTransactionMap(operations).mapSelection(beforeSelection);
213
+ if (opts.selectionOwnerId || opts.preserveSelectionById) {
214
+ const beforeDocument = this.editor.document;
215
+ const preview = applyOperations(beforeDocument, operations);
216
+ const beforeCellAnchor = beforeSelection.type === "cell" ? cellIdAt(beforeDocument, beforeSelection.anchor) : null;
217
+ const beforeCellHead = beforeSelection.type === "cell" ? cellIdAt(beforeDocument, beforeSelection.head) : null;
218
+ let preservedCellSelection = false;
219
+ if (opts.preserveSelectionById && beforeCellAnchor && beforeCellHead) {
220
+ // Structural table commands may retire the head cell (merge) while
221
+ // preserving the anchor. Keep the selection on the surviving cell so
222
+ // the next table command still has an unambiguous scope.
223
+ const mappedCells = cellSelectionIn(preview, beforeCellAnchor, beforeCellHead);
224
+ if (mappedCells) {
225
+ selection = mappedCells;
226
+ preservedCellSelection = true;
227
+ }
228
+ }
229
+ const point = (pos, forcedId) => {
230
+ const beforeOwner = nodeAtPath(beforeDocument, pos.path);
231
+ const ownerId = forcedId || (!beforeOwner || isTextNode(beforeOwner) ? undefined : beforeOwner.id);
232
+ if (!ownerId)
233
+ return null;
234
+ const path = pathOfNode(preview, ownerId);
235
+ if (!path)
236
+ return null;
237
+ const owner = nodeAtPath(preview, path);
238
+ if (!owner)
239
+ return null;
240
+ return { path, offset: Math.min(forcedId ? opts.selectionOffset ?? 0 : pos.offset, inlineWidth(owner)) };
241
+ };
242
+ if (preservedCellSelection) {
243
+ // The cell selection was mapped above; do not reinterpret its
244
+ // structural endpoints as a text selection.
245
+ }
246
+ else if (opts.selectionOwnerId) {
247
+ const caret = point(beforeSelection.head, opts.selectionOwnerId);
248
+ if (caret)
249
+ selection = { type: "text", anchor: caret, head: caret };
250
+ }
251
+ else {
252
+ const anchor = point(beforeSelection.anchor);
253
+ const head = point(beforeSelection.head);
254
+ if (anchor && head)
255
+ selection = { ...beforeSelection, anchor, head };
256
+ }
257
+ }
258
+ this.editor.transact((builder) => {
259
+ builder.operations.push(...operations);
260
+ builder.setSelection(selection);
261
+ }, {
262
+ source: "toolbar",
263
+ addToHistory: opts.addToHistory ?? true,
264
+ ...(opts.historyGroup ? { historyGroup: opts.historyGroup } : {}),
265
+ });
266
+ // Skipped for a non-history (preview) operation: focusing the main
267
+ // editor surface would steal keyboard/pointer focus away from whatever
268
+ // UI is driving the preview (e.g. ColorPickerPopover's native color
269
+ // input mid-drag) on every single preview frame - breaking Escape-to-
270
+ // cancel and, in a real browser, potentially interrupting the drag
271
+ // itself. A real, history-eligible commit still focuses as before.
272
+ if (opts.addToHistory ?? true)
273
+ this.focus();
274
+ }
275
+ createCheckpoint() {
276
+ return {
277
+ envelope: this.getValue(),
278
+ selection: this.editor.selection,
279
+ ...(this.editor.storedMarks?.length ? { storedMarks: [...structuredClone(this.editor.storedMarks)] } : {}),
280
+ savedRevision: this.savedRevision,
281
+ };
282
+ }
283
+ restoreCheckpoint(checkpoint) {
284
+ this.editor.replaceState(checkpoint.envelope, {
285
+ selection: checkpoint.selection,
286
+ storedMarks: checkpoint.storedMarks,
287
+ });
288
+ this.savedRevision = checkpoint.savedRevision;
289
+ }
290
+ /**
291
+ * A pure read of current state, like `createCheckpoint` - never touches
292
+ * the undo stack, never a `transact()` call. The runtime doesn't call a
293
+ * `VersionProvider` itself (same division as `mediaProvider.upload`
294
+ * being called directly from the React component, not the runtime) -
295
+ * this just builds the payload a caller then hands to a provider.
296
+ */
297
+ saveVersion(opts = {}) {
298
+ return { id: createNodeId(), createdAt: Date.now(), envelope: this.getValue(), ...(opts.label ? { label: opts.label } : {}), ...(opts.authorId ? { authorId: opts.authorId } : {}) };
299
+ }
300
+ /**
301
+ * Reuses `replaceValue` (the same "load a full snapshot" mechanism
302
+ * `restoreCheckpoint` already uses) rather than replaying the version as
303
+ * a sequence of operations - a version can be arbitrarily old, and
304
+ * `replaceState` already validates/repairs an incoming document the same
305
+ * way loading any document does. Deliberately does NOT reuse the
306
+ * version's own stored `revision`: `replaceState` sets the live
307
+ * revision counter to whatever it's given verbatim, so restoring an old
308
+ * version's envelope as-is would roll a monotonic counter backward -
309
+ * colliding with `StaleTransactionError`'s bookkeeping and this
310
+ * runtime's own `isDirty()` comparison. Always re-stamps to
311
+ * `currentRevision + 1` instead. Not undoable, same as checkpoint
312
+ * restore today (`replaceState`'s emitted transaction always sets
313
+ * `addToHistory: false`). Per the Phase 12a spec's explicit
314
+ * recommendation, restore is non-destructive of version history at the
315
+ * UI layer: the caller (`VersionHistoryPanel`) saves a new version
316
+ * recording the restored state immediately after calling this, rather
317
+ * than this method silently discarding anything - the version list only
318
+ * ever grows, never rewrites.
319
+ */
320
+ restoreVersion(version, opts = {}) {
321
+ const restamped = {
322
+ schemaVersion: version.envelope.schemaVersion,
323
+ revision: this.getRevision() + 1,
324
+ document: version.envelope.document,
325
+ };
326
+ this.replaceValue(restamped, opts);
327
+ }
328
+ /** Instrumentation for product-path composition and takeover tests. */
329
+ get surface() { return { root: this.root, renderer: this.renderer, pipeline: this.pipeline }; }
330
+ }
331
+ export const createCanonicalEditorRuntime = (options = {}) => new CanonicalEditorRuntime(options);
@@ -0,0 +1,27 @@
1
+ import { type PluginRegistry } from "smartrte-core/foundation";
2
+ /**
3
+ * A named, host-facing restriction of which built-in plugins an editor
4
+ * instance is constructed with - a small extension of Phase 10's existing
5
+ * plugin enable/disable mechanism (`createPluginRegistry`,
6
+ * `docs/PLUGIN_ARCHITECTURE.md`), not new architecture. `FoundationEditor`
7
+ * already accepted a custom `schema`/`commands`/`keyboardShortcuts`/
8
+ * `contextMenu` (`editor.ts`'s `FoundationEditorOptions`) since Phase 10 -
9
+ * nothing here builds new core capability, it just gives the react layer a
10
+ * convenient, named way to reach for it.
11
+ *
12
+ * This is an editor-construction-time, host/integrator-level setting - not
13
+ * end-user-facing toolbar customization. There is no UI for a person typing
14
+ * in the editor to change their own preset.
15
+ *
16
+ * Real-world driver: Sootr's question-style content (MCQ, Anomaly, PYEQ)
17
+ * deliberately excludes tables (`enableTable={false}` in every one of those
18
+ * call sites - see `docs/SOOTR_MIGRATION_READINESS.md` gap #3), while its
19
+ * study-material block editor wants everything. Two presets, not more,
20
+ * because that is the one dimension any real caller has ever needed to
21
+ * vary; nothing in this codebase's history has asked to disable media or
22
+ * formula independently of each other (they're both owned by the single
23
+ * "atom" plugin today, which would need splitting to support that -
24
+ * deliberately out of scope here, since nothing needs it yet).
25
+ */
26
+ export type EditorCapabilityPreset = "simple" | "full";
27
+ export declare const capabilityPresetRegistry: (preset: EditorCapabilityPreset) => PluginRegistry;
@@ -0,0 +1,21 @@
1
+ import { baseSchema, builtInPlugins, createPluginRegistry, foundationSchema } from "smartrte-core/foundation";
2
+ /** Plugin ids to exclude for each preset - "full" excludes nothing, matching today's only behavior exactly (so every existing consumer who never sets `preset` is 100% unaffected). */
3
+ const PRESET_EXCLUDED_PLUGIN_IDS = {
4
+ simple: ["table"],
5
+ full: [],
6
+ };
7
+ /**
8
+ * Registries are real work to build (schema construction + validation, per
9
+ * `createPluginRegistry`) - computed once per preset at module load, not
10
+ * per editor instance, since the preset set is small and fixed.
11
+ */
12
+ const registryCache = new Map();
13
+ export const capabilityPresetRegistry = (preset) => {
14
+ const cached = registryCache.get(preset);
15
+ if (cached)
16
+ return cached;
17
+ const excluded = new Set(PRESET_EXCLUDED_PLUGIN_IDS[preset]);
18
+ const registry = createPluginRegistry(builtInPlugins.filter((plugin) => !excluded.has(plugin.id)), { baseSchema, schemaVersion: foundationSchema.version });
19
+ registryCache.set(preset, registry);
20
+ return registry;
21
+ };
@@ -0,0 +1,66 @@
1
+ import type { SmartSelection, SmartTransaction } from "smartrte-core/foundation";
2
+ /**
3
+ * A remote collaborator's live cursor/selection, mirroring
4
+ * `DocumentVersion`'s "reuse the existing shape, add nothing new" approach:
5
+ * `selection` is the same `SmartSelection` the editor already tracks
6
+ * locally, so it can be mapped through a rebase with the exact same
7
+ * `mapOperation`-based machinery the annotation-range and comment
8
+ * primitives already use (`FoundationTransactionMap`), not a bespoke
9
+ * position type needing its own mapping logic.
10
+ *
11
+ * This is a data shape only - rendering a remote cursor from this
12
+ * (a colored caret, a name label, etc.) is host/UI work, the same boundary
13
+ * `MediaProvider` draws around actual upload UI: the package defines what
14
+ * a presence update *is*, not how it looks on screen.
15
+ */
16
+ export interface PresenceUpdate {
17
+ readonly authorId: string;
18
+ readonly selection?: SmartSelection;
19
+ readonly lastActiveAt: number;
20
+ }
21
+ /**
22
+ * Host-owned realtime transport boundary, mirroring `MediaProvider`'s and
23
+ * `VersionProvider`'s shape and contract: the host implements the actual
24
+ * connection (WebSocket, WebRTC, long-polling, whatever - nothing here
25
+ * assumes a specific transport), the package never holds a socket or
26
+ * server credential itself. There is deliberately no shipped default
27
+ * implementation, exactly like `MediaProvider`/`VersionProvider` - when a
28
+ * host doesn't supply one, the editor behaves exactly as it does today:
29
+ * single-writer, `dispatch()`'s rebase path is never triggered because
30
+ * `baseRevision` always matches (see `FoundationEditor.dispatch`).
31
+ *
32
+ * This interface is Phase 12b-client's contract only - no implementation
33
+ * of it ships from this package. The actual server/socket work
34
+ * (12b-server/transport) is a separate effort building *against* this
35
+ * contract, not part of it.
36
+ */
37
+ export interface CollabTransport {
38
+ /**
39
+ * Submit a local transaction. `accepted: false` (e.g. a server-side
40
+ * permissions check failing) is a distinct outcome from a rebase
41
+ * conflict - see `revision`, present only when accepted, for the
42
+ * transaction's resulting revision as recorded by the transport's own
43
+ * source of truth.
44
+ */
45
+ sendTransaction(transaction: SmartTransaction): Promise<{
46
+ accepted: boolean;
47
+ revision?: number;
48
+ }>;
49
+ /** Fires for every transaction the transport delivers from another author, in commit order. Returns an unsubscribe function. */
50
+ onRemoteTransaction(handler: (transaction: SmartTransaction, revision: number) => void): () => void;
51
+ /** Fires whenever another author's presence changes (selection moved, went idle, etc.). Returns an unsubscribe function. */
52
+ onPresenceUpdate(handler: (presence: PresenceUpdate) => void): () => void;
53
+ /** Broadcast this client's own presence. Fire-and-forget - unlike sendTransaction, presence has no durability or ordering guarantee to report back. */
54
+ sendPresence(update: PresenceUpdate): void;
55
+ /**
56
+ * Transactions committed since `sinceRevision`, oldest first - what a
57
+ * client needs to catch up after a disconnect, or what
58
+ * `FoundationEditor.dispatch`'s own rebase path would ask for if this
59
+ * editor's local revision log had already evicted that far back (see
60
+ * `FoundationEditorOptions.revisionLogLimit`). An empty array means
61
+ * nothing has changed since; the transport signaling "that revision no
62
+ * longer exists" (e.g. server-side history compaction) is a
63
+ * transport-specific error, not modeled here.
64
+ */
65
+ getRevisionHistory(sinceRevision: number): Promise<readonly SmartTransaction[]>;
66
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { CommentThread } from "smartrte-core/foundation";
2
+ /**
3
+ * Host-owned comment-storage boundary, mirroring `VersionProvider`/
4
+ * `MediaProvider`'s shape and "the editor never persists anything itself"
5
+ * contract. Unlike `VersionProvider`, there is no separate cheap-listing
6
+ * projection: a `CommentThread` (an id, a range, a handful of short replies)
7
+ * is already small enough that a host's `list()` can return full threads
8
+ * directly, with no equivalent of `VersionProvider`'s envelope/entry split
9
+ * needed to avoid shipping large payloads up front.
10
+ *
11
+ * `save` both creates a new thread and persists an updated one (a reply
12
+ * added, `resolved` toggled) - the caller always passes the complete
13
+ * `CommentThread` it wants stored, so there is one code path for both,
14
+ * exactly like `CommentReply`'s own "one path for adding a message"
15
+ * decision in `foundation/comments/types.ts`. There is deliberately no
16
+ * shipped default implementation, exactly like `VersionProvider`/
17
+ * `MediaProvider` - when a host doesn't supply one, the comments feature is
18
+ * simply unavailable.
19
+ */
20
+ export interface CommentProvider {
21
+ list(): Promise<readonly CommentThread[]>;
22
+ save(thread: CommentThread): Promise<void>;
23
+ remove(threadId: string): Promise<void>;
24
+ }
@@ -0,0 +1 @@
1
+ export {};