@sqren/docx-react-component
v1.0.190-a
Published
Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.
Downloads
1,303
Maintainers
Readme
title: Beyond Work Components summary: Backoffice work components landing page and primary router into consumer, wrapper, agent, and maintainer documentation. audience: consumer, wrapper, agent, maintainer stability: main-path docRole: main-path canonical: true
Beyond Work Components
This repository builds backoffice work components for the Beyond Work runtime — embeddable, composable modules that handle document editing, review workflows, data processing, and task automation inside enterprise workspaces.
The design principle across every component: advanced tasks should be equally easy for AI agents and human users. Every capability exposed through the UI is also available through a structured API. Every API is designed so an agent can inspect state, reason about it, and act on it without special accommodation. Humans and agents share the same runtime, the same commands, the same read models, and the same fidelity guarantees.
Use This README For
- confirming what is shipped today vs coming
- installing the package
- finding the right documentation lane quickly
Do not use this README as the full package contract. The canonical consumer path starts in docs/README.md, docs/reference/public-api.md (V1 + V2), docs/reference/public-api-3.md (V3 additive surface), and docs/reference/integration-guide.md.
The target end-state architecture — 11-layer stack, Runtime/AI/UI API split, semantic scope compiler, and Phase 0–7 migration plan — is documented in docs/architecture/00-overview.md. Read it before designing cross-cutting changes. The technical wiki under docs/wiki/ has feature-level pages (API v1/v2/v3, runtime-truth, debugging, doc-debug-service-guide, chrome-composition / chrome-composition-backlog, testing-and-fixtures, etc.); navigate via docs/README.md.
Components
Document Editor (@beyondwork/docx-react-component)
A fidelity-first React .docx editor centered on WordReviewEditor. Built for legal review, contract negotiation, and any workflow where documents need to survive a round-trip through Microsoft Word without damage.
Shipped capabilities:
- Full tracked-change and comment support (add, resolve, accept, reject) — including Lane 7b X4.a/X4.b container + range revision markers and move-promotion
- Suggesting mode where every edit automatically becomes a tracked change
- Round-trip OOXML preservation — open, edit, export, reopen in Word without repair prompts; preserve-first unknown handling (Lanes 7b/7c closed)
- Workflow overlays with scope-based editing constraints, marker-backed scope identity, and three-way scope-source discrimination (marker-backed / overlay-only / detached)
- Runtime-backed read models for document structure, review state, selection, compatibility, and fields
- Document comparison with LCS-based diffing and redline export
- Legal document analysis (bookmarks, cross-references, defined terms, signature blocks)
- Real-time collaboration via Yjs — Lane 4 Phase C shipped (R1 remote-replay coalescing, R3 idle-priority abort signal, pairRuntimes migration)
- Layout engine proven at 97.77% (615/629) external layout-structure truth and 96.34% (606/629) rendered-word geometry over the private contract corpus (see
services/truth-baseline/) - Charts v1 — native SVG renderer for 7 chart families with progressive time-slicing (Lane 5 Stages 0–6 shipped)
- Debug substrate Phase 0 + Phase 1 — runtime projector + 12-channel
TelemetryBus+DebugInspectorSnapshot+UxResponseevent stream - Additive V3 public surface —
createApiV3(runtime)with Runtime + AI API split, 31 functions under a 4-status taxonomy (live/live-with-adapter/partial/mock); Phase P' closed 2026-04-21
Human-AI parity in practice:
- The human clicks "Add Comment" on selected text; the agent calls
ref.addComment({ anchor, body })(V1) orapi.runtime.review.resolveComment(...)(V3) on the same selection - The human reviews tracked changes in the sidebar; the agent reads
getTrackedChanges()(V1) orapi.runtime.review.getChanges()(V3) and makes the same accept/reject decisions - The human exports via toolbar; the agent calls
exportDocx()(V1) orapi.runtime.document.export()(V3) after the same compatibility check - Workflow overlays constrain both humans and agents identically —
getInteractionGuardSnapshot()/api.runtime.workflow.getGuard()is the single source of posture truth - The AI action policy module gates 37 discrete agent operations with risk classification and context validation
- Every
uiVisibleV3 function emits exactly oneUxResponseevent on theapitelemetry channel — the stream the Phase Q debug UX consumes to render mock-or-live visual traces identically
Workblocks
Composable task modules that automate business processes. Each workblock is a self-contained package of Python backend logic, React UI components, and a declarative task graph defined in manifest.yaml. They execute on the Beyond Work platform via Temporal workflows.
Categories:
- Document processing — invoice extraction, PDF template OCR, bulk processing, email-to-action
- AI agents — explore, flow debugging, data querying, task launching, authoring
- Analytics — execution history, company insights, timeline queries
- Learning — human correction capture, learning set curation, embedding management
- Infrastructure — registry, health checks, credential management (9 types)
Human-AI parity in practice:
- Users create workblocks through VS Code Studio with prompts and visual editing; authoring agents create them through the same manifest schema and file tools
- Users validate invoice fields in a React form; agents validate the same fields through typed task inputs/outputs
- Both humans and agents operate within the same workblock execution engine — same task graph, same data flow, same error handling
The Beyond Work Runtime
These components plug into a four-layer platform:
User-Facing (Layer 4)
neui (Next.js 15), bport (BFF), MCP server, VS Code extension
|
Workblocks (Layer 3)
20+ composable task modules: agents, data processing, credentials
|
Platform Services (Layer 2)
bworker (Go), modelproxy (LiteLLM), registry, pythonworker, Temporal
|
AWS Infrastructure (Layer 1)
EKS, Crossplane, Terraform, PostgreSQL, Redis, Qdrant, S3, SQSThe document editor lives at Layer 4 as an embeddable surface. Workblocks live at Layer 3 and are orchestrated by the platform. Both expose their capabilities symmetrically to human UIs and agent APIs.
Shipped vs Coming
The shipped contract today is docx-first. The table below delineates what is ready to build on vs what is planned end-state work. For the full migration narrative, see docs/architecture/00-overview.md §"Migration Plan" (Phases 0–7).
| Area | Shipped today | Coming |
|---|---|---|
| Package runtime | docx — the only shipped runtime contract | xlsx planned; pdf future work outside current package boundary |
| Public API | V1 flat-method WordReviewEditorRef (public-stable) + V2 facet projections + dual-error EditorDiagnostic (advanced-supported) + V3 createApiV3(runtime) Runtime + AI split (advanced-supported; Phase P' 2026-04-21) | V3 UI API surface (Phase 3 remainder); semantic scope compiler (Phase 4); replacement-scope lifecycle (Phase 5); semantic-scope-first AI layer (Phase 6) |
| Middle stack | Canonical model (src/model/canonical-document.ts); runtime core (src/runtime/document-runtime.ts); layout engine (src/runtime/layout/**); render kernel MVP (src/runtime/render/**) | Formal src/session/** package/session boundary (Phase 1); formatting semantics layer (Phase 2); explicit geometry projection layer (Phase 2); semantic scope compiler (Phase 4) |
| Layout / geometry | External truth baseline proven at 97.77% layout / 96.34% rendered-word on 629 private corpus docs | First-class runtime-owned geometry contract for caret/hit-test/replacement envelopes |
| Debug substrate | Runtime projector + TelemetryBus (12 channels, 8 active/wired, 4 reserved) + DebugInspectorSnapshot + UxResponse stream + wrapRefForTelemetry (Phase 0 + Phase 1 shipped 2026-04-20) | services/debug/ Railway HTTPS service (Phase 2 in progress); MCP proxy + dev-drawer retarget (Phase 3 planned); Phase Q debug UX at src/ui-tailwind/debug/** |
| External oracles | OpenXML SDK validator MCP (round-trip proof); LibreOffice-backed services/truth-baseline for layout + geometry; visual-smoke MCP for private contract templates | Phase F full oracle integration: stage-token lineage, alignment keys, known-divergence register, deterministic environment |
| BW / CLM integration | Stateless DOCX runtime service at vendor/beyondwork/; Yjs transport; timeline/audit substrate | Grouped service families aligned with Runtime/AI API (Phase 7); scope-native CLM primitives replacing raw blob_ref + story/start/end glue |
The shipped primitives are correct for their respective layers; the target architecture widens the middle and tops the stack, without replacing anything below.
Install
pnpm add @beyondwork/docx-react-component react react-dom tailwindcss \
prosemirror-commands prosemirror-keymap prosemirror-model \
prosemirror-state prosemirror-tables prosemirror-transform prosemirror-viewCurrent packaging truth:
- the package is ESM-only
- exports point at shipped TypeScript source entry points
typesand subpathtypesentries resolve to the shipped source-backed TypeScript contracts- consumers need a bundler or runtime that can resolve
.tsand.tsxESM imports - consumers should import
@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.cssonce and provide a Tailwind v4 CSS pipeline for it - package and source identifiers remain docx-first until a deliberate rename lands
Shipped Product
The primary shipped surface is:
import { WordReviewEditor } from "@beyondwork/docx-react-component";
<WordReviewEditor />WordReviewEditor remains uncontrolled by default:
- host passes
initialDocx,initialSessionState, orinitialSnapshot, or provideshostAdapter.load()/datastore.load() - runtime owns the live working session
- host receives events, warnings, errors, session state, compatibility snapshots, and exported artifacts
Persistence direction:
EditorSessionStateis the canonical host-facing live-session contractEditorHostAdapteris the preferred persistence boundary forload,saveSession,saveExport, and telemetryEditorDatastoreAdapterremains the legacy snapshot bridge for hosts that still persistPersistedEditorSnapshot
Snapshot/export note:
- when a session starts from a real
.docx, persisted snapshots now carry embedded source-package provenance so later snapshot-origin.docxexport can use the same package-backed exporter - legacy snapshots without that provenance still load, but
.docxexport is intentionally blocked rather than falling back to a lossy minimal package
The current shipped ESM exports include:
- stable default entrypoints:
@beyondwork/docx-react-component@beyondwork/docx-react-component/public-types@beyondwork/docx-react-component/ui-tailwind@beyondwork/docx-react-component/legal@beyondwork/docx-react-component/compare@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css
- V3 public API (additive, Phase P' 2026-04-21):
@beyondwork/docx-react-component/api/v3—createApiV3(runtime)+ metadata primitives +UxResponse
- advanced-supported subpaths for wrapper teams and package-adjacent integrations:
@beyondwork/docx-react-component/runtime/document-runtime@beyondwork/docx-react-component/io/docx-session@beyondwork/docx-react-component/core/commands/*@beyondwork/docx-react-component/core/selection/mapping@beyondwork/docx-react-component/core/state/editor-state@beyondwork/docx-react-component/ui-tailwind/editor-surface/search-plugin
- alias subpaths:
@beyondwork/docx-react-component/tailwind@beyondwork/docx-react-component/api/public-types
Use docs/reference/public-api.md + docs/reference/public-api.manifest.json for the V1/V2 contract inventory, and docs/reference/public-api-3.md for the V3 surface. Stability guidance for each V3 namespace is in the docs/wiki/api-v3.md page (reached via docs/README.md).
Product Contract
For every component this repo ships, the standard is:
Open -> edit -> save -> reopen in the host application without damage.
For the current shipped docx implementation, that specifically means:
- open in recent Microsoft Word without repair prompts
- preserve supported content and review structures
- preserve unsupported but preservable OOXML
- remain editable in Word after export
For the normative API and host contract, use:
Documentation Map
Start Here
Main Consumer Path
docs/reference/quick-start.mddocs/reference/consumer-matrix.mddocs/reference/public-api.md— V1 + V2 canonical contractdocs/reference/public-api-3.md— V3 Runtime + AI API (additive)docs/reference/integration-guide.mddocs/reference/glossary.md
Architecture & Mental Model
docs/architecture/00-overview.md— target end-state: 11-layer stack, Runtime/AI/UI API split, semantic-scope compiler, Phase 0–7 migration plandocs/wiki/runtime-truth.md— canonical-truth narrative: package > model > runtime > PM view, inverted-PM model, 10 perf invariants (navigate viadocs/README.md)docs/wiki/future-architecture.md— forward-looking design anchors + proof status
Wrapper And Agent Path
docs/reference/consumer-matrix.mddocs/reference/agent-integration-guide.mddocs/reference/public-api.md— V1 + V2docs/reference/public-api-3.md— V3docs/reference/service-wrapper-guidance.mddocs/reference/agent-capability-map.mddocs/reference/agent-read-models-and-snapshots.mddocs/reference/agent-workflow-and-suggestions.mddocs/ai-actions.mddocs/reference/scope-aware-selection-tooling.mddocs/reference/glossary.mddocs/reference/editor-integration-style.mddocs/reference/ui-theming-and-styling.mddocs/reference/beyondwork-runtime-environment.md
Wrapper and agent docs stay on main, but they are downstream integration guidance rather than the canonical package contract.
Current integration honesty:
- the shipped React host surface is still
WordReviewEditor showReviewPanel={false}only hides the bundled review rail- a distinct public chromeless React surface is not yet shipped
- V3 is additive — React editor mounts keep using V1/V2; V3 is for stateless services, out-of-process agents, workblocks, and debug consumers
ai.*mutations (resolveReference,getScopeBundle,proposeReplacementScope,validateReplacementScope,applyReplacementScope,attachExplanation,createIssue) are mock today pending Phase 4 (scope compiler) and Phase 5 (replacement lifecycle) — the shape is stable, the behavior will be replaced
Maintainer Path
docs/maintainers/README.md- private/local contract corpus notes for maintainers
docs/plans/README.md— current plans/audits router + archived execution history
Private contract corpus assets are maintainer/operator inputs for agreement-heavy validation and wrapper or agent benchmarking. They should remain local or in controlled artifact storage rather than being advertised as part of the package contract.
Plans and audits
docs/plans/ now keeps only the maintainer-facing artifacts that are still in
scope for current audit/debug work:
chrome-composition-audit.mdchrome-component-index.jsonux-inventory.mdscope-audit.md+scope-audit-classification.jsondebug-tooling-v2-architecture.mddoc-debug-stack.mddoc-debug-test-protocol.md
Closed lane and one-off execution plans now live under
docs/plans/archived/. The gap-analysis/ and v2/
subdirectories remain only where current docs or generators still depend on
them. (The former lane-prompts subdirectory was deleted on 2026-04-21;
its content was consolidated into docs/wiki/shipped-history.md.)
Technical Wiki
docs/wiki/ is the feature-by-feature technical layer (40+ topics covering OOXML, ProseMirror, runtime, debug, and platform). Enter via docs/README.md. Highlights:
api-v1·api-v2·api-v3— the three public API generations, delineatedruntime-truth— the canonical-truth narrative every agent should internalizedebugging+doc-debug-service-guide— runtime debug substrate + HTTPS debug servicechrome-composition+chrome-composition-backlog— UX composition audit split: shipped vs outstandingtesting-and-fixtures— F01–F52 corpus + private large-document corpus + truth-baseline numbers + MCP tooling map
Agent Integration
Agents consume the editor through a consistent pipeline. Two entry shapes, same pipeline:
V1/V2 ref (React editor mounts):
inspect --> locate --> mutate --> validate --> exportV3 createApiV3(runtime) (stateless services, workblocks, out-of-process agents):
inspect --> listScopes --> getScopeBundle --> propose --> validate --> apply --> export
└── mock today (Phase 4/5) ──┘Beyond Work platform agents extend the V1/V2 pipeline with propose/approve gates:
inspect --> locate --> propose --> approve --> mutate --> validate --> exportThe package exposes purpose-built read models — not a monolithic state dump. V1 getters and V3 equivalents dispatch through the same runtime:
| Read model | V1 getter | V3 equivalent | Purpose |
|---|---|---|---|
| Document surface | getRenderSnapshot() | (composed via runtime.content.*) | Structure, blocks, text, selection |
| Comments | getComments() | api.runtime.review.getComments() (partial) | Thread state, resolution, anchors |
| Tracked changes | getTrackedChanges() | api.runtime.review.getChanges() (partial) | Revision inventory, actionability |
| Workflow scope | getWorkflowScopeSnapshot() | api.runtime.workflow.queryScopes() (live) | Scope boundaries, work items, mode |
| Interaction guard | getInteractionGuardSnapshot() | api.runtime.workflow.getGuard() (live) | Effective mode, blocked reasons |
| Compatibility | getCompatibilityReport() | api.runtime.document.validate() (live-with-adapter) | Export risk assessment |
| Warnings | getWarnings() | (bundled into validate()) | Active fidelity or mutation warnings |
| Layout | ref.layout.getPage(...) | api.runtime.layout.getPage(...) (live-with-adapter) | Page/block layout reads |
| Debug snapshot | runtime.debug.getSnapshot() | same (shared projector) | Canonical/workflow/review/scope/layout one-shot JSON |
Agents should read the narrow snapshot that answers the next question, not pull the full render snapshot for every decision. Every V3 uiVisible call emits exactly one UxResponse on the api telemetry channel — subscribe via docs/wiki/debugging.md (navigate through docs/README.md) for mock-or-live traces.
Packaging And Release
.github/workflows/publish.ymlpublishes onv*tags after verifying the tag matchespackage.jsonpnpm pack --dry-runis the baseline package proof- npm provenance is enabled in
publishConfigand in the publish workflow invocation - the published package currently ships source ESM entry points plus TypeScript source-backed
typesexports - the Microsoft Open XML SDK remains CI/internal-service only, never part of the shipped browser runtime
Contribution Rules
- respect the runtime-owned state model
- do not bypass commands and transactions
- do not treat the DOM as canonical state
- do not silently drop unknown OOXML
- keep docs honest about shipped versus planned behavior
- add or extend fixtures for compatibility-critical changes
bash scripts/validate-fixtures.shnow uses the Railway validator service and requiresOPENXML_VALIDATOR_AUTH_TOKENwhen hitting the public domainnode scripts/validate-docs-navigation.mjsenforces the core docs catalog, required indexes, and frontmatter contract
Guiding Principle
This repo builds backoffice work components where humans and AI agents are first-class peers. Every capability is designed for both audiences from the start — not bolted on for one after the other. The result is tools that are powerful enough for complex enterprise workflows and structured enough that an agent can operate them safely and predictably.
Using the package
WordReviewEditor
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.
Installation
import {
WordReviewEditor,
type WordReviewEditorRef,
type WordReviewEditorProps,
type WordReviewEditorEvent,
} from "@beyondwork/docx-react-component";Basic mount
import { useRef } from "react";
import { WordReviewEditor, type WordReviewEditorRef } from "@beyondwork/docx-react-component";
export function MyEditor({ docxBytes }: { docxBytes: Uint8Array }) {
const editorRef = useRef<WordReviewEditorRef>(null);
return (
<WordReviewEditor
ref={editorRef}
documentId="doc-001"
currentUser={{ userId: "u1", displayName: "Alice" }}
initialDocx={docxBytes}
onEvent={(event) => console.log(event)}
/>
);
}Props reference
| Prop | Type | Description |
|---|---|---|
| documentId | string | Required. Stable identifier for this document. |
| currentUser | EditorUser | Required. The user performing edits and adding comments. |
| initialDocx | Uint8Array \| ArrayBuffer | Raw .docx bytes to load on first mount. |
| initialSessionState | EditorSessionState | Previously saved session state to restore. |
| initialSnapshot | PersistedEditorSnapshot | Previously saved snapshot to restore. |
| externalDocSource | ExternalDocumentSource | Alternative source with explicit kind ("docx", "session", "snapshot"). |
| readOnly | boolean | When true, all editing commands are disabled. |
| reviewMode | "editing" \| "review" | Shell layout hint — affects toolbar/panel arrangement but not editing authority. |
| markupDisplay | "clean" \| "simple" \| "all" | Controls tracked-change visibility. |
| showReviewPanel | boolean | Shows or hides the right-side comment and tracked-change panel. |
| autosave | AutosaveConfig | Enables automatic saving. |
| hostAdapter | EditorHostAdapter | Callbacks for load, saveSession, saveExport. |
| datastore | EditorDatastoreAdapter | Alternative persistence adapter with load, saveSnapshot. |
| onEvent | (event: WordReviewEditorEvent) => void | Unified event handler (see Events). |
| onWarning | (warning: EditorWarning) => void | Fired for non-fatal warnings. |
| onError | (error: EditorError) => void | Fired for fatal errors. |
EditorUser
interface EditorUser {
userId: string;
displayName: string;
email?: string;
avatarUrl?: string;
}AutosaveConfig
interface AutosaveConfig {
enabled?: boolean;
debounceMs?: number; // default: 2000
}Show / hide UI regions
Review panel
The right-side panel lists comment threads and tracked changes.
<WordReviewEditor showReviewPanel={false} ... /> // hide panel
<WordReviewEditor showReviewPanel={true} ... /> // show panel (default)Tracked-change display mode
markupDisplay controls how tracked changes appear in the document body.
| Value | Behaviour |
|---|---|
| "clean" | Show the accepted version — insertions visible, deletions hidden. |
| "simple" | Show a simplified view of changes without inline markup. |
| "all" | Show all insertion and deletion marks inline (Word's "Show Markup" mode). |
<WordReviewEditor markupDisplay="clean" ... />You can also change the display mode at runtime:
// no ref method for markupDisplay — pass as a prop; React re-renders propagate the change.Document mode
DocumentMode controls editing authority, not just appearance.
| Mode | Effect |
|---|---|
| "editing" | Edits are applied directly (no tracking). |
| "suggesting" | Every edit is automatically wrapped in a tracked change. |
| "viewing" | Document is read-only regardless of the readOnly prop. |
Set via ref:
editorRef.current.setDocumentMode("suggesting");Or pass reviewMode="review" as a prop to start in a review-friendly shell layout (the component internally maps this to "suggesting" document mode).
Read-only mode
<WordReviewEditor readOnly={true} ... />All editing, commenting, and tracked-change commands are blocked. The toolbar is still rendered but all buttons are disabled.
Workspace layout
editorRef.current.setWorkspaceMode("canvas"); // continuous scroll
editorRef.current.setWorkspaceMode("page"); // paginated viewImperative ref
Obtain the ref via useRef<WordReviewEditorRef>():
const editorRef = useRef<WordReviewEditorRef>(null);
<WordReviewEditor ref={editorRef} ... />
// then:
editorRef.current?.addComment({ body: "Needs revision" });Comment operations
Add a comment
addComment(params: AddCommentParams): string
// returns the new commentIdinterface AddCommentParams {
anchor?: EditorAnchorProjection; // defaults to the current selection
body?: string;
authorId?: string; // defaults to currentUser.userId
}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.
// 1. Capture anchor while text is still selected
const snapshot = editorRef.current.getRenderSnapshot();
const anchor = snapshot.selection.activeRange;
// 2. Open your draft UI, let user type a message...
// 3. On submit:
const commentId = editorRef.current.addComment({ anchor, body: draftText });Resolve a comment
editorRef.current.resolveComment(commentId);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.
Reopen a resolved comment
editorRef.current.reopenComment(commentId);Delete a comment permanently
editorRef.current.deleteComment(commentId);Removes the comment entirely. Use this to clean up failed or unwanted drafts.
Add a reply to an existing thread
editorRef.current.addCommentReply(commentId, "Reply text here");Edit a comment body
editorRef.current.editCommentBody(commentId, "Updated text");Scroll to a comment
editorRef.current.scrollToComment(commentId);Open (focus) a comment in the sidebar
editorRef.current.openComment(commentId);Get all comments
const sidebar: CommentSidebarSnapshot = editorRef.current.getComments();interface CommentSidebarSnapshot {
activeCommentId?: string;
openCommentIds: string[];
resolvedCommentIds: string[];
detachedCommentIds: string[];
totalCount: number;
threads: CommentSidebarThreadSnapshot[];
}
interface CommentSidebarThreadSnapshot {
commentId: string;
status: "open" | "resolved" | "detached";
anchor: EditorAnchorProjection;
excerpt: string; // the anchored text snippet
entries: CommentSidebarThreadEntrySnapshot[];
entryCount: number;
createdAt: string; // ISO 8601
createdBy: string; // userId
resolvedAt?: string;
resolvedBy?: string;
}Detached comments
A comment becomes detached when the text it was anchored to is deleted. Detached comments:
- Still appear in
sidebar.detachedCommentIdsand havestatus: "detached". - Have
anchor.kind === "detached"with alastKnownRangeand areason("deleted","invalidatedByStructureChange", or"importAmbiguity"). - Do not block DOCX export.
- Can be resolved, reopened, or deleted via the same methods above.
Tracked-change operations
Get all tracked changes
const changes: TrackedChangesSnapshot = editorRef.current.getTrackedChanges();interface TrackedChangesSnapshot {
pendingChangeIds: string[];
acceptedChangeIds: string[];
rejectedChangeIds: string[];
detachedChangeIds: string[];
actionableChangeIds: string[];
preserveOnlyChangeIds: string[];
totalCount: number;
revisions: TrackedChangeEntrySnapshot[];
}
interface TrackedChangeEntrySnapshot {
revisionId: string;
kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
status: "active" | "accepted" | "rejected" | "detached";
actionability: "actionable" | "preserve-only";
canAccept: boolean;
canReject: boolean;
anchor: EditorAnchorProjection;
anchorLabel: string;
excerpt?: string;
detail?: string;
authorId: string;
createdAt: string;
}preserve-only revisions (formatting, move) can be displayed but cannot be individually accepted or rejected through the API.
Accept a single change
editorRef.current.acceptChange(revisionId);Reject a single change
editorRef.current.rejectChange(revisionId);Accept all pending changes
editorRef.current.acceptAllChanges();Reject all pending changes
editorRef.current.rejectAllChanges();Scroll to a tracked change
editorRef.current.scrollToRevision(revisionId);Export
const result = await editorRef.current.exportDocx({ fileName: "output.docx" });
// result.bytes is the Uint8Array of the .docx fileExport 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.
Events
All events are dispatched through the single onEvent prop. The type discriminator narrows the payload.
<WordReviewEditor
onEvent={(event) => {
if (event.type === "comment_added") {
console.log("New comment:", event.commentId);
}
}}
/>ready
Fired once after the document finishes loading and the editor is interactive.
{
type: "ready";
documentId: string;
sessionId: string;
source: "docx" | "session" | "snapshot";
stats: DocumentStats; // storyLength, commentCount, revisionCount
compatibility: CompatibilityReport;
comments: CommentSidebarSnapshot;
trackedChanges: TrackedChangesSnapshot;
}comment_added
Fired when a new comment thread is created (via addComment or the toolbar).
{
type: "comment_added";
documentId: string;
commentId: string;
anchor: EditorAnchorProjection;
}comment_resolved
Fired when a comment is resolved (via resolveComment or the sidebar).
{
type: "comment_resolved";
documentId: string;
commentId: string;
}For a general-purpose "something about comments changed" signal (covering deletions, reply appends, body edits, reopen, and every other mutation — including Yjs-driven mutations that route through the editor's imperative ref), subscribe to comments_changed.
comments_changed
Fired once per commit whenever the runtime's comment snapshot differs from the previous commit. Use this when you render comments in a sibling component and need to know when to re-fetch via editorRef.current.getComments().
{
type: "comments_changed";
documentId: string;
changedCommentIds: string[]; // always non-empty
}Covers: addComment, deleteComment, resolveComment, reopenComment, addCommentReply, editCommentBody, and any remote-origin mutation that funnels through the same runtime APIs. Does NOT fire on selection/focus changes.
change_accepted
Fired when a tracked change is accepted.
{
type: "change_accepted";
documentId: string;
changeId: string;
}change_rejected
Fired when a tracked change is rejected.
{
type: "change_rejected";
documentId: string;
changeId: string;
}selection_changed
Fired whenever the editor cursor or selection changes.
{
type: "selection_changed";
documentId: string;
selection: SelectionSnapshot;
}
interface SelectionSnapshot {
anchor: number;
head: number;
isCollapsed: boolean;
activeRange: EditorAnchorProjection;
storyTarget?: EditorStoryTarget;
}Use selection.activeRange as the anchor argument to addComment — but capture it before opening any modal UI.
dirty_changed
Fired when the document transitions between clean and dirty (unsaved) states.
{
type: "dirty_changed";
documentId: string;
isDirty: boolean;
}story_changed
Fired when the user navigates between document stories (e.g. main body -> header/footer).
{
type: "story_changed";
documentId: string;
activeStory: EditorStoryTarget;
}export_completed
Fired after a successful exportDocx call, after the host saveExport callback (if any) has resolved.
{
type: "export_completed";
documentId: string;
result: ExportResult; // result.bytes, result.fileName, result.mimeType
}session_saved
Fired after the host saveSession callback resolves.
{
type: "session_saved";
documentId: string;
sessionState: EditorSessionState;
savedAt: string;
isAutosave: boolean;
}snapshot_saved
Fired after the datastore saveSnapshot callback resolves.
{
type: "snapshot_saved";
documentId: string;
snapshot: PersistedEditorSnapshot;
isAutosave: boolean;
}autosave_state
Fired when the autosave lifecycle transitions.
{
type: "autosave_state";
documentId: string;
state: "idle" | "pending" | "saving" | "saved" | "error";
}warning_added / warning_cleared
Non-fatal import or rendering warnings.
{ type: "warning_added"; documentId: string; warning: EditorWarning; }
{ type: "warning_cleared"; documentId: string; warningId: string; code: EditorWarningCode; }error
Fatal editor error. The editor may be in an unrecoverable state after this event.
{
type: "error";
documentId: string;
error: EditorError; // error.code, error.message, error.isFatal
}Workflow events
These events relate to the optional workflow overlay feature.
{ type: "workflow_overlay_changed"; documentId: string; snapshot: WorkflowScopeSnapshot; }
{ type: "workflow_active_work_item_changed"; documentId: string; activeWorkItemId: string | null; }
{ type: "command_blocked"; documentId: string; command: string; reasons: WorkflowBlockedCommandReason[]; }Key types
EditorAnchorProjection
Describes a position or range in the document. Returned by selection.activeRange and stored on comments/revisions.
type EditorAnchorProjection =
| { kind: "range"; from: number; to: number; assoc: { start: -1|1; end: -1|1 } }
| { kind: "node"; at: number; assoc: -1|1 }
| { kind: "detached"; lastKnownRange: { from: number; to: number };
reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity" };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.
DocumentMode
type DocumentMode = "editing" | "suggesting" | "viewing";WorkspaceMode
type WorkspaceMode = "canvas" | "page";Common patterns
Full add-comment flow with custom UI
function CommentButton({ editorRef }: { editorRef: React.RefObject<WordReviewEditorRef> }) {
const [draft, setDraft] = useState<{ anchor: EditorAnchorProjection; text: string } | null>(null);
function openDraft() {
// Capture anchor BEFORE the modal steals focus from the editor
const snapshot = editorRef.current?.getRenderSnapshot();
if (!snapshot) return;
const anchor = snapshot.selection.activeRange;
if (anchor.kind !== "range" || anchor.from === anchor.to) return;
setDraft({ anchor, text: "" });
}
function submit() {
if (!draft) return;
editorRef.current?.addComment({ anchor: draft.anchor, body: draft.text });
setDraft(null);
}
return (
<>
<button onClick={openDraft}>Comment</button>
{draft && (
<dialog open>
<textarea value={draft.text} onChange={(e) => setDraft({ ...draft, text: e.target.value })} />
<button onClick={submit}>Add</button>
<button onClick={() => setDraft(null)}>Cancel</button>
</dialog>
)}
</>
);
}Listen for comment and review-change events
<WordReviewEditor
onEvent={(event) => {
switch (event.type) {
case "comment_added":
console.log("comment added:", event.commentId, event.anchor);
break;
case "comment_resolved":
console.log("comment resolved:", event.commentId);
break;
case "comments_changed":
console.log("comments changed:", event.changedCommentIds);
break;
case "change_accepted":
console.log("change accepted:", event.changeId);
break;
case "change_rejected":
console.log("change rejected:", event.changeId);
break;
}
}}
/>Resolve all open comments programmatically
const { threads } = editorRef.current.getComments();
for (const thread of threads) {
if (thread.status === "open") {
editorRef.current.resolveComment(thread.commentId);
}
}Accept or reject all actionable changes
editorRef.current.acceptAllChanges();
// or
editorRef.current.rejectAllChanges();