@beyondwork/docx-react-component 1.0.38 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -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,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2354
2431
  selectionToolbarRef={selectionToolbarElementRef}
2355
2432
  commands={commands}
2356
2433
  document={documentElement}
2434
+ onReviewSidebarTrackedChanges={onReviewSidebarTrackedChanges}
2435
+ onReviewSidebarComments={onReviewSidebarComments}
2357
2436
  />
2358
2437
  );
2359
2438
  },
@@ -3453,7 +3532,7 @@ function buildTablesFacet(
3453
3532
  ) {
3454
3533
  const getCapabilities = () => {
3455
3534
  const snapshot = runtime.getRenderSnapshot();
3456
- const document = runtime.getSessionState().canonicalDocument;
3535
+ const document = runtime.getCanonicalDocument();
3457
3536
  return (
3458
3537
  clonePublicValue(
3459
3538
  getTableStructureContext(
@@ -3464,6 +3543,178 @@ function buildTablesFacet(
3464
3543
  ) ?? null
3465
3544
  );
3466
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
+
3467
3718
  return {
3468
3719
  apply(op: TableOp): TableOpResult {
3469
3720
  if (op.kind === "insert") {
@@ -3487,9 +3738,19 @@ function buildTablesFacet(
3487
3738
  };
3488
3739
  },
3489
3740
  getCapabilities,
3741
+ getTables,
3742
+ getTable,
3743
+ getTableForSelection,
3744
+ getRenderPlan,
3745
+ getColumnWidths,
3746
+ getRowHeights,
3747
+ getStyleCatalog,
3748
+ subscribe,
3490
3749
  };
3491
3750
  }
3492
3751
 
3752
+ export { buildTablesFacet as __buildTablesFacet };
3753
+
3493
3754
  function applyRuntimeTextCommand(
3494
3755
  runtime: WordReviewEditorRuntime,
3495
3756
  command:
@@ -3982,6 +4243,25 @@ function clonePublicValue<T>(value: T): T {
3982
4243
  return structuredClone(value);
3983
4244
  }
3984
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
+
3985
4265
  function openDefaultStoryVariant(
3986
4266
  runtime: WordReviewEditorRuntime,
3987
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,
@@ -83,6 +83,10 @@ 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;
86
90
  }
87
91
 
88
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<
@@ -31,6 +31,8 @@ export type ToolbarChromeItemId =
31
31
  | "export"
32
32
  // R1: role-scoped action region entries
33
33
  | "editor-scope-posture-menu"
34
+ | "review-sidebar-tracked-changes"
35
+ | "review-sidebar-comments"
34
36
  | "review-queue-prev"
35
37
  | "review-queue-next"
36
38
  | "review-queue-counts"
@@ -72,7 +74,7 @@ export interface ToolbarChromeRegistryEntry extends ChromeRegistryEntryBase {
72
74
  roles: ReadonlyArray<EditorRole>;
73
75
  fullPlacement: Exclude<ToolbarChromePlacement, "hidden">;
74
76
  compactPlacement: ToolbarChromePlacement;
75
- runtimeBehavior: "always" | "formatting" | "structure" | "comment";
77
+ runtimeBehavior: "always" | "formatting" | "structure" | "comment" | "sidebar-panel";
76
78
  scopeBehavior?: "default" | "scoped-only" | "hidden-when-scoped";
77
79
  /**
78
80
  * Optional per-role placement override. When a role overrides the
@@ -265,6 +267,9 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
265
267
  surfaces: ["top-toolbar"],
266
268
  group: "review",
267
269
  presets: ["simple", "advanced", "review", "workflow"],
270
+ // Visible in every role, but editor/review roles render it inside the
271
+ // role action region (see ROLE_ACTION_SETS) and the right cluster
272
+ // suppresses it via `isChromeItemOwnedByRoleRegion` to avoid duplication.
268
273
  roles: ALL_ROLES,
269
274
  fullPlacement: "inline",
270
275
  compactPlacement: "inline",
@@ -274,9 +279,8 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
274
279
  id: "tracked-changes-toggle",
275
280
  surfaces: ["top-toolbar"],
276
281
  group: "review",
277
- // R1 promotes this toggle to every preset that shows a toolbar so the
278
- // editor role can see inline tracked changes without switching preset.
279
282
  presets: ["simple", "advanced", "review", "workflow"],
283
+ // Same ownership rule as `comment` — editor/review role regions own it.
280
284
  roles: ALL_ROLES,
281
285
  fullPlacement: "inline",
282
286
  compactPlacement: "inline",
@@ -323,18 +327,43 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
323
327
  runtimeBehavior: "always",
324
328
  },
325
329
 
326
- // ───── R1: Editor-role primaries ──────────────────────────────────────
330
+ // ───── R1: Workflow-role scope posture menu ───────────────────────────
327
331
  {
328
332
  id: "editor-scope-posture-menu",
329
333
  surfaces: ["top-toolbar"],
330
334
  group: "scope",
331
335
  presets: ["advanced", "review", "workflow"],
332
- roles: EDITOR_ONLY,
336
+ // Scoping/tagging is a workflow-primary action; moved from editor role.
337
+ roles: WORKFLOW_ONLY,
333
338
  fullPlacement: "inline",
334
339
  compactPlacement: "overflow",
335
340
  runtimeBehavior: "always",
336
341
  },
337
342
 
343
+ // ───── R1: Review-role sidebar panel toggles (optional, host-provided) ─
344
+ {
345
+ id: "review-sidebar-tracked-changes",
346
+ surfaces: ["top-toolbar"],
347
+ group: "review-sidebar",
348
+ presets: ["review"],
349
+ roles: REVIEW_ONLY,
350
+ fullPlacement: "inline",
351
+ compactPlacement: "inline",
352
+ // Hidden unless the host provides the sidebar panel callback via
353
+ // hasSidebarPanelAccess in ResolveScopedChromePolicyInput.
354
+ runtimeBehavior: "sidebar-panel",
355
+ },
356
+ {
357
+ id: "review-sidebar-comments",
358
+ surfaces: ["top-toolbar"],
359
+ group: "review-sidebar",
360
+ presets: ["review"],
361
+ roles: REVIEW_ONLY,
362
+ fullPlacement: "inline",
363
+ compactPlacement: "inline",
364
+ runtimeBehavior: "sidebar-panel",
365
+ },
366
+
338
367
  // ───── R1: Review-role primaries ──────────────────────────────────────
339
368
  {
340
369
  id: "review-queue-prev",
@@ -11,6 +11,7 @@ import {
11
11
  type ToolbarChromeItemId,
12
12
  type ToolbarChromePlacement,
13
13
  } from "./chrome-registry";
14
+ import { ROLE_ACTION_SETS } from "../../ui-tailwind/chrome/role-action-sets";
14
15
  import type { SelectionToolKind } from "./selection-tool-types";
15
16
 
16
17
  export interface ToolbarChromeItemPolicy {
@@ -44,6 +45,13 @@ export interface ResolveScopedChromePolicyInput {
44
45
  * callers that haven't adopted the role model yet).
45
46
  */
46
47
  role?: EditorRole;
48
+ /**
49
+ * Whether the host has wired sidebar-panel callbacks
50
+ * (`onReviewSidebarTrackedChanges` / `onReviewSidebarComments`).
51
+ * Defaults to `false` — sidebar panel buttons are hidden unless the host
52
+ * explicitly opts in (typically the harness).
53
+ */
54
+ hasSidebarPanelAccess?: boolean;
47
55
  }
48
56
 
49
57
  export function resolveScopedChromePolicy(
@@ -88,6 +96,9 @@ export function resolveScopedChromePolicy(
88
96
  case "comment":
89
97
  visible = canAddComment;
90
98
  break;
99
+ case "sidebar-panel":
100
+ visible = Boolean(input.hasSidebarPanelAccess);
101
+ break;
91
102
  default:
92
103
  visible = true;
93
104
  break;
@@ -152,6 +163,24 @@ export function isToolbarChromeItemVisible(
152
163
  return policy.toolbar[itemId]?.visible ?? false;
153
164
  }
154
165
 
166
+ /**
167
+ * Returns true when the given chrome item is owned by the active role's
168
+ * inline role-action region (i.e. it appears in `ROLE_ACTION_SETS[role]`).
169
+ * The right-cluster render path uses this to suppress its own copy so we
170
+ * don't render the same button twice.
171
+ *
172
+ * When `role` is undefined (legacy host, no role model adopted) this
173
+ * returns `false` — the right cluster keeps ownership per the pre-R1
174
+ * layout.
175
+ */
176
+ export function isChromeItemOwnedByRoleRegion(
177
+ itemId: ToolbarChromeItemId,
178
+ role: EditorRole | undefined,
179
+ ): boolean {
180
+ if (!role) return false;
181
+ return ROLE_ACTION_SETS[role].includes(itemId);
182
+ }
183
+
155
184
  export function getToolbarChromePlacement(
156
185
  policy: ScopedChromePolicy,
157
186
  itemId: ToolbarChromeItemId,