@beyondwork/docx-react-component 1.0.37 → 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 (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -48,6 +48,13 @@ import type {
48
48
  StyleCatalogSnapshot,
49
49
  SurfaceBlockSnapshot,
50
50
  SurfaceInlineSegment,
51
+ TableOp,
52
+ TableOpResult,
53
+ PublicTableEvent,
54
+ PublicTableRenderPlan,
55
+ PublicTableRowHeight,
56
+ PublicTableStyle,
57
+ PublicTableSummary,
51
58
  TrackedChangeEntrySnapshot,
52
59
  TocRefreshResult,
53
60
  UpdateFieldsResult,
@@ -113,6 +120,7 @@ import {
113
120
  import {
114
121
  applyTableStructureOperation,
115
122
  getTableStructureContext,
123
+ type TableStructureOperation,
116
124
  } from "../core/commands/table-structure-commands.ts";
117
125
  import {
118
126
  deleteSelectionOrBackward,
@@ -198,7 +206,12 @@ import {
198
206
  resolveChromePreset,
199
207
  resolveChromeVisibilityForPreset,
200
208
  } from "../ui-tailwind/chrome/chrome-preset-model.ts";
201
- import { createCollabReviewSync } from "../runtime/collab-review-sync.ts";
209
+ import { createRuntimeCollabSync } from "../runtime/collab/runtime-collab-sync.ts";
210
+ import {
211
+ clearLocalCursorState,
212
+ getCursorColorForUser,
213
+ setLocalCursorState,
214
+ } from "../runtime/collab/remote-cursor-awareness.ts";
202
215
 
203
216
  export {
204
217
  __createFallbackRuntime,
@@ -505,6 +518,12 @@ export function __createWordReviewEditorRefBridge(
505
518
  setZoom: (level) => {
506
519
  runtime.setZoom(level);
507
520
  },
521
+ setEditorRole: (role) => {
522
+ runtime.setEditorRole(role);
523
+ },
524
+ setChromePin: (surface, pin) => {
525
+ runtime.setChromePin(surface, pin);
526
+ },
508
527
  insertSectionBreak: (type, options) => {
509
528
  applyRuntimeInsertSectionBreak(runtime, type, options);
510
529
  },
@@ -579,6 +598,7 @@ export function __createWordReviewEditorRefBridge(
579
598
  return clonePublicValue(runtime.getRuntimeContextAnalytics(query));
580
599
  },
581
600
  layout: runtime.layout,
601
+ tables: buildTablesFacet(runtime, mountedSurface ?? null),
582
602
  goToNextReviewItem: () => {
583
603
  return clonePublicValue(navigateReviewQueue(runtime, "next"));
584
604
  },
@@ -638,6 +658,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
638
658
  onError,
639
659
  onEvent,
640
660
  onWarning,
661
+ onReviewSidebarTrackedChanges,
662
+ onReviewSidebarComments,
641
663
  readOnly = false,
642
664
  reviewMode = "review",
643
665
  suggestionsEnabled = false,
@@ -669,6 +691,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
669
691
  runtime,
670
692
  loadError,
671
693
  activeRuntime,
694
+ commandAppliedBridge,
672
695
  fallbackSnapshot,
673
696
  loadingSessionState,
674
697
  loadingViewState,
@@ -989,9 +1012,48 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
989
1012
 
990
1013
  useEffect(() => {
991
1014
  if (!ydoc || !runtime) return;
992
- const handle = createCollabReviewSync(ydoc, runtime);
1015
+ const handle = createRuntimeCollabSync({
1016
+ ydoc,
1017
+ runtime,
1018
+ authorId: currentUser.userId,
1019
+ commandAppliedBridge,
1020
+ });
993
1021
  return () => handle.destroy();
994
- }, [ydoc, runtime]);
1022
+ }, [commandAppliedBridge, currentUser.userId, runtime, ydoc]);
1023
+
1024
+ useEffect(() => {
1025
+ if (!awareness) {
1026
+ return;
1027
+ }
1028
+ return () => clearLocalCursorState(awareness);
1029
+ }, [awareness]);
1030
+
1031
+ useEffect(() => {
1032
+ if (!awareness) {
1033
+ return;
1034
+ }
1035
+ if (!runtime) {
1036
+ clearLocalCursorState(awareness);
1037
+ return;
1038
+ }
1039
+
1040
+ setLocalCursorState(awareness, {
1041
+ userId: currentUser.userId,
1042
+ displayName: currentUser.displayName,
1043
+ color: getCursorColorForUser(currentUser.userId),
1044
+ anchor: snapshot.selection.anchor,
1045
+ head: snapshot.selection.head,
1046
+ storyTarget: snapshot.activeStory,
1047
+ });
1048
+ }, [
1049
+ awareness,
1050
+ currentUser.displayName,
1051
+ currentUser.userId,
1052
+ runtime,
1053
+ snapshot.activeStory,
1054
+ snapshot.selection.anchor,
1055
+ snapshot.selection.head,
1056
+ ]);
995
1057
 
996
1058
  useEffect(() => {
997
1059
  runtimeViewStateSeedRef.current = {
@@ -1327,6 +1389,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1327
1389
  setZoom: (level) => {
1328
1390
  activeRuntime.setZoom(level);
1329
1391
  },
1392
+ setEditorRole: (role) => {
1393
+ activeRuntime.setEditorRole(role);
1394
+ },
1395
+ setChromePin: (surface, pin) => {
1396
+ activeRuntime.setChromePin(surface, pin);
1397
+ },
1330
1398
  insertSectionBreak: (type, options) => {
1331
1399
  applyRuntimeInsertSectionBreak(activeRuntime, type, options);
1332
1400
  },
@@ -1394,6 +1462,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1394
1462
  return clonePublicValue(activeRuntime.getRuntimeContextAnalytics(query));
1395
1463
  },
1396
1464
  layout: activeRuntime.layout,
1465
+ tables: buildTablesFacet(activeRuntime, surfaceRef.current ?? null),
1397
1466
  goToNextReviewItem: () => {
1398
1467
  return clonePublicValue(navigateMountedReviewQueue("next"));
1399
1468
  },
@@ -2183,6 +2252,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2183
2252
  openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header"),
2184
2253
  onOpenFooterStory: () =>
2185
2254
  openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer"),
2255
+ onOpenHeaderStoryForPage: (pageIndex: number) =>
2256
+ openStoryForPage(activeRuntime, pageIndex, "header"),
2257
+ onOpenFooterStoryForPage: (pageIndex: number) =>
2258
+ openStoryForPage(activeRuntime, pageIndex, "footer"),
2186
2259
  onDeleteSectionBreak: (sectionIndex) =>
2187
2260
  applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
2188
2261
  onUpdateSectionLayout: (sectionIndex, patch) =>
@@ -2230,7 +2303,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2230
2303
  <EditorSurfaceController
2231
2304
  ref={surfaceRef}
2232
2305
  currentUser={currentUser}
2233
- ydoc={ydoc}
2234
2306
  awareness={awareness}
2235
2307
  snapshot={snapshot}
2236
2308
  canonicalDocument={canonicalDocument}
@@ -2256,6 +2328,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2256
2328
  dispatchRuntimeCommand={(command) =>
2257
2329
  activeRuntime.applyActiveStoryTextCommand(command as never)
2258
2330
  }
2331
+ layoutFacet={activeRuntime.layout}
2332
+ pageChromeHeaderBandPx={isPageWorkspace ? 32 : 0}
2333
+ pageChromeFooterBandPx={isPageWorkspace ? 32 : 0}
2334
+ pageChromeInterGapPx={isPageWorkspace ? 24 : 16}
2335
+ onOpenHeaderStoryForPage={(pageIndex: number) =>
2336
+ openStoryForPage(activeRuntime, pageIndex, "header")
2337
+ }
2338
+ onOpenFooterStoryForPage={(pageIndex: number) =>
2339
+ openStoryForPage(activeRuntime, pageIndex, "footer")
2340
+ }
2259
2341
  onCommentActivated={(commentId) => {
2260
2342
  activeRuntime.openComment(commentId);
2261
2343
  setActiveRailTab("comments");
@@ -2300,6 +2382,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2300
2382
  activeRevisionId={activeRevisionId}
2301
2383
  showTrackedChanges={showTrackedChanges}
2302
2384
  workflowScopeSnapshot={workflowScopeSnapshot}
2385
+ layoutFacet={activeRuntime.layout}
2303
2386
  interactionGuardSnapshot={interactionGuardSnapshot}
2304
2387
  chromePreset={effectiveChromePreset}
2305
2388
  chromeOptions={chromeOptions}
@@ -2348,6 +2431,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2348
2431
  selectionToolbarRef={selectionToolbarElementRef}
2349
2432
  commands={commands}
2350
2433
  document={documentElement}
2434
+ onReviewSidebarTrackedChanges={onReviewSidebarTrackedChanges}
2435
+ onReviewSidebarComments={onReviewSidebarComments}
2351
2436
  />
2352
2437
  );
2353
2438
  },
@@ -3388,28 +3473,19 @@ function applyRuntimeImageReposition(
3388
3473
  function applyRuntimeTableStructureOperation(
3389
3474
  runtime: WordReviewEditorRuntime,
3390
3475
  mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
3391
- operation:
3392
- | { type: "add-row-before" }
3393
- | { type: "add-row-after" }
3394
- | { type: "add-column-before" }
3395
- | { type: "add-column-after" }
3396
- | { type: "delete-row" }
3397
- | { type: "delete-column" }
3398
- | { type: "delete-table" }
3399
- | { type: "merge-cells" }
3400
- | { type: "split-cell" }
3401
- | { type: "set-cell-background"; color: string },
3402
- ): void {
3476
+ operation: TableStructureOperation,
3477
+ ): { changed: boolean; coercedReason: string | null } {
3403
3478
  if (isSelectionSuggesting(runtime)) {
3479
+ const coercedReason = `Table operation "${operation.type}" is not supported in suggesting mode.`;
3404
3480
  runtime.emitBlockedCommand(`table.${operation.type}`, [{
3405
3481
  code: "unsupported_surface",
3406
- message: `Table operation "${operation.type}" is not supported in suggesting mode.`,
3482
+ message: coercedReason,
3407
3483
  }]);
3408
- return;
3484
+ return { changed: false, coercedReason };
3409
3485
  }
3410
3486
  const context = getStoryMutationContext(runtime, `table.${operation.type}`);
3411
3487
  if (!context) {
3412
- return;
3488
+ return { changed: false, coercedReason: "No active mutation context." };
3413
3489
  }
3414
3490
 
3415
3491
  const result = applyTableStructureOperation(
@@ -3419,8 +3495,262 @@ function applyRuntimeTableStructureOperation(
3419
3495
  operation,
3420
3496
  );
3421
3497
  dispatchStoryMutationResult(runtime, context, result, context.timestamp);
3498
+ return {
3499
+ changed: result.changed,
3500
+ coercedReason: result.changed ? null : "Op was a no-op against the active selection.",
3501
+ };
3502
+ }
3503
+
3504
+ /**
3505
+ * Translate a public-API `TableOp` (kebab-case `kind` discriminator) to
3506
+ * the internal `TableStructureOperation` (`type` discriminator). The
3507
+ * shape values are identical aside from the discriminator name.
3508
+ */
3509
+ export function __publicTableOpToInternal(op: TableOp): TableStructureOperation {
3510
+ return publicTableOpToInternal(op);
3511
+ }
3512
+
3513
+ function publicTableOpToInternal(op: TableOp): TableStructureOperation {
3514
+ const { kind, ...rest } = op as { kind: string } & Record<string, unknown>;
3515
+ if (kind === "insert") {
3516
+ throw new Error(
3517
+ "TableOp kind \"insert\" is not routed through ref.tables.apply; use ref.insertTable(...).",
3518
+ );
3519
+ }
3520
+ return { type: kind, ...rest } as TableStructureOperation;
3521
+ }
3522
+
3523
+ /**
3524
+ * Build the `ref.tables` facet: a typed dispatch boundary + capability
3525
+ * read. Delegates every op through the same `applyRuntimeTableStructureOperation`
3526
+ * helper the flat ref verbs use, so there is exactly one server-side path
3527
+ * for every table mutation.
3528
+ */
3529
+ function buildTablesFacet(
3530
+ runtime: WordReviewEditorRuntime,
3531
+ mountedSurface: TwProseMirrorSurfaceRef | null,
3532
+ ) {
3533
+ const getCapabilities = () => {
3534
+ const snapshot = runtime.getRenderSnapshot();
3535
+ const document = runtime.getCanonicalDocument();
3536
+ return (
3537
+ clonePublicValue(
3538
+ getTableStructureContext(
3539
+ document,
3540
+ snapshot,
3541
+ mountedSurface?.getTableSelection() ?? null,
3542
+ ),
3543
+ ) ?? null
3544
+ );
3545
+ };
3546
+
3547
+ const buildSummary = (
3548
+ table: Extract<ReturnType<typeof runtime.getCanonicalDocument>["content"]["children"][number], { type: "table" }>,
3549
+ tableBlockIndex: number,
3550
+ ): PublicTableSummary => {
3551
+ const blockId = `table-${tableBlockIndex}`;
3552
+ const hasVerticalMerges = table.rows.some((row) =>
3553
+ row.cells.some((cell) => cell.verticalMerge !== undefined),
3554
+ );
3555
+ const hasHorizontalSpans = table.rows.some((row) =>
3556
+ row.cells.some((cell) => (cell.gridSpan ?? 1) > 1),
3557
+ );
3558
+ return {
3559
+ tableBlockIndex,
3560
+ blockId,
3561
+ styleId: table.styleId ?? null,
3562
+ rowCount: table.rows.length,
3563
+ columnCount: table.gridColumns.length,
3564
+ gridColumnsTwips: table.gridColumns,
3565
+ alignment: table.alignment ?? null,
3566
+ hasVerticalMerges,
3567
+ hasHorizontalSpans,
3568
+ pageIndex: runtime.layout.getFirstPageIndexForBlock(blockId) ?? null,
3569
+ };
3570
+ };
3571
+
3572
+ const getTables = (options?: { sectionIndex?: number }): PublicTableSummary[] => {
3573
+ const document = runtime.getCanonicalDocument();
3574
+ const summaries: PublicTableSummary[] = [];
3575
+ let idx = 0;
3576
+ for (const block of document.content.children) {
3577
+ if (block.type === "table") {
3578
+ summaries.push(clonePublicValue(buildSummary(block, idx)));
3579
+ idx++;
3580
+ }
3581
+ }
3582
+ if (options?.sectionIndex != null) {
3583
+ const section = runtime.layout.getSection(options.sectionIndex);
3584
+ if (!section) return [];
3585
+ return summaries.filter(
3586
+ (s) =>
3587
+ s.pageIndex != null &&
3588
+ s.pageIndex >= section.firstPageIndex &&
3589
+ s.pageIndex <= section.lastPageIndex,
3590
+ );
3591
+ }
3592
+ return summaries;
3593
+ };
3594
+
3595
+ const getTable = (tableBlockIndex: number): PublicTableSummary | null => {
3596
+ const document = runtime.getCanonicalDocument();
3597
+ let idx = 0;
3598
+ for (const block of document.content.children) {
3599
+ if (block.type === "table") {
3600
+ if (idx === tableBlockIndex) {
3601
+ return clonePublicValue(buildSummary(block, idx));
3602
+ }
3603
+ idx++;
3604
+ }
3605
+ }
3606
+ return null;
3607
+ };
3608
+
3609
+ const getTableForSelection = (): PublicTableSummary | null => {
3610
+ const sel = mountedSurface?.getTableSelection() ?? null;
3611
+ if (!sel) return null;
3612
+ return getTable(sel.tableBlockIndex);
3613
+ };
3614
+
3615
+ const getRenderPlan = (
3616
+ tableBlockIndex: number,
3617
+ pageIndex?: number,
3618
+ ): PublicTableRenderPlan | null => {
3619
+ const blockId = `table-${tableBlockIndex}`;
3620
+ const effectivePage =
3621
+ pageIndex ??
3622
+ runtime.layout.getFirstPageIndexForBlock(blockId) ??
3623
+ 0;
3624
+ const plan = runtime.layout.getTableRenderPlan(blockId, effectivePage);
3625
+ if (!plan) return null;
3626
+ return clonePublicValue({
3627
+ blockId: plan.blockId,
3628
+ pageIndex: plan.pageIndex,
3629
+ columnsTwips: plan.columnsTwips,
3630
+ bandClasses: plan.bandClasses,
3631
+ verticalMerges: plan.verticalMerges,
3632
+ repeatedHeaderRows: plan.repeatedHeaderRows,
3633
+ columnResizeHandles: plan.columnResizeHandles,
3634
+ } satisfies PublicTableRenderPlan);
3635
+ };
3636
+
3637
+ const getColumnWidths = (tableBlockIndex: number): readonly number[] => {
3638
+ const document = runtime.getCanonicalDocument();
3639
+ let idx = 0;
3640
+ for (const block of document.content.children) {
3641
+ if (block.type === "table") {
3642
+ if (idx === tableBlockIndex) {
3643
+ return block.gridColumns;
3644
+ }
3645
+ idx++;
3646
+ }
3647
+ }
3648
+ return [];
3649
+ };
3650
+
3651
+ const getRowHeights = (tableBlockIndex: number): readonly PublicTableRowHeight[] => {
3652
+ const document = runtime.getCanonicalDocument();
3653
+ let idx = 0;
3654
+ for (const block of document.content.children) {
3655
+ if (block.type === "table") {
3656
+ if (idx === tableBlockIndex) {
3657
+ const blockId = `table-${tableBlockIndex}`;
3658
+ const measurement = runtime.layout.getMeasurement(blockId);
3659
+ const totalMeasured = measurement?.heightTwips ?? 0;
3660
+ const rows = block.rows;
3661
+ const perRowFallback = rows.length > 0 ? totalMeasured / rows.length : 0;
3662
+ return rows.map((row): PublicTableRowHeight => ({
3663
+ measured: perRowFallback,
3664
+ ...(row.height != null ? { explicit: row.height } : {}),
3665
+ ...(row.heightRule != null ? { rule: row.heightRule } : {}),
3666
+ isHeader: row.isHeader ?? false,
3667
+ }));
3668
+ }
3669
+ idx++;
3670
+ }
3671
+ }
3672
+ return [];
3673
+ };
3674
+
3675
+ const getStyleCatalog = (): readonly PublicTableStyle[] => {
3676
+ const document = runtime.getCanonicalDocument();
3677
+ return Object.values(document.styles.tables).map(
3678
+ (style): PublicTableStyle =>
3679
+ clonePublicValue({
3680
+ styleId: style.styleId,
3681
+ displayName: style.displayName,
3682
+ ...(style.basedOn != null ? { basedOn: style.basedOn } : {}),
3683
+ isDefault: style.isDefault,
3684
+ }),
3685
+ );
3686
+ };
3687
+
3688
+ const subscribe = (
3689
+ listener: (event: PublicTableEvent) => void,
3690
+ ): (() => void) => {
3691
+ const unsubRuntime = runtime.subscribeToEvents((event) => {
3692
+ if (event.type === "dirty_changed" && event.isDirty) {
3693
+ listener({
3694
+ kind: "table_structure_changed",
3695
+ revisionToken: runtime.getRenderSnapshot().revisionToken,
3696
+ });
3697
+ } else if (event.type === "selection_changed") {
3698
+ listener({ kind: "table_capabilities_changed" });
3699
+ }
3700
+ });
3701
+ const unsubLayout = runtime.layout.subscribe((event) => {
3702
+ if (
3703
+ event.kind === "layout_recomputed" ||
3704
+ event.kind === "incremental_relayout"
3705
+ ) {
3706
+ listener({
3707
+ kind: "table_render_plan_ready",
3708
+ revision: event.revision,
3709
+ });
3710
+ }
3711
+ });
3712
+ return () => {
3713
+ unsubRuntime();
3714
+ unsubLayout();
3715
+ };
3716
+ };
3717
+
3718
+ return {
3719
+ apply(op: TableOp): TableOpResult {
3720
+ if (op.kind === "insert") {
3721
+ applyRuntimeInsertTable(runtime, { rows: op.rows, columns: op.columns });
3722
+ return {
3723
+ changed: true,
3724
+ coercedReason: null,
3725
+ capabilities: getCapabilities(),
3726
+ };
3727
+ }
3728
+ const internal = publicTableOpToInternal(op);
3729
+ const outcome = applyRuntimeTableStructureOperation(
3730
+ runtime,
3731
+ mountedSurface,
3732
+ internal,
3733
+ );
3734
+ return {
3735
+ changed: outcome.changed,
3736
+ coercedReason: outcome.coercedReason,
3737
+ capabilities: getCapabilities(),
3738
+ };
3739
+ },
3740
+ getCapabilities,
3741
+ getTables,
3742
+ getTable,
3743
+ getTableForSelection,
3744
+ getRenderPlan,
3745
+ getColumnWidths,
3746
+ getRowHeights,
3747
+ getStyleCatalog,
3748
+ subscribe,
3749
+ };
3422
3750
  }
3423
3751
 
3752
+ export { buildTablesFacet as __buildTablesFacet };
3753
+
3424
3754
  function applyRuntimeTextCommand(
3425
3755
  runtime: WordReviewEditorRuntime,
3426
3756
  command:
@@ -3913,6 +4243,25 @@ function clonePublicValue<T>(value: T): T {
3913
4243
  return structuredClone(value);
3914
4244
  }
3915
4245
 
4246
+ /**
4247
+ * Open the correct header/footer story for a specific page. The page's
4248
+ * resolved `stories.header` / `stories.footer` already carries the
4249
+ * right variant (default / first / even) for that page's section, so we
4250
+ * can hand it directly to `runtime.openStory()` without re-running the
4251
+ * variant-pick logic.
4252
+ */
4253
+ function openStoryForPage(
4254
+ runtime: WordReviewEditorRuntime,
4255
+ pageIndex: number,
4256
+ kind: "header" | "footer",
4257
+ ): void {
4258
+ const page = runtime.layout?.getPage(pageIndex);
4259
+ if (!page) return;
4260
+ const target = kind === "header" ? page.stories.header : page.stories.footer;
4261
+ if (!target) return;
4262
+ runtime.openStory(target);
4263
+ }
4264
+
3916
4265
  function openDefaultStoryVariant(
3917
4266
  runtime: WordReviewEditorRuntime,
3918
4267
  pageLayout: PageLayoutSnapshot | undefined,
@@ -89,6 +89,10 @@ export interface EditorCommandBag {
89
89
  onCloseStory?(): void;
90
90
  onOpenHeaderStory?(): void;
91
91
  onOpenFooterStory?(): void;
92
+ /** Open the header story for a specific page (double-click on its band). */
93
+ onOpenHeaderStoryForPage?(pageIndex: number): void;
94
+ /** Open the footer story for a specific page (double-click on its band). */
95
+ onOpenFooterStoryForPage?(pageIndex: number): void;
92
96
  onSetParagraphIndentation?(indentation: {
93
97
  left?: number;
94
98
  right?: number;
@@ -34,6 +34,10 @@ import {
34
34
  type DocumentRuntimeEvent,
35
35
  type DocumentRuntime,
36
36
  } from "../runtime/document-runtime.ts";
37
+ import {
38
+ createRuntimeCommandAppliedBridge,
39
+ type RuntimeCommandAppliedBridge,
40
+ } from "../runtime/collab/runtime-collab-sync.ts";
37
41
  import { createInertLayoutFacet } from "../runtime/layout/index.ts";
38
42
  import { loadDocxEditorSession } from "../io/docx-session.ts";
39
43
  import {
@@ -71,6 +75,7 @@ export interface CreateRuntimeArgs {
71
75
  hostAdapter?: EditorHostAdapter;
72
76
  datastore?: EditorDatastoreAdapter;
73
77
  currentUserId?: string;
78
+ commandAppliedBridge?: RuntimeCommandAppliedBridge;
74
79
  }
75
80
 
76
81
  interface RuntimeLifecycleHandlers {
@@ -103,6 +108,7 @@ export interface EditorRuntimeBoundaryState {
103
108
  runtime: WordReviewEditorRuntime | null;
104
109
  loadError: EditorError | null;
105
110
  activeRuntime: WordReviewEditorRuntime;
111
+ commandAppliedBridge: RuntimeCommandAppliedBridge;
106
112
  fallbackSnapshot: RuntimeRenderSnapshot;
107
113
  loadingSessionState: EditorSessionState;
108
114
  loadingViewState: EditorViewStateSnapshot;
@@ -280,6 +286,10 @@ export function useEditorRuntimeBoundary(
280
286
  const onWarningRef = useRef(onWarning);
281
287
  const onErrorRef = useRef(onError);
282
288
  const currentUserIdRef = useRef(currentUser.userId);
289
+ const commandAppliedBridge = useMemo(
290
+ () => createRuntimeCommandAppliedBridge(),
291
+ [documentId],
292
+ );
283
293
  const runtimeViewStateSeedRef = useRef<{
284
294
  workspaceMode: WorkspaceMode;
285
295
  zoomLevel: ZoomLevel;
@@ -374,6 +384,7 @@ export function useEditorRuntimeBoundary(
374
384
  hostAdapter: hostAdapterRef.current,
375
385
  datastore: datastoreRef.current,
376
386
  currentUserId: currentUserIdRef.current,
387
+ commandAppliedBridge,
377
388
  },
378
389
  {
379
390
  onWarning: onWarningRef.current,
@@ -536,6 +547,7 @@ export function useEditorRuntimeBoundary(
536
547
  runtime,
537
548
  loadError,
538
549
  activeRuntime: runtime ?? loadingRuntimeBridge,
550
+ commandAppliedBridge,
539
551
  fallbackSnapshot,
540
552
  loadingSessionState,
541
553
  loadingViewState,
@@ -616,6 +628,7 @@ function createRuntime(
616
628
  bootstrapEvents.push(event);
617
629
  },
618
630
  defaultAuthorId: args.currentUserId,
631
+ onCommandApplied: args.commandAppliedBridge?.onCommandApplied,
619
632
  }), {
620
633
  drainBootstrapEvents: () => bootstrapEvents.splice(0, bootstrapEvents.length),
621
634
  emitBlockedCommand: (
@@ -782,6 +795,7 @@ function createLoadingRuntimeBridge(input: {
782
795
  ],
783
796
  }),
784
797
  dispatch: () => undefined,
798
+ applyRemoteCommand: () => undefined,
785
799
  undo: () => undefined,
786
800
  redo: () => undefined,
787
801
  focus: () => undefined,
@@ -807,6 +821,8 @@ function createLoadingRuntimeBridge(input: {
807
821
  getProtectionSnapshot: () => input.snapshot.protectionSnapshot,
808
822
  setWorkspaceMode: () => undefined,
809
823
  setZoom: () => undefined,
824
+ setEditorRole: () => undefined,
825
+ setChromePin: () => undefined,
810
826
  getPageLayoutSnapshot: () => null,
811
827
  getDocumentNavigationSnapshot: () => input.navigation,
812
828
  layout: inertLayoutFacet,
@@ -55,6 +55,12 @@ export interface EditorShellViewProps {
55
55
  activeRevisionId?: string;
56
56
  showTrackedChanges: boolean;
57
57
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
58
+ /**
59
+ * Runtime-owned layout facet passed through to the workspace so the
60
+ * ChromeOverlay (scope rail, workflow dock, etc.) can render over the
61
+ * document column and the review rail's Workflow tab can read segments.
62
+ */
63
+ layoutFacet?: import("../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
58
64
  interactionGuardSnapshot?: InteractionGuardSnapshot;
59
65
  chromePreset?: WordReviewEditorChromePreset;
60
66
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;
@@ -77,6 +83,10 @@ export interface EditorShellViewProps {
77
83
  onSelectionToolbarBlurCapture?: React.FocusEventHandler<HTMLDivElement>;
78
84
  selectionToolbarRef?: React.Ref<HTMLDivElement>;
79
85
  chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
86
+ /** Review-role sidebar panel: open sidebar to tracked-changes panel. */
87
+ onReviewSidebarTrackedChanges?: () => void;
88
+ /** Review-role sidebar panel: open sidebar to comments panel. */
89
+ onReviewSidebarComments?: () => void;
80
90
  }
81
91
 
82
92
  export function EditorShellView(props: EditorShellViewProps) {
@@ -23,7 +23,6 @@ import type { MediaPreviewDescriptor } from "../ui-tailwind/editor-surface/pm-st
23
23
 
24
24
  export interface EditorSurfaceControllerProps {
25
25
  currentUser: EditorUser;
26
- ydoc?: import('yjs').Doc;
27
26
  awareness?: import("y-protocols/awareness").Awareness;
28
27
  snapshot: RuntimeRenderSnapshot;
29
28
  canonicalDocument: CanonicalDocumentEnvelope;
@@ -46,6 +45,8 @@ export interface EditorSurfaceControllerProps {
46
45
  onDeleteForward?: () => void;
47
46
  onInsertTab?: () => void;
48
47
  onOutdentTab?: () => void;
48
+ onListIndent?: () => void;
49
+ onListOutdent?: () => void;
49
50
  onInsertHardBreak?: () => void;
50
51
  onSplitParagraph?: () => void;
51
52
  onUndo?: () => void;
@@ -63,6 +64,13 @@ export interface EditorSurfaceControllerProps {
63
64
  dispatchRuntimeCommand?: (
64
65
  command: import("../ui-tailwind/editor-surface/fast-text-edit-lane.ts").LaneRuntimeCommand,
65
66
  ) => import("../api/public-types.ts").TextCommandAck;
67
+ layoutFacet?: import("../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
68
+ pageChromeHeaderBandPx?: number;
69
+ pageChromeFooterBandPx?: number;
70
+ pageChromeInterGapPx?: number;
71
+ pageBreakRevision?: number;
72
+ onOpenHeaderStoryForPage?: (pageIndex: number) => void;
73
+ onOpenFooterStoryForPage?: (pageIndex: number) => void;
66
74
  }
67
75
 
68
76
  export const EditorSurfaceController = forwardRef<