@beyondwork/docx-react-component 1.0.57 → 1.0.59

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 (135) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/src/api/awareness-identity-types.ts +4 -2
  4. package/src/api/comment-negotiation-types.ts +4 -1
  5. package/src/api/external-custody-types.ts +16 -0
  6. package/src/api/internal/build-ref-projections.ts +108 -0
  7. package/src/api/package-version.ts +1 -1
  8. package/src/api/participants-types.ts +11 -1
  9. package/src/api/public-types.ts +1149 -8
  10. package/src/api/scope-metadata-resolver-types.ts +6 -0
  11. package/src/compare/diff-engine.ts +3 -0
  12. package/src/core/commands/formatting-commands.ts +1 -0
  13. package/src/core/commands/index.ts +225 -16
  14. package/src/core/commands/legacy-form-field-commands.ts +181 -0
  15. package/src/core/commands/table-structure-commands.ts +149 -31
  16. package/src/core/selection/mapping.ts +20 -0
  17. package/src/core/state/editor-state.ts +2 -1
  18. package/src/index.ts +28 -0
  19. package/src/io/docx-session.ts +22 -3
  20. package/src/io/export/export-session.ts +11 -7
  21. package/src/io/export/ooxml-namespaces.ts +47 -0
  22. package/src/io/export/reattach-preserved-parts.ts +4 -16
  23. package/src/io/export/serialize-comments.ts +3 -131
  24. package/src/io/export/serialize-ffdata.ts +89 -0
  25. package/src/io/export/serialize-headers-footers.ts +5 -0
  26. package/src/io/export/serialize-main-document.ts +224 -34
  27. package/src/io/export/serialize-numbering.ts +22 -2
  28. package/src/io/export/serialize-revisions.ts +99 -0
  29. package/src/io/export/serialize-tables.ts +9 -0
  30. package/src/io/export/split-review-boundaries.ts +1 -0
  31. package/src/io/export/table-properties-xml.ts +14 -0
  32. package/src/io/load-scheduler.ts +70 -28
  33. package/src/io/normalize/normalize-text.ts +13 -0
  34. package/src/io/ooxml/_mini-xml.ts +198 -0
  35. package/src/io/ooxml/canonicalize-payload.ts +1 -4
  36. package/src/io/ooxml/chart/chart-style-table.ts +4 -3
  37. package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
  38. package/src/io/ooxml/chart/parse-series.ts +2 -1
  39. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  40. package/src/io/ooxml/chart/types.ts +6 -434
  41. package/src/io/ooxml/comment-presentation-payload.ts +6 -5
  42. package/src/io/ooxml/highlight-colors.ts +8 -5
  43. package/src/io/ooxml/parse-anchor.ts +68 -53
  44. package/src/io/ooxml/parse-comments.ts +14 -142
  45. package/src/io/ooxml/parse-complex-content.ts +3 -106
  46. package/src/io/ooxml/parse-drawing.ts +100 -195
  47. package/src/io/ooxml/parse-ffdata.ts +93 -0
  48. package/src/io/ooxml/parse-fields.ts +7 -146
  49. package/src/io/ooxml/parse-fill.ts +88 -8
  50. package/src/io/ooxml/parse-font-table.ts +5 -105
  51. package/src/io/ooxml/parse-footnotes.ts +28 -152
  52. package/src/io/ooxml/parse-headers-footers.ts +106 -212
  53. package/src/io/ooxml/parse-inline-media.ts +3 -200
  54. package/src/io/ooxml/parse-main-document.ts +180 -217
  55. package/src/io/ooxml/parse-numbering.ts +154 -335
  56. package/src/io/ooxml/parse-object.ts +147 -0
  57. package/src/io/ooxml/parse-ole-relationship.ts +82 -0
  58. package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
  59. package/src/io/ooxml/parse-picture-sdt.ts +85 -0
  60. package/src/io/ooxml/parse-picture.ts +120 -39
  61. package/src/io/ooxml/parse-revisions.ts +285 -51
  62. package/src/io/ooxml/parse-settings.ts +6 -99
  63. package/src/io/ooxml/parse-shapes.ts +25 -140
  64. package/src/io/ooxml/parse-styles.ts +3 -218
  65. package/src/io/ooxml/parse-tables.ts +76 -256
  66. package/src/io/ooxml/parse-theme.ts +1 -4
  67. package/src/io/ooxml/property-grab-bag.ts +5 -47
  68. package/src/io/ooxml/xml-element-serialize.ts +32 -0
  69. package/src/io/ooxml/xml-parser.ts +183 -0
  70. package/src/legal/bookmarks.ts +1 -1
  71. package/src/legal/cross-references.ts +1 -1
  72. package/src/legal/defined-terms.ts +1 -1
  73. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  74. package/src/legal/signature-blocks.ts +1 -1
  75. package/src/model/canonical-document.ts +165 -6
  76. package/src/model/chart-types.ts +439 -0
  77. package/src/model/snapshot.ts +3 -1
  78. package/src/review/store/comment-remapping.ts +24 -11
  79. package/src/review/store/revision-actions.ts +482 -2
  80. package/src/review/store/revision-store.ts +15 -0
  81. package/src/review/store/revision-types.ts +76 -0
  82. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  83. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  84. package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
  85. package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
  86. package/src/runtime/document-runtime.ts +544 -35
  87. package/src/runtime/document-search.ts +176 -0
  88. package/src/runtime/edit-ops/index.ts +18 -2
  89. package/src/runtime/footnote-resolver.ts +130 -0
  90. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  91. package/src/runtime/layout/layout-engine-version.ts +37 -1
  92. package/src/runtime/layout/page-graph.ts +14 -1
  93. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  94. package/src/runtime/numbering-prefix.ts +17 -0
  95. package/src/runtime/query-scopes.ts +183 -0
  96. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  97. package/src/runtime/revision-runtime.ts +27 -1
  98. package/src/runtime/scope-resolver.ts +60 -0
  99. package/src/runtime/selection/post-edit-validator.ts +60 -6
  100. package/src/runtime/structure-ops/index.ts +20 -4
  101. package/src/runtime/surface-projection.ts +293 -18
  102. package/src/runtime/table-schema.ts +6 -0
  103. package/src/runtime/theme-color-resolver.ts +2 -2
  104. package/src/runtime/units.ts +9 -0
  105. package/src/runtime/workflow-rail-segments.ts +4 -0
  106. package/src/ui/WordReviewEditor.tsx +258 -44
  107. package/src/ui/editor-runtime-boundary.ts +13 -0
  108. package/src/ui/editor-shell-view.tsx +4 -1
  109. package/src/ui/headless/chrome-registry.ts +53 -0
  110. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  111. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  112. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  113. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  114. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  115. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  116. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  117. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
  118. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
  119. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  120. package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
  121. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
  122. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  124. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  125. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  126. package/src/ui-tailwind/index.ts +9 -0
  127. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  128. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  129. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  130. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  131. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  132. package/src/ui-tailwind/theme/tokens.ts +14 -0
  133. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  134. package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
  135. package/src/validation/diagnostics.ts +1 -0
@@ -44,6 +44,7 @@ import type {
44
44
  SectionLayoutPatch,
45
45
  SectionPageNumberingPatch,
46
46
  SearchOptions,
47
+ SetSelectionOptions,
47
48
  SearchResultSnapshot,
48
49
  SelectionSnapshot as PublicSelectionSnapshot,
49
50
  SuggestionEntrySnapshot,
@@ -76,6 +77,7 @@ import type {
76
77
  ZoomLevel,
77
78
  } from "../api/public-types";
78
79
  import { MetadataResolverMissingError } from "../api/public-types";
80
+ import { buildRefProjections } from "../api/internal/build-ref-projections.ts";
79
81
  import { readHarnessDebugPortsFlag } from "../internal/harness-debug-ports.ts";
80
82
  import type { ScopeMetadataResolver } from "../api/scope-metadata-resolver-types.ts";
81
83
  import {
@@ -151,7 +153,7 @@ import {
151
153
  } from "../io/source-package-provenance.ts";
152
154
  import { readOpcPackage } from "../io/opc/package-reader.ts";
153
155
  import { deriveCapabilities } from "../runtime/session-capabilities";
154
- import { searchDocument } from "../runtime/document-search.ts";
156
+ import { findTextMatches, searchDocument } from "../runtime/document-search.ts";
155
157
  import {
156
158
  resolveCurrentContextAnalyticsQuery,
157
159
  runtimeContextAnalyticsSnapshotsEqual,
@@ -189,6 +191,7 @@ import type {
189
191
  SuggestionCardModel,
190
192
  } from "./headless/selection-toolbar-model";
191
193
  import { resolveActiveSelectionTool } from "./headless/selection-tool-resolver";
194
+ import { resolveSelectionToolRegistry } from "./headless/chrome-registry";
192
195
  import { type EditorCommandBag, useCommandBag } from "./editor-command-bag.ts";
193
196
  import {
194
197
  resolveHeadingShortcutStyleId,
@@ -566,7 +569,16 @@ export function __createWordReviewEditorRefBridge(
566
569
  // Pending-conflict queue for this bridge instance. Keyed by conflictKey(...).
567
570
  const pendingConflicts = new Map<string, PendingConflict>();
568
571
 
569
- return {
572
+ // v2.0.0 Track D — purpose-grouped projections dispatch via this holder
573
+ // so they stay in lockstep with the ref even if a rebuild is triggered.
574
+ const refHolder: { current: WordReviewEditorRef | null } = { current: null };
575
+ const projections = buildRefProjections(() => {
576
+ const r = refHolder.current;
577
+ if (!r) throw new Error("ref projection used before ref initialization");
578
+ return r;
579
+ });
580
+
581
+ const refValue: WordReviewEditorRef = {
570
582
  focus: () => runtime.focus(),
571
583
  blur: () => runtime.blur(),
572
584
  undo: () => runtime.undo(),
@@ -595,6 +607,12 @@ export function __createWordReviewEditorRefBridge(
595
607
  addScope: (params) => runtime.addScope(params),
596
608
  getScope: (scopeId) => runtime.getScope(scopeId),
597
609
  removeScope: (scopeId) => runtime.removeScope(scopeId),
610
+ addInvisibleScope: (params) => runtime.addInvisibleScope(params),
611
+ setScopeVisibility: (scopeId, visibility) => runtime.setScopeVisibility(scopeId, visibility),
612
+ getScopeVisibility: (scopeId) => runtime.getScopeVisibility(scopeId),
613
+ setScopeChromeVisibility: (state) => runtime.setScopeChromeVisibility(state),
614
+ getScopeChromeVisibility: () => runtime.getScopeChromeVisibility(),
615
+ subscribeToScopeQuery: (filter, callback) => runtime.subscribeToScopeQuery(filter, callback),
598
616
  acceptChange: (changeId) => runtime.acceptChange(changeId),
599
617
  rejectChange: (changeId) => runtime.rejectChange(changeId),
600
618
  acceptAllChanges: () => runtime.acceptAllChanges(),
@@ -760,7 +778,7 @@ export function __createWordReviewEditorRefBridge(
760
778
  clearSearch: () => {
761
779
  mountedSurface?.clearSearch();
762
780
  },
763
- setSelection: (selection) => {
781
+ setSelection: (selection, _options?: SetSelectionOptions) => {
764
782
  applyRuntimeSelection(
765
783
  runtime,
766
784
  normalizeRequestedSelection(runtime.getRenderSnapshot(), selection),
@@ -909,6 +927,36 @@ export function __createWordReviewEditorRefBridge(
909
927
  getWorkflowMetadataSnapshot: () => {
910
928
  return clonePublicValue(runtime.getWorkflowMetadataSnapshot());
911
929
  },
930
+ queryScopes: (filter) => {
931
+ return clonePublicValue(runtime.queryScopes(filter));
932
+ },
933
+ findScopesAt: (position, options) => {
934
+ return clonePublicValue(runtime.findScopesAt(position, options));
935
+ },
936
+ findScopesIntersecting: (range, options) => {
937
+ return clonePublicValue(runtime.findScopesIntersecting(range, options));
938
+ },
939
+ findFirstText: (query, opts) => {
940
+ const hits = findTextMatchesForRuntime(runtime, query, opts);
941
+ return hits.length > 0 ? (hits[0] ?? null) : null;
942
+ },
943
+ findAllText: (query, opts) => {
944
+ return findTextMatchesForRuntime(runtime, query, opts);
945
+ },
946
+ selectFirstText: (query, opts) => {
947
+ const hits = findTextMatchesForRuntime(runtime, query, opts);
948
+ if (hits.length === 0) return false;
949
+ applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
950
+ return true;
951
+ },
952
+ selectAllText: (query, opts) => {
953
+ const hits = findTextMatchesForRuntime(runtime, query, opts);
954
+ if (hits.length === 0) return 0;
955
+ applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
956
+ return hits.length;
957
+ },
958
+ findTextWithStyle: (query, filter, opts) => runtime.findTextWithStyle(query, filter, opts),
959
+ selectTextWithStyle: (query, filter, opts) => runtime.selectTextWithStyle(query, filter, opts),
912
960
  // P17 — metadata persistence toggle + convert methods.
913
961
  setMetadataPersistenceMode: (mode) => {
914
962
  if (mode === "external" && !(options?.resolverRef?.current ?? null)) {
@@ -1112,7 +1160,10 @@ export function __createWordReviewEditorRefBridge(
1112
1160
  replaceWorkflowMarkupText: (markupId, text) => {
1113
1161
  runtime.replaceWorkflowMarkupText(markupId, text);
1114
1162
  },
1163
+ ...projections,
1115
1164
  };
1165
+ refHolder.current = refValue;
1166
+ return refValue;
1116
1167
  }
1117
1168
 
1118
1169
  export function __applyRuntimeTextCommand(
@@ -1126,6 +1177,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1126
1177
  function WordReviewEditor(props, ref) {
1127
1178
  const {
1128
1179
  currentUser,
1180
+ shellHeader,
1129
1181
  ydoc,
1130
1182
  awareness,
1131
1183
  hostAdapter,
@@ -1163,6 +1215,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1163
1215
  suggestionsEnabled = false,
1164
1216
  showReviewPanel = true,
1165
1217
  chromeVisibility,
1218
+ density,
1219
+ customSelectionTools,
1166
1220
  } = props;
1167
1221
 
1168
1222
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
@@ -1190,6 +1244,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1190
1244
  new Map<string, { choice: string; mergedValue?: Record<string, unknown> }>(),
1191
1245
  );
1192
1246
  const metadataConflictPendingRef = useRef(new Map<string, PendingConflict>());
1247
+ const suppressNextAwarenessPublishRef = useRef(false);
1193
1248
  const {
1194
1249
  runtime,
1195
1250
  loadError,
@@ -1444,6 +1499,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1444
1499
  activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
1445
1500
  }, [activeRuntime, suggestionsEnabled]);
1446
1501
 
1502
+ // design-close-chrome Phase 2 — density contract (designsystem §4.2).
1503
+ // When the `density` prop is supplied, drive the root `data-density`
1504
+ // attribute that powers the `--space-density-multiplier` cascade in
1505
+ // `tokens.css`. When the prop is omitted, leave the attribute alone
1506
+ // so the host (or `useDensity()`) retains control — the prop is
1507
+ // strictly an opt-in override, mirroring React's controlled vs.
1508
+ // uncontrolled input pattern. On unmount the attribute is left in
1509
+ // place so a subsequent host-managed setter (e.g. the density picker
1510
+ // in the shell view menu) can persist without a flicker.
1511
+ useEffect(() => {
1512
+ if (density === undefined) return;
1513
+ if (typeof document === "undefined") return;
1514
+ document.documentElement.dataset.density = density;
1515
+ }, [density]);
1516
+
1447
1517
  const markCurrentSectionForReview = useCallback((input?: {
1448
1518
  sectionIndex?: number;
1449
1519
  label?: string;
@@ -1540,14 +1610,18 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1540
1610
  return;
1541
1611
  }
1542
1612
 
1543
- setLocalCursorState(awareness, {
1544
- userId: currentUser.userId,
1545
- displayName: currentUser.displayName,
1546
- color: getCursorColorForUser(currentUser.userId),
1547
- anchor: snapshot.selection.anchor,
1548
- head: snapshot.selection.head,
1549
- storyTarget: snapshot.activeStory,
1550
- });
1613
+ if (suppressNextAwarenessPublishRef.current) {
1614
+ suppressNextAwarenessPublishRef.current = false;
1615
+ } else {
1616
+ setLocalCursorState(awareness, {
1617
+ userId: currentUser.userId,
1618
+ displayName: currentUser.displayName,
1619
+ color: getCursorColorForUser(currentUser.userId),
1620
+ anchor: snapshot.selection.anchor,
1621
+ head: snapshot.selection.head,
1622
+ storyTarget: snapshot.activeStory,
1623
+ });
1624
+ }
1551
1625
  }, [
1552
1626
  awareness,
1553
1627
  currentUser.displayName,
@@ -1572,7 +1646,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1572
1646
 
1573
1647
  useImperativeHandle(
1574
1648
  ref,
1575
- () => ({
1649
+ () => {
1650
+ // v2.0.0 Track D — projections dispatch through a holder so they
1651
+ // always point at the freshly-built ref on every
1652
+ // useImperativeHandle re-run.
1653
+ const refHolder: { current: WordReviewEditorRef | null } = { current: null };
1654
+ const projections = buildRefProjections(() => {
1655
+ const r = refHolder.current;
1656
+ if (!r) throw new Error("ref projection used before ref initialization");
1657
+ return r;
1658
+ });
1659
+ const refValue: WordReviewEditorRef = ({
1576
1660
  focus: () => activeRuntime.focus(),
1577
1661
  blur: () => activeRuntime.blur(),
1578
1662
  undo: () => activeRuntime.undo(),
@@ -1607,6 +1691,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1607
1691
  addScope: (params) => activeRuntime.addScope(params),
1608
1692
  getScope: (scopeId) => activeRuntime.getScope(scopeId),
1609
1693
  removeScope: (scopeId) => activeRuntime.removeScope(scopeId),
1694
+ addInvisibleScope: (params) => activeRuntime.addInvisibleScope(params),
1695
+ setScopeVisibility: (scopeId, visibility) => activeRuntime.setScopeVisibility(scopeId, visibility),
1696
+ getScopeVisibility: (scopeId) => activeRuntime.getScopeVisibility(scopeId),
1697
+ setScopeChromeVisibility: (state) => activeRuntime.setScopeChromeVisibility(state),
1698
+ getScopeChromeVisibility: () => activeRuntime.getScopeChromeVisibility(),
1699
+ subscribeToScopeQuery: (filter, callback) => activeRuntime.subscribeToScopeQuery(filter, callback),
1610
1700
  acceptChange: (changeId) => activeRuntime.acceptChange(changeId),
1611
1701
  rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
1612
1702
  acceptAllChanges: () => activeRuntime.acceptAllChanges(),
@@ -1821,11 +1911,22 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1821
1911
  clearSearch: () => {
1822
1912
  surfaceRef.current?.clearSearch();
1823
1913
  },
1824
- setSelection: (selection) => {
1825
- applyRuntimeSelection(
1826
- activeRuntime,
1827
- normalizeRequestedSelection(activeRuntime.getRenderSnapshot(), selection),
1828
- );
1914
+ setSelection: (selection, options?: SetSelectionOptions) => {
1915
+ const snap = activeRuntime.getRenderSnapshot();
1916
+ const normalized = normalizeRequestedSelection(snap, selection);
1917
+ if (options?.silent) {
1918
+ // Only suppress awareness publish if the selection will actually change.
1919
+ // If positions are identical the cursor-state useEffect won't re-run,
1920
+ // which would leave the flag set and silently suppress the next real
1921
+ // selection change (stale-flag bug).
1922
+ if (
1923
+ normalized.anchor !== snap.selection.anchor ||
1924
+ normalized.head !== snap.selection.head
1925
+ ) {
1926
+ suppressNextAwarenessPublishRef.current = true;
1927
+ }
1928
+ }
1929
+ applyRuntimeSelection(activeRuntime, normalized);
1829
1930
  },
1830
1931
  scrollToRevision: (revisionId: string) => {
1831
1932
  const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
@@ -1976,6 +2077,36 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1976
2077
  getWorkflowMetadataSnapshot: () => {
1977
2078
  return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
1978
2079
  },
2080
+ queryScopes: (filter) => {
2081
+ return clonePublicValue(activeRuntime.queryScopes(filter));
2082
+ },
2083
+ findScopesAt: (position, options) => {
2084
+ return clonePublicValue(activeRuntime.findScopesAt(position, options));
2085
+ },
2086
+ findScopesIntersecting: (range, options) => {
2087
+ return clonePublicValue(activeRuntime.findScopesIntersecting(range, options));
2088
+ },
2089
+ findFirstText: (query, opts) => {
2090
+ const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
2091
+ return hits.length > 0 ? (hits[0] ?? null) : null;
2092
+ },
2093
+ findAllText: (query, opts) => {
2094
+ return findTextMatchesForRuntime(activeRuntime, query, opts);
2095
+ },
2096
+ selectFirstText: (query, opts) => {
2097
+ const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
2098
+ if (hits.length === 0) return false;
2099
+ applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
2100
+ return true;
2101
+ },
2102
+ selectAllText: (query, opts) => {
2103
+ const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
2104
+ if (hits.length === 0) return 0;
2105
+ applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
2106
+ return hits.length;
2107
+ },
2108
+ findTextWithStyle: (query, filter, opts) => activeRuntime.findTextWithStyle(query, filter, opts),
2109
+ selectTextWithStyle: (query, filter, opts) => activeRuntime.selectTextWithStyle(query, filter, opts),
1979
2110
  // P17 — metadata persistence toggle + convert methods.
1980
2111
  setMetadataPersistenceMode: (mode) => {
1981
2112
  if (mode === "external" && scopeMetadataResolverRef.current === null) {
@@ -2170,7 +2301,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2170
2301
  replaceWorkflowMarkupText: (markupId, text) => {
2171
2302
  activeRuntime.replaceWorkflowMarkupText(markupId, text);
2172
2303
  },
2173
- }),
2304
+ ...projections,
2305
+ }) as WordReviewEditorRef;
2306
+ refHolder.current = refValue;
2307
+ return refValue;
2308
+ },
2174
2309
  [
2175
2310
  activeRuntime,
2176
2311
  clearReviewSectionMarkById,
@@ -2484,29 +2619,40 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2484
2619
  suggestionsSnapshot,
2485
2620
  viewState.activeStory,
2486
2621
  ]);
2487
- const activeSelectionTool = resolveActiveSelectionTool({
2488
- snapshot,
2489
- viewState,
2490
- capabilities,
2491
- documentNavigation,
2492
- styleCatalog,
2493
- formattingState,
2494
- workflowScopeSnapshot,
2495
- interactionGuardSnapshot,
2496
- workflowMarkupSnapshot: workflowMarkupSnapshot ?? undefined,
2497
- suggestionsSnapshot,
2498
- activeRevisionId,
2499
- activeCommentId: snapshot.comments.activeCommentId,
2500
- activeCommentThread,
2501
- activeTableContext,
2502
- activeImageContext,
2503
- activeObjectContext,
2504
- activeListContext: viewState.activeListContext,
2505
- preferListStructureContext: viewState.workspaceMode === "page",
2506
- addCommentDisabledReason,
2507
- suppressedSuggestionRevisionId,
2508
- scopedChromePolicy,
2509
- });
2622
+ // R9 host may supply custom selection-tool registry entries; merge
2623
+ // with the default before resolving. The merger is a pure function
2624
+ // returning the canonical default array by reference when `custom`
2625
+ // is empty, so the typical no-custom case is a single identity call.
2626
+ const selectionToolRegistry = useMemo(
2627
+ () => resolveSelectionToolRegistry(customSelectionTools),
2628
+ [customSelectionTools],
2629
+ );
2630
+ const activeSelectionTool = resolveActiveSelectionTool(
2631
+ {
2632
+ snapshot,
2633
+ viewState,
2634
+ capabilities,
2635
+ documentNavigation,
2636
+ styleCatalog,
2637
+ formattingState,
2638
+ workflowScopeSnapshot,
2639
+ interactionGuardSnapshot,
2640
+ workflowMarkupSnapshot: workflowMarkupSnapshot ?? undefined,
2641
+ suggestionsSnapshot,
2642
+ activeRevisionId,
2643
+ activeCommentId: snapshot.comments.activeCommentId,
2644
+ activeCommentThread,
2645
+ activeTableContext,
2646
+ activeImageContext,
2647
+ activeObjectContext,
2648
+ activeListContext: viewState.activeListContext,
2649
+ preferListStructureContext: viewState.workspaceMode === "page",
2650
+ addCommentDisabledReason,
2651
+ suppressedSuggestionRevisionId,
2652
+ scopedChromePolicy,
2653
+ },
2654
+ selectionToolRegistry,
2655
+ );
2510
2656
  const selectionToolbarSelectionKey = useMemo(
2511
2657
  () =>
2512
2658
  createSelectionToolbarSelectionKey(
@@ -3237,6 +3383,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3237
3383
  )}
3238
3384
  visuallyHiddenStyles={VISUALLY_HIDDEN_STYLES}
3239
3385
  onShellKeyDownCapture={handleShellKeyDownCapture}
3386
+ shellHeader={shellHeader}
3240
3387
  viewState={viewState}
3241
3388
  markupDisplay={liveMarkupDisplay}
3242
3389
  currentUserId={currentUser.userId}
@@ -3339,6 +3486,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3339
3486
  onScopeRejectSuggestionGroup={(payload) => {
3340
3487
  applySuggestionGroupAction(activeRuntime, payload.groupId, "reject");
3341
3488
  }}
3489
+ onDeselectObject={() => activeRuntime.deselectObject()}
3342
3490
  onScopeAskAgent={(payload) => {
3343
3491
  // Resolve the scope's anchor + story from the facet's card
3344
3492
  // model so the agent request carries the canonical range.
@@ -3393,13 +3541,28 @@ function applySuggestionGroupAction(
3393
3541
  ): void {
3394
3542
  const snapshot = runtime.getSuggestionsSnapshot();
3395
3543
  const group = snapshot.groups?.find((entry) => entry.groupId === groupId);
3396
- if (!group) return;
3544
+ const op = action === "accept" ? "acceptSuggestionGroup" : "rejectSuggestionGroup";
3545
+ if (!group) {
3546
+ runtime.emitTransientWarning({
3547
+ warningId: `suggestion-group-unknown-${groupId}-${Date.now()}`,
3548
+ code: "review_target_not_found",
3549
+ severity: "info",
3550
+ message: `${op}("${groupId}") skipped: unknown groupId.`,
3551
+ source: "review",
3552
+ details: { op, targetId: groupId, reason: "group_unknown" },
3553
+ });
3554
+ return;
3555
+ }
3397
3556
  const byId = new Map(
3398
3557
  snapshot.suggestions.map((entry) => [entry.suggestionId, entry]),
3399
3558
  );
3559
+ const skippedSuggestions: string[] = [];
3400
3560
  for (const suggestionId of group.suggestionIds) {
3401
3561
  const suggestion = byId.get(suggestionId);
3402
- if (!suggestion) continue;
3562
+ if (!suggestion) {
3563
+ skippedSuggestions.push(suggestionId);
3564
+ continue;
3565
+ }
3403
3566
  for (const changeId of suggestion.changeIds) {
3404
3567
  if (action === "accept") {
3405
3568
  runtime.acceptChange(changeId);
@@ -3408,6 +3571,21 @@ function applySuggestionGroupAction(
3408
3571
  }
3409
3572
  }
3410
3573
  }
3574
+ if (skippedSuggestions.length > 0) {
3575
+ runtime.emitTransientWarning({
3576
+ warningId: `suggestion-group-stale-${groupId}-${Date.now()}`,
3577
+ code: "review_target_not_found",
3578
+ severity: "info",
3579
+ message: `${op}("${groupId}") partially skipped: ${skippedSuggestions.length} suggestion(s) no longer in snapshot.`,
3580
+ source: "review",
3581
+ details: {
3582
+ op,
3583
+ targetId: groupId,
3584
+ reason: "suggestion_stale",
3585
+ skippedSuggestionIds: skippedSuggestions,
3586
+ },
3587
+ });
3588
+ }
3411
3589
  }
3412
3590
 
3413
3591
  function applyRuntimeFormattingOperation(
@@ -4980,12 +5158,34 @@ function applyRuntimeDeleteComment(
4980
5158
  commentId: string,
4981
5159
  ): void {
4982
5160
  const snapshot = runtime.getRenderSnapshot();
4983
- if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
5161
+ // Pre-ready / fatal states stay silent: the host called too early, and
5162
+ // there is no meaningful document yet to signal against. Emitting a
5163
+ // warning here would only add noise to load-time error handling.
5164
+ if (!snapshot.isReady || snapshot.fatalError) {
5165
+ return;
5166
+ }
5167
+ if (snapshot.readOnly) {
5168
+ runtime.emitTransientWarning({
5169
+ warningId: `delete-comment-readonly-${commentId}-${Date.now()}`,
5170
+ code: "review_target_not_found",
5171
+ severity: "info",
5172
+ message: `deleteComment("${commentId}") skipped: editor is read-only.`,
5173
+ source: "review",
5174
+ details: { op: "deleteComment", targetId: commentId, reason: "read_only" },
5175
+ });
4984
5176
  return;
4985
5177
  }
4986
5178
 
4987
5179
  const sessionState = runtime.getSessionState();
4988
5180
  if (!sessionState.canonicalDocument.review.comments[commentId]) {
5181
+ runtime.emitTransientWarning({
5182
+ warningId: `delete-comment-unknown-${commentId}-${Date.now()}`,
5183
+ code: "review_target_not_found",
5184
+ severity: "info",
5185
+ message: `deleteComment("${commentId}") skipped: unknown commentId.`,
5186
+ source: "review",
5187
+ details: { op: "deleteComment", targetId: commentId, reason: "comment_unknown" },
5188
+ });
4989
5189
  return;
4990
5190
  }
4991
5191
 
@@ -5056,6 +5256,20 @@ function clonePublicValue<T>(value: T): T {
5056
5256
  return structuredClone(value);
5057
5257
  }
5058
5258
 
5259
+ function findTextMatchesForRuntime(
5260
+ runtime: WordReviewEditorRuntime,
5261
+ query: string,
5262
+ options: SearchOptions | undefined,
5263
+ ): EditorAnchorProjection[] {
5264
+ const snapshot = runtime.getRenderSnapshot();
5265
+ return findTextMatches(
5266
+ runtime.getSessionState().canonicalDocument,
5267
+ snapshot.selection,
5268
+ query,
5269
+ options ?? {},
5270
+ );
5271
+ }
5272
+
5059
5273
  /**
5060
5274
  * Open the correct header/footer story for a specific page. The page's
5061
5275
  * resolved `stories.header` / `stories.footer` already carries the
@@ -1015,6 +1015,7 @@ function createLoadingRuntimeBridge(input: {
1015
1015
  subscribe: () => () => undefined,
1016
1016
  subscribeToEvents: () => () => undefined,
1017
1017
  emitBlockedCommand: () => undefined,
1018
+ emitTransientWarning: () => undefined,
1018
1019
  getRenderSnapshot: () => {
1019
1020
  const progressive = input.progressiveSurfaceRef?.current;
1020
1021
  if (progressive == null) return input.snapshot;
@@ -1154,6 +1155,9 @@ function createLoadingRuntimeBridge(input: {
1154
1155
  definitions: [],
1155
1156
  entries: [],
1156
1157
  }),
1158
+ queryScopes: () => [],
1159
+ findScopesAt: () => [],
1160
+ findScopesIntersecting: () => [],
1157
1161
  setHostAnnotationOverlay: () => undefined,
1158
1162
  clearHostAnnotationOverlay: () => undefined,
1159
1163
  getHostAnnotationSnapshot: () => ({
@@ -1183,6 +1187,15 @@ function createLoadingRuntimeBridge(input: {
1183
1187
  resetPerfCounters: () => undefined,
1184
1188
  setVisibleBlockRange: () => undefined,
1185
1189
  requestViewportRefresh: () => undefined,
1190
+ addInvisibleScope: () => ({ scopeId: "", anchor: { kind: "range", from: 0, to: 0, assoc: { start: -1, end: 1 } } }),
1191
+ setScopeVisibility: () => undefined,
1192
+ getScopeVisibility: () => "visible" as const,
1193
+ setScopeChromeVisibility: () => undefined,
1194
+ getScopeChromeVisibility: () => ({ mode: "all" as const }),
1195
+ subscribeToScopeQuery: (_filter, _callback) => () => undefined,
1196
+ findAllText: () => [],
1197
+ findTextWithStyle: () => [],
1198
+ selectTextWithStyle: () => 0,
1186
1199
  };
1187
1200
  }
1188
1201
 
@@ -82,6 +82,7 @@ export interface EditorShellViewProps {
82
82
  selectionToolAnchor?: SelectionToolAnchor | null;
83
83
  documentNavigation?: DocumentNavigationSnapshot;
84
84
  commands: EditorCommandBag;
85
+ shellHeader?: ReactNode;
85
86
  document: ReactNode;
86
87
  onAddCommentFromSelection?: () => void;
87
88
  onAddCommentFromSuggestion?: () => void;
@@ -126,6 +127,8 @@ export interface EditorShellViewProps {
126
127
  }) => void;
127
128
  /** K2 — forwarded from workspace to WordReviewEditor. */
128
129
  onScopeAskAgent?: (payload: { scopeId: string }) => void;
130
+ /** N6 — deselects the currently grabbed object; wired to runtime.deselectObject(). */
131
+ onDeselectObject?: () => void;
129
132
  }
130
133
 
131
134
  export function EditorShellView(props: EditorShellViewProps) {
@@ -154,7 +157,7 @@ export function EditorShellView(props: EditorShellViewProps) {
154
157
  aria-describedby={`${accessibilityInstructionsId} ${accessibilityStatusId}${
155
158
  diagnosticsModeMessage ? ` ${accessibilityAlertId}` : ""
156
159
  }`}
157
- className="wre-editor relative h-full"
160
+ className="wre-editor wre-editor-root relative h-full"
158
161
  onKeyDownCapture={onShellKeyDownCapture}
159
162
  >
160
163
  <p id={accessibilityInstructionsId} style={visuallyHiddenStyles}>
@@ -102,6 +102,59 @@ export const SELECTION_TOOL_REGISTRY: ReadonlyArray<SelectionToolRegistryEntry>
102
102
  { id: "blocked-explainer", surfaces: ["selection-tool"], group: "blocked", precedence: 60 },
103
103
  ];
104
104
 
105
+ /**
106
+ * R10 — options for `resolveSelectionToolRegistry`.
107
+ *
108
+ * `remove` lets a host suppress a default kind entirely (the dedicated
109
+ * builder for that kind is skipped at resolve time). Used together
110
+ * with `custom` when the host wants to both add a new precedence and
111
+ * silence an existing one.
112
+ */
113
+ export interface ResolveSelectionToolRegistryOptions {
114
+ remove?: ReadonlyArray<SelectionToolRegistryEntry["id"]>;
115
+ }
116
+
117
+ /**
118
+ * Merge host-supplied selection-tool entries with the default registry.
119
+ *
120
+ * design-close-chrome Phase 4 / designsystem §6 extensibility. Pure
121
+ * function — callers supply `custom` as a stable array (memoize in
122
+ * React), this returns a fresh, precedence-sorted copy that the
123
+ * resolver can iterate. Dedupe rule: host entries win on id collision
124
+ * (they override default precedence for that id). When `options.remove`
125
+ * lists ids, those are filtered out of the default set before merging.
126
+ *
127
+ * Note: as of this Phase, the resolver builder matrix (see
128
+ * `buildSelectionToolFromRegistryEntry` in `selection-tool-resolver.ts`)
129
+ * still only understands the built-in `SelectionToolKind` union.
130
+ * Widening that union to accept host render functions is a downstream
131
+ * change coordinated with Lane 8 API ergonomics; the merger is shipped
132
+ * first so callers can depend on the final shape while the renderer
133
+ * catches up.
134
+ */
135
+ export function resolveSelectionToolRegistry(
136
+ custom?: ReadonlyArray<SelectionToolRegistryEntry>,
137
+ options?: ResolveSelectionToolRegistryOptions,
138
+ ): ReadonlyArray<SelectionToolRegistryEntry> {
139
+ const remove = options?.remove;
140
+ const hasCustom = custom && custom.length > 0;
141
+ const hasRemove = remove && remove.length > 0;
142
+ if (!hasCustom && !hasRemove) return SELECTION_TOOL_REGISTRY;
143
+ const removed = new Set(remove ?? []);
144
+ const byId = new Map<SelectionToolRegistryEntry["id"], SelectionToolRegistryEntry>();
145
+ for (const entry of SELECTION_TOOL_REGISTRY) {
146
+ if (removed.has(entry.id)) continue;
147
+ byId.set(entry.id, entry);
148
+ }
149
+ for (const entry of custom ?? []) {
150
+ // Host entries are NOT filtered by `remove` — callers can always
151
+ // install an entry they meant to keep even when a matching default
152
+ // would otherwise have been suppressed.
153
+ byId.set(entry.id, entry);
154
+ }
155
+ return [...byId.values()].sort((a, b) => a.precedence - b.precedence);
156
+ }
157
+
105
158
  export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry> = [
106
159
  {
107
160
  id: "history",
@@ -23,10 +23,20 @@ import {
23
23
  } from "./chrome-registry";
24
24
  import { shouldRenderSelectionToolKind } from "./scoped-chrome-policy";
25
25
 
26
+ /**
27
+ * Resolve the active selection tool for `input`.
28
+ *
29
+ * `registry` defaults to the canonical `SELECTION_TOOL_REGISTRY`. Hosts
30
+ * that want to reorder or shadow default precedences pass in the result
31
+ * of `resolveSelectionToolRegistry(props.customSelectionTools, options)`.
32
+ * Entries iterate in the order provided — callers are responsible for
33
+ * supplying a precedence-sorted array.
34
+ */
26
35
  export function resolveActiveSelectionTool(
27
36
  input: SelectionToolResolverInput,
37
+ registry: ReadonlyArray<SelectionToolRegistryEntry> = SELECTION_TOOL_REGISTRY,
28
38
  ): ActiveSelectionToolModel | null {
29
- for (const entry of SELECTION_TOOL_REGISTRY) {
39
+ for (const entry of registry) {
30
40
  if (!shouldRenderSelectionToolKind(input.scopedChromePolicy, entry.id)) {
31
41
  continue;
32
42
  }
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ EditorRole,
2
3
  WordReviewEditorChromeOptions,
3
4
  WordReviewEditorChromePreset,
4
5
  WordReviewEditorChromeVisibility,
@@ -21,6 +22,7 @@ export function resolveChromePreset(
21
22
  export function resolveChromePresetOptions(
22
23
  chromePreset: WordReviewEditorChromePreset,
23
24
  overrides?: Partial<WordReviewEditorChromeOptions>,
25
+ editorRole?: EditorRole,
24
26
  ): WordReviewEditorChromeOptions {
25
27
  const defaults: Record<WordReviewEditorChromePreset, WordReviewEditorChromeOptions> = {
26
28
  selection: {
@@ -73,10 +75,21 @@ export function resolveChromePresetOptions(
73
75
 
74
76
  return {
75
77
  ...defaults[chromePreset],
78
+ ...resolveRoleAdjustments(editorRole),
76
79
  ...overrides,
77
80
  };
78
81
  }
79
82
 
83
+ function resolveRoleAdjustments(role: EditorRole | undefined): Partial<WordReviewEditorChromeOptions> {
84
+ if (role === "editor") {
85
+ return { showReviewRail: false, showReviewQueueBar: false };
86
+ }
87
+ if (role === "workflow") {
88
+ return { showReviewQueueBar: false };
89
+ }
90
+ return {};
91
+ }
92
+
80
93
  export function resolveChromeVisibilityForPreset(input: {
81
94
  chromePreset: WordReviewEditorChromePreset;
82
95
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;