@beyondwork/docx-react-component 1.0.21 → 1.0.23

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 (33) hide show
  1. package/README.md +763 -38
  2. package/package.json +25 -36
  3. package/src/api/public-types.ts +66 -1
  4. package/src/core/commands/index.ts +574 -5
  5. package/src/index.ts +5 -0
  6. package/src/io/docx-session.ts +181 -2
  7. package/src/io/export/serialize-main-document.ts +21 -1
  8. package/src/io/normalize/normalize-text.ts +4 -0
  9. package/src/io/ooxml/parse-main-document.ts +88 -7
  10. package/src/model/canonical-document.ts +22 -0
  11. package/src/review/store/revision-store.ts +1 -0
  12. package/src/review/store/revision-types.ts +2 -0
  13. package/src/runtime/document-runtime.ts +503 -51
  14. package/src/runtime/session-capabilities.ts +6 -5
  15. package/src/runtime/surface-projection.ts +2 -0
  16. package/src/runtime/table-schema.ts +2 -0
  17. package/src/runtime/workflow-markup.ts +5 -1
  18. package/src/ui/WordReviewEditor.tsx +661 -132
  19. package/src/ui/editor-runtime-boundary.ts +10 -1
  20. package/src/ui/editor-shell-view.tsx +8 -0
  21. package/src/ui/editor-surface-controller.tsx +5 -0
  22. package/src/ui/headless/selection-toolbar-model.ts +12 -0
  23. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  24. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
  25. package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
  26. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
  27. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
  29. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
  30. package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
  31. package/src/ui-tailwind/theme/editor-theme.css +10 -0
  32. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
  33. package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
package/README.md CHANGED
@@ -1,14 +1,33 @@
1
+ ---
2
+ title: React OOXML Office
3
+ summary: Shipped docx package landing page and primary router into consumer, wrapper, agent, and maintainer documentation.
4
+ audience: consumer, wrapper, agent, maintainer
5
+ stability: main-path
6
+ docRole: main-path
7
+ canonical: true
8
+ ---
9
+
1
10
  # React OOXML Office
2
11
 
3
12
  [![CI](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml)
4
13
 
5
- `@beyondwork/docx-react-component` is the shipped product in this repository: `WordReviewEditor`, a fidelity-first React editor for legal-review-safe `docx` workflows. Waves 20 through 23 land live table editing, package distribution, validator-backed CI, and legal workflow helpers. Wave 24 is the next production-hardening packet.
14
+ `@beyondwork/docx-react-component` is the shipped product in this repository: a fidelity-first React docx editor centered on `WordReviewEditor`.
15
+
16
+ ## Use This README For
17
+
18
+ - confirming what is shipped today
19
+ - installing the package
20
+ - finding the right documentation lane quickly
21
+
22
+ Do not use this README as the full package contract. The canonical consumer path starts in `docs/README.md`, `docs/reference/public-api.md`, and `docs/reference/integration-guide.md`.
6
23
 
7
- The broader repository is still evolving toward a layered `react-ooxml-office` platform, but the source reality is unchanged:
24
+ ## Current Package Reality
25
+
26
+ The repository may also carry broader branch-local work, but the shipped contract today is still docx-first:
8
27
 
9
28
  - `docx` is the only implemented and shipped runtime contract
10
- - `xlsx` is the next planned OOXML sibling
11
- - `pdf` is future work and outside the first shared OOXML platform contract
29
+ - `xlsx` is planned work, not part of the current package contract
30
+ - `pdf` remains future work outside the current package boundary
12
31
 
13
32
  ## Install
14
33
 
@@ -54,12 +73,27 @@ Snapshot/export note:
54
73
  - when a session starts from a real `.docx`, persisted snapshots now carry embedded source-package provenance so later snapshot-origin `.docx` export can use the same package-backed exporter
55
74
  - legacy snapshots without that provenance still load, but `.docx` export is intentionally blocked rather than falling back to a lossy minimal package
56
75
 
57
- The current public ESM exports are:
58
-
59
- - `@beyondwork/docx-react-component` -> `WordReviewEditor`
60
- - `@beyondwork/docx-react-component/public-types` -> public TypeScript contracts
61
- - `@beyondwork/docx-react-component/ui-tailwind` -> current Tailwind UI primitives
62
- - `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css` -> shipped theme variables and Tailwind theme import
76
+ The current shipped ESM exports include:
77
+
78
+ - stable default entrypoints:
79
+ - `@beyondwork/docx-react-component`
80
+ - `@beyondwork/docx-react-component/public-types`
81
+ - `@beyondwork/docx-react-component/ui-tailwind`
82
+ - `@beyondwork/docx-react-component/legal`
83
+ - `@beyondwork/docx-react-component/compare`
84
+ - `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css`
85
+ - advanced-supported subpaths for wrapper teams and package-adjacent integrations:
86
+ - `@beyondwork/docx-react-component/runtime/document-runtime`
87
+ - `@beyondwork/docx-react-component/io/docx-session`
88
+ - `@beyondwork/docx-react-component/core/commands/*`
89
+ - `@beyondwork/docx-react-component/core/selection/mapping`
90
+ - `@beyondwork/docx-react-component/core/state/editor-state`
91
+ - `@beyondwork/docx-react-component/ui-tailwind/editor-surface/search-plugin`
92
+ - alias subpaths:
93
+ - `@beyondwork/docx-react-component/tailwind`
94
+ - `@beyondwork/docx-react-component/api/public-types`
95
+
96
+ Use `docs/reference/public-api.md` and `docs/reference/public-api.manifest.json` for the current contract inventory and stability guidance.
63
97
 
64
98
  ## Product Contract
65
99
 
@@ -74,48 +108,56 @@ For the current shipped `docx` implementation, that specifically means:
74
108
  - preserve unsupported but preservable OOXML
75
109
  - remain editable in Word after export
76
110
 
77
- ## Repository Direction
111
+ For the normative API and host contract, use:
78
112
 
79
- This repo is broadening from the original docx-first editor into a layered shared-OOXML story:
113
+ - [`docs/reference/public-api.md`](docs/reference/public-api.md)
114
+ - [`docs/reference/integration-guide.md`](docs/reference/integration-guide.md)
115
+ - [`docs/reference/ooxml-compliance.md`](docs/reference/ooxml-compliance.md)
80
116
 
81
- - shared platform concerns for package IO, preservation, diagnostics, and validation
82
- - format-specific runtimes with explicit capability boundaries
83
- - host-facing review and editing surfaces for each supported format
117
+ ## Documentation Map
84
118
 
85
- The canonical shared-platform overview lives in `docs/architecture/platform/shared-openxml-editor-platform.md`.
119
+ ### Start Here
86
120
 
87
- ## Documentation Map
121
+ - [`docs/README.md`](docs/README.md)
88
122
 
89
- Start here:
123
+ ### Main Consumer Path
90
124
 
91
- - `AGENTS.md`
92
- - `DESIGN.md`
93
- - `docs/README.md`
94
- - `docs/roadmap.md`
95
- - `docs/plans/current-state.md`
96
- - `docs/plans/master-plan.md`
125
+ - [`docs/reference/quick-start.md`](docs/reference/quick-start.md)
126
+ - [`docs/reference/consumer-matrix.md`](docs/reference/consumer-matrix.md)
127
+ - [`docs/reference/public-api.md`](docs/reference/public-api.md)
128
+ - [`docs/reference/integration-guide.md`](docs/reference/integration-guide.md)
97
129
 
98
- Current shipped docx contracts:
130
+ ### Wrapper And Agent Path
99
131
 
100
- - `docs/reference/public-api.md`
101
- This doc separates the shipped Waves 20-23 surface from the future Waves 25 through 27 ref expansion.
102
- - `docs/reference/ooxml-compliance.md`
103
- - `docs/reference/word-review-editor-frontend-architecture.md`
104
- - `docs/reference/word-review-editor-ux-guide.md`
132
+ - [`docs/reference/consumer-matrix.md`](docs/reference/consumer-matrix.md)
133
+ - [`docs/reference/agent-integration-guide.md`](docs/reference/agent-integration-guide.md)
134
+ - [`docs/reference/public-api.md`](docs/reference/public-api.md)
135
+ - [`docs/reference/service-wrapper-guidance.md`](docs/reference/service-wrapper-guidance.md)
136
+ - [`docs/reference/agent-capability-map.md`](docs/reference/agent-capability-map.md)
137
+ - [`docs/reference/agent-read-models-and-snapshots.md`](docs/reference/agent-read-models-and-snapshots.md)
138
+ - [`docs/reference/agent-workflow-and-suggestions.md`](docs/reference/agent-workflow-and-suggestions.md)
139
+ - [`docs/reference/scope-aware-selection-tooling.md`](docs/reference/scope-aware-selection-tooling.md)
140
+ - [`docs/reference/editor-integration-style.md`](docs/reference/editor-integration-style.md)
141
+ - [`docs/reference/beyondwork-runtime-environment.md`](docs/reference/beyondwork-runtime-environment.md)
105
142
 
106
- Shared platform and planned xlsx docs:
143
+ Wrapper and agent docs stay on `main`, but they are downstream integration guidance rather than the canonical package contract.
107
144
 
108
- - `docs/architecture/platform/shared-openxml-editor-platform.md`
109
- - `docs/architecture/xlsx/spreadsheet-editor-frontend-architecture.md`
110
- - `docs/architecture/xlsx/canonical-workbook-model-and-commands.md`
111
- - `docs/reference/xlsx/xlsx-ooxml-compliance.md`
112
- - `docs/plans/xlsx/xlsx-fixture-corpus-and-certification-plan.md`
113
- - `docs/xlsx-react/README.md`
145
+ Current integration honesty:
146
+
147
+ - the shipped React host surface is still `WordReviewEditor`
148
+ - `showReviewPanel={false}` only hides the bundled review rail
149
+ - a distinct public chromeless React surface is not yet shipped
150
+
151
+ ### Maintainer Path
152
+
153
+ - [`docs/maintainers/README.md`](docs/maintainers/README.md)
154
+
155
+ Maintainer prompts, operator runbooks, and proof/closure material remain important, but they are not the first reading path for package consumers.
114
156
 
115
157
  ## Packaging And Release
116
158
 
117
159
  - `.github/workflows/publish.yml` publishes on `v*` tags after verifying the tag matches `package.json`
118
- - `pnpm pack --dry-run` is the baseline package proof for this wave
160
+ - `pnpm pack --dry-run` is the baseline package proof
119
161
  - npm provenance is enabled in `publishConfig` and in the publish workflow invocation
120
162
  - the published package currently ships source ESM entry points plus TypeScript source-backed `types` exports
121
163
  - the Microsoft Open XML SDK remains CI/internal-service only, never part of the shipped browser runtime
@@ -129,9 +171,692 @@ Shared platform and planned xlsx docs:
129
171
  - keep docs honest about shipped versus planned behavior
130
172
  - add or extend fixtures for compatibility-critical changes
131
173
  - `bash scripts/validate-fixtures.sh` now uses the Railway validator service and requires `OPENXML_VALIDATOR_AUTH_TOKEN` when hitting the public domain
174
+ - `node scripts/validate-docs-navigation.mjs` enforces the core docs catalog, required indexes, and frontmatter contract
132
175
 
133
176
  ## Guiding Principle
134
177
 
135
178
  This repo is not trying to become a generic office clone.
136
179
 
137
180
  It is building fidelity-first office-document runtimes with explicit preservation and calm, reviewable UI.
181
+
182
+ ## Using the package
183
+
184
+ ### WordReviewEditor
185
+
186
+ `WordReviewEditor` is a React component for loading, editing, and exporting `.docx` files with full comment and tracked-change (redline) support. It is exported from `@beyondwork/docx-react-component`.
187
+
188
+ ### Installation
189
+
190
+ ```tsx
191
+ import {
192
+ WordReviewEditor,
193
+ type WordReviewEditorRef,
194
+ type WordReviewEditorProps,
195
+ type WordReviewEditorEvent,
196
+ } from "@beyondwork/docx-react-component";
197
+ ```
198
+
199
+ ### Basic mount
200
+
201
+ ```tsx
202
+ import { useRef } from "react";
203
+ import { WordReviewEditor, type WordReviewEditorRef } from "@beyondwork/docx-react-component";
204
+
205
+ export function MyEditor({ docxBytes }: { docxBytes: Uint8Array }) {
206
+ const editorRef = useRef<WordReviewEditorRef>(null);
207
+
208
+ return (
209
+ <WordReviewEditor
210
+ ref={editorRef}
211
+ documentId="doc-001"
212
+ currentUser={{ userId: "u1", displayName: "Alice" }}
213
+ initialDocx={docxBytes}
214
+ onEvent={(event) => console.log(event)}
215
+ />
216
+ );
217
+ }
218
+ ```
219
+
220
+ ---
221
+
222
+ ### Props reference
223
+
224
+ | Prop | Type | Description |
225
+ |---|---|---|
226
+ | `documentId` | `string` | **Required.** Stable identifier for this document. |
227
+ | `currentUser` | `EditorUser` | **Required.** The user performing edits and adding comments. |
228
+ | `initialDocx` | `Uint8Array \| ArrayBuffer` | Raw `.docx` bytes to load on first mount. |
229
+ | `initialSessionState` | `EditorSessionState` | Previously saved session state to restore. |
230
+ | `initialSnapshot` | `PersistedEditorSnapshot` | Previously saved snapshot to restore. |
231
+ | `externalDocSource` | `ExternalDocumentSource` | Alternative source with explicit `kind` (`"docx"`, `"session"`, `"snapshot"`). |
232
+ | `readOnly` | `boolean` | When `true`, all editing commands are disabled. |
233
+ | `reviewMode` | `"editing" \| "review"` | Shell layout hint — affects toolbar/panel arrangement but not editing authority. |
234
+ | `markupDisplay` | `"clean" \| "simple" \| "all"` | Controls tracked-change visibility. |
235
+ | `showReviewPanel` | `boolean` | Shows or hides the right-side comment and tracked-change panel. |
236
+ | `autosave` | `AutosaveConfig` | Enables automatic saving. |
237
+ | `hostAdapter` | `EditorHostAdapter` | Callbacks for `load`, `saveSession`, `saveExport`. |
238
+ | `datastore` | `EditorDatastoreAdapter` | Alternative persistence adapter with `load`, `saveSnapshot`. |
239
+ | `onEvent` | `(event: WordReviewEditorEvent) => void` | Unified event handler (see [Events](#events)). |
240
+ | `onWarning` | `(warning: EditorWarning) => void` | Fired for non-fatal warnings. |
241
+ | `onError` | `(error: EditorError) => void` | Fired for fatal errors. |
242
+
243
+ #### EditorUser
244
+
245
+ ```ts
246
+ interface EditorUser {
247
+ userId: string;
248
+ displayName: string;
249
+ email?: string;
250
+ avatarUrl?: string;
251
+ }
252
+ ```
253
+
254
+ #### AutosaveConfig
255
+
256
+ ```ts
257
+ interface AutosaveConfig {
258
+ enabled?: boolean;
259
+ debounceMs?: number; // default: 2000
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ### Show / hide UI regions
266
+
267
+ #### Review panel
268
+
269
+ The right-side panel lists comment threads and tracked changes.
270
+
271
+ ```tsx
272
+ <WordReviewEditor showReviewPanel={false} ... /> // hide panel
273
+ <WordReviewEditor showReviewPanel={true} ... /> // show panel (default)
274
+ ```
275
+
276
+ #### Tracked-change display mode
277
+
278
+ `markupDisplay` controls how tracked changes appear in the document body.
279
+
280
+ | Value | Behaviour |
281
+ |---|---|
282
+ | `"clean"` | Show the accepted version — insertions visible, deletions hidden. |
283
+ | `"simple"` | Show a simplified view of changes without inline markup. |
284
+ | `"all"` | Show all insertion and deletion marks inline (Word's "Show Markup" mode). |
285
+
286
+ ```tsx
287
+ <WordReviewEditor markupDisplay="clean" ... />
288
+ ```
289
+
290
+ You can also change the display mode at runtime:
291
+
292
+ ```ts
293
+ // no ref method for markupDisplay — pass as a prop; React re-renders propagate the change.
294
+ ```
295
+
296
+ #### Document mode
297
+
298
+ `DocumentMode` controls editing authority, not just appearance.
299
+
300
+ | Mode | Effect |
301
+ |---|---|
302
+ | `"editing"` | Edits are applied directly (no tracking). |
303
+ | `"suggesting"` | Every edit is automatically wrapped in a tracked change. |
304
+ | `"viewing"` | Document is read-only regardless of the `readOnly` prop. |
305
+
306
+ Set via ref:
307
+
308
+ ```ts
309
+ editorRef.current.setDocumentMode("suggesting");
310
+ ```
311
+
312
+ Or pass `reviewMode="review"` as a prop to start in a review-friendly shell layout (the component internally maps this to `"suggesting"` document mode).
313
+
314
+ #### Read-only mode
315
+
316
+ ```tsx
317
+ <WordReviewEditor readOnly={true} ... />
318
+ ```
319
+
320
+ All editing, commenting, and tracked-change commands are blocked. The toolbar is still rendered but all buttons are disabled.
321
+
322
+ #### Workspace layout
323
+
324
+ ```ts
325
+ editorRef.current.setWorkspaceMode("canvas"); // continuous scroll
326
+ editorRef.current.setWorkspaceMode("page"); // paginated view
327
+ ```
328
+
329
+ ---
330
+
331
+ ### Imperative ref
332
+
333
+ Obtain the ref via `useRef<WordReviewEditorRef>()`:
334
+
335
+ ```tsx
336
+ const editorRef = useRef<WordReviewEditorRef>(null);
337
+ <WordReviewEditor ref={editorRef} ... />
338
+
339
+ // then:
340
+ editorRef.current?.addComment({ body: "Needs revision" });
341
+ ```
342
+
343
+ ---
344
+
345
+ ### Comment operations
346
+
347
+ #### Add a comment
348
+
349
+ ```ts
350
+ addComment(params: AddCommentParams): string
351
+ // returns the new commentId
352
+ ```
353
+
354
+ ```ts
355
+ interface AddCommentParams {
356
+ anchor?: EditorAnchorProjection; // defaults to the current selection
357
+ body?: string;
358
+ authorId?: string; // defaults to currentUser.userId
359
+ }
360
+ ```
361
+
362
+ **Important**: if you want the comment to land on a specific text selection, capture the anchor *before* opening any draft UI (e.g. a modal or popover), because opening a modal typically collapses the editor selection.
363
+
364
+ ```ts
365
+ // 1. Capture anchor while text is still selected
366
+ const snapshot = editorRef.current.getRenderSnapshot();
367
+ const anchor = snapshot.selection.activeRange;
368
+
369
+ // 2. Open your draft UI, let user type a message...
370
+ // 3. On submit:
371
+ const commentId = editorRef.current.addComment({ anchor, body: draftText });
372
+ ```
373
+
374
+ #### Resolve a comment
375
+
376
+ ```ts
377
+ editorRef.current.resolveComment(commentId);
378
+ ```
379
+
380
+ Marks the thread as resolved. The comment remains in the document and can be exported; it is moved to the resolved list in the sidebar.
381
+
382
+ #### Reopen a resolved comment
383
+
384
+ ```ts
385
+ editorRef.current.reopenComment(commentId);
386
+ ```
387
+
388
+ #### Delete a comment permanently
389
+
390
+ ```ts
391
+ editorRef.current.deleteComment(commentId);
392
+ ```
393
+
394
+ Removes the comment entirely. Use this to clean up failed or unwanted drafts.
395
+
396
+ #### Add a reply to an existing thread
397
+
398
+ ```ts
399
+ editorRef.current.addCommentReply(commentId, "Reply text here");
400
+ ```
401
+
402
+ #### Edit a comment body
403
+
404
+ ```ts
405
+ editorRef.current.editCommentBody(commentId, "Updated text");
406
+ ```
407
+
408
+ #### Scroll to a comment
409
+
410
+ ```ts
411
+ editorRef.current.scrollToComment(commentId);
412
+ ```
413
+
414
+ #### Open (focus) a comment in the sidebar
415
+
416
+ ```ts
417
+ editorRef.current.openComment(commentId);
418
+ ```
419
+
420
+ #### Get all comments
421
+
422
+ ```ts
423
+ const sidebar: CommentSidebarSnapshot = editorRef.current.getComments();
424
+ ```
425
+
426
+ ```ts
427
+ interface CommentSidebarSnapshot {
428
+ activeCommentId?: string;
429
+ openCommentIds: string[];
430
+ resolvedCommentIds: string[];
431
+ detachedCommentIds: string[];
432
+ totalCount: number;
433
+ threads: CommentSidebarThreadSnapshot[];
434
+ }
435
+
436
+ interface CommentSidebarThreadSnapshot {
437
+ commentId: string;
438
+ status: "open" | "resolved" | "detached";
439
+ anchor: EditorAnchorProjection;
440
+ excerpt: string; // the anchored text snippet
441
+ entries: CommentSidebarThreadEntrySnapshot[];
442
+ entryCount: number;
443
+ createdAt: string; // ISO 8601
444
+ createdBy: string; // userId
445
+ resolvedAt?: string;
446
+ resolvedBy?: string;
447
+ }
448
+ ```
449
+
450
+ #### Detached comments
451
+
452
+ A comment becomes **detached** when the text it was anchored to is deleted. Detached comments:
453
+
454
+ - Still appear in `sidebar.detachedCommentIds` and have `status: "detached"`.
455
+ - Have `anchor.kind === "detached"` with a `lastKnownRange` and a `reason` (`"deleted"`, `"invalidatedByStructureChange"`, or `"importAmbiguity"`).
456
+ - Do **not** block DOCX export.
457
+ - Can be resolved, reopened, or deleted via the same methods above.
458
+
459
+ ---
460
+
461
+ ### Tracked-change operations
462
+
463
+ #### Get all tracked changes
464
+
465
+ ```ts
466
+ const changes: TrackedChangesSnapshot = editorRef.current.getTrackedChanges();
467
+ ```
468
+
469
+ ```ts
470
+ interface TrackedChangesSnapshot {
471
+ pendingChangeIds: string[];
472
+ acceptedChangeIds: string[];
473
+ rejectedChangeIds: string[];
474
+ detachedChangeIds: string[];
475
+ actionableChangeIds: string[];
476
+ preserveOnlyChangeIds: string[];
477
+ totalCount: number;
478
+ revisions: TrackedChangeEntrySnapshot[];
479
+ }
480
+
481
+ interface TrackedChangeEntrySnapshot {
482
+ revisionId: string;
483
+ kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
484
+ status: "active" | "accepted" | "rejected" | "detached";
485
+ actionability: "actionable" | "preserve-only";
486
+ canAccept: boolean;
487
+ canReject: boolean;
488
+ anchor: EditorAnchorProjection;
489
+ anchorLabel: string;
490
+ excerpt?: string;
491
+ detail?: string;
492
+ authorId: string;
493
+ createdAt: string;
494
+ }
495
+ ```
496
+
497
+ `preserve-only` revisions (`formatting`, `move`) can be displayed but cannot be individually accepted or rejected through the API.
498
+
499
+ #### Accept a single change
500
+
501
+ ```ts
502
+ editorRef.current.acceptChange(revisionId);
503
+ ```
504
+
505
+ #### Reject a single change
506
+
507
+ ```ts
508
+ editorRef.current.rejectChange(revisionId);
509
+ ```
510
+
511
+ #### Accept all pending changes
512
+
513
+ ```ts
514
+ editorRef.current.acceptAllChanges();
515
+ ```
516
+
517
+ #### Reject all pending changes
518
+
519
+ ```ts
520
+ editorRef.current.rejectAllChanges();
521
+ ```
522
+
523
+ #### Scroll to a tracked change
524
+
525
+ ```ts
526
+ editorRef.current.scrollToRevision(revisionId);
527
+ ```
528
+
529
+ ---
530
+
531
+ ### Export
532
+
533
+ ```ts
534
+ const result = await editorRef.current.exportDocx({ fileName: "output.docx" });
535
+ // result.bytes is the Uint8Array of the .docx file
536
+ ```
537
+
538
+ Export will throw if any non-detached comment no longer maps to a serializable range in the document. To diagnose, check `getComments().threads` for entries where `status !== "detached"` but whose `anchor.kind` is unexpected.
539
+
540
+ ---
541
+
542
+ ### Events
543
+
544
+ All events are dispatched through the single `onEvent` prop. The `type` discriminator narrows the payload.
545
+
546
+ ```tsx
547
+ <WordReviewEditor
548
+ onEvent={(event) => {
549
+ if (event.type === "comment_added") {
550
+ console.log("New comment:", event.commentId);
551
+ }
552
+ }}
553
+ />
554
+ ```
555
+
556
+ #### `ready`
557
+
558
+ Fired once after the document finishes loading and the editor is interactive.
559
+
560
+ ```ts
561
+ {
562
+ type: "ready";
563
+ documentId: string;
564
+ sessionId: string;
565
+ source: "docx" | "session" | "snapshot";
566
+ stats: DocumentStats; // storyLength, commentCount, revisionCount
567
+ compatibility: CompatibilityReport;
568
+ comments: CommentSidebarSnapshot;
569
+ trackedChanges: TrackedChangesSnapshot;
570
+ }
571
+ ```
572
+
573
+ #### `comment_added`
574
+
575
+ Fired when a new comment thread is created (via `addComment` or the toolbar).
576
+
577
+ ```ts
578
+ {
579
+ type: "comment_added";
580
+ documentId: string;
581
+ commentId: string;
582
+ anchor: EditorAnchorProjection;
583
+ }
584
+ ```
585
+
586
+ #### `comment_resolved`
587
+
588
+ Fired when a comment is resolved (via `resolveComment` or the sidebar).
589
+
590
+ ```ts
591
+ {
592
+ type: "comment_resolved";
593
+ documentId: string;
594
+ commentId: string;
595
+ }
596
+ ```
597
+
598
+ There is no separate `comment_removed` event — deletions are silent. Query `getComments()` after a `dirty_changed` event if you need to detect deletions.
599
+
600
+ #### `change_accepted`
601
+
602
+ Fired when a tracked change is accepted.
603
+
604
+ ```ts
605
+ {
606
+ type: "change_accepted";
607
+ documentId: string;
608
+ changeId: string;
609
+ }
610
+ ```
611
+
612
+ #### `change_rejected`
613
+
614
+ Fired when a tracked change is rejected.
615
+
616
+ ```ts
617
+ {
618
+ type: "change_rejected";
619
+ documentId: string;
620
+ changeId: string;
621
+ }
622
+ ```
623
+
624
+ #### `selection_changed`
625
+
626
+ Fired whenever the editor cursor or selection changes.
627
+
628
+ ```ts
629
+ {
630
+ type: "selection_changed";
631
+ documentId: string;
632
+ selection: SelectionSnapshot;
633
+ }
634
+
635
+ interface SelectionSnapshot {
636
+ anchor: number;
637
+ head: number;
638
+ isCollapsed: boolean;
639
+ activeRange: EditorAnchorProjection;
640
+ storyTarget?: EditorStoryTarget;
641
+ }
642
+ ```
643
+
644
+ Use `selection.activeRange` as the `anchor` argument to `addComment` — but capture it *before* opening any modal UI.
645
+
646
+ #### `dirty_changed`
647
+
648
+ Fired when the document transitions between clean and dirty (unsaved) states.
649
+
650
+ ```ts
651
+ {
652
+ type: "dirty_changed";
653
+ documentId: string;
654
+ isDirty: boolean;
655
+ }
656
+ ```
657
+
658
+ #### `story_changed`
659
+
660
+ Fired when the user navigates between document stories (e.g. main body → header/footer).
661
+
662
+ ```ts
663
+ {
664
+ type: "story_changed";
665
+ documentId: string;
666
+ activeStory: EditorStoryTarget;
667
+ }
668
+ ```
669
+
670
+ #### `export_completed`
671
+
672
+ Fired after a successful `exportDocx` call, after the host `saveExport` callback (if any) has resolved.
673
+
674
+ ```ts
675
+ {
676
+ type: "export_completed";
677
+ documentId: string;
678
+ result: ExportResult; // result.bytes, result.fileName, result.mimeType
679
+ }
680
+ ```
681
+
682
+ #### `session_saved`
683
+
684
+ Fired after the host `saveSession` callback resolves.
685
+
686
+ ```ts
687
+ {
688
+ type: "session_saved";
689
+ documentId: string;
690
+ sessionState: EditorSessionState;
691
+ savedAt: string;
692
+ isAutosave: boolean;
693
+ }
694
+ ```
695
+
696
+ #### `snapshot_saved`
697
+
698
+ Fired after the datastore `saveSnapshot` callback resolves.
699
+
700
+ ```ts
701
+ {
702
+ type: "snapshot_saved";
703
+ documentId: string;
704
+ snapshot: PersistedEditorSnapshot;
705
+ isAutosave: boolean;
706
+ }
707
+ ```
708
+
709
+ #### `autosave_state`
710
+
711
+ Fired when the autosave lifecycle transitions.
712
+
713
+ ```ts
714
+ {
715
+ type: "autosave_state";
716
+ documentId: string;
717
+ state: "idle" | "pending" | "saving" | "saved" | "error";
718
+ }
719
+ ```
720
+
721
+ #### `warning_added` / `warning_cleared`
722
+
723
+ Non-fatal import or rendering warnings.
724
+
725
+ ```ts
726
+ { type: "warning_added"; documentId: string; warning: EditorWarning; }
727
+ { type: "warning_cleared"; documentId: string; warningId: string; code: EditorWarningCode; }
728
+ ```
729
+
730
+ #### `error`
731
+
732
+ Fatal editor error. The editor may be in an unrecoverable state after this event.
733
+
734
+ ```ts
735
+ {
736
+ type: "error";
737
+ documentId: string;
738
+ error: EditorError; // error.code, error.message, error.isFatal
739
+ }
740
+ ```
741
+
742
+ #### Workflow events
743
+
744
+ These events relate to the optional workflow overlay feature.
745
+
746
+ ```ts
747
+ { type: "workflow_overlay_changed"; documentId: string; snapshot: WorkflowScopeSnapshot; }
748
+ { type: "workflow_active_work_item_changed"; documentId: string; activeWorkItemId: string | null; }
749
+ { type: "command_blocked"; documentId: string; command: string; reasons: WorkflowBlockedCommandReason[]; }
750
+ ```
751
+
752
+ ---
753
+
754
+ ### Key types
755
+
756
+ #### EditorAnchorProjection
757
+
758
+ Describes a position or range in the document. Returned by `selection.activeRange` and stored on comments/revisions.
759
+
760
+ ```ts
761
+ type EditorAnchorProjection =
762
+ | { kind: "range"; from: number; to: number; assoc: { start: -1|1; end: -1|1 } }
763
+ | { kind: "node"; at: number; assoc: -1|1 }
764
+ | { kind: "detached"; lastKnownRange: { from: number; to: number };
765
+ reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity" };
766
+ ```
767
+
768
+ A `"range"` anchor is required for `addComment` if the document contains tables or the selection spans multiple characters. The `from`/`to` positions are in the editor's internal runtime coordinate space — always capture them from `getRenderSnapshot().selection.activeRange`, never construct them manually.
769
+
770
+ #### DocumentMode
771
+
772
+ ```ts
773
+ type DocumentMode = "editing" | "suggesting" | "viewing";
774
+ ```
775
+
776
+ #### WorkspaceMode
777
+
778
+ ```ts
779
+ type WorkspaceMode = "canvas" | "page";
780
+ ```
781
+
782
+ ---
783
+
784
+ ### Common patterns
785
+
786
+ #### Full add-comment flow with custom UI
787
+
788
+ ```tsx
789
+ function CommentButton({ editorRef }: { editorRef: React.RefObject<WordReviewEditorRef> }) {
790
+ const [draft, setDraft] = useState<{ anchor: EditorAnchorProjection; text: string } | null>(null);
791
+
792
+ function openDraft() {
793
+ // Capture anchor BEFORE the modal steals focus from the editor
794
+ const snapshot = editorRef.current?.getRenderSnapshot();
795
+ if (!snapshot) return;
796
+ const anchor = snapshot.selection.activeRange;
797
+ if (anchor.kind !== "range" || anchor.from === anchor.to) return;
798
+ setDraft({ anchor, text: "" });
799
+ }
800
+
801
+ function submit() {
802
+ if (!draft) return;
803
+ editorRef.current?.addComment({ anchor: draft.anchor, body: draft.text });
804
+ setDraft(null);
805
+ }
806
+
807
+ return (
808
+ <>
809
+ <button onClick={openDraft}>Comment</button>
810
+ {draft && (
811
+ <dialog open>
812
+ <textarea value={draft.text} onChange={(e) => setDraft({ ...draft, text: e.target.value })} />
813
+ <button onClick={submit}>Add</button>
814
+ <button onClick={() => setDraft(null)}>Cancel</button>
815
+ </dialog>
816
+ )}
817
+ </>
818
+ );
819
+ }
820
+ ```
821
+
822
+ #### Listen for comment and review-change events
823
+
824
+ ```tsx
825
+ <WordReviewEditor
826
+ onEvent={(event) => {
827
+ switch (event.type) {
828
+ case "comment_added":
829
+ console.log("comment added:", event.commentId, event.anchor);
830
+ break;
831
+ case "comment_resolved":
832
+ console.log("comment resolved:", event.commentId);
833
+ break;
834
+ case "change_accepted":
835
+ console.log("change accepted:", event.changeId);
836
+ break;
837
+ case "change_rejected":
838
+ console.log("change rejected:", event.changeId);
839
+ break;
840
+ }
841
+ }}
842
+ />
843
+ ```
844
+
845
+ #### Resolve all open comments programmatically
846
+
847
+ ```ts
848
+ const { threads } = editorRef.current.getComments();
849
+ for (const thread of threads) {
850
+ if (thread.status === "open") {
851
+ editorRef.current.resolveComment(thread.commentId);
852
+ }
853
+ }
854
+ ```
855
+
856
+ #### Accept or reject all actionable changes
857
+
858
+ ```ts
859
+ editorRef.current.acceptAllChanges();
860
+ // or
861
+ editorRef.current.rejectAllChanges();
862
+ ```