@beyondwork/docx-react-component 1.0.28 → 1.0.30

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 (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -27,11 +27,15 @@ import type {
27
27
  FormattingAlignment,
28
28
  FormattingStateSnapshot,
29
29
  HeaderFooterLinkPatch,
30
+ HostAnnotationItem,
31
+ HostAnnotationOverlay,
30
32
  InteractionGuardSnapshot,
31
33
  InsertImageOptions,
32
34
  InsertTableOptions,
33
35
  PageLayoutSnapshot,
34
36
  PersistedEditorSnapshot,
37
+ ReviewQueueItem,
38
+ ReviewQueueSnapshot,
35
39
  RuntimeRenderSnapshot,
36
40
  SectionBreakType,
37
41
  SectionLayoutPatch,
@@ -39,8 +43,11 @@ import type {
39
43
  SearchOptions,
40
44
  SearchResultSnapshot,
41
45
  SelectionSnapshot as PublicSelectionSnapshot,
46
+ SuggestionEntrySnapshot,
47
+ SuggestionsSnapshot,
42
48
  StyleCatalogSnapshot,
43
49
  SurfaceBlockSnapshot,
50
+ SurfaceInlineSegment,
44
51
  TrackedChangeEntrySnapshot,
45
52
  TocRefreshResult,
46
53
  UpdateFieldsResult,
@@ -48,6 +55,8 @@ import type {
48
55
  WorkflowBlockedCommandReason,
49
56
  WorkflowMarkupSnapshot,
50
57
  WorkflowScopeSnapshot,
58
+ WordReviewEditorChromeOptions,
59
+ WordReviewEditorChromePreset,
51
60
  WordReviewEditorChromeVisibility,
52
61
  WordReviewEditorEvent,
53
62
  WordReviewEditorProps,
@@ -81,6 +90,8 @@ import {
81
90
  outdentListItems,
82
91
  restartNumbering as restartListNumbering,
83
92
  splitListParagraph,
93
+ toggleBulletedList,
94
+ toggleNumberedList,
84
95
  } from "../core/commands/list-commands.ts";
85
96
  import {
86
97
  resolveActiveParagraphIndex,
@@ -101,6 +112,7 @@ import {
101
112
  } from "../core/commands/image-commands.ts";
102
113
  import {
103
114
  applyTableStructureOperation,
115
+ getTableStructureContext,
104
116
  } from "../core/commands/table-structure-commands.ts";
105
117
  import {
106
118
  deleteSelectionOrBackward,
@@ -124,6 +136,10 @@ import {
124
136
  import { readOpcPackage } from "../io/opc/package-reader.ts";
125
137
  import { deriveCapabilities } from "../runtime/session-capabilities";
126
138
  import { searchDocument } from "../runtime/document-search.ts";
139
+ import {
140
+ resolveCurrentContextAnalyticsQuery,
141
+ runtimeContextAnalyticsSnapshotsEqual,
142
+ } from "../runtime/context-analytics.ts";
127
143
  import {
128
144
  createEditorViewStateSnapshot,
129
145
  createViewState,
@@ -154,7 +170,12 @@ import type {
154
170
  SelectionToolbarModel,
155
171
  SuggestionCardModel,
156
172
  } from "./headless/selection-toolbar-model";
173
+ import { resolveActiveSelectionTool } from "./headless/selection-tool-resolver";
157
174
  import { type EditorCommandBag, useCommandBag } from "./editor-command-bag.ts";
175
+ import {
176
+ resolveHeadingShortcutStyleId,
177
+ resolveShellShortcut,
178
+ } from "./runtime-shortcut-dispatch";
158
179
  import { deriveVisibleWorkflowBlockedRails } from "./workflow-surface-blocked-rails.ts";
159
180
  import {
160
181
  type WordReviewEditorRuntime,
@@ -169,6 +190,10 @@ import {
169
190
  } from "./browser-export";
170
191
  import { EditorShellView } from "./editor-shell-view.tsx";
171
192
  import { EditorSurfaceController } from "./editor-surface-controller.tsx";
193
+ import {
194
+ resolveChromePreset,
195
+ resolveChromeVisibilityForPreset,
196
+ } from "../ui-tailwind/chrome/chrome-preset-model.ts";
172
197
 
173
198
  export {
174
199
  __createFallbackRuntime,
@@ -244,12 +269,20 @@ export function __createWordReviewEditorRefBridge(
244
269
  clonePublicValue(runtime.getRenderSnapshot().comments),
245
270
  getTrackedChangesSnapshot: () =>
246
271
  clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
272
+ getSuggestionsSnapshot: () =>
273
+ clonePublicValue(runtime.getSuggestionsSnapshot()),
247
274
  getComments: () => clonePublicValue(runtime.getRenderSnapshot().comments),
248
275
  getTrackedChanges: () =>
249
276
  clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
250
277
  isDirty: () => runtime.getRenderSnapshot().isDirty,
251
278
  getFormattingState: () => getFormattingStateFromRenderSnapshot(runtime.getRenderSnapshot()),
252
279
  getStyleCatalog: () => getRuntimeStyleCatalog(runtime),
280
+ toggleBulletedList: () => {
281
+ applyRuntimeListToggle(runtime, "bulleted");
282
+ },
283
+ toggleNumberedList: () => {
284
+ applyRuntimeListToggle(runtime, "numbered");
285
+ },
253
286
  toggleBold: () => {
254
287
  applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "bold" });
255
288
  },
@@ -310,6 +343,12 @@ export function __createWordReviewEditorRefBridge(
310
343
  outdent: () => {
311
344
  applyRuntimeFormattingOperation(runtime, { type: "outdent" });
312
345
  },
346
+ restartNumbering: () => {
347
+ applyRuntimeNumberingFlow(runtime, { type: "restart" });
348
+ },
349
+ continueNumbering: () => {
350
+ applyRuntimeNumberingFlow(runtime, { type: "continue" });
351
+ },
313
352
  insertPageBreak: () => {
314
353
  applyRuntimeInsertPageBreak(runtime);
315
354
  },
@@ -407,6 +446,36 @@ export function __createWordReviewEditorRefBridge(
407
446
  getDocumentNavigationSnapshot: () => {
408
447
  return clonePublicValue(runtime.getDocumentNavigationSnapshot());
409
448
  },
449
+ getCurrentLocation: () => {
450
+ return clonePublicValue(runtime.getCurrentLocation());
451
+ },
452
+ getLocationForSelection: (selection) => {
453
+ return clonePublicValue(runtime.getLocationForSelection(selection));
454
+ },
455
+ getLocationForAnchor: (anchor, storyTarget) => {
456
+ return clonePublicValue(runtime.getLocationForAnchor(anchor, storyTarget));
457
+ },
458
+ captureRestorePoint: (input) => {
459
+ return clonePublicValue(runtime.captureRestorePoint(input));
460
+ },
461
+ restoreToPoint: (point, options) => {
462
+ return clonePublicValue(runtime.restoreToPoint(point, options));
463
+ },
464
+ getOutlineSnapshot: () => {
465
+ return clonePublicValue(runtime.getOutlineSnapshot());
466
+ },
467
+ getTocSnapshot: () => {
468
+ return clonePublicValue(runtime.getTocSnapshot());
469
+ },
470
+ getSections: () => {
471
+ return clonePublicValue(runtime.getSections());
472
+ },
473
+ getSectionSnapshot: (input) => {
474
+ return clonePublicValue(runtime.getSectionSnapshot(input));
475
+ },
476
+ describeEventImpact: (event) => {
477
+ return clonePublicValue(runtime.describeEventImpact(event));
478
+ },
410
479
  getFieldSnapshot: () => {
411
480
  return clonePublicValue(runtime.getFieldSnapshot());
412
481
  },
@@ -467,6 +536,21 @@ export function __createWordReviewEditorRefBridge(
467
536
  getWorkflowMarkupSnapshot: () => {
468
537
  return clonePublicValue(runtime.getWorkflowMarkupSnapshot());
469
538
  },
539
+ setWorkflowMetadataDefinitions: (definitions) => {
540
+ runtime.setWorkflowMetadataDefinitions(clonePublicValue(definitions));
541
+ },
542
+ clearWorkflowMetadataDefinitions: () => {
543
+ runtime.clearWorkflowMetadataDefinitions();
544
+ },
545
+ setWorkflowMetadataEntries: (entries) => {
546
+ runtime.setWorkflowMetadataEntries(clonePublicValue(entries));
547
+ },
548
+ clearWorkflowMetadataEntries: () => {
549
+ runtime.clearWorkflowMetadataEntries();
550
+ },
551
+ getWorkflowMetadataSnapshot: () => {
552
+ return clonePublicValue(runtime.getWorkflowMetadataSnapshot());
553
+ },
470
554
  setHostAnnotationOverlay: (overlay) => {
471
555
  runtime.setHostAnnotationOverlay(clonePublicValue(overlay));
472
556
  },
@@ -476,6 +560,31 @@ export function __createWordReviewEditorRefBridge(
476
560
  getHostAnnotationSnapshot: () => {
477
561
  return clonePublicValue(runtime.getHostAnnotationSnapshot());
478
562
  },
563
+ getReviewQueueSnapshot: () => {
564
+ return clonePublicValue(
565
+ deriveReviewQueueSnapshot({
566
+ sections: runtime.getSections(),
567
+ comments: runtime.getRenderSnapshot().comments.threads,
568
+ trackedChanges: runtime.getRenderSnapshot().trackedChanges.revisions,
569
+ hostAnnotations: runtime.getHostAnnotationSnapshot().annotations,
570
+ }),
571
+ );
572
+ },
573
+ getRuntimeContextAnalytics: (query) => {
574
+ return clonePublicValue(runtime.getRuntimeContextAnalytics(query));
575
+ },
576
+ goToNextReviewItem: () => {
577
+ return clonePublicValue(navigateReviewQueue(runtime, "next"));
578
+ },
579
+ goToPreviousReviewItem: () => {
580
+ return clonePublicValue(navigateReviewQueue(runtime, "previous"));
581
+ },
582
+ markSectionForReview: (input) => {
583
+ return markSectionForReview(runtime, input);
584
+ },
585
+ clearSectionReviewMark: (annotationId) => {
586
+ clearSectionReviewMark(runtime, annotationId);
587
+ },
479
588
  getWorkflowCandidateRanges: (options) => {
480
589
  return clonePublicValue(runtime.getWorkflowCandidateRanges(options));
481
590
  },
@@ -514,7 +623,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
514
623
  initialSessionState,
515
624
  initialSnapshot,
516
625
  initialSourceLabel,
626
+ chromePreset,
627
+ chromeOptions,
517
628
  markupDisplay,
629
+ showUnsupportedObjectPreviews = false,
518
630
  onError,
519
631
  onEvent,
520
632
  onWarning,
@@ -532,6 +644,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
532
644
  const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
533
645
  const [selectionToolbarDismissedKey, setSelectionToolbarDismissedKey] = useState<string | null>(null);
534
646
  const [selectionToolbarFocusWithin, setSelectionToolbarFocusWithin] = useState(false);
647
+ const [hostAnnotationOverlayState, setHostAnnotationOverlayState] =
648
+ useState<HostAnnotationOverlay | null>(null);
649
+ const [activeReviewQueueItemId, setActiveReviewQueueItemId] = useState<string | null>(null);
650
+ const [reviewSectionMarks, setReviewSectionMarks] = useState<Array<{
651
+ annotationId: string;
652
+ sectionIndex: number;
653
+ label: string;
654
+ }>>([]);
535
655
  const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
536
656
  const selectionToolbarElementRef = useRef<HTMLDivElement | null>(null);
537
657
  const shellRef = useRef<HTMLDivElement | null>(null);
@@ -676,10 +796,75 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
676
796
  { effectiveMode: "edit", blockedReasons: [] } satisfies InteractionGuardSnapshot,
677
797
  interactionGuardSnapshotsEqual,
678
798
  );
799
+ const documentContextAnalytics = useRuntimeValue(
800
+ runtime
801
+ ? {
802
+ subscribe: (listener) => runtime.subscribe(listener),
803
+ getValue: () =>
804
+ runtime.getRuntimeContextAnalytics({
805
+ scopeKind: "document",
806
+ }),
807
+ }
808
+ : null,
809
+ null,
810
+ runtimeContextAnalyticsSnapshotsEqual,
811
+ );
812
+ const selectionContextAnalytics = useRuntimeValue(
813
+ runtime
814
+ ? {
815
+ subscribe: (listener) => runtime.subscribe(listener),
816
+ getValue: () => runtime.getRuntimeContextAnalytics(),
817
+ }
818
+ : null,
819
+ null,
820
+ runtimeContextAnalyticsSnapshotsEqual,
821
+ );
822
+ const currentScopeContextAnalytics = useRuntimeValue(
823
+ runtime
824
+ ? {
825
+ subscribe: (listener) => runtime.subscribe(listener),
826
+ getValue: () =>
827
+ runtime.getRuntimeContextAnalytics(
828
+ resolveCurrentContextAnalyticsQuery({
829
+ workflowScopeSnapshot: runtime.getWorkflowScopeSnapshot(),
830
+ interactionGuardSnapshot: runtime.getInteractionGuardSnapshot(),
831
+ }),
832
+ ),
833
+ }
834
+ : null,
835
+ null,
836
+ runtimeContextAnalyticsSnapshotsEqual,
837
+ );
679
838
  const workflowMarkupSnapshot = useMemo(
680
839
  () => (runtime ? runtime.getWorkflowMarkupSnapshot() : null),
681
840
  [runtime, snapshot.revisionToken],
682
841
  );
842
+ const sections = useMemo(
843
+ () => (runtime ? runtime.getSections() : []),
844
+ [runtime, snapshot.revisionToken],
845
+ );
846
+ const reviewQueueSnapshot = useMemo(
847
+ () =>
848
+ deriveReviewQueueSnapshot({
849
+ sections,
850
+ comments: snapshot.comments.threads,
851
+ trackedChanges: snapshot.trackedChanges.revisions,
852
+ hostAnnotations: mergeHostAndReviewSectionAnnotations(
853
+ hostAnnotationOverlayState?.annotations ?? [],
854
+ reviewSectionMarks,
855
+ sections,
856
+ ),
857
+ activeItemId: activeReviewQueueItemId,
858
+ }),
859
+ [
860
+ activeReviewQueueItemId,
861
+ hostAnnotationOverlayState,
862
+ reviewSectionMarks,
863
+ sections,
864
+ snapshot.comments.threads,
865
+ snapshot.trackedChanges.revisions,
866
+ ],
867
+ );
683
868
  const workflowBlockedRails = useMemo(
684
869
  () => deriveVisibleWorkflowBlockedRails(snapshot.surface, workflowMarkupSnapshot),
685
870
  [snapshot.surface, workflowMarkupSnapshot],
@@ -688,12 +873,104 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
688
873
  () => (runtime ? runtime.getCanonicalDocument() : loadingSessionState.canonicalDocument),
689
874
  [loadingSessionState.canonicalDocument, runtime, snapshot.revisionToken],
690
875
  );
876
+ const loadingSnapshot = useMemo(
877
+ () =>
878
+ persistedSnapshotFromEditorSessionState(loadingSessionState, {
879
+ savedAt: loadingSessionState.updatedAt,
880
+ }),
881
+ [loadingSessionState],
882
+ );
691
883
  const effectiveViewMode = deriveEditorViewMode(snapshot.readOnly, reviewMode);
692
884
 
885
+ useEffect(() => {
886
+ const mergedAnnotations = mergeHostAndReviewSectionAnnotations(
887
+ hostAnnotationOverlayState?.annotations ?? [],
888
+ reviewSectionMarks,
889
+ sections,
890
+ );
891
+ if (mergedAnnotations.length === 0) {
892
+ activeRuntime.clearHostAnnotationOverlay();
893
+ return;
894
+ }
895
+ activeRuntime.setHostAnnotationOverlay({
896
+ overlayVersion: "host-annotation-overlay/1",
897
+ annotations: mergedAnnotations,
898
+ });
899
+ }, [activeRuntime, hostAnnotationOverlayState, reviewSectionMarks, sections]);
900
+
693
901
  useEffect(() => {
694
902
  activeRuntime.setViewMode(effectiveViewMode);
695
903
  }, [activeRuntime, effectiveViewMode]);
696
904
 
905
+ const markCurrentSectionForReview = useCallback((input?: {
906
+ sectionIndex?: number;
907
+ label?: string;
908
+ }): string | null => {
909
+ const sectionIndex = input?.sectionIndex ?? documentNavigation.activeSectionIndex;
910
+ const targetSection = sections.find((section) => section.sectionIndex === sectionIndex);
911
+ if (!targetSection) {
912
+ return null;
913
+ }
914
+ const annotationId = createReviewSectionMarkId(targetSection.sectionIndex);
915
+ const label =
916
+ input?.label ??
917
+ targetSection.primaryHeadingText ??
918
+ `Section ${targetSection.sectionIndex + 1}`;
919
+ setReviewSectionMarks((current) => [
920
+ ...current.filter((entry) => entry.annotationId !== annotationId),
921
+ {
922
+ annotationId,
923
+ sectionIndex: targetSection.sectionIndex,
924
+ label,
925
+ },
926
+ ]);
927
+ setActiveReviewQueueItemId(`section:${annotationId}`);
928
+ return annotationId;
929
+ }, [documentNavigation.activeSectionIndex, sections]);
930
+
931
+ const clearReviewSectionMarkById = useCallback((annotationId: string) => {
932
+ setActiveReviewQueueItemId((current) =>
933
+ current === `section:${annotationId}` ? null : current);
934
+ setReviewSectionMarks((current) =>
935
+ current.filter((entry) => entry.annotationId !== annotationId));
936
+ }, []);
937
+
938
+ useEffect(() => {
939
+ if (snapshot.comments.activeCommentId) {
940
+ setActiveReviewQueueItemId(`comment:${snapshot.comments.activeCommentId}`);
941
+ return;
942
+ }
943
+ if (activeRevisionId) {
944
+ setActiveReviewQueueItemId(`change:${activeRevisionId}`);
945
+ }
946
+ }, [activeRevisionId, snapshot.comments.activeCommentId]);
947
+
948
+ useEffect(() => {
949
+ if (
950
+ activeReviewQueueItemId &&
951
+ !reviewQueueSnapshot.items.some((item) => item.itemId === activeReviewQueueItemId)
952
+ ) {
953
+ setActiveReviewQueueItemId(null);
954
+ }
955
+ }, [activeReviewQueueItemId, reviewQueueSnapshot.items]);
956
+
957
+ const navigateMountedReviewQueue = useCallback(
958
+ (direction: "next" | "previous"): ReviewQueueItem | null => {
959
+ const nextItem = getAdjacentReviewQueueItem(
960
+ reviewQueueSnapshot,
961
+ activeReviewQueueItemId,
962
+ direction,
963
+ );
964
+ if (!nextItem) {
965
+ return null;
966
+ }
967
+ focusReviewQueueItem(activeRuntime, nextItem);
968
+ setActiveReviewQueueItemId(nextItem.itemId);
969
+ return nextItem;
970
+ },
971
+ [activeReviewQueueItemId, activeRuntime, reviewQueueSnapshot],
972
+ );
973
+
697
974
  useEffect(() => {
698
975
  activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
699
976
  }, [activeRuntime, suggestionsEnabled]);
@@ -750,6 +1027,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
750
1027
  lastSavedRevisionTokenRef,
751
1028
  autosaveTimerRef,
752
1029
  })
1030
+ : loadError
1031
+ ? Promise.reject(loadError)
753
1032
  : rejectExportWhileLoadingFromBoundary({
754
1033
  documentId,
755
1034
  hostAdapter: hostAdapterRef.current,
@@ -757,8 +1036,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
757
1036
  onError: onErrorRef.current,
758
1037
  onEvent: onEventRef.current,
759
1038
  }),
760
- getSessionState: () => activeRuntime.getSessionState(),
761
- getSnapshot: () => activeRuntime.getPersistedSnapshot(),
1039
+ getSessionState: () =>
1040
+ clonePublicValue(runtime ? runtime.getSessionState() : loadingSessionState),
1041
+ getSnapshot: () =>
1042
+ clonePublicValue(runtime ? runtime.getPersistedSnapshot() : loadingSnapshot),
762
1043
  getRenderSnapshot: () => clonePublicValue(activeRuntime.getRenderSnapshot()),
763
1044
  getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
764
1045
  getWarnings: () => activeRuntime.getWarnings(),
@@ -766,6 +1047,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
766
1047
  clonePublicValue(activeRuntime.getRenderSnapshot().comments),
767
1048
  getTrackedChangesSnapshot: () =>
768
1049
  clonePublicValue(activeRuntime.getRenderSnapshot().trackedChanges),
1050
+ getSuggestionsSnapshot: () =>
1051
+ clonePublicValue(activeRuntime.getSuggestionsSnapshot()),
769
1052
  getComments: () =>
770
1053
  clonePublicValue(activeRuntime.getRenderSnapshot().comments),
771
1054
  getTrackedChanges: () =>
@@ -774,6 +1057,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
774
1057
  getFormattingState: () =>
775
1058
  getFormattingStateFromRenderSnapshot(activeRuntime.getRenderSnapshot()),
776
1059
  getStyleCatalog: () => getRuntimeStyleCatalog(activeRuntime),
1060
+ toggleBulletedList: () => {
1061
+ applyRuntimeListToggle(activeRuntime, "bulleted");
1062
+ },
1063
+ toggleNumberedList: () => {
1064
+ applyRuntimeListToggle(activeRuntime, "numbered");
1065
+ },
777
1066
  toggleBold: () => {
778
1067
  applyRuntimeFormattingOperation(activeRuntime, {
779
1068
  type: "toggle",
@@ -852,6 +1141,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
852
1141
  outdent: () => {
853
1142
  applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" });
854
1143
  },
1144
+ restartNumbering: () => {
1145
+ applyRuntimeNumberingFlow(activeRuntime, { type: "restart" });
1146
+ },
1147
+ continueNumbering: () => {
1148
+ applyRuntimeNumberingFlow(activeRuntime, { type: "continue" });
1149
+ },
855
1150
  insertPageBreak: () => {
856
1151
  applyRuntimeInsertPageBreak(activeRuntime);
857
1152
  },
@@ -960,6 +1255,36 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
960
1255
  getDocumentNavigationSnapshot: () => {
961
1256
  return clonePublicValue(activeRuntime.getDocumentNavigationSnapshot());
962
1257
  },
1258
+ getCurrentLocation: () => {
1259
+ return clonePublicValue(activeRuntime.getCurrentLocation());
1260
+ },
1261
+ getLocationForSelection: (selection) => {
1262
+ return clonePublicValue(activeRuntime.getLocationForSelection(selection));
1263
+ },
1264
+ getLocationForAnchor: (anchor, storyTarget) => {
1265
+ return clonePublicValue(activeRuntime.getLocationForAnchor(anchor, storyTarget));
1266
+ },
1267
+ captureRestorePoint: (input) => {
1268
+ return clonePublicValue(activeRuntime.captureRestorePoint(input));
1269
+ },
1270
+ restoreToPoint: (point, options) => {
1271
+ return clonePublicValue(activeRuntime.restoreToPoint(point, options));
1272
+ },
1273
+ getOutlineSnapshot: () => {
1274
+ return clonePublicValue(activeRuntime.getOutlineSnapshot());
1275
+ },
1276
+ getTocSnapshot: () => {
1277
+ return clonePublicValue(activeRuntime.getTocSnapshot());
1278
+ },
1279
+ getSections: () => {
1280
+ return clonePublicValue(activeRuntime.getSections());
1281
+ },
1282
+ getSectionSnapshot: (input) => {
1283
+ return clonePublicValue(activeRuntime.getSectionSnapshot(input));
1284
+ },
1285
+ describeEventImpact: (event) => {
1286
+ return clonePublicValue(activeRuntime.describeEventImpact(event));
1287
+ },
963
1288
  getFieldSnapshot: () => {
964
1289
  return clonePublicValue(activeRuntime.getFieldSnapshot());
965
1290
  },
@@ -1020,15 +1345,48 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1020
1345
  getWorkflowMarkupSnapshot: () => {
1021
1346
  return clonePublicValue(activeRuntime.getWorkflowMarkupSnapshot());
1022
1347
  },
1348
+ setWorkflowMetadataDefinitions: (definitions) => {
1349
+ activeRuntime.setWorkflowMetadataDefinitions(clonePublicValue(definitions));
1350
+ },
1351
+ clearWorkflowMetadataDefinitions: () => {
1352
+ activeRuntime.clearWorkflowMetadataDefinitions();
1353
+ },
1354
+ setWorkflowMetadataEntries: (entries) => {
1355
+ activeRuntime.setWorkflowMetadataEntries(clonePublicValue(entries));
1356
+ },
1357
+ clearWorkflowMetadataEntries: () => {
1358
+ activeRuntime.clearWorkflowMetadataEntries();
1359
+ },
1360
+ getWorkflowMetadataSnapshot: () => {
1361
+ return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
1362
+ },
1023
1363
  setHostAnnotationOverlay: (overlay) => {
1024
- activeRuntime.setHostAnnotationOverlay(clonePublicValue(overlay));
1364
+ setHostAnnotationOverlayState(clonePublicValue(overlay));
1025
1365
  },
1026
1366
  clearHostAnnotationOverlay: () => {
1027
- activeRuntime.clearHostAnnotationOverlay();
1367
+ setHostAnnotationOverlayState(null);
1028
1368
  },
1029
1369
  getHostAnnotationSnapshot: () => {
1030
1370
  return clonePublicValue(activeRuntime.getHostAnnotationSnapshot());
1031
1371
  },
1372
+ getReviewQueueSnapshot: () => {
1373
+ return clonePublicValue(reviewQueueSnapshot);
1374
+ },
1375
+ getRuntimeContextAnalytics: (query) => {
1376
+ return clonePublicValue(activeRuntime.getRuntimeContextAnalytics(query));
1377
+ },
1378
+ goToNextReviewItem: () => {
1379
+ return clonePublicValue(navigateMountedReviewQueue("next"));
1380
+ },
1381
+ goToPreviousReviewItem: () => {
1382
+ return clonePublicValue(navigateMountedReviewQueue("previous"));
1383
+ },
1384
+ markSectionForReview: (input) => {
1385
+ return markCurrentSectionForReview(input);
1386
+ },
1387
+ clearSectionReviewMark: (annotationId) => {
1388
+ clearReviewSectionMarkById(annotationId);
1389
+ },
1032
1390
  getWorkflowCandidateRanges: (options) => {
1033
1391
  return clonePublicValue(activeRuntime.getWorkflowCandidateRanges(options));
1034
1392
  },
@@ -1036,7 +1394,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1036
1394
  activeRuntime.replaceWorkflowMarkupText(markupId, text);
1037
1395
  },
1038
1396
  }),
1039
- [activeRuntime, currentUser.userId, documentId, runtime],
1397
+ [
1398
+ activeRuntime,
1399
+ clearReviewSectionMarkById,
1400
+ currentUser.userId,
1401
+ documentId,
1402
+ markCurrentSectionForReview,
1403
+ navigateMountedReviewQueue,
1404
+ reviewQueueSnapshot,
1405
+ runtime,
1406
+ ],
1040
1407
  );
1041
1408
 
1042
1409
  useEffect(() => {
@@ -1154,6 +1521,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1154
1521
  }
1155
1522
 
1156
1523
  function exportCurrentDocument(): void {
1524
+ const fileName =
1525
+ typeof snapshot.sourceLabel === "string" && snapshot.sourceLabel.trim().length > 0
1526
+ ? snapshot.sourceLabel
1527
+ : undefined;
1157
1528
  void (runtime
1158
1529
  ? persistAndExportFromBoundary({
1159
1530
  hostAdapter: hostAdapterRef.current,
@@ -1162,9 +1533,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1162
1533
  runtime,
1163
1534
  onError: onErrorRef.current,
1164
1535
  onEvent: onEventRef.current,
1536
+ ...(fileName ? { options: { fileName } } : {}),
1165
1537
  lastSavedRevisionTokenRef,
1166
1538
  autosaveTimerRef,
1167
1539
  })
1540
+ : loadError
1541
+ ? Promise.reject(loadError)
1168
1542
  : rejectExportWhileLoadingFromBoundary({
1169
1543
  documentId,
1170
1544
  hostAdapter: hostAdapterRef.current,
@@ -1179,7 +1553,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1179
1553
  reviewMode,
1180
1554
  workflowScopeSnapshot,
1181
1555
  );
1556
+ const effectiveChromePreset = resolveChromePreset(chromePreset, reviewMode);
1182
1557
  const resolvedChromeVisibility = resolveWordReviewEditorChromeVisibility(
1558
+ effectiveChromePreset,
1559
+ chromeOptions,
1183
1560
  chromeVisibility,
1184
1561
  showReviewPanel,
1185
1562
  );
@@ -1263,6 +1640,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1263
1640
  : null,
1264
1641
  [viewState.activeObjectFrame],
1265
1642
  );
1643
+ const activeTableContext = useMemo(
1644
+ () =>
1645
+ getTableStructureContext(
1646
+ canonicalDocument,
1647
+ snapshot,
1648
+ surfaceRef.current?.getTableSelection() ?? null,
1649
+ ),
1650
+ [canonicalDocument, snapshot],
1651
+ );
1652
+ const suggestionsSnapshot = activeRuntime.getSuggestionsSnapshot();
1653
+ const activeCommentThread =
1654
+ snapshot.comments.activeCommentId
1655
+ ? snapshot.comments.threads.find((thread) => thread.commentId === snapshot.comments.activeCommentId)
1656
+ : undefined;
1266
1657
  const selectionToolbar = buildSelectionToolbarModel({
1267
1658
  snapshot,
1268
1659
  viewState,
@@ -1276,6 +1667,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1276
1667
  });
1277
1668
  const suggestionCard = buildSuggestionCardModel({
1278
1669
  snapshot,
1670
+ suggestionsSnapshot,
1279
1671
  viewState,
1280
1672
  capabilities,
1281
1673
  workflowScopeSnapshot,
@@ -1284,15 +1676,70 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1284
1676
  suppressedSuggestionRevisionId,
1285
1677
  addCommentDisabledReason,
1286
1678
  });
1679
+ useEffect(() => {
1680
+ if (!activeRevisionId) {
1681
+ return;
1682
+ }
1683
+ if (snapshot.comments.activeCommentId) {
1684
+ setActiveRevisionId(undefined);
1685
+ return;
1686
+ }
1687
+ if (!isActiveRevisionStillFocused(
1688
+ activeRevisionId,
1689
+ suggestionsSnapshot,
1690
+ snapshot.selection,
1691
+ viewState.activeStory,
1692
+ )) {
1693
+ setActiveRevisionId(undefined);
1694
+ }
1695
+ }, [
1696
+ activeRevisionId,
1697
+ snapshot.comments.activeCommentId,
1698
+ snapshot.selection,
1699
+ suggestionsSnapshot,
1700
+ viewState.activeStory,
1701
+ ]);
1702
+ const activeSelectionTool = resolveActiveSelectionTool({
1703
+ snapshot,
1704
+ viewState,
1705
+ capabilities,
1706
+ documentNavigation,
1707
+ styleCatalog,
1708
+ formattingState,
1709
+ workflowScopeSnapshot,
1710
+ interactionGuardSnapshot,
1711
+ workflowMarkupSnapshot: workflowMarkupSnapshot ?? undefined,
1712
+ suggestionsSnapshot,
1713
+ activeRevisionId,
1714
+ activeCommentId: snapshot.comments.activeCommentId,
1715
+ activeCommentThread,
1716
+ activeTableContext,
1717
+ activeImageContext,
1718
+ activeObjectContext,
1719
+ activeListContext: viewState.activeListContext,
1720
+ preferListStructureContext: viewState.workspaceMode === "page",
1721
+ addCommentDisabledReason,
1722
+ suppressedSuggestionRevisionId,
1723
+ });
1287
1724
  const selectionToolbarSelectionKey = useMemo(
1288
- () => createSelectionToolbarSelectionKey(snapshot.selection, viewState.activeStory, activeRevisionId),
1289
- [activeRevisionId, snapshot.selection, viewState.activeStory],
1725
+ () =>
1726
+ createSelectionToolbarSelectionKey(
1727
+ snapshot.selection,
1728
+ viewState.activeStory,
1729
+ activeRevisionId,
1730
+ snapshot.comments.activeCommentId,
1731
+ ),
1732
+ [activeRevisionId, snapshot.comments.activeCommentId, snapshot.selection, viewState.activeStory],
1290
1733
  );
1291
1734
  const shouldRenderSelectionChrome = Boolean(
1292
- (selectionToolbar || suggestionCard) &&
1735
+ activeSelectionTool &&
1293
1736
  selectionToolbarSelectionKey &&
1294
1737
  selectionToolbarDismissedKey !== selectionToolbarSelectionKey &&
1295
- (viewState.isFocused || selectionToolbarFocusWithin),
1738
+ (
1739
+ viewState.isFocused ||
1740
+ selectionToolbarFocusWithin ||
1741
+ activeSelectionTool.kind === "structure-context"
1742
+ ),
1296
1743
  );
1297
1744
  const accessibilityInstructionsId = `${documentId}-accessibility-instructions`;
1298
1745
  const accessibilityStatusId = `${documentId}-accessibility-status`;
@@ -1426,11 +1873,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1426
1873
  }, [selectionToolbarSelectionKey]);
1427
1874
 
1428
1875
  useEffect(() => {
1429
- if (!selectionToolbar && !suggestionCard) {
1876
+ if (!activeSelectionTool) {
1430
1877
  setSelectionToolbarAnchor(null);
1431
1878
  setSelectionToolbarFocusWithin(false);
1432
1879
  }
1433
- }, [selectionToolbar, suggestionCard]);
1880
+ }, [activeSelectionTool]);
1434
1881
 
1435
1882
  useEffect(() => {
1436
1883
  const shell = shellRef.current;
@@ -1460,50 +1907,85 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1460
1907
 
1461
1908
  function handleShellKeyDownCapture(event: React.KeyboardEvent<HTMLDivElement>): void {
1462
1909
  const targetWithinDocument = isTargetWithinDocumentSurface(event.target);
1463
- const isUndoShortcut = (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === "z";
1464
- const isRedoShortcut =
1465
- ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === "z") ||
1466
- (event.ctrlKey && event.key.toLowerCase() === "y");
1467
-
1468
- if ((isUndoShortcut || isRedoShortcut) && targetWithinDocument) {
1469
- event.preventDefault();
1470
- event.stopPropagation();
1471
- if (isUndoShortcut) {
1472
- activeRuntime.undo();
1473
- } else {
1474
- activeRuntime.redo();
1475
- }
1476
- return;
1477
- }
1478
-
1479
- if (
1480
- event.key === "Escape" &&
1481
- shouldRenderSelectionChrome &&
1482
- (isTargetWithinDocumentSurface(event.target) || isTargetWithinSelectionToolbar(event.target))
1483
- ) {
1484
- event.preventDefault();
1485
- event.stopPropagation();
1486
- const restoreSurfaceFocus = isTargetWithinSelectionToolbar(event.target);
1487
- dismissSelectionToolbar("escape");
1488
- if (restoreSurfaceFocus) {
1489
- queueMicrotask(() => {
1490
- focusDocumentSurface();
1491
- });
1492
- }
1493
- return;
1494
- }
1495
-
1496
- if (event.key !== "F6") {
1497
- return;
1498
- }
1910
+ const targetWithinToolbar = isTargetWithinSelectionToolbar(event.target);
1911
+ const shortcut = resolveShellShortcut(
1912
+ {
1913
+ key: event.key,
1914
+ ctrlKey: event.ctrlKey,
1915
+ metaKey: event.metaKey,
1916
+ altKey: event.altKey,
1917
+ shiftKey: event.shiftKey,
1918
+ },
1919
+ {
1920
+ target: targetWithinDocument
1921
+ ? "document"
1922
+ : targetWithinToolbar
1923
+ ? "selection-toolbar"
1924
+ : "shell",
1925
+ selectionToolbarVisible: shouldRenderSelectionChrome,
1926
+ },
1927
+ );
1499
1928
 
1500
- const shell = shellRef.current;
1501
- if (!shell) {
1929
+ if (shortcut.kind === "none" || shortcut.kind === "delegate") {
1502
1930
  return;
1503
1931
  }
1504
1932
 
1505
1933
  event.preventDefault();
1506
- focusRelativeRegion(shell, event.shiftKey ? -1 : 1);
1934
+ event.stopPropagation();
1935
+
1936
+ switch (shortcut.kind) {
1937
+ case "history":
1938
+ if (shortcut.history === "undo") {
1939
+ commands.onUndo();
1940
+ } else {
1941
+ commands.onRedo();
1942
+ }
1943
+ return;
1944
+ case "dismiss-selection-toolbar": {
1945
+ dismissSelectionToolbar("escape");
1946
+ if (targetWithinToolbar) {
1947
+ queueMicrotask(() => {
1948
+ focusDocumentSurface();
1949
+ });
1950
+ }
1951
+ return;
1952
+ }
1953
+ case "focus-region": {
1954
+ const shell = shellRef.current;
1955
+ if (!shell) {
1956
+ return;
1957
+ }
1958
+ focusRelativeRegion(shell, shortcut.direction);
1959
+ return;
1960
+ }
1961
+ case "toggle-formatting":
1962
+ if (shortcut.mark === "bold") {
1963
+ commands.onToggleBold?.();
1964
+ } else if (shortcut.mark === "italic") {
1965
+ commands.onToggleItalic?.();
1966
+ } else {
1967
+ commands.onToggleUnderline?.();
1968
+ }
1969
+ return;
1970
+ case "add-comment":
1971
+ commands.onAddComment();
1972
+ return;
1973
+ case "set-heading-level": {
1974
+ const styleId = resolveHeadingShortcutStyleId(styleCatalog, shortcut.level);
1975
+ if (!styleId) {
1976
+ activeRuntime.emitBlockedCommand("setParagraphStyle", [{
1977
+ code: "unsupported_surface",
1978
+ message: `Heading ${shortcut.level} is not available in this document's style catalog.`,
1979
+ }]);
1980
+ return;
1981
+ }
1982
+ commands.onSetParagraphStyle?.(styleId);
1983
+ return;
1984
+ }
1985
+ case "block":
1986
+ activeRuntime.emitBlockedCommand(shortcut.command, [shortcut.reason]);
1987
+ return;
1988
+ }
1507
1989
  }
1508
1990
 
1509
1991
  const editorCallbacks = {
@@ -1614,6 +2096,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1614
2096
  applyRuntimeFormattingOperation(activeRuntime, { type: "set-highlight-color", color }),
1615
2097
  onSetAlignment: (alignment) =>
1616
2098
  applyRuntimeFormattingOperation(activeRuntime, { type: "set-alignment", alignment }),
2099
+ onToggleBulletedList: () => applyRuntimeListToggle(activeRuntime, "bulleted"),
2100
+ onToggleNumberedList: () => applyRuntimeListToggle(activeRuntime, "numbered"),
1617
2101
  onSetParagraphStyle: (styleId) => applyRuntimeParagraphStyle(activeRuntime, styleId),
1618
2102
  onOutdent: () => applyRuntimeFormattingOperation(activeRuntime, { type: "outdent" }),
1619
2103
  onIndent: () => applyRuntimeFormattingOperation(activeRuntime, { type: "indent" }),
@@ -1687,6 +2171,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1687
2171
  applyRuntimeNumberingFlow(activeRuntime, { type: "restart" }),
1688
2172
  onContinueNumbering: () =>
1689
2173
  applyRuntimeNumberingFlow(activeRuntime, { type: "continue" }),
2174
+ onUpdateFields: () => {
2175
+ activeRuntime.updateFields();
2176
+ },
2177
+ onUpdateTableOfContents: () => {
2178
+ activeRuntime.updateTableOfContents();
2179
+ },
2180
+ onGoToPreviousReviewItem: () => {
2181
+ navigateMountedReviewQueue("previous");
2182
+ },
2183
+ onGoToNextReviewItem: () => {
2184
+ navigateMountedReviewQueue("next");
2185
+ },
2186
+ onMarkSectionForReview: () => markCurrentSectionForReview(),
1690
2187
  onNavigateHeading: (headingId) => {
1691
2188
  const heading = documentNavigation.headings.find(
1692
2189
  (entry) => entry.headingId === headingId,
@@ -1710,7 +2207,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1710
2207
  documentNavigation={documentNavigation}
1711
2208
  reviewMode={reviewMode}
1712
2209
  markupDisplay={liveMarkupDisplay}
2210
+ showUnsupportedObjectPreviews={showUnsupportedObjectPreviews}
1713
2211
  activeRevisionId={activeRevisionId}
2212
+ activeSelectionToolKind={activeSelectionTool?.kind ?? null}
1714
2213
  showTrackedChanges={showTrackedChanges}
1715
2214
  suggestionsEnabled={suggestionsEnabled}
1716
2215
  mediaPreviews={mediaPreviews}
@@ -1720,6 +2219,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1720
2219
  workflowBlockedReasons={workflowBlockedRails}
1721
2220
  activeWorkflowWorkItemId={workflowScopeSnapshot?.activeWorkItemId ?? null}
1722
2221
  activeWorkflowScopeIds={workflowScopeSnapshot?.activeWorkItem?.scopeIds ?? []}
2222
+ workflowMetadata={workflowMarkupSnapshot?.metadata}
1723
2223
  onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
1724
2224
  {...editorCallbacks}
1725
2225
  onCommentActivated={(commentId) => {
@@ -1759,6 +2259,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1759
2259
  workspaceMode={viewState.workspaceMode}
1760
2260
  zoomLevel={viewState.zoomLevel}
1761
2261
  formattingState={formattingState}
2262
+ activeListContext={viewState.activeListContext}
1762
2263
  styleCatalog={styleCatalog}
1763
2264
  activeRailTab={activeRailTab}
1764
2265
  activeCommentId={snapshot.comments.activeCommentId}
@@ -1766,34 +2267,51 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1766
2267
  showTrackedChanges={showTrackedChanges}
1767
2268
  workflowScopeSnapshot={workflowScopeSnapshot}
1768
2269
  interactionGuardSnapshot={interactionGuardSnapshot}
1769
- selectionToolbar={shouldRenderSelectionChrome ? selectionToolbar : null}
1770
- suggestionCard={shouldRenderSelectionChrome ? suggestionCard : null}
1771
- selectionToolbarAnchor={shouldRenderSelectionChrome ? selectionToolbarAnchor : null}
2270
+ chromePreset={effectiveChromePreset}
2271
+ chromeOptions={chromeOptions}
2272
+ reviewQueue={reviewQueueSnapshot}
2273
+ documentContextAnalytics={documentContextAnalytics}
2274
+ selectionContextAnalytics={selectionContextAnalytics}
2275
+ currentScopeContextAnalytics={currentScopeContextAnalytics}
2276
+ activeSelectionTool={shouldRenderSelectionChrome ? activeSelectionTool : null}
2277
+ selectionToolAnchor={shouldRenderSelectionChrome ? selectionToolbarAnchor : null}
1772
2278
  onAddCommentFromSelection={addSelectionToolbarComment}
1773
2279
  onAddCommentFromSuggestion={addSelectionToolbarComment}
1774
- onAcceptSuggestion={suggestionCard
2280
+ onAcceptSuggestion={activeSelectionTool?.kind === "suggestion-review"
1775
2281
  ? () => {
1776
- activeRuntime.acceptChange(suggestionCard.revisionId);
2282
+ for (const changeId of activeSelectionTool.changeIds) {
2283
+ activeRuntime.acceptChange(changeId);
2284
+ }
1777
2285
  dismissSelectionToolbar("chrome-action");
1778
2286
  }
1779
2287
  : undefined}
1780
- onRejectSuggestion={suggestionCard
2288
+ onRejectSuggestion={activeSelectionTool?.kind === "suggestion-review"
1781
2289
  ? () => {
1782
- activeRuntime.rejectChange(suggestionCard.revisionId);
2290
+ for (const changeId of activeSelectionTool.changeIds) {
2291
+ activeRuntime.rejectChange(changeId);
2292
+ }
1783
2293
  dismissSelectionToolbar("chrome-action");
1784
2294
  }
1785
2295
  : undefined}
1786
- onEditSuggestion={suggestionCard
2296
+ onEditSuggestion={activeSelectionTool?.kind === "suggestion-review"
1787
2297
  ? () => {
1788
- setSuppressedSuggestionRevisionId(suggestionCard.revisionId);
2298
+ setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
2299
+ const activeSuggestion = suggestionsSnapshot.suggestions.find(
2300
+ (suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
2301
+ );
2302
+ if (activeSuggestion) {
2303
+ applyRuntimeSelection(
2304
+ activeRuntime,
2305
+ createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
2306
+ );
2307
+ }
2308
+ setSelectionToolbarFocusWithin(true);
1789
2309
  }
1790
2310
  : undefined}
1791
2311
  onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
1792
2312
  onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
1793
2313
  onSelectionToolbarBlurCapture={handleSelectionToolbarBlurCapture}
1794
2314
  selectionToolbarRef={selectionToolbarElementRef}
1795
- activeImageContext={activeImageContext}
1796
- activeObjectContext={activeObjectContext}
1797
2315
  commands={commands}
1798
2316
  document={documentElement}
1799
2317
  />
@@ -1813,6 +2331,11 @@ function applyRuntimeFormattingOperation(
1813
2331
  | { type: "indent" }
1814
2332
  | { type: "outdent" },
1815
2333
  ): void {
2334
+ if (isSelectionSuggesting(runtime)) {
2335
+ if (applySuggestingFormattingOperation(runtime, operation)) {
2336
+ return;
2337
+ }
2338
+ }
1816
2339
  if (emitSuggestingUnsupportedMutation(runtime, getFormattingOperationCommandName(operation))) {
1817
2340
  return;
1818
2341
  }
@@ -1837,6 +2360,159 @@ function applyRuntimeFormattingOperation(
1837
2360
  );
1838
2361
  }
1839
2362
 
2363
+ function applySuggestingFormattingOperation(
2364
+ runtime: WordReviewEditorRuntime,
2365
+ operation:
2366
+ | { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
2367
+ | { type: "set-font-family"; fontFamily: string | null }
2368
+ | { type: "set-font-size"; size: number | null }
2369
+ | { type: "set-text-color"; color: string | null }
2370
+ | { type: "set-highlight-color"; color: string | null }
2371
+ | { type: "set-alignment"; alignment: FormattingAlignment }
2372
+ | { type: "indent" }
2373
+ | { type: "outdent" },
2374
+ ): boolean {
2375
+ const commandName = getFormattingOperationCommandName(operation);
2376
+ const context = getStoryMutationContext(runtime, commandName);
2377
+ if (!context) {
2378
+ return true;
2379
+ }
2380
+ if (context.activeStory.kind !== "main") {
2381
+ runtime.emitBlockedCommand(commandName, [{
2382
+ code: "suggesting_unsupported",
2383
+ message: `"${commandName}" is not supported in suggesting mode for this story.`,
2384
+ }]);
2385
+ return true;
2386
+ }
2387
+
2388
+ if (operation.type === "set-alignment" || operation.type === "indent" || operation.type === "outdent") {
2389
+ const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
2390
+ if (!paragraphContext) {
2391
+ return true;
2392
+ }
2393
+ const beforeXml = buildParagraphPropertyBeforeXml(paragraphContext.paragraph);
2394
+ const result = applyFormattingOperationToDocument(
2395
+ context.localDocument,
2396
+ context.localSnapshot,
2397
+ operation,
2398
+ );
2399
+ if (!result.changed) {
2400
+ return true;
2401
+ }
2402
+ const nextDocument = appendPropertyChangeSuggestion(
2403
+ result.document,
2404
+ {
2405
+ from: paragraphContext.paragraph.from,
2406
+ to: paragraphContext.paragraph.to,
2407
+ },
2408
+ {
2409
+ originalRevisionType: "pPrChange",
2410
+ xmlTag: "pPrChange",
2411
+ beforeXml,
2412
+ semanticKind: "paragraph-property-change",
2413
+ storyTarget: context.activeStory,
2414
+ authorId: runtime.getDefaultAuthorId?.(),
2415
+ },
2416
+ context.timestamp,
2417
+ );
2418
+ dispatchStoryMutationResult(
2419
+ runtime,
2420
+ context,
2421
+ {
2422
+ changed: true,
2423
+ document: nextDocument,
2424
+ selection: toRuntimeSelectionSnapshot(result.selection),
2425
+ },
2426
+ context.timestamp,
2427
+ );
2428
+ return true;
2429
+ }
2430
+
2431
+ const segment = findSingleSelectedTextSegment(context.localSnapshot);
2432
+ if (!segment) {
2433
+ runtime.emitBlockedCommand(commandName, [{
2434
+ code: "suggesting_unsupported",
2435
+ message: `"${commandName}" requires one bounded text segment in suggesting mode.`,
2436
+ }]);
2437
+ return true;
2438
+ }
2439
+ const beforeXml = buildRunPropertyBeforeXml(segment);
2440
+ const result = applyFormattingOperationToDocument(
2441
+ context.localDocument,
2442
+ context.localSnapshot,
2443
+ operation,
2444
+ );
2445
+ if (!result.changed) {
2446
+ return true;
2447
+ }
2448
+ const nextDocument = appendPropertyChangeSuggestion(
2449
+ result.document,
2450
+ {
2451
+ from: segment.from,
2452
+ to: segment.to,
2453
+ },
2454
+ {
2455
+ originalRevisionType: "rPrChange",
2456
+ xmlTag: "rPrChange",
2457
+ beforeXml,
2458
+ semanticKind: "formatting-change",
2459
+ storyTarget: context.activeStory,
2460
+ authorId: runtime.getDefaultAuthorId?.(),
2461
+ },
2462
+ context.timestamp,
2463
+ );
2464
+ dispatchStoryMutationResult(
2465
+ runtime,
2466
+ context,
2467
+ {
2468
+ changed: true,
2469
+ document: nextDocument,
2470
+ selection: toRuntimeSelectionSnapshot(result.selection),
2471
+ },
2472
+ context.timestamp,
2473
+ );
2474
+ return true;
2475
+ }
2476
+
2477
+ function applyRuntimeListToggle(
2478
+ runtime: WordReviewEditorRuntime,
2479
+ kind: "bulleted" | "numbered",
2480
+ ): void {
2481
+ const commandName =
2482
+ kind === "bulleted" ? "toggleBulletedList" : "toggleNumberedList";
2483
+ if (emitSuggestingUnsupportedMutation(runtime, commandName)) {
2484
+ return;
2485
+ }
2486
+ const context = getStoryMutationContext(runtime, commandName);
2487
+ if (!context) {
2488
+ return;
2489
+ }
2490
+
2491
+ const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
2492
+ if (!paragraphContext) {
2493
+ return;
2494
+ }
2495
+
2496
+ const result =
2497
+ kind === "bulleted"
2498
+ ? toggleBulletedList(
2499
+ context.localDocument,
2500
+ [paragraphContext.paragraphIndex],
2501
+ { timestamp: context.timestamp },
2502
+ )
2503
+ : toggleNumberedList(
2504
+ context.localDocument,
2505
+ [paragraphContext.paragraphIndex],
2506
+ { timestamp: context.timestamp },
2507
+ );
2508
+ dispatchStoryMutationResult(
2509
+ runtime,
2510
+ context,
2511
+ createListMutationResult(result, context.localSnapshot.selection),
2512
+ context.timestamp,
2513
+ );
2514
+ }
2515
+
1840
2516
  function getRuntimeStyleCatalog(
1841
2517
  input:
1842
2518
  | WordReviewEditorRuntime
@@ -2113,6 +2789,183 @@ function emitSuggestingUnsupportedMutation(
2113
2789
  return true;
2114
2790
  }
2115
2791
 
2792
+ function appendPropertyChangeSuggestion(
2793
+ document: EditorSessionState["canonicalDocument"],
2794
+ anchor: { from: number; to: number },
2795
+ input: {
2796
+ originalRevisionType: "rPrChange" | "pPrChange";
2797
+ xmlTag: "rPrChange" | "pPrChange";
2798
+ beforeXml: string;
2799
+ semanticKind: "formatting-change" | "paragraph-property-change";
2800
+ storyTarget: EditorStoryTarget;
2801
+ authorId?: string;
2802
+ },
2803
+ timestamp: string,
2804
+ ): EditorSessionState["canonicalDocument"] {
2805
+ const existing = document.review.revisions;
2806
+ const changeId = createRuntimeSuggestionChangeId(existing, timestamp);
2807
+ const resolvedAuthorId = input.authorId ?? "unknown";
2808
+ return {
2809
+ ...document,
2810
+ review: {
2811
+ ...document.review,
2812
+ revisions: {
2813
+ ...existing,
2814
+ [changeId]: {
2815
+ changeId,
2816
+ kind: "property-change",
2817
+ anchor: createRangeAnchor(anchor.from, anchor.to, { start: 1, end: -1 }),
2818
+ authorId: resolvedAuthorId,
2819
+ createdAt: timestamp,
2820
+ warningIds: [],
2821
+ metadata: {
2822
+ source: "runtime",
2823
+ storyTarget: input.storyTarget,
2824
+ suggestionId: changeId,
2825
+ semanticKind: input.semanticKind,
2826
+ originalRevisionType: input.originalRevisionType,
2827
+ propertyChangeData: {
2828
+ xmlTag: input.xmlTag,
2829
+ beforeXml: input.beforeXml,
2830
+ },
2831
+ },
2832
+ status: "open",
2833
+ },
2834
+ },
2835
+ },
2836
+ };
2837
+ }
2838
+
2839
+ function createRuntimeSuggestionChangeId(
2840
+ existing: EditorSessionState["canonicalDocument"]["review"]["revisions"],
2841
+ timestamp: string,
2842
+ ): string {
2843
+ const base = `change-${timestamp.replace(/[^0-9]/gu, "")}`;
2844
+ let counter = Object.keys(existing).length + 1;
2845
+ let candidate = `${base}-p${counter}`;
2846
+ while (existing[candidate]) {
2847
+ counter += 1;
2848
+ candidate = `${base}-p${counter}`;
2849
+ }
2850
+ return candidate;
2851
+ }
2852
+
2853
+ function findSingleSelectedTextSegment(
2854
+ snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
2855
+ ): Extract<SurfaceInlineSegment, { kind: "text" }> | null {
2856
+ if (!snapshot.surface || snapshot.selection.activeRange.kind !== "range" || snapshot.selection.isCollapsed) {
2857
+ return null;
2858
+ }
2859
+ const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
2860
+ const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
2861
+ const segments = collectSelectedTextSegments(snapshot.surface.blocks, selectionFrom, selectionTo);
2862
+ if (segments.length !== 1) {
2863
+ return null;
2864
+ }
2865
+ const [segment] = segments;
2866
+ if (!segment || segment.from !== selectionFrom || segment.to !== selectionTo) {
2867
+ return null;
2868
+ }
2869
+ return segment;
2870
+ }
2871
+
2872
+ function collectSelectedTextSegments(
2873
+ blocks: readonly SurfaceBlockSnapshot[],
2874
+ selectionFrom: number,
2875
+ selectionTo: number,
2876
+ output: Array<Extract<SurfaceInlineSegment, { kind: "text" }>> = [],
2877
+ ): Array<Extract<SurfaceInlineSegment, { kind: "text" }>> {
2878
+ for (const block of blocks) {
2879
+ if (block.kind === "paragraph") {
2880
+ for (const segment of block.segments) {
2881
+ if (
2882
+ segment.kind === "text" &&
2883
+ rangesOverlap(selectionFrom, selectionTo, segment.from, segment.to)
2884
+ ) {
2885
+ output.push(segment);
2886
+ }
2887
+ }
2888
+ continue;
2889
+ }
2890
+ if (block.kind === "table") {
2891
+ for (const row of block.rows) {
2892
+ for (const cell of row.cells) {
2893
+ collectSelectedTextSegments(cell.content, selectionFrom, selectionTo, output);
2894
+ }
2895
+ }
2896
+ continue;
2897
+ }
2898
+ if (block.kind === "sdt_block") {
2899
+ collectSelectedTextSegments(block.children, selectionFrom, selectionTo, output);
2900
+ }
2901
+ }
2902
+ return output;
2903
+ }
2904
+
2905
+ function buildRunPropertyBeforeXml(
2906
+ segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
2907
+ ): string {
2908
+ const parts: string[] = [];
2909
+ const marks = new Set(segment.marks ?? []);
2910
+ if (marks.has("bold")) parts.push("<w:b/>");
2911
+ if (marks.has("italic")) parts.push("<w:i/>");
2912
+ if (marks.has("underline")) parts.push("<w:u w:val=\"single\"/>");
2913
+ if (marks.has("strikethrough")) parts.push("<w:strike/>");
2914
+ if (marks.has("superscript")) parts.push("<w:vertAlign w:val=\"superscript\"/>");
2915
+ if (marks.has("subscript")) parts.push("<w:vertAlign w:val=\"subscript\"/>");
2916
+ if (segment.markAttrs?.fontFamily) {
2917
+ parts.push(`<w:rFonts w:ascii="${escapeAttributeXml(segment.markAttrs.fontFamily)}" w:hAnsi="${escapeAttributeXml(segment.markAttrs.fontFamily)}"/>`);
2918
+ }
2919
+ if (segment.markAttrs?.fontSize !== undefined) {
2920
+ parts.push(`<w:sz w:val="${segment.markAttrs.fontSize}"/>`);
2921
+ }
2922
+ if (segment.markAttrs?.textColor) {
2923
+ parts.push(`<w:color w:val="${escapeAttributeXml(segment.markAttrs.textColor)}"/>`);
2924
+ }
2925
+ if (segment.markAttrs?.backgroundColor) {
2926
+ parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttributeXml(segment.markAttrs.backgroundColor)}"/>`);
2927
+ }
2928
+ return `<w:rPr>${parts.join("")}</w:rPr>`;
2929
+ }
2930
+
2931
+ function buildParagraphPropertyBeforeXml(
2932
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
2933
+ ): string {
2934
+ const parts: string[] = [];
2935
+ if (paragraph.styleId) {
2936
+ parts.push(`<w:pStyle w:val="${escapeAttributeXml(paragraph.styleId)}"/>`);
2937
+ }
2938
+ if (paragraph.numbering) {
2939
+ parts.push(
2940
+ `<w:numPr><w:ilvl w:val="${paragraph.numbering.level}"/><w:numId w:val="${escapeAttributeXml(
2941
+ paragraph.numbering.numberingInstanceId.replace(/^num:/u, ""),
2942
+ )}"/></w:numPr>`,
2943
+ );
2944
+ }
2945
+ if (paragraph.alignment) {
2946
+ parts.push(`<w:jc w:val="${escapeAttributeXml(paragraph.alignment)}"/>`);
2947
+ }
2948
+ if (paragraph.indentation) {
2949
+ const attrs: string[] = [];
2950
+ if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
2951
+ if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
2952
+ if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
2953
+ if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
2954
+ if (attrs.length > 0) {
2955
+ parts.push(`<w:ind ${attrs.join(" ")}/>`);
2956
+ }
2957
+ }
2958
+ return `<w:pPr>${parts.join("")}</w:pPr>`;
2959
+ }
2960
+
2961
+ function escapeAttributeXml(value: string): string {
2962
+ return value
2963
+ .replace(/&/g, "&amp;")
2964
+ .replace(/</g, "&lt;")
2965
+ .replace(/>/g, "&gt;")
2966
+ .replace(/"/g, "&quot;");
2967
+ }
2968
+
2116
2969
  function isSelectionSuggesting(runtime: WordReviewEditorRuntime): boolean {
2117
2970
  return runtime.getInteractionGuardSnapshot().effectiveMode === "suggest";
2118
2971
  }
@@ -3262,6 +4115,8 @@ function deriveEditorViewMode(
3262
4115
  }
3263
4116
 
3264
4117
  function resolveWordReviewEditorChromeVisibility(
4118
+ chromePreset: WordReviewEditorChromePreset,
4119
+ chromeOptions: Partial<WordReviewEditorChromeOptions> | undefined,
3265
4120
  chromeVisibility: WordReviewEditorProps["chromeVisibility"],
3266
4121
  showReviewPanel: boolean,
3267
4122
  ): Partial<WordReviewEditorChromeVisibility> {
@@ -3269,10 +4124,14 @@ function resolveWordReviewEditorChromeVisibility(
3269
4124
  showReviewPanel
3270
4125
  ? {}
3271
4126
  : { reviewRail: false } satisfies Partial<WordReviewEditorChromeVisibility>;
3272
- return {
3273
- ...legacyVisibility,
3274
- ...chromeVisibility,
3275
- };
4127
+ return resolveChromeVisibilityForPreset({
4128
+ chromePreset,
4129
+ chromeOptions,
4130
+ chromeVisibility: {
4131
+ ...legacyVisibility,
4132
+ ...chromeVisibility,
4133
+ },
4134
+ });
3276
4135
  }
3277
4136
 
3278
4137
  function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
@@ -3508,6 +4367,7 @@ function createSelectionToolbarSelectionKey(
3508
4367
  selection: RuntimeRenderSnapshot["selection"],
3509
4368
  activeStory: EditorStoryTarget,
3510
4369
  activeRevisionId?: string,
4370
+ activeCommentId?: string | null,
3511
4371
  ): string | null {
3512
4372
  if (activeRevisionId) {
3513
4373
  return JSON.stringify({
@@ -3515,7 +4375,19 @@ function createSelectionToolbarSelectionKey(
3515
4375
  revisionId: activeRevisionId,
3516
4376
  });
3517
4377
  }
3518
- if (selection.isCollapsed || selection.activeRange.kind !== "range") {
4378
+ if (activeCommentId && selection.isCollapsed) {
4379
+ return JSON.stringify({
4380
+ story: activeStory,
4381
+ commentId: activeCommentId,
4382
+ });
4383
+ }
4384
+ if (selection.activeRange.kind !== "range") {
4385
+ if (selection.activeRange.kind === "node") {
4386
+ return JSON.stringify({
4387
+ story: activeStory,
4388
+ nodeAt: selection.activeRange.at,
4389
+ });
4390
+ }
3519
4391
  return null;
3520
4392
  }
3521
4393
 
@@ -3523,9 +4395,38 @@ function createSelectionToolbarSelectionKey(
3523
4395
  story: activeStory,
3524
4396
  from: selection.activeRange.from,
3525
4397
  to: selection.activeRange.to,
4398
+ collapsed: selection.isCollapsed,
3526
4399
  });
3527
4400
  }
3528
4401
 
4402
+ function isActiveRevisionStillFocused(
4403
+ activeRevisionId: string,
4404
+ suggestionsSnapshot: SuggestionsSnapshot | null,
4405
+ selection: RuntimeRenderSnapshot["selection"],
4406
+ activeStory: EditorStoryTarget,
4407
+ ): boolean {
4408
+ const suggestion = suggestionsSnapshot?.suggestions.find((entry) =>
4409
+ entry.status === "active" &&
4410
+ entry.actionability === "actionable" &&
4411
+ entry.anchor.kind === "range" &&
4412
+ storyTargetsEqual(entry.storyTarget, activeStory) &&
4413
+ entry.changeIds.includes(activeRevisionId)
4414
+ );
4415
+ if (!suggestion || suggestion.anchor.kind !== "range") {
4416
+ return false;
4417
+ }
4418
+ if (selection.activeRange.kind !== "range") {
4419
+ return false;
4420
+ }
4421
+ if (selection.isCollapsed) {
4422
+ const point = selection.activeRange.from;
4423
+ return point >= suggestion.anchor.from && point <= suggestion.anchor.to;
4424
+ }
4425
+ const selectionFrom = Math.min(selection.activeRange.from, selection.activeRange.to);
4426
+ const selectionTo = Math.max(selection.activeRange.from, selection.activeRange.to);
4427
+ return selectionFrom < suggestion.anchor.to && suggestion.anchor.from < selectionTo;
4428
+ }
4429
+
3529
4430
  function buildSelectionToolbarModel(args: {
3530
4431
  snapshot: RuntimeRenderSnapshot;
3531
4432
  viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>;
@@ -3596,6 +4497,7 @@ function buildSelectionToolbarModel(args: {
3596
4497
  (canAddComment ? undefined : addCommentDisabledReason);
3597
4498
 
3598
4499
  return {
4500
+ kind: "formatting-inline",
3599
4501
  previewText,
3600
4502
  badges,
3601
4503
  canToggleFormatting,
@@ -3609,6 +4511,7 @@ function buildSelectionToolbarModel(args: {
3609
4511
 
3610
4512
  function buildSuggestionCardModel(args: {
3611
4513
  snapshot: RuntimeRenderSnapshot;
4514
+ suggestionsSnapshot: SuggestionsSnapshot;
3612
4515
  viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>;
3613
4516
  capabilities: ReturnType<typeof deriveCapabilities>;
3614
4517
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
@@ -3619,6 +4522,7 @@ function buildSuggestionCardModel(args: {
3619
4522
  }): SuggestionCardModel | null {
3620
4523
  const {
3621
4524
  snapshot,
4525
+ suggestionsSnapshot,
3622
4526
  viewState,
3623
4527
  capabilities,
3624
4528
  workflowScopeSnapshot,
@@ -3646,32 +4550,32 @@ function buildSuggestionCardModel(args: {
3646
4550
  const selectionTo = activeRange
3647
4551
  ? Math.max(activeRange.from, activeRange.to)
3648
4552
  : null;
3649
- const candidateRevisions = snapshot.trackedChanges.revisions.filter((revision) =>
3650
- storyTargetsEqual(revision.storyTarget ?? { kind: "main" }, viewState.activeStory) &&
3651
- revision.status === "active" &&
3652
- revision.actionability === "actionable" &&
3653
- revision.anchor.kind === "range" &&
4553
+ const candidateSuggestions = suggestionsSnapshot.suggestions.filter((suggestion) =>
4554
+ storyTargetsEqual(suggestion.storyTarget, viewState.activeStory) &&
4555
+ suggestion.status === "active" &&
4556
+ suggestion.actionability === "actionable" &&
4557
+ suggestion.anchor.kind === "range" &&
3654
4558
  (
3655
- activeRevisionId === revision.revisionId ||
4559
+ (activeRevisionId !== undefined && suggestion.changeIds.includes(activeRevisionId)) ||
3656
4560
  (
3657
4561
  selectionFrom !== null &&
3658
4562
  selectionTo !== null &&
3659
4563
  rangesOverlap(
3660
4564
  selectionFrom,
3661
4565
  selectionTo,
3662
- revision.anchor.from,
3663
- revision.anchor.to,
4566
+ suggestion.anchor.from,
4567
+ suggestion.anchor.to,
3664
4568
  )
3665
4569
  )
3666
4570
  )
3667
4571
  );
3668
- const focusedRevision = (
4572
+ const focusedSuggestion = (
3669
4573
  activeRevisionId
3670
- ? candidateRevisions.find((revision) => revision.revisionId === activeRevisionId)
4574
+ ? candidateSuggestions.find((suggestion) => suggestion.changeIds.includes(activeRevisionId))
3671
4575
  : null
3672
- ) ?? candidateRevisions[0];
4576
+ ) ?? candidateSuggestions[0];
3673
4577
 
3674
- if (!focusedRevision || focusedRevision.revisionId === suppressedSuggestionRevisionId) {
4578
+ if (!focusedSuggestion || focusedSuggestion.suggestionId === suppressedSuggestionRevisionId) {
3675
4579
  return null;
3676
4580
  }
3677
4581
 
@@ -3699,17 +4603,18 @@ function buildSuggestionCardModel(args: {
3699
4603
  (canAddComment ? undefined : addCommentDisabledReason);
3700
4604
 
3701
4605
  return {
3702
- revisionId: focusedRevision.revisionId,
3703
- kindLabel: getSuggestionKindLabel(focusedRevision.kind),
4606
+ kind: "suggestion-review",
4607
+ suggestionId: focusedSuggestion.suggestionId,
4608
+ changeIds: focusedSuggestion.changeIds,
4609
+ kindLabel: getSuggestionKindLabel(focusedSuggestion.kind),
3704
4610
  previewText:
3705
- focusedRevision.excerpt ??
3706
- focusedRevision.detail ??
3707
- focusedRevision.label ??
4611
+ focusedSuggestion.excerpt ??
4612
+ focusedSuggestion.detail ??
3708
4613
  "Suggested change",
3709
4614
  badges,
3710
- canAccept: canReviewSuggestion && capabilities.canAcceptChange && focusedRevision.canAccept,
3711
- canReject: canReviewSuggestion && capabilities.canRejectChange && focusedRevision.canReject,
3712
- canEditSuggestion: canReviewSuggestion,
4615
+ canAccept: canReviewSuggestion && capabilities.canAcceptChange && focusedSuggestion.canAccept,
4616
+ canReject: canReviewSuggestion && capabilities.canRejectChange && focusedSuggestion.canReject,
4617
+ canEditSuggestion: canReviewSuggestion && focusedSuggestion.editable,
3713
4618
  canAddComment,
3714
4619
  ...(disabledReason ? { disabledReason } : {}),
3715
4620
  };
@@ -3897,18 +4802,28 @@ function rangesOverlap(
3897
4802
  return leftFrom < rightTo && rightFrom < leftTo;
3898
4803
  }
3899
4804
 
3900
- function getSuggestionKindLabel(kind: TrackedChangeEntrySnapshot["kind"]): string {
4805
+ function getSuggestionKindLabel(
4806
+ kind: TrackedChangeEntrySnapshot["kind"] | SuggestionEntrySnapshot["kind"],
4807
+ ): string {
3901
4808
  switch (kind) {
3902
4809
  case "insertion":
3903
4810
  return "Suggested insertion";
3904
4811
  case "deletion":
3905
4812
  return "Suggested deletion";
4813
+ case "replacement":
4814
+ return "Suggested replacement";
3906
4815
  case "formatting":
4816
+ case "formatting-change":
3907
4817
  return "Suggested formatting change";
3908
4818
  case "property-change":
4819
+ case "paragraph-property-change":
3909
4820
  return "Suggested property change";
3910
4821
  case "move":
3911
4822
  return "Suggested move";
4823
+ case "structural-change":
4824
+ return "Suggested structural change";
4825
+ case "object-change":
4826
+ return "Suggested object change";
3912
4827
  }
3913
4828
  }
3914
4829
 
@@ -4084,3 +4999,270 @@ function bytesToBase64(bytes: Uint8Array): string {
4084
4999
  }
4085
5000
  return btoa(binary);
4086
5001
  }
5002
+
5003
+ function deriveReviewQueueSnapshot(input: {
5004
+ sections: Array<{
5005
+ sectionIndex: number;
5006
+ pageRange: { start: number; end: number };
5007
+ anchor: EditorAnchorProjection;
5008
+ }>;
5009
+ comments: RuntimeRenderSnapshot["comments"]["threads"];
5010
+ trackedChanges: RuntimeRenderSnapshot["trackedChanges"]["revisions"];
5011
+ hostAnnotations: HostAnnotationItem[];
5012
+ activeItemId?: string | null;
5013
+ }): ReviewQueueSnapshot {
5014
+ const items: ReviewQueueItem[] = [
5015
+ ...input.comments
5016
+ .filter((thread) => thread.status !== "detached")
5017
+ .map((thread) => {
5018
+ const section = findSectionForAnchor(input.sections, thread.anchor);
5019
+ const status: ReviewQueueItem["status"] =
5020
+ thread.status === "resolved" ? "resolved" : "open";
5021
+ return {
5022
+ itemId: `comment:${thread.commentId}`,
5023
+ kind: "comment" as const,
5024
+ label: thread.excerpt || thread.anchorLabel || "Comment",
5025
+ anchor: thread.anchor,
5026
+ actionable: thread.status === "open",
5027
+ status,
5028
+ sectionIndex: section?.sectionIndex,
5029
+ };
5030
+ }),
5031
+ ...input.trackedChanges
5032
+ .filter((revision) => revision.status !== "detached")
5033
+ .map((revision) => {
5034
+ const section = findSectionForAnchor(input.sections, revision.anchor);
5035
+ const status: ReviewQueueItem["status"] =
5036
+ revision.status === "active" ? "open" : "resolved";
5037
+ return {
5038
+ itemId: `change:${revision.revisionId}`,
5039
+ kind: "change" as const,
5040
+ label: revision.label,
5041
+ anchor: revision.anchor,
5042
+ storyTarget: revision.storyTarget,
5043
+ actionable:
5044
+ revision.status === "active" &&
5045
+ revision.actionability === "actionable" &&
5046
+ (revision.canAccept || revision.canReject),
5047
+ status,
5048
+ sectionIndex: section?.sectionIndex,
5049
+ };
5050
+ }),
5051
+ ...input.hostAnnotations.filter(isSectionReviewAnnotation).map((annotation) => {
5052
+ const section = findSectionForAnchor(input.sections, annotation.anchor);
5053
+ return {
5054
+ itemId: `section:${annotation.annotationId}`,
5055
+ kind: "section_mark" as const,
5056
+ label: annotation.label,
5057
+ anchor: annotation.anchor,
5058
+ storyTarget: annotation.storyTarget,
5059
+ actionable: true,
5060
+ status: "open" as const,
5061
+ sectionIndex: section?.sectionIndex,
5062
+ };
5063
+ }),
5064
+ ].sort(compareReviewQueueItems);
5065
+
5066
+ const activeIndex = items.length === 0
5067
+ ? -1
5068
+ : input.activeItemId
5069
+ ? items.findIndex((item) => item.itemId === input.activeItemId)
5070
+ : items.findIndex((item) => item.actionable);
5071
+ const openCount = items.filter((item) => item.status === "open").length;
5072
+
5073
+ return {
5074
+ totalCount: items.length,
5075
+ openCount,
5076
+ activeIndex: activeIndex >= 0 ? activeIndex : items.length === 0 ? -1 : 0,
5077
+ items,
5078
+ };
5079
+ }
5080
+
5081
+ function navigateReviewQueue(
5082
+ runtime: WordReviewEditorRuntime,
5083
+ direction: "next" | "previous",
5084
+ ): ReviewQueueItem | null {
5085
+ const queue = deriveReviewQueueSnapshot({
5086
+ sections: runtime.getSections(),
5087
+ comments: runtime.getRenderSnapshot().comments.threads,
5088
+ trackedChanges: runtime.getRenderSnapshot().trackedChanges.revisions,
5089
+ hostAnnotations: runtime.getHostAnnotationSnapshot().annotations,
5090
+ });
5091
+ const nextItem = getAdjacentReviewQueueItem(queue, null, direction);
5092
+ if (!nextItem) {
5093
+ return null;
5094
+ }
5095
+ focusReviewQueueItem(runtime, nextItem);
5096
+ return nextItem;
5097
+ }
5098
+
5099
+ function getAdjacentReviewQueueItem(
5100
+ queue: ReviewQueueSnapshot,
5101
+ activeItemId: string | null,
5102
+ direction: "next" | "previous",
5103
+ ): ReviewQueueItem | null {
5104
+ if (queue.items.length === 0) {
5105
+ return null;
5106
+ }
5107
+ const currentIndex = activeItemId
5108
+ ? queue.items.findIndex((item) => item.itemId === activeItemId)
5109
+ : queue.activeIndex;
5110
+ const safeIndex = currentIndex >= 0 ? currentIndex : 0;
5111
+ const nextIndex =
5112
+ direction === "next"
5113
+ ? (safeIndex + 1) % queue.items.length
5114
+ : (safeIndex - 1 + queue.items.length) % queue.items.length;
5115
+ return queue.items[nextIndex] ?? queue.items[0] ?? null;
5116
+ }
5117
+
5118
+ function focusReviewQueueItem(
5119
+ runtime: WordReviewEditorRuntime,
5120
+ item: ReviewQueueItem,
5121
+ ): void {
5122
+ if (item.kind === "comment") {
5123
+ runtime.openComment(item.itemId.replace(/^comment:/u, ""));
5124
+ }
5125
+ if (item.kind === "change") {
5126
+ const revisionId = item.itemId.replace(/^change:/u, "");
5127
+ const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
5128
+ (entry) => entry.revisionId === revisionId,
5129
+ );
5130
+ if (revision?.storyTarget) {
5131
+ runtime.openStory(revision.storyTarget);
5132
+ }
5133
+ }
5134
+ applyRuntimeSelection(
5135
+ runtime,
5136
+ createSelectionFromAnchor(item.anchor, item.storyTarget),
5137
+ );
5138
+ }
5139
+
5140
+ function markSectionForReview(
5141
+ runtime: WordReviewEditorRuntime,
5142
+ input?: { sectionIndex?: number; label?: string },
5143
+ ): string | null {
5144
+ const targetIndex =
5145
+ input?.sectionIndex ?? runtime.getDocumentNavigationSnapshot().activeSectionIndex;
5146
+ const section = runtime.getSectionSnapshot({ sectionIndex: targetIndex });
5147
+ if (!section) {
5148
+ return null;
5149
+ }
5150
+ const annotationId = createReviewSectionMarkId(section.sectionIndex);
5151
+ runtime.setHostAnnotationOverlay({
5152
+ overlayVersion: "host-annotation-overlay/1",
5153
+ annotations: [
5154
+ ...runtime.getHostAnnotationSnapshot().annotations.filter(
5155
+ (annotation) => annotation.annotationId !== annotationId,
5156
+ ),
5157
+ {
5158
+ annotationId,
5159
+ kind: "note",
5160
+ label:
5161
+ input?.label ??
5162
+ section.primaryHeadingText ??
5163
+ `Section ${section.sectionIndex + 1}`,
5164
+ anchor: section.anchor,
5165
+ provenance: "host",
5166
+ detail: "Marked for review",
5167
+ },
5168
+ ],
5169
+ });
5170
+ return annotationId;
5171
+ }
5172
+
5173
+ function clearSectionReviewMark(
5174
+ runtime: WordReviewEditorRuntime,
5175
+ annotationId: string,
5176
+ ): void {
5177
+ const nextAnnotations = runtime
5178
+ .getHostAnnotationSnapshot()
5179
+ .annotations.filter((annotation) => annotation.annotationId !== annotationId);
5180
+ if (nextAnnotations.length === 0) {
5181
+ runtime.clearHostAnnotationOverlay();
5182
+ return;
5183
+ }
5184
+ runtime.setHostAnnotationOverlay({
5185
+ overlayVersion: "host-annotation-overlay/1",
5186
+ annotations: nextAnnotations,
5187
+ });
5188
+ }
5189
+
5190
+ function mergeHostAndReviewSectionAnnotations(
5191
+ hostAnnotations: HostAnnotationItem[],
5192
+ reviewSectionMarks: Array<{ annotationId: string; sectionIndex: number; label: string }>,
5193
+ sections: Array<{
5194
+ sectionIndex: number;
5195
+ anchor: EditorAnchorProjection;
5196
+ }>,
5197
+ ): HostAnnotationItem[] {
5198
+ const reviewAnnotations = reviewSectionMarks.flatMap((entry) => {
5199
+ const section = sections.find((candidate) => candidate.sectionIndex === entry.sectionIndex);
5200
+ if (!section) {
5201
+ return [];
5202
+ }
5203
+ return [{
5204
+ annotationId: entry.annotationId,
5205
+ kind: "note" as const,
5206
+ label: entry.label,
5207
+ anchor: section.anchor,
5208
+ provenance: "host" as const,
5209
+ detail: "Marked for review",
5210
+ }];
5211
+ });
5212
+ return [...hostAnnotations, ...reviewAnnotations];
5213
+ }
5214
+
5215
+ function isSectionReviewAnnotation(annotation: HostAnnotationItem): boolean {
5216
+ return annotation.kind === "note" && annotation.detail === "Marked for review";
5217
+ }
5218
+
5219
+ function createReviewSectionMarkId(sectionIndex: number): string {
5220
+ return `review-section:${sectionIndex}`;
5221
+ }
5222
+
5223
+ function findSectionForAnchor(
5224
+ sections: Array<{
5225
+ sectionIndex: number;
5226
+ pageRange: { start: number; end: number };
5227
+ anchor: EditorAnchorProjection;
5228
+ }>,
5229
+ anchor: EditorAnchorProjection,
5230
+ ) {
5231
+ const anchorStart = getAnchorStart(anchor);
5232
+ return sections.find((section) => {
5233
+ const sectionStart = getAnchorStart(section.anchor);
5234
+ const sectionEnd = getAnchorEnd(section.anchor);
5235
+ return anchorStart >= sectionStart && anchorStart <= sectionEnd;
5236
+ });
5237
+ }
5238
+
5239
+ function compareReviewQueueItems(left: ReviewQueueItem, right: ReviewQueueItem): number {
5240
+ const leftSection = left.sectionIndex ?? Number.MAX_SAFE_INTEGER;
5241
+ const rightSection = right.sectionIndex ?? Number.MAX_SAFE_INTEGER;
5242
+ if (leftSection !== rightSection) {
5243
+ return leftSection - rightSection;
5244
+ }
5245
+ return getAnchorStart(left.anchor) - getAnchorStart(right.anchor);
5246
+ }
5247
+
5248
+ function getAnchorStart(anchor: EditorAnchorProjection): number {
5249
+ switch (anchor.kind) {
5250
+ case "range":
5251
+ return anchor.from;
5252
+ case "node":
5253
+ return anchor.at;
5254
+ case "detached":
5255
+ return anchor.lastKnownRange?.from ?? Number.MAX_SAFE_INTEGER;
5256
+ }
5257
+ }
5258
+
5259
+ function getAnchorEnd(anchor: EditorAnchorProjection): number {
5260
+ switch (anchor.kind) {
5261
+ case "range":
5262
+ return anchor.to;
5263
+ case "node":
5264
+ return anchor.at;
5265
+ case "detached":
5266
+ return anchor.lastKnownRange?.to ?? Number.MAX_SAFE_INTEGER;
5267
+ }
5268
+ }