@beyondwork/docx-react-component 1.0.38 → 1.0.40

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 (85) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +305 -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/index.ts +9 -0
  6. package/src/io/docx-session.ts +1 -0
  7. package/src/io/export/serialize-numbering.ts +42 -8
  8. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  9. package/src/io/export/serialize-run-formatting.ts +90 -0
  10. package/src/io/export/serialize-styles.ts +212 -0
  11. package/src/io/ooxml/parse-fields.ts +10 -3
  12. package/src/io/ooxml/parse-numbering.ts +41 -1
  13. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  14. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  15. package/src/io/ooxml/parse-styles.ts +31 -0
  16. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  17. package/src/io/ooxml/xml-element.ts +19 -0
  18. package/src/model/canonical-document.ts +83 -3
  19. package/src/runtime/collab/event-types.ts +165 -0
  20. package/src/runtime/collab/index.ts +22 -0
  21. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  22. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  23. package/src/runtime/document-runtime.ts +141 -18
  24. package/src/runtime/layout/docx-font-loader.ts +30 -11
  25. package/src/runtime/layout/index.ts +2 -0
  26. package/src/runtime/layout/inert-layout-facet.ts +3 -0
  27. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  28. package/src/runtime/layout/layout-invalidation.ts +14 -5
  29. package/src/runtime/layout/page-graph.ts +36 -0
  30. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  31. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  32. package/src/runtime/layout/project-block-fragments.ts +154 -20
  33. package/src/runtime/layout/public-facet.ts +81 -1
  34. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  35. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  36. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  37. package/src/runtime/layout/table-render-plan.ts +21 -1
  38. package/src/runtime/numbering-prefix.ts +5 -0
  39. package/src/runtime/paragraph-style-resolver.ts +194 -0
  40. package/src/runtime/render/render-kernel.ts +5 -1
  41. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  42. package/src/runtime/surface-projection.ts +129 -9
  43. package/src/runtime/table-schema.ts +11 -0
  44. package/src/runtime/workflow-rail-segments.ts +149 -1
  45. package/src/ui/WordReviewEditor.tsx +302 -5
  46. package/src/ui/editor-command-bag.ts +4 -0
  47. package/src/ui/editor-runtime-boundary.ts +16 -0
  48. package/src/ui/editor-shell-view.tsx +22 -0
  49. package/src/ui/editor-surface-controller.tsx +9 -1
  50. package/src/ui/headless/chrome-registry.ts +34 -5
  51. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  52. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  53. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  54. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
  55. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  56. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  57. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  58. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  59. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  60. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
  61. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  62. package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
  63. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
  64. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
  65. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
  66. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
  67. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  68. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  69. package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
  70. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  71. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  72. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
  73. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  74. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  75. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  76. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  77. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  78. package/src/ui-tailwind/index.ts +6 -5
  79. package/src/ui-tailwind/theme/editor-theme.css +108 -15
  80. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  81. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  82. package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
  83. package/src/runtime/collab-review-sync.ts +0 -254
  84. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  85. 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.40",
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,5 +1,10 @@
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";
4
+ import type { RenderFrameRect } from "../runtime/render/index.ts";
5
+ import type { ScopeRailPosture } from "../runtime/workflow-rail-segments.ts";
6
+
7
+ export type { CanonicalParagraphFormatting, CanonicalRunFormatting };
3
8
 
4
9
  export type {
5
10
  WordReviewEditorLayoutFacet,
@@ -701,6 +706,8 @@ export type SurfaceInlineSegment =
701
706
  textColor?: string;
702
707
  };
703
708
  hyperlinkHref?: string;
709
+ /** 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. */
710
+ resolvedRunFormatting?: CanonicalRunFormatting;
704
711
  }
705
712
  | {
706
713
  segmentId: string;
@@ -767,6 +774,13 @@ export interface SurfaceTableCellSnapshot {
767
774
  borderRight?: string | null;
768
775
  borderBottom?: string | null;
769
776
  borderLeft?: string | null;
777
+ /**
778
+ * R2a: space-joined CSS class names from the resolved table-style conditional
779
+ * regions (e.g. "band-firstRow band-band1Horz"). Consumers apply these to the
780
+ * cell so theme toggles repaint via CSS vars instead of re-projecting the
781
+ * surface. Direct shading overrides still win over band defaults at render.
782
+ */
783
+ bandClasses?: string | null;
770
784
  content: SurfaceBlockSnapshot[];
771
785
  }
772
786
 
@@ -777,6 +791,8 @@ export interface SurfaceTableRowSnapshot {
777
791
  height?: number;
778
792
  heightRule?: "auto" | "atLeast" | "exact";
779
793
  isHeader?: boolean;
794
+ /** R1b: row carries `w:cantSplit` — pagination keeps the whole table on one page. */
795
+ cantSplit?: boolean;
780
796
  }
781
797
 
782
798
  export interface ResolvedNumberingGeometrySnapshot {
@@ -797,6 +813,8 @@ export interface ResolvedNumberingSnapshot {
797
813
  isLegalNumbering?: boolean;
798
814
  suffix?: "tab" | "space" | "nothing";
799
815
  geometry: ResolvedNumberingGeometrySnapshot;
816
+ /** 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. */
817
+ markerRunProperties?: CanonicalRunFormatting;
800
818
  }
801
819
 
802
820
  export type SurfaceBlockSnapshot =
@@ -813,6 +831,8 @@ export type SurfaceBlockSnapshot =
813
831
  numberingPrefix?: string;
814
832
  numberingSuffix?: "tab" | "space" | "nothing";
815
833
  resolvedNumbering?: ResolvedNumberingSnapshot;
834
+ /** Cascaded paragraph formatting: docDefaults → style chain → direct paragraph properties. Added in Task 11. */
835
+ resolvedParagraphFormatting?: CanonicalParagraphFormatting;
816
836
  alignment?: "left" | "center" | "right" | "both" | "distribute";
817
837
  spacing?: { before?: number; after?: number; line?: number; lineRule?: string };
818
838
  contextualSpacing?: boolean;
@@ -838,6 +858,8 @@ export type SurfaceBlockSnapshot =
838
858
  gridColumns: number[];
839
859
  alignment?: "left" | "center" | "right";
840
860
  tblLook?: {
861
+ /** R2d: raw `w:tblLook/@w:val` hex so vendor-extended bits survive round-trip. */
862
+ val?: string;
841
863
  firstRow?: boolean;
842
864
  lastRow?: boolean;
843
865
  firstColumn?: boolean;
@@ -1288,11 +1310,129 @@ export interface TableOpResult {
1288
1310
  * read. Read-only geometry queries (columns, rows, render plan, ...)
1289
1311
  * land with the layout engine + render kernel integration.
1290
1312
  */
1313
+ /**
1314
+ * Compact summary for one canonical table in the document.
1315
+ */
1316
+ export interface PublicTableSummary {
1317
+ /** 0-based index across top-level canonical table blocks (walk order). */
1318
+ tableBlockIndex: number;
1319
+ /** The projected surface block id ("table-{index}"). */
1320
+ blockId: string;
1321
+ /** Style chain head, if declared. */
1322
+ styleId: string | null;
1323
+ /** Rows and logical columns (including gridBefore/after padding). */
1324
+ rowCount: number;
1325
+ columnCount: number;
1326
+ /** Raw canonical gridColumns in twips. */
1327
+ gridColumnsTwips: readonly number[];
1328
+ /** Table-level alignment if declared. */
1329
+ alignment: "left" | "center" | "right" | null;
1330
+ /** Whether any row has a vMerge chain crossing it. */
1331
+ hasVerticalMerges: boolean;
1332
+ /** Whether any cell in the table has colspan > 1. */
1333
+ hasHorizontalSpans: boolean;
1334
+ /** 0-based page index where the table's start offset lands, if known. */
1335
+ pageIndex: number | null;
1336
+ }
1337
+
1338
+ /**
1339
+ * Public shape of the per-page render plan. Sanitized copy of the
1340
+ * internal `TableRenderPlan`: chrome consumers read here, not directly
1341
+ * from the runtime layout module.
1342
+ */
1343
+ export interface PublicTableRenderPlan {
1344
+ blockId: string;
1345
+ pageIndex: number;
1346
+ columnsTwips: readonly number[];
1347
+ bandClasses: {
1348
+ rows: readonly { rowIndex: number; regions: readonly string[] }[];
1349
+ cells: readonly {
1350
+ rowIndex: number;
1351
+ columnIndex: number;
1352
+ regions: readonly string[];
1353
+ }[];
1354
+ };
1355
+ verticalMerges: readonly {
1356
+ columnIndex: number;
1357
+ startRowIndex: number;
1358
+ endRowIndex: number;
1359
+ columnSpan: number;
1360
+ }[];
1361
+ repeatedHeaderRows: readonly {
1362
+ sourceRowIndex: number;
1363
+ virtualFragmentId: string;
1364
+ }[];
1365
+ columnResizeHandles: readonly {
1366
+ columnIndex: number;
1367
+ originTwips: number;
1368
+ heightTwips: number;
1369
+ }[];
1370
+ }
1371
+
1372
+ /**
1373
+ * Per-row height read derived from pagination measurement + explicit
1374
+ * row height declarations.
1375
+ */
1376
+ export interface PublicTableRowHeight {
1377
+ /** Measured height in twips from the pagination engine (fallback estimate
1378
+ * when no fragments exist for the row). */
1379
+ measured: number;
1380
+ /** Explicit w:trHeight value when declared. */
1381
+ explicit?: number;
1382
+ /** w:trHeight rule when declared. */
1383
+ rule?: "auto" | "atLeast" | "exact";
1384
+ /** Whether the row is marked as an OOXML header row (tblHeader). */
1385
+ isHeader: boolean;
1386
+ }
1387
+
1388
+ /**
1389
+ * Minimal public representation of a table style definition.
1390
+ */
1391
+ export interface PublicTableStyle {
1392
+ styleId: string;
1393
+ displayName: string;
1394
+ basedOn?: string;
1395
+ isDefault: boolean;
1396
+ }
1397
+
1398
+ /**
1399
+ * Events emitted by `ref.tables.subscribe(listener)`. Sourced from the
1400
+ * runtime change stream + layout facet events so chrome surfaces can
1401
+ * refresh their derived reads without polling.
1402
+ */
1403
+ export type PublicTableEvent =
1404
+ | { kind: "table_structure_changed"; revisionToken: string }
1405
+ | { kind: "table_style_changed"; revisionToken: string }
1406
+ | { kind: "table_render_plan_ready"; revision: number }
1407
+ | { kind: "table_capabilities_changed" };
1408
+
1291
1409
  export interface WordReviewEditorTablesFacet {
1292
1410
  /** Dispatch a typed table op through the runtime. */
1293
1411
  apply(op: TableOp): TableOpResult;
1294
1412
  /** Current capability snapshot for the active table selection. */
1295
1413
  getCapabilities(): TableStructureContextSnapshot | null;
1414
+ /** List every top-level table in the main document. */
1415
+ getTables(options?: { sectionIndex?: number }): PublicTableSummary[];
1416
+ /** Summary for a single table by tableBlockIndex, or `null` when out of range. */
1417
+ getTable(tableBlockIndex: number): PublicTableSummary | null;
1418
+ /** The table the active selection currently sits in, or `null`. */
1419
+ getTableForSelection(): PublicTableSummary | null;
1420
+ /** Per-page render plan for a table, or `null` when no table / no kernel. */
1421
+ getRenderPlan(
1422
+ tableBlockIndex: number,
1423
+ pageIndex?: number,
1424
+ ): PublicTableRenderPlan | null;
1425
+ /** Current gridColumns in twips for the table. */
1426
+ getColumnWidths(tableBlockIndex: number): readonly number[];
1427
+ /** Per-row height reads, combining pagination measurement + explicit declarations. */
1428
+ getRowHeights(tableBlockIndex: number): readonly PublicTableRowHeight[];
1429
+ /** Table-style subset of the document style catalog. */
1430
+ getStyleCatalog(): readonly PublicTableStyle[];
1431
+ /**
1432
+ * Subscribe to table-facet events. Returns an unsubscribe function.
1433
+ * Chrome consumers use this instead of polling `getTables()`.
1434
+ */
1435
+ subscribe(listener: (event: PublicTableEvent) => void): () => void;
1296
1436
  }
1297
1437
 
1298
1438
  export interface PageRegionHitTest {
@@ -1343,12 +1483,19 @@ export interface EditorViewStateSnapshot {
1343
1483
  /**
1344
1484
  * Role-scoped chrome dimension (spec §6.4 of runtime-rendering-and-chrome-phase.md).
1345
1485
  *
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.
1486
+ * - `"editor"` — authoring posture. Role action region surfaces the two
1487
+ * review-layer icons: add comment + inline tracked-changes toggle. Left
1488
+ * cluster carries formatting + insert/update actions as usual. Scope
1489
+ * posture is owned by the `"workflow"` role, not editor.
1490
+ * - `"review"` — reviewing posture. Role action region surfaces optional
1491
+ * sidebar-panel shortcuts (`onReviewSidebarTrackedChanges` /
1492
+ * `onReviewSidebarComments`, hidden unless the host provides callbacks),
1493
+ * add comment + inline tracked-changes toggle, review-queue prev/next +
1494
+ * counts + active label, per-item accept/reject, batch accept-all /
1495
+ * reject-all, and markup-mode selector.
1496
+ * - `"workflow"` — workflow-actor posture. Role action region surfaces
1497
+ * the scope posture menu + work-item prev/next, claim, skip, mark
1498
+ * complete, mark blocked, jump to scope.
1352
1499
  */
1353
1500
  export type EditorRole = "editor" | "review" | "workflow";
1354
1501
 
@@ -1530,6 +1677,102 @@ export interface WorkflowMetadataSnapshot {
1530
1677
  entries: WorkflowMetadataEntry[];
1531
1678
  }
1532
1679
 
1680
+ // ---------------------------------------------------------------------------
1681
+ // R2 — issue metadata (scope-card-overlay P1)
1682
+ // ---------------------------------------------------------------------------
1683
+
1684
+ /**
1685
+ * Canonical metadata id for playbook-raised issues. Hosts register a
1686
+ * `WorkflowMetadataDefinition` with this id and push one
1687
+ * `WorkflowMetadataEntry` per issue; the scope card reads the entries
1688
+ * whose `workItemId` or `scopeId` matches a rendered scope.
1689
+ *
1690
+ * Field names line up 1:1 with the CCEP Playbook Engine rule shape so
1691
+ * hosts can push rule output without remapping.
1692
+ */
1693
+ export const ISSUE_METADATA_ID = "workflow.metadata.issue" as const;
1694
+
1695
+ export type IssueSeverity = "low" | "medium" | "high" | "blocker";
1696
+
1697
+ /**
1698
+ * Playbook rule modes. `guidance` is an advisory preference; `fallback`
1699
+ * is a tiered alternative; `mandatory` cannot be silently removed;
1700
+ * `escalate` requires named-owner sign-off; `block` refuses the edit
1701
+ * outright.
1702
+ */
1703
+ export type IssueMode =
1704
+ | "guidance"
1705
+ | "fallback"
1706
+ | "mandatory"
1707
+ | "escalate"
1708
+ | "block";
1709
+
1710
+ /**
1711
+ * Named escalation / ownership targets for playbook issues. Hosts can
1712
+ * extend the literal via a widened `string` alias if a tenant needs a
1713
+ * custom owner, but the canonical set matches CCEP's playbook rule
1714
+ * shape.
1715
+ */
1716
+ export type IssueOwner =
1717
+ | "procurement"
1718
+ | "legal"
1719
+ | "risk"
1720
+ | "finance"
1721
+ | "sustainability";
1722
+
1723
+ export interface IssueMetadataValue {
1724
+ issueId: string;
1725
+ ruleId?: string;
1726
+ topic: string;
1727
+ severity: IssueSeverity;
1728
+ mode: IssueMode;
1729
+ owner?: IssueOwner;
1730
+ escalateTo?: IssueOwner;
1731
+ checklistState: "open" | "acknowledged" | "resolved" | "waived";
1732
+ escalationState?: "none" | "requested" | "approved" | "rejected";
1733
+ suggestionIds?: string[];
1734
+ rationale?: string;
1735
+ title: string;
1736
+ summary?: string;
1737
+ }
1738
+
1739
+ /**
1740
+ * Per-scope action the scope card's issue row exposes. Dispatched as a
1741
+ * `scope-issue-action-requested` event — host decides what the action
1742
+ * means and updates the `IssueMetadataValue.checklistState` /
1743
+ * `escalationState` accordingly. The card never mutates runtime state.
1744
+ */
1745
+ export type ScopeIssueAction =
1746
+ | "resolve"
1747
+ | "waive"
1748
+ | "escalate"
1749
+ | "acknowledge";
1750
+
1751
+ /**
1752
+ * Scope card projection consumed by the chrome overlay's card layer.
1753
+ * Joins a `WorkflowScope` with its attached issue metadata (R2),
1754
+ * suggestion groups (R3 — P2 populates), review-action count (K1 —
1755
+ * P2 populates), and agent-pending flag (K2 — P2 populates).
1756
+ *
1757
+ * `primaryAnchorRect` is the position the card should hover next to —
1758
+ * resolved via `RenderAnchorIndex.bySelection(fromOffset, toOffset)`
1759
+ * on the active page, or `null` when the scope is off-screen.
1760
+ */
1761
+ export interface ScopeCardModel {
1762
+ scopeId: string;
1763
+ workItemId?: string;
1764
+ posture: ScopeRailPosture;
1765
+ label: string;
1766
+ primaryAnchorRect: RenderFrameRect | null;
1767
+ issue?: IssueMetadataValue;
1768
+ /** R3 suggestion groups attached to the scope. P2 populates; P1 = []. */
1769
+ suggestionGroupIds: readonly string[];
1770
+ /** K1 review-action count for the scope's issue. P2 populates; P1 = 0. */
1771
+ reviewActionCount: number;
1772
+ /** K2 agent-pending flag (overlapping WorkflowCandidateRange source:"ai"). P2 populates; P1 = false. */
1773
+ agentPending: boolean;
1774
+ }
1775
+
1533
1776
  export interface WorkflowBlockedCommandReason {
1534
1777
  code:
1535
1778
  | "outside_workflow_scope"
@@ -2047,6 +2290,30 @@ export type WordReviewEditorEvent =
2047
2290
  documentId: string;
2048
2291
  command: string;
2049
2292
  reasons: WorkflowBlockedCommandReason[];
2293
+ }
2294
+ | {
2295
+ /**
2296
+ * Scope card mode selector fired a mode change. Host relays to the
2297
+ * existing `setWorkflowOverlay` path (or the CCEP workflow
2298
+ * endpoint) — the card never mutates runtime state directly.
2299
+ */
2300
+ type: "scope-mode-change-requested";
2301
+ documentId: string;
2302
+ scopeId: string;
2303
+ mode: WorkflowScopeMode;
2304
+ }
2305
+ | {
2306
+ /**
2307
+ * Scope card issue row fired an action (resolve / waive /
2308
+ * escalate / acknowledge). Host updates the attached
2309
+ * `IssueMetadataValue.checklistState` / `escalationState` and
2310
+ * re-pushes via `setWorkflowMetadataEntries`.
2311
+ */
2312
+ type: "scope-issue-action-requested";
2313
+ documentId: string;
2314
+ scopeId: string;
2315
+ issueId: string;
2316
+ action: ScopeIssueAction;
2050
2317
  };
2051
2318
 
2052
2319
  export interface LoadResult {
@@ -2204,6 +2471,10 @@ export interface WordReviewEditorRef {
2204
2471
  getProtectionSnapshot(): ProtectionSnapshot;
2205
2472
  setWorkspaceMode(mode: WorkspaceMode): void;
2206
2473
  setZoom(level: ZoomLevel): void;
2474
+ /** Switch the per-role top-chrome action set. Drives toolbar + review rail. */
2475
+ setEditorRole(role: EditorRole): void;
2476
+ /** Persist a detach/attach pin for a chrome surface across snapshot rebuilds. */
2477
+ setChromePin(surface: ChromePinSurface, pin: PinState | null): void;
2207
2478
  insertSectionBreak(type: SectionBreakType, options?: { afterSectionIndex?: number }): void;
2208
2479
  deleteSectionBreak(sectionIndex: number): void;
2209
2480
  updateSectionLayout(sectionIndex: number, patch: SectionLayoutPatch): void;
@@ -2279,7 +2550,20 @@ export type WordReviewEditorChromePreset =
2279
2550
  | "workflow";
2280
2551
 
2281
2552
  export interface WordReviewEditorChromeOptions {
2553
+ /**
2554
+ * @deprecated The legacy second-strip review queue bar has been collapsed
2555
+ * into the role action region (spec §6.4). Set `role="review"` on the
2556
+ * editor to surface queue Prev/Next + accept/reject inline in the top
2557
+ * toolbar. This flag is retained for type back-compat and has no runtime
2558
+ * effect; it will be removed in a future release.
2559
+ */
2282
2560
  showReviewQueueBar: boolean;
2561
+ /**
2562
+ * @deprecated The "Mark section" button lived on the legacy
2563
+ * `TwReviewQueueBar`. Scope tagging is now the workflow role's posture
2564
+ * menu (`editor-scope-posture-menu`, surfaced via `role="workflow"`).
2565
+ * Retained for type back-compat; will be removed in a future release.
2566
+ */
2283
2567
  showSectionTagAction: boolean;
2284
2568
  showReviewRail: boolean;
2285
2569
  }
@@ -2312,6 +2596,21 @@ export interface WordReviewEditorProps {
2312
2596
  onEvent?: (event: WordReviewEditorEvent) => void;
2313
2597
  onWarning?: (warning: EditorWarning) => void;
2314
2598
  onError?: (error: EditorError) => void;
2599
+ /**
2600
+ * Optional: opens the host's sidebar to the tracked-changes panel. When
2601
+ * supplied, the review role surfaces an inline "Tracked changes panel"
2602
+ * icon in the top toolbar's role action region (spec §6.4). The button
2603
+ * is hidden when this callback is not provided, so the base runtime
2604
+ * chrome is sidebar-agnostic by default and only the harness / host that
2605
+ * owns a sidebar opts in.
2606
+ */
2607
+ onReviewSidebarTrackedChanges?: () => void;
2608
+ /**
2609
+ * Optional: opens the host's sidebar to the comments panel. Same
2610
+ * hidden-by-default gating as `onReviewSidebarTrackedChanges` — the
2611
+ * inline "Comments panel" icon appears only when a callback is wired.
2612
+ */
2613
+ onReviewSidebarComments?: () => void;
2315
2614
  }
2316
2615
 
2317
2616
  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;