@beyondwork/docx-react-component 1.0.38 → 1.0.39

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 (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.38",
4
+ "version": "1.0.39",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
+ "packageManager": "pnpm@10.30.3",
6
7
  "type": "module",
7
8
  "sideEffects": [
8
9
  "**/*.css"
@@ -49,6 +50,10 @@
49
50
  "types": "./src/runtime/document-runtime.ts",
50
51
  "import": "./src/runtime/document-runtime.ts"
51
52
  },
53
+ "./runtime/collab": {
54
+ "types": "./src/runtime/collab/index.ts",
55
+ "import": "./src/runtime/collab/index.ts"
56
+ },
52
57
  "./core/commands/formatting-commands": {
53
58
  "types": "./src/core/commands/formatting-commands.ts",
54
59
  "import": "./src/core/commands/formatting-commands.ts"
@@ -88,6 +93,31 @@
88
93
  "./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
89
94
  },
90
95
  "types": "./src/index.ts",
96
+ "scripts": {
97
+ "build": "tsup",
98
+ "test": "bash scripts/run-workspace-tests.sh",
99
+ "test:repo": "node scripts/run-repo-tests.mjs core",
100
+ "test:repo:all": "node scripts/run-repo-tests.mjs all",
101
+ "test:repo:optional": "node scripts/run-repo-tests.mjs optional",
102
+ "test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
103
+ "test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
104
+ "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
105
+ "lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
106
+ "lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
107
+ "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
108
+ "lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
109
+ "lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
110
+ "context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
111
+ "wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
112
+ "wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
113
+ "wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
114
+ "wave:launch:managed": "bash scripts/wave-launch.sh",
115
+ "wave:status": "bash scripts/wave-status.sh",
116
+ "wave:watch": "bash scripts/wave-watch.sh --follow",
117
+ "wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
118
+ "wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
119
+ "harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
120
+ },
91
121
  "keywords": [
92
122
  "docx",
93
123
  "word",
@@ -132,16 +162,12 @@
132
162
  "react-dom": "^19.2.0",
133
163
  "tailwindcss": "^4.2.2",
134
164
  "yjs": "^13.6.0",
135
- "y-prosemirror": "^1.2.0",
136
165
  "y-protocols": "^1.0.0"
137
166
  },
138
167
  "peerDependenciesMeta": {
139
168
  "yjs": {
140
169
  "optional": true
141
170
  },
142
- "y-prosemirror": {
143
- "optional": true
144
- },
145
171
  "y-protocols": {
146
172
  "optional": true
147
173
  }
@@ -163,33 +189,17 @@
163
189
  "react-dom": "19.2.4",
164
190
  "tsup": "^8.3.0",
165
191
  "tsx": "^4.21.0",
166
- "y-prosemirror": "^1.3.7",
167
192
  "y-protocols": "^1.0.7",
168
193
  "yjs": "^13.6.30"
169
194
  },
170
- "scripts": {
171
- "build": "tsup",
172
- "test": "bash scripts/run-workspace-tests.sh",
173
- "test:repo": "node scripts/run-repo-tests.mjs core",
174
- "test:repo:all": "node scripts/run-repo-tests.mjs all",
175
- "test:repo:optional": "node scripts/run-repo-tests.mjs optional",
176
- "test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
177
- "test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
178
- "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
179
- "lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
180
- "lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
181
- "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
182
- "lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
183
- "lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
184
- "context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
185
- "wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
186
- "wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
187
- "wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
188
- "wave:launch:managed": "bash scripts/wave-launch.sh",
189
- "wave:status": "bash scripts/wave-status.sh",
190
- "wave:watch": "bash scripts/wave-watch.sh --follow",
191
- "wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
192
- "wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
193
- "harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
195
+ "pnpm": {
196
+ "onlyBuiltDependencies": [
197
+ "esbuild",
198
+ "sharp"
199
+ ],
200
+ "overrides": {
201
+ "react": "19.2.4",
202
+ "react-dom": "19.2.4"
203
+ }
194
204
  }
195
- }
205
+ }
@@ -1,6 +1,9 @@
1
1
  import type { PersistedEditorSnapshot as RuntimePersistedEditorSnapshot } from "../core/state/editor-state.ts";
2
+ import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
2
3
  import type { WordReviewEditorLayoutFacet } from "../runtime/layout/public-facet.ts";
3
4
 
5
+ export type { CanonicalParagraphFormatting, CanonicalRunFormatting };
6
+
4
7
  export type {
5
8
  WordReviewEditorLayoutFacet,
6
9
  LayoutFacetEvent,
@@ -701,6 +704,8 @@ export type SurfaceInlineSegment =
701
704
  textColor?: string;
702
705
  };
703
706
  hyperlinkHref?: string;
707
+ /** Cascaded run formatting for this text segment (docDefaults → paragraph style chain → character style → direct). Added in Task 11. TODO Task 15: wire up from resolveEffectiveRunFormatting in surface-projection. */
708
+ resolvedRunFormatting?: CanonicalRunFormatting;
704
709
  }
705
710
  | {
706
711
  segmentId: string;
@@ -767,6 +772,13 @@ export interface SurfaceTableCellSnapshot {
767
772
  borderRight?: string | null;
768
773
  borderBottom?: string | null;
769
774
  borderLeft?: string | null;
775
+ /**
776
+ * R2a: space-joined CSS class names from the resolved table-style conditional
777
+ * regions (e.g. "band-firstRow band-band1Horz"). Consumers apply these to the
778
+ * cell so theme toggles repaint via CSS vars instead of re-projecting the
779
+ * surface. Direct shading overrides still win over band defaults at render.
780
+ */
781
+ bandClasses?: string | null;
770
782
  content: SurfaceBlockSnapshot[];
771
783
  }
772
784
 
@@ -777,6 +789,8 @@ export interface SurfaceTableRowSnapshot {
777
789
  height?: number;
778
790
  heightRule?: "auto" | "atLeast" | "exact";
779
791
  isHeader?: boolean;
792
+ /** R1b: row carries `w:cantSplit` — pagination keeps the whole table on one page. */
793
+ cantSplit?: boolean;
780
794
  }
781
795
 
782
796
  export interface ResolvedNumberingGeometrySnapshot {
@@ -797,6 +811,8 @@ export interface ResolvedNumberingSnapshot {
797
811
  isLegalNumbering?: boolean;
798
812
  suffix?: "tab" | "space" | "nothing";
799
813
  geometry: ResolvedNumberingGeometrySnapshot;
814
+ /** CASCADED marker run formatting: docDefaults → paragraph style chain rPr → paragraph-mark rPr → level rPr. Added in Task 11. The raw level rPr is still available at geometry.markerRunProperties. */
815
+ markerRunProperties?: CanonicalRunFormatting;
800
816
  }
801
817
 
802
818
  export type SurfaceBlockSnapshot =
@@ -813,6 +829,8 @@ export type SurfaceBlockSnapshot =
813
829
  numberingPrefix?: string;
814
830
  numberingSuffix?: "tab" | "space" | "nothing";
815
831
  resolvedNumbering?: ResolvedNumberingSnapshot;
832
+ /** Cascaded paragraph formatting: docDefaults → style chain → direct paragraph properties. Added in Task 11. */
833
+ resolvedParagraphFormatting?: CanonicalParagraphFormatting;
816
834
  alignment?: "left" | "center" | "right" | "both" | "distribute";
817
835
  spacing?: { before?: number; after?: number; line?: number; lineRule?: string };
818
836
  contextualSpacing?: boolean;
@@ -838,6 +856,8 @@ export type SurfaceBlockSnapshot =
838
856
  gridColumns: number[];
839
857
  alignment?: "left" | "center" | "right";
840
858
  tblLook?: {
859
+ /** R2d: raw `w:tblLook/@w:val` hex so vendor-extended bits survive round-trip. */
860
+ val?: string;
841
861
  firstRow?: boolean;
842
862
  lastRow?: boolean;
843
863
  firstColumn?: boolean;
@@ -1288,11 +1308,129 @@ export interface TableOpResult {
1288
1308
  * read. Read-only geometry queries (columns, rows, render plan, ...)
1289
1309
  * land with the layout engine + render kernel integration.
1290
1310
  */
1311
+ /**
1312
+ * Compact summary for one canonical table in the document.
1313
+ */
1314
+ export interface PublicTableSummary {
1315
+ /** 0-based index across top-level canonical table blocks (walk order). */
1316
+ tableBlockIndex: number;
1317
+ /** The projected surface block id ("table-{index}"). */
1318
+ blockId: string;
1319
+ /** Style chain head, if declared. */
1320
+ styleId: string | null;
1321
+ /** Rows and logical columns (including gridBefore/after padding). */
1322
+ rowCount: number;
1323
+ columnCount: number;
1324
+ /** Raw canonical gridColumns in twips. */
1325
+ gridColumnsTwips: readonly number[];
1326
+ /** Table-level alignment if declared. */
1327
+ alignment: "left" | "center" | "right" | null;
1328
+ /** Whether any row has a vMerge chain crossing it. */
1329
+ hasVerticalMerges: boolean;
1330
+ /** Whether any cell in the table has colspan > 1. */
1331
+ hasHorizontalSpans: boolean;
1332
+ /** 0-based page index where the table's start offset lands, if known. */
1333
+ pageIndex: number | null;
1334
+ }
1335
+
1336
+ /**
1337
+ * Public shape of the per-page render plan. Sanitized copy of the
1338
+ * internal `TableRenderPlan`: chrome consumers read here, not directly
1339
+ * from the runtime layout module.
1340
+ */
1341
+ export interface PublicTableRenderPlan {
1342
+ blockId: string;
1343
+ pageIndex: number;
1344
+ columnsTwips: readonly number[];
1345
+ bandClasses: {
1346
+ rows: readonly { rowIndex: number; regions: readonly string[] }[];
1347
+ cells: readonly {
1348
+ rowIndex: number;
1349
+ columnIndex: number;
1350
+ regions: readonly string[];
1351
+ }[];
1352
+ };
1353
+ verticalMerges: readonly {
1354
+ columnIndex: number;
1355
+ startRowIndex: number;
1356
+ endRowIndex: number;
1357
+ columnSpan: number;
1358
+ }[];
1359
+ repeatedHeaderRows: readonly {
1360
+ sourceRowIndex: number;
1361
+ virtualFragmentId: string;
1362
+ }[];
1363
+ columnResizeHandles: readonly {
1364
+ columnIndex: number;
1365
+ originTwips: number;
1366
+ heightTwips: number;
1367
+ }[];
1368
+ }
1369
+
1370
+ /**
1371
+ * Per-row height read derived from pagination measurement + explicit
1372
+ * row height declarations.
1373
+ */
1374
+ export interface PublicTableRowHeight {
1375
+ /** Measured height in twips from the pagination engine (fallback estimate
1376
+ * when no fragments exist for the row). */
1377
+ measured: number;
1378
+ /** Explicit w:trHeight value when declared. */
1379
+ explicit?: number;
1380
+ /** w:trHeight rule when declared. */
1381
+ rule?: "auto" | "atLeast" | "exact";
1382
+ /** Whether the row is marked as an OOXML header row (tblHeader). */
1383
+ isHeader: boolean;
1384
+ }
1385
+
1386
+ /**
1387
+ * Minimal public representation of a table style definition.
1388
+ */
1389
+ export interface PublicTableStyle {
1390
+ styleId: string;
1391
+ displayName: string;
1392
+ basedOn?: string;
1393
+ isDefault: boolean;
1394
+ }
1395
+
1396
+ /**
1397
+ * Events emitted by `ref.tables.subscribe(listener)`. Sourced from the
1398
+ * runtime change stream + layout facet events so chrome surfaces can
1399
+ * refresh their derived reads without polling.
1400
+ */
1401
+ export type PublicTableEvent =
1402
+ | { kind: "table_structure_changed"; revisionToken: string }
1403
+ | { kind: "table_style_changed"; revisionToken: string }
1404
+ | { kind: "table_render_plan_ready"; revision: number }
1405
+ | { kind: "table_capabilities_changed" };
1406
+
1291
1407
  export interface WordReviewEditorTablesFacet {
1292
1408
  /** Dispatch a typed table op through the runtime. */
1293
1409
  apply(op: TableOp): TableOpResult;
1294
1410
  /** Current capability snapshot for the active table selection. */
1295
1411
  getCapabilities(): TableStructureContextSnapshot | null;
1412
+ /** List every top-level table in the main document. */
1413
+ getTables(options?: { sectionIndex?: number }): PublicTableSummary[];
1414
+ /** Summary for a single table by tableBlockIndex, or `null` when out of range. */
1415
+ getTable(tableBlockIndex: number): PublicTableSummary | null;
1416
+ /** The table the active selection currently sits in, or `null`. */
1417
+ getTableForSelection(): PublicTableSummary | null;
1418
+ /** Per-page render plan for a table, or `null` when no table / no kernel. */
1419
+ getRenderPlan(
1420
+ tableBlockIndex: number,
1421
+ pageIndex?: number,
1422
+ ): PublicTableRenderPlan | null;
1423
+ /** Current gridColumns in twips for the table. */
1424
+ getColumnWidths(tableBlockIndex: number): readonly number[];
1425
+ /** Per-row height reads, combining pagination measurement + explicit declarations. */
1426
+ getRowHeights(tableBlockIndex: number): readonly PublicTableRowHeight[];
1427
+ /** Table-style subset of the document style catalog. */
1428
+ getStyleCatalog(): readonly PublicTableStyle[];
1429
+ /**
1430
+ * Subscribe to table-facet events. Returns an unsubscribe function.
1431
+ * Chrome consumers use this instead of polling `getTables()`.
1432
+ */
1433
+ subscribe(listener: (event: PublicTableEvent) => void): () => void;
1296
1434
  }
1297
1435
 
1298
1436
  export interface PageRegionHitTest {
@@ -1343,12 +1481,19 @@ export interface EditorViewStateSnapshot {
1343
1481
  /**
1344
1482
  * Role-scoped chrome dimension (spec §6.4 of runtime-rendering-and-chrome-phase.md).
1345
1483
  *
1346
- * - `"editor"` — authoring posture. Toolbar surfaces format, insert menu,
1347
- * Mark-section posture menu, tracked-changes display toggle, comment.
1348
- * - `"review"` reviewing posture. Toolbar surfaces prev/next, accept,
1349
- * reject, accept-all / reject-all in scope, markup mode, comment.
1350
- * - `"workflow"` — workflow-actor posture. Toolbar surfaces prev/next
1351
- * work item, claim, skip, mark complete, mark blocked, jump to scope.
1484
+ * - `"editor"` — authoring posture. Role action region surfaces the two
1485
+ * review-layer icons: add comment + inline tracked-changes toggle. Left
1486
+ * cluster carries formatting + insert/update actions as usual. Scope
1487
+ * posture is owned by the `"workflow"` role, not editor.
1488
+ * - `"review"` — reviewing posture. Role action region surfaces optional
1489
+ * sidebar-panel shortcuts (`onReviewSidebarTrackedChanges` /
1490
+ * `onReviewSidebarComments`, hidden unless the host provides callbacks),
1491
+ * add comment + inline tracked-changes toggle, review-queue prev/next +
1492
+ * counts + active label, per-item accept/reject, batch accept-all /
1493
+ * reject-all, and markup-mode selector.
1494
+ * - `"workflow"` — workflow-actor posture. Role action region surfaces
1495
+ * the scope posture menu + work-item prev/next, claim, skip, mark
1496
+ * complete, mark blocked, jump to scope.
1352
1497
  */
1353
1498
  export type EditorRole = "editor" | "review" | "workflow";
1354
1499
 
@@ -2204,6 +2349,10 @@ export interface WordReviewEditorRef {
2204
2349
  getProtectionSnapshot(): ProtectionSnapshot;
2205
2350
  setWorkspaceMode(mode: WorkspaceMode): void;
2206
2351
  setZoom(level: ZoomLevel): void;
2352
+ /** Switch the per-role top-chrome action set. Drives toolbar + review rail. */
2353
+ setEditorRole(role: EditorRole): void;
2354
+ /** Persist a detach/attach pin for a chrome surface across snapshot rebuilds. */
2355
+ setChromePin(surface: ChromePinSurface, pin: PinState | null): void;
2207
2356
  insertSectionBreak(type: SectionBreakType, options?: { afterSectionIndex?: number }): void;
2208
2357
  deleteSectionBreak(sectionIndex: number): void;
2209
2358
  updateSectionLayout(sectionIndex: number, patch: SectionLayoutPatch): void;
@@ -2279,7 +2428,20 @@ export type WordReviewEditorChromePreset =
2279
2428
  | "workflow";
2280
2429
 
2281
2430
  export interface WordReviewEditorChromeOptions {
2431
+ /**
2432
+ * @deprecated The legacy second-strip review queue bar has been collapsed
2433
+ * into the role action region (spec §6.4). Set `role="review"` on the
2434
+ * editor to surface queue Prev/Next + accept/reject inline in the top
2435
+ * toolbar. This flag is retained for type back-compat and has no runtime
2436
+ * effect; it will be removed in a future release.
2437
+ */
2282
2438
  showReviewQueueBar: boolean;
2439
+ /**
2440
+ * @deprecated The "Mark section" button lived on the legacy
2441
+ * `TwReviewQueueBar`. Scope tagging is now the workflow role's posture
2442
+ * menu (`editor-scope-posture-menu`, surfaced via `role="workflow"`).
2443
+ * Retained for type back-compat; will be removed in a future release.
2444
+ */
2283
2445
  showSectionTagAction: boolean;
2284
2446
  showReviewRail: boolean;
2285
2447
  }
@@ -2312,6 +2474,21 @@ export interface WordReviewEditorProps {
2312
2474
  onEvent?: (event: WordReviewEditorEvent) => void;
2313
2475
  onWarning?: (warning: EditorWarning) => void;
2314
2476
  onError?: (error: EditorError) => void;
2477
+ /**
2478
+ * Optional: opens the host's sidebar to the tracked-changes panel. When
2479
+ * supplied, the review role surfaces an inline "Tracked changes panel"
2480
+ * icon in the top toolbar's role action region (spec §6.4). The button
2481
+ * is hidden when this callback is not provided, so the base runtime
2482
+ * chrome is sidebar-agnostic by default and only the harness / host that
2483
+ * owns a sidebar opts in.
2484
+ */
2485
+ onReviewSidebarTrackedChanges?: () => void;
2486
+ /**
2487
+ * Optional: opens the host's sidebar to the comments panel. Same
2488
+ * hidden-by-default gating as `onReviewSidebarTrackedChanges` — the
2489
+ * inline "Comments panel" icon appears only when a callback is wired.
2490
+ */
2491
+ onReviewSidebarComments?: () => void;
2315
2492
  }
2316
2493
 
2317
2494
  export interface WordReviewEditorChromeVisibility {
@@ -358,7 +358,14 @@ export function getTableStructureContext(
358
358
  ? disabledCapability(
359
359
  "Selection cuts through a merged cell. Extend the selection to fully enclose the span.",
360
360
  )
361
- : enabledCapability(),
361
+ : mergeRectCoverage &&
362
+ mergeRectCoverage.fullyCoveredOrigins.some(
363
+ (origin) => origin.columnSpan > 1 || origin.rowSpan > 1,
364
+ )
365
+ ? disabledCapability(
366
+ "Selection encloses an already-merged cell. Split it first, then merge.",
367
+ )
368
+ : enabledCapability(),
362
369
  splitCell:
363
370
  splitWidth === 1 && splitHeight === 1
364
371
  ? disabledCapability("Select a merged or spanning cell to split it.")
@@ -803,14 +810,36 @@ function mergeSelectedCells(
803
810
  selection: TableSelectionDescriptor,
804
811
  fallbackSelection: SelectionSnapshot,
805
812
  ): StructuralMutationResult {
813
+ // R1a: the merge command was previously gated on `!isSimpleTable(table)`,
814
+ // which blocked merging on any table that already carried a single merged
815
+ // span anywhere in the table, even when the user's current selection was
816
+ // over a simple region. The correct gate is whether the rect itself is:
817
+ // (a) clean — every origin it touches is fully enclosed (analyzeRect);
818
+ // (b) internally simple — no pre-existing merged origin lives inside
819
+ // the rect. findCellAtColumn in the splice body below assumes cells
820
+ // at logical column C correspond 1:1 with row.cells entries; that
821
+ // assumption holds iff the rect's internal cells are all gridSpan=1
822
+ // and verticalMerge=undefined.
823
+ // A rect that satisfies both is safe to merge even on a table that has
824
+ // merged spans elsewhere (the common imported-agreement case).
806
825
  if (
807
- !isSimpleTable(table) ||
808
826
  selection.selectionKind !== "cell" ||
809
827
  selection.rect.bottom - selection.rect.top < 1 ||
810
828
  selection.rect.right - selection.rect.left < 1
811
829
  ) {
812
830
  return createNoopStructuralMutation(document, fallbackSelection);
813
831
  }
832
+ const grid = buildLogicalGrid(table);
833
+ const coverage = analyzeRect(grid, selection.rect);
834
+ if (!coverage.clean) {
835
+ return createNoopStructuralMutation(document, fallbackSelection);
836
+ }
837
+ const rectInternallySimple = coverage.fullyCoveredOrigins.every(
838
+ (origin) => origin.columnSpan === 1 && origin.rowSpan === 1,
839
+ );
840
+ if (!rectInternallySimple) {
841
+ return createNoopStructuralMutation(document, fallbackSelection);
842
+ }
814
843
 
815
844
  const height = selection.rect.bottom - selection.rect.top;
816
845
  const width = selection.rect.right - selection.rect.left;
@@ -1,5 +1,10 @@
1
1
  import type { InsertTableOptions } from "../../api/public-types";
2
- import type { ParagraphNode } from "../../model/canonical-document.ts";
2
+ import type {
3
+ DocumentRootNode,
4
+ ParagraphNode,
5
+ ParagraphStyleDefinition,
6
+ StylesCatalog,
7
+ } from "../../model/canonical-document.ts";
3
8
  import {
4
9
  createSelectionSnapshot,
5
10
  type CanonicalDocumentEnvelope,
@@ -18,11 +23,102 @@ import {
18
23
  resolveParagraphScope,
19
24
  type StructuralMutationResult,
20
25
  } from "./structural-helpers.ts";
26
+ import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
21
27
 
22
28
  export interface TextCommandContext {
23
29
  timestamp: string;
24
30
  }
25
31
 
32
+ /**
33
+ * Walk the `basedOn` chain of paragraph styles looking for a `nextStyle`
34
+ * definition. Returns the resolved `nextStyle` id, or undefined if none is
35
+ * found. Caps the walk at 32 steps to guard against circular `basedOn` chains.
36
+ */
37
+ function resolveNextStyle(
38
+ styleId: string | undefined,
39
+ catalog: StylesCatalog,
40
+ ): string | undefined {
41
+ if (!styleId) {
42
+ return undefined;
43
+ }
44
+ let current: string | undefined = styleId;
45
+ let steps = 0;
46
+ while (current && steps < 32) {
47
+ steps += 1;
48
+ const def: ParagraphStyleDefinition | undefined = catalog.paragraphs[current];
49
+ if (!def) {
50
+ return undefined;
51
+ }
52
+ if (def.nextStyle && catalog.paragraphs[def.nextStyle]) {
53
+ return def.nextStyle;
54
+ }
55
+ current = def.basedOn;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Given the result document and the head of the new selection (which sits at
62
+ * the start of the newly-created paragraph), locate that paragraph in the
63
+ * top-level `doc` children and return a new document with its `styleId` set to
64
+ * `nextStyleId` and its `numbering` cleared.
65
+ */
66
+ function applyNextStyleToNewParagraph(
67
+ result: TextTransactionResult,
68
+ nextStyleId: string,
69
+ ): TextTransactionResult {
70
+ const root = result.document.content as DocumentRootNode;
71
+ if (!root || root.type !== "doc") {
72
+ return result;
73
+ }
74
+
75
+ const head = result.selection.head;
76
+ const surface = createEditorSurfaceSnapshot(result.document, result.selection);
77
+
78
+ let targetBlockIndex = -1;
79
+ for (let i = 0; i < surface.blocks.length; i += 1) {
80
+ const surfaceBlock = surface.blocks[i];
81
+ if (
82
+ surfaceBlock?.kind === "paragraph" &&
83
+ surfaceBlock.from === head
84
+ ) {
85
+ targetBlockIndex = i;
86
+ break;
87
+ }
88
+ }
89
+
90
+ if (targetBlockIndex === -1) {
91
+ return result;
92
+ }
93
+
94
+ const targetBlock = root.children[targetBlockIndex];
95
+ if (!targetBlock || targetBlock.type !== "paragraph") {
96
+ return result;
97
+ }
98
+
99
+ const { numbering: _numbering, ...restProps } = targetBlock;
100
+ const updatedParagraph: ParagraphNode = {
101
+ ...restProps,
102
+ styleId: nextStyleId,
103
+ children: targetBlock.children,
104
+ };
105
+
106
+ return {
107
+ ...result,
108
+ document: {
109
+ ...result.document,
110
+ content: {
111
+ ...root,
112
+ children: [
113
+ ...root.children.slice(0, targetBlockIndex),
114
+ updatedParagraph,
115
+ ...root.children.slice(targetBlockIndex + 1),
116
+ ],
117
+ },
118
+ },
119
+ };
120
+ }
121
+
26
122
  export function insertText(
27
123
  document: CanonicalDocumentEnvelope,
28
124
  selection: SelectionSnapshot,
@@ -122,7 +218,11 @@ export function splitParagraph(
122
218
  selection: SelectionSnapshot,
123
219
  context: TextCommandContext,
124
220
  ): TextTransactionResult {
125
- return applyTextTransaction(
221
+ // Resolve the current paragraph's styleId before the split so we can look up
222
+ // `nextStyle` from the styles catalog.
223
+ const scope = resolveParagraphScope(document, selection);
224
+
225
+ const result = applyTextTransaction(
126
226
  document,
127
227
  selection,
128
228
  {
@@ -131,6 +231,26 @@ export function splitParagraph(
131
231
  },
132
232
  context,
133
233
  );
234
+
235
+ // Only apply nextStyle for top-level paragraphs; table-cell traversal
236
+ // would require walking into nested blocks which the surface snapshot doesn't expose.
237
+ if (scope?.kind !== "top-level") {
238
+ return result;
239
+ }
240
+
241
+ const originalStyleId = scope.paragraph.styleId;
242
+ const nextStyleId =
243
+ originalStyleId !== undefined
244
+ ? resolveNextStyle(originalStyleId, document.styles)
245
+ : undefined;
246
+
247
+ // If the original paragraph's style specifies a `nextStyle`, apply it to the
248
+ // newly-created paragraph (the one at result.selection.head).
249
+ if (nextStyleId !== undefined) {
250
+ return applyNextStyleToNewParagraph(result, nextStyleId);
251
+ }
252
+
253
+ return result;
134
254
  }
135
255
 
136
256
  export function insertPageBreak(
@@ -1560,6 +1560,7 @@ function filterValidStyleIds(
1560
1560
  characters: filterRecord(catalog.characters),
1561
1561
  tables: filterRecord(catalog.tables),
1562
1562
  ...(catalog.latentStyles ? { latentStyles: catalog.latentStyles } : {}),
1563
+ ...(catalog.docDefaults ? { docDefaults: catalog.docDefaults } : {}),
1563
1564
  ...(catalog.fromPackage !== undefined ? { fromPackage: catalog.fromPackage } : {}),
1564
1565
  };
1565
1566
  }