@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
@@ -15,14 +15,21 @@
15
15
  import type {
16
16
  EditorAnchorProjection,
17
17
  EditorStoryTarget,
18
+ IssueMetadataValue,
19
+ ScopeCardModel,
18
20
  WorkflowBlockedCommandReason,
19
21
  WorkflowCandidateRange,
20
22
  WorkflowLockedZone,
23
+ WorkflowMetadataMarkup,
21
24
  WorkflowScope,
22
25
  } from "../api/public-types";
26
+ import { ISSUE_METADATA_ID } from "../api/public-types";
23
27
  import { MAIN_STORY_TARGET, storyTargetsEqual } from "../core/selection/mapping.ts";
24
28
  import type { RuntimePageGraph } from "./layout/page-graph.ts";
25
- import type { RenderFrameRect } from "./render/render-frame-types.ts";
29
+ import type {
30
+ RenderAnchorIndex,
31
+ RenderFrameRect,
32
+ } from "./render/render-frame-types.ts";
26
33
 
27
34
  // ---------------------------------------------------------------------------
28
35
  // Public shape
@@ -278,3 +285,144 @@ function resolveScopePosture(scope: WorkflowScope): ScopeRailPosture {
278
285
  return "view";
279
286
  }
280
287
  }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Scope card projection (P1b — consumed by ref.layout.getAllScopeCardModels)
291
+ // ---------------------------------------------------------------------------
292
+
293
+ export interface AttachScopeCardModelInput {
294
+ /**
295
+ * Segments produced by `collectScopeRailSegments`. The card model
296
+ * keys off segments so per-scope card state stays aligned with the
297
+ * rail stripe rendering.
298
+ */
299
+ segments: readonly ScopeRailSegment[];
300
+ /**
301
+ * Original workflow scopes — used to resolve `workItemId` for each
302
+ * segment (the segment shape predates workItemId passthrough).
303
+ * Optional; when absent, cards drop `workItemId` and issue matching
304
+ * falls back to scope-id only.
305
+ */
306
+ scopes?: readonly WorkflowScope[];
307
+ /**
308
+ * Workflow metadata markup from `WorkflowMarkupSnapshot.metadata`.
309
+ * Entries with `metadataId === ISSUE_METADATA_ID` that match a
310
+ * segment's `workItemId` or `scopeId` attach as the card's issue.
311
+ */
312
+ metadata?: readonly WorkflowMetadataMarkup[];
313
+ /**
314
+ * Optional anchor index used to resolve each card's
315
+ * `primaryAnchorRect`. When omitted, cards are produced without
316
+ * rects — chrome consumers fall back to on-render positioning.
317
+ */
318
+ anchorIndex?: RenderAnchorIndex | null;
319
+ }
320
+
321
+ /**
322
+ * Project rail segments into per-scope `ScopeCardModel` values.
323
+ *
324
+ * For each unique `scopeId` in the segment list:
325
+ * - use the first segment (document order) as the card's anchor
326
+ * - look up an `ISSUE_METADATA_ID` metadata entry whose
327
+ * `workItemId` (preferred) or `scopeId` matches the segment
328
+ * - coerce that entry's `value` into `IssueMetadataValue` when the
329
+ * required shape is present; drop the issue silently otherwise
330
+ * so malformed host input never breaks chrome rendering
331
+ *
332
+ * P2 fields (`suggestionGroupIds`, `reviewActionCount`,
333
+ * `agentPending`) are populated as empty defaults and wired in a
334
+ * later phase.
335
+ */
336
+ export function attachScopeCardModel(
337
+ input: AttachScopeCardModelInput,
338
+ ): ScopeCardModel[] {
339
+ if (input.segments.length === 0) return [];
340
+
341
+ // Take the first segment per scopeId (document order preserved by
342
+ // the segment collector).
343
+ const firstByScope = new Map<string, ScopeRailSegment>();
344
+ for (const segment of input.segments) {
345
+ if (!firstByScope.has(segment.scopeId)) {
346
+ firstByScope.set(segment.scopeId, segment);
347
+ }
348
+ }
349
+
350
+ const workItemByScope = new Map<string, string>();
351
+ for (const scope of input.scopes ?? []) {
352
+ if (scope.workItemId) {
353
+ workItemByScope.set(scope.scopeId, scope.workItemId);
354
+ }
355
+ }
356
+
357
+ const models: ScopeCardModel[] = [];
358
+ for (const segment of firstByScope.values()) {
359
+ const workItemId = workItemByScope.get(segment.scopeId);
360
+ const issue = resolveIssueForScope(
361
+ segment.scopeId,
362
+ workItemId,
363
+ input.metadata,
364
+ );
365
+ const primaryAnchorRect = input.anchorIndex
366
+ ? input.anchorIndex.bySelection(segment.fromOffset, segment.toOffset)
367
+ : null;
368
+
369
+ models.push({
370
+ scopeId: segment.scopeId,
371
+ ...(workItemId ? { workItemId } : {}),
372
+ label: segment.label ?? "",
373
+ posture: segment.posture,
374
+ primaryAnchorRect,
375
+ ...(issue ? { issue } : {}),
376
+ suggestionGroupIds: [],
377
+ reviewActionCount: 0,
378
+ agentPending: false,
379
+ });
380
+ }
381
+
382
+ return models;
383
+ }
384
+
385
+ function resolveIssueForScope(
386
+ scopeId: string,
387
+ workItemId: string | undefined,
388
+ metadata: readonly WorkflowMetadataMarkup[] | undefined,
389
+ ): IssueMetadataValue | undefined {
390
+ if (!metadata || metadata.length === 0) return undefined;
391
+ for (const entry of metadata) {
392
+ if (entry.metadataId !== ISSUE_METADATA_ID) continue;
393
+ const matchesWorkItem =
394
+ workItemId !== undefined &&
395
+ entry.workItemId !== undefined &&
396
+ entry.workItemId === workItemId;
397
+ const matchesScope =
398
+ entry.scopeId !== undefined && entry.scopeId === scopeId;
399
+ if (!matchesWorkItem && !matchesScope) continue;
400
+ const coerced = coerceIssueValue(entry.value);
401
+ if (coerced) return coerced;
402
+ }
403
+ return undefined;
404
+ }
405
+
406
+ /**
407
+ * Validate that a host-supplied metadata value looks like an
408
+ * `IssueMetadataValue`. Guards against malformed input — the card
409
+ * reads strongly-typed fields, so a missing severity or title must
410
+ * drop the issue rather than render `undefined` in the UI.
411
+ */
412
+ function coerceIssueValue(
413
+ value: unknown,
414
+ ): IssueMetadataValue | undefined {
415
+ if (!value || typeof value !== "object") return undefined;
416
+ const candidate = value as Partial<IssueMetadataValue>;
417
+ if (
418
+ typeof candidate.issueId !== "string" ||
419
+ typeof candidate.topic !== "string" ||
420
+ typeof candidate.severity !== "string" ||
421
+ typeof candidate.mode !== "string" ||
422
+ typeof candidate.checklistState !== "string" ||
423
+ typeof candidate.title !== "string"
424
+ ) {
425
+ return undefined;
426
+ }
427
+ return candidate as IssueMetadataValue;
428
+ }
@@ -50,6 +50,11 @@ import type {
50
50
  SurfaceInlineSegment,
51
51
  TableOp,
52
52
  TableOpResult,
53
+ PublicTableEvent,
54
+ PublicTableRenderPlan,
55
+ PublicTableRowHeight,
56
+ PublicTableStyle,
57
+ PublicTableSummary,
53
58
  TrackedChangeEntrySnapshot,
54
59
  TocRefreshResult,
55
60
  UpdateFieldsResult,
@@ -201,7 +206,12 @@ import {
201
206
  resolveChromePreset,
202
207
  resolveChromeVisibilityForPreset,
203
208
  } from "../ui-tailwind/chrome/chrome-preset-model.ts";
204
- 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";
205
215
 
206
216
  export {
207
217
  __createFallbackRuntime,
@@ -508,6 +518,12 @@ export function __createWordReviewEditorRefBridge(
508
518
  setZoom: (level) => {
509
519
  runtime.setZoom(level);
510
520
  },
521
+ setEditorRole: (role) => {
522
+ runtime.setEditorRole(role);
523
+ },
524
+ setChromePin: (surface, pin) => {
525
+ runtime.setChromePin(surface, pin);
526
+ },
511
527
  insertSectionBreak: (type, options) => {
512
528
  applyRuntimeInsertSectionBreak(runtime, type, options);
513
529
  },
@@ -642,6 +658,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
642
658
  onError,
643
659
  onEvent,
644
660
  onWarning,
661
+ onReviewSidebarTrackedChanges,
662
+ onReviewSidebarComments,
645
663
  readOnly = false,
646
664
  reviewMode = "review",
647
665
  suggestionsEnabled = false,
@@ -673,6 +691,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
673
691
  runtime,
674
692
  loadError,
675
693
  activeRuntime,
694
+ commandAppliedBridge,
676
695
  fallbackSnapshot,
677
696
  loadingSessionState,
678
697
  loadingViewState,
@@ -993,9 +1012,48 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
993
1012
 
994
1013
  useEffect(() => {
995
1014
  if (!ydoc || !runtime) return;
996
- const handle = createCollabReviewSync(ydoc, runtime);
1015
+ const handle = createRuntimeCollabSync({
1016
+ ydoc,
1017
+ runtime,
1018
+ authorId: currentUser.userId,
1019
+ commandAppliedBridge,
1020
+ });
997
1021
  return () => handle.destroy();
998
- }, [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
+ ]);
999
1057
 
1000
1058
  useEffect(() => {
1001
1059
  runtimeViewStateSeedRef.current = {
@@ -1331,6 +1389,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1331
1389
  setZoom: (level) => {
1332
1390
  activeRuntime.setZoom(level);
1333
1391
  },
1392
+ setEditorRole: (role) => {
1393
+ activeRuntime.setEditorRole(role);
1394
+ },
1395
+ setChromePin: (surface, pin) => {
1396
+ activeRuntime.setChromePin(surface, pin);
1397
+ },
1334
1398
  insertSectionBreak: (type, options) => {
1335
1399
  applyRuntimeInsertSectionBreak(activeRuntime, type, options);
1336
1400
  },
@@ -2188,6 +2252,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2188
2252
  openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header"),
2189
2253
  onOpenFooterStory: () =>
2190
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"),
2191
2259
  onDeleteSectionBreak: (sectionIndex) =>
2192
2260
  applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
2193
2261
  onUpdateSectionLayout: (sectionIndex, patch) =>
@@ -2235,7 +2303,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2235
2303
  <EditorSurfaceController
2236
2304
  ref={surfaceRef}
2237
2305
  currentUser={currentUser}
2238
- ydoc={ydoc}
2239
2306
  awareness={awareness}
2240
2307
  snapshot={snapshot}
2241
2308
  canonicalDocument={canonicalDocument}
@@ -2261,6 +2328,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2261
2328
  dispatchRuntimeCommand={(command) =>
2262
2329
  activeRuntime.applyActiveStoryTextCommand(command as never)
2263
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
+ }
2264
2341
  onCommentActivated={(commentId) => {
2265
2342
  activeRuntime.openComment(commentId);
2266
2343
  setActiveRailTab("comments");
@@ -2354,6 +2431,25 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2354
2431
  selectionToolbarRef={selectionToolbarElementRef}
2355
2432
  commands={commands}
2356
2433
  document={documentElement}
2434
+ onReviewSidebarTrackedChanges={onReviewSidebarTrackedChanges}
2435
+ onReviewSidebarComments={onReviewSidebarComments}
2436
+ onScopeModeChangeRequested={(payload) => {
2437
+ onEventRef.current?.({
2438
+ type: "scope-mode-change-requested",
2439
+ documentId,
2440
+ scopeId: payload.scopeId,
2441
+ mode: payload.mode,
2442
+ });
2443
+ }}
2444
+ onScopeIssueActionRequested={(payload) => {
2445
+ onEventRef.current?.({
2446
+ type: "scope-issue-action-requested",
2447
+ documentId,
2448
+ scopeId: payload.scopeId,
2449
+ issueId: payload.issueId,
2450
+ action: payload.action,
2451
+ });
2452
+ }}
2357
2453
  />
2358
2454
  );
2359
2455
  },
@@ -3453,7 +3549,7 @@ function buildTablesFacet(
3453
3549
  ) {
3454
3550
  const getCapabilities = () => {
3455
3551
  const snapshot = runtime.getRenderSnapshot();
3456
- const document = runtime.getSessionState().canonicalDocument;
3552
+ const document = runtime.getCanonicalDocument();
3457
3553
  return (
3458
3554
  clonePublicValue(
3459
3555
  getTableStructureContext(
@@ -3464,6 +3560,178 @@ function buildTablesFacet(
3464
3560
  ) ?? null
3465
3561
  );
3466
3562
  };
3563
+
3564
+ const buildSummary = (
3565
+ table: Extract<ReturnType<typeof runtime.getCanonicalDocument>["content"]["children"][number], { type: "table" }>,
3566
+ tableBlockIndex: number,
3567
+ ): PublicTableSummary => {
3568
+ const blockId = `table-${tableBlockIndex}`;
3569
+ const hasVerticalMerges = table.rows.some((row) =>
3570
+ row.cells.some((cell) => cell.verticalMerge !== undefined),
3571
+ );
3572
+ const hasHorizontalSpans = table.rows.some((row) =>
3573
+ row.cells.some((cell) => (cell.gridSpan ?? 1) > 1),
3574
+ );
3575
+ return {
3576
+ tableBlockIndex,
3577
+ blockId,
3578
+ styleId: table.styleId ?? null,
3579
+ rowCount: table.rows.length,
3580
+ columnCount: table.gridColumns.length,
3581
+ gridColumnsTwips: table.gridColumns,
3582
+ alignment: table.alignment ?? null,
3583
+ hasVerticalMerges,
3584
+ hasHorizontalSpans,
3585
+ pageIndex: runtime.layout.getFirstPageIndexForBlock(blockId) ?? null,
3586
+ };
3587
+ };
3588
+
3589
+ const getTables = (options?: { sectionIndex?: number }): PublicTableSummary[] => {
3590
+ const document = runtime.getCanonicalDocument();
3591
+ const summaries: PublicTableSummary[] = [];
3592
+ let idx = 0;
3593
+ for (const block of document.content.children) {
3594
+ if (block.type === "table") {
3595
+ summaries.push(clonePublicValue(buildSummary(block, idx)));
3596
+ idx++;
3597
+ }
3598
+ }
3599
+ if (options?.sectionIndex != null) {
3600
+ const section = runtime.layout.getSection(options.sectionIndex);
3601
+ if (!section) return [];
3602
+ return summaries.filter(
3603
+ (s) =>
3604
+ s.pageIndex != null &&
3605
+ s.pageIndex >= section.firstPageIndex &&
3606
+ s.pageIndex <= section.lastPageIndex,
3607
+ );
3608
+ }
3609
+ return summaries;
3610
+ };
3611
+
3612
+ const getTable = (tableBlockIndex: number): PublicTableSummary | null => {
3613
+ const document = runtime.getCanonicalDocument();
3614
+ let idx = 0;
3615
+ for (const block of document.content.children) {
3616
+ if (block.type === "table") {
3617
+ if (idx === tableBlockIndex) {
3618
+ return clonePublicValue(buildSummary(block, idx));
3619
+ }
3620
+ idx++;
3621
+ }
3622
+ }
3623
+ return null;
3624
+ };
3625
+
3626
+ const getTableForSelection = (): PublicTableSummary | null => {
3627
+ const sel = mountedSurface?.getTableSelection() ?? null;
3628
+ if (!sel) return null;
3629
+ return getTable(sel.tableBlockIndex);
3630
+ };
3631
+
3632
+ const getRenderPlan = (
3633
+ tableBlockIndex: number,
3634
+ pageIndex?: number,
3635
+ ): PublicTableRenderPlan | null => {
3636
+ const blockId = `table-${tableBlockIndex}`;
3637
+ const effectivePage =
3638
+ pageIndex ??
3639
+ runtime.layout.getFirstPageIndexForBlock(blockId) ??
3640
+ 0;
3641
+ const plan = runtime.layout.getTableRenderPlan(blockId, effectivePage);
3642
+ if (!plan) return null;
3643
+ return clonePublicValue({
3644
+ blockId: plan.blockId,
3645
+ pageIndex: plan.pageIndex,
3646
+ columnsTwips: plan.columnsTwips,
3647
+ bandClasses: plan.bandClasses,
3648
+ verticalMerges: plan.verticalMerges,
3649
+ repeatedHeaderRows: plan.repeatedHeaderRows,
3650
+ columnResizeHandles: plan.columnResizeHandles,
3651
+ } satisfies PublicTableRenderPlan);
3652
+ };
3653
+
3654
+ const getColumnWidths = (tableBlockIndex: number): readonly number[] => {
3655
+ const document = runtime.getCanonicalDocument();
3656
+ let idx = 0;
3657
+ for (const block of document.content.children) {
3658
+ if (block.type === "table") {
3659
+ if (idx === tableBlockIndex) {
3660
+ return block.gridColumns;
3661
+ }
3662
+ idx++;
3663
+ }
3664
+ }
3665
+ return [];
3666
+ };
3667
+
3668
+ const getRowHeights = (tableBlockIndex: number): readonly PublicTableRowHeight[] => {
3669
+ const document = runtime.getCanonicalDocument();
3670
+ let idx = 0;
3671
+ for (const block of document.content.children) {
3672
+ if (block.type === "table") {
3673
+ if (idx === tableBlockIndex) {
3674
+ const blockId = `table-${tableBlockIndex}`;
3675
+ const measurement = runtime.layout.getMeasurement(blockId);
3676
+ const totalMeasured = measurement?.heightTwips ?? 0;
3677
+ const rows = block.rows;
3678
+ const perRowFallback = rows.length > 0 ? totalMeasured / rows.length : 0;
3679
+ return rows.map((row): PublicTableRowHeight => ({
3680
+ measured: perRowFallback,
3681
+ ...(row.height != null ? { explicit: row.height } : {}),
3682
+ ...(row.heightRule != null ? { rule: row.heightRule } : {}),
3683
+ isHeader: row.isHeader ?? false,
3684
+ }));
3685
+ }
3686
+ idx++;
3687
+ }
3688
+ }
3689
+ return [];
3690
+ };
3691
+
3692
+ const getStyleCatalog = (): readonly PublicTableStyle[] => {
3693
+ const document = runtime.getCanonicalDocument();
3694
+ return Object.values(document.styles.tables).map(
3695
+ (style): PublicTableStyle =>
3696
+ clonePublicValue({
3697
+ styleId: style.styleId,
3698
+ displayName: style.displayName,
3699
+ ...(style.basedOn != null ? { basedOn: style.basedOn } : {}),
3700
+ isDefault: style.isDefault,
3701
+ }),
3702
+ );
3703
+ };
3704
+
3705
+ const subscribe = (
3706
+ listener: (event: PublicTableEvent) => void,
3707
+ ): (() => void) => {
3708
+ const unsubRuntime = runtime.subscribeToEvents((event) => {
3709
+ if (event.type === "dirty_changed" && event.isDirty) {
3710
+ listener({
3711
+ kind: "table_structure_changed",
3712
+ revisionToken: runtime.getRenderSnapshot().revisionToken,
3713
+ });
3714
+ } else if (event.type === "selection_changed") {
3715
+ listener({ kind: "table_capabilities_changed" });
3716
+ }
3717
+ });
3718
+ const unsubLayout = runtime.layout.subscribe((event) => {
3719
+ if (
3720
+ event.kind === "layout_recomputed" ||
3721
+ event.kind === "incremental_relayout"
3722
+ ) {
3723
+ listener({
3724
+ kind: "table_render_plan_ready",
3725
+ revision: event.revision,
3726
+ });
3727
+ }
3728
+ });
3729
+ return () => {
3730
+ unsubRuntime();
3731
+ unsubLayout();
3732
+ };
3733
+ };
3734
+
3467
3735
  return {
3468
3736
  apply(op: TableOp): TableOpResult {
3469
3737
  if (op.kind === "insert") {
@@ -3487,9 +3755,19 @@ function buildTablesFacet(
3487
3755
  };
3488
3756
  },
3489
3757
  getCapabilities,
3758
+ getTables,
3759
+ getTable,
3760
+ getTableForSelection,
3761
+ getRenderPlan,
3762
+ getColumnWidths,
3763
+ getRowHeights,
3764
+ getStyleCatalog,
3765
+ subscribe,
3490
3766
  };
3491
3767
  }
3492
3768
 
3769
+ export { buildTablesFacet as __buildTablesFacet };
3770
+
3493
3771
  function applyRuntimeTextCommand(
3494
3772
  runtime: WordReviewEditorRuntime,
3495
3773
  command:
@@ -3982,6 +4260,25 @@ function clonePublicValue<T>(value: T): T {
3982
4260
  return structuredClone(value);
3983
4261
  }
3984
4262
 
4263
+ /**
4264
+ * Open the correct header/footer story for a specific page. The page's
4265
+ * resolved `stories.header` / `stories.footer` already carries the
4266
+ * right variant (default / first / even) for that page's section, so we
4267
+ * can hand it directly to `runtime.openStory()` without re-running the
4268
+ * variant-pick logic.
4269
+ */
4270
+ function openStoryForPage(
4271
+ runtime: WordReviewEditorRuntime,
4272
+ pageIndex: number,
4273
+ kind: "header" | "footer",
4274
+ ): void {
4275
+ const page = runtime.layout?.getPage(pageIndex);
4276
+ if (!page) return;
4277
+ const target = kind === "header" ? page.stories.header : page.stories.footer;
4278
+ if (!target) return;
4279
+ runtime.openStory(target);
4280
+ }
4281
+
3985
4282
  function openDefaultStoryVariant(
3986
4283
  runtime: WordReviewEditorRuntime,
3987
4284
  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,
@@ -83,6 +83,28 @@ export interface EditorShellViewProps {
83
83
  onSelectionToolbarBlurCapture?: React.FocusEventHandler<HTMLDivElement>;
84
84
  selectionToolbarRef?: React.Ref<HTMLDivElement>;
85
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;
90
+ /**
91
+ * Scope card mode selector fired a mode change (forwarded from the
92
+ * workspace). The editor turns this into a
93
+ * `scope-mode-change-requested` event.
94
+ */
95
+ onScopeModeChangeRequested?: (payload: {
96
+ scopeId: string;
97
+ mode: import("../api/public-types.ts").WorkflowScopeMode;
98
+ }) => void;
99
+ /**
100
+ * Scope card issue action fired (forwarded from the workspace).
101
+ * The editor turns this into a `scope-issue-action-requested` event.
102
+ */
103
+ onScopeIssueActionRequested?: (payload: {
104
+ scopeId: string;
105
+ issueId: string;
106
+ action: import("../api/public-types.ts").ScopeIssueAction;
107
+ }) => void;
86
108
  }
87
109
 
88
110
  export function EditorShellView(props: EditorShellViewProps) {