@beyondwork/docx-react-component 1.0.35 → 1.0.37

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 (65) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +84 -1
  5. package/src/core/commands/index.ts +19 -2
  6. package/src/core/selection/mapping.ts +6 -0
  7. package/src/io/docx-session.ts +24 -9
  8. package/src/io/export/build-app-properties-xml.ts +88 -0
  9. package/src/io/export/serialize-comments.ts +6 -1
  10. package/src/io/export/serialize-footnotes.ts +10 -9
  11. package/src/io/export/serialize-headers-footers.ts +11 -10
  12. package/src/io/export/serialize-main-document.ts +337 -50
  13. package/src/io/export/serialize-numbering.ts +115 -24
  14. package/src/io/export/serialize-tables.ts +13 -11
  15. package/src/io/export/table-properties-xml.ts +35 -16
  16. package/src/io/export/twip.ts +66 -0
  17. package/src/io/normalize/normalize-text.ts +5 -0
  18. package/src/io/ooxml/parse-footnotes.ts +2 -1
  19. package/src/io/ooxml/parse-headers-footers.ts +2 -1
  20. package/src/io/ooxml/parse-main-document.ts +21 -1
  21. package/src/legal/bookmarks.ts +78 -0
  22. package/src/model/canonical-document.ts +11 -0
  23. package/src/review/store/scope-tag-diff.ts +130 -0
  24. package/src/runtime/document-navigation.ts +1 -305
  25. package/src/runtime/document-runtime.ts +178 -16
  26. package/src/runtime/layout/docx-font-loader.ts +143 -0
  27. package/src/runtime/layout/index.ts +188 -0
  28. package/src/runtime/layout/inert-layout-facet.ts +45 -0
  29. package/src/runtime/layout/layout-engine-instance.ts +618 -0
  30. package/src/runtime/layout/layout-invalidation.ts +257 -0
  31. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  32. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  33. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  34. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  35. package/src/runtime/layout/page-graph.ts +433 -0
  36. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  37. package/src/runtime/layout/page-story-resolver.ts +195 -0
  38. package/src/runtime/layout/paginated-layout-engine.ts +788 -0
  39. package/src/runtime/layout/public-facet.ts +705 -0
  40. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  41. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  42. package/src/runtime/scope-tag-registry.ts +95 -0
  43. package/src/runtime/session-capabilities.ts +7 -4
  44. package/src/runtime/surface-projection.ts +1 -0
  45. package/src/runtime/text-ack-range.ts +49 -0
  46. package/src/ui/WordReviewEditor.tsx +15 -0
  47. package/src/ui/editor-runtime-boundary.ts +10 -1
  48. package/src/ui/editor-surface-controller.tsx +3 -0
  49. package/src/ui/headless/chrome-registry.ts +235 -0
  50. package/src/ui/headless/scoped-chrome-policy.ts +164 -0
  51. package/src/ui/headless/selection-tool-context.ts +2 -0
  52. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  53. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
  54. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
  57. package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
  58. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  59. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  60. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  61. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
  62. package/src/ui-tailwind/theme/editor-theme.css +40 -14
  63. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  64. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
  65. package/src/ui-tailwind/tw-review-workspace.tsx +27 -1
@@ -34,6 +34,7 @@ import {
34
34
  type DocumentRuntimeEvent,
35
35
  type DocumentRuntime,
36
36
  } from "../runtime/document-runtime.ts";
37
+ import { createInertLayoutFacet } from "../runtime/layout/index.ts";
37
38
  import { loadDocxEditorSession } from "../io/docx-session.ts";
38
39
  import {
39
40
  decodePersistedSourcePackageBytes,
@@ -738,6 +739,7 @@ function createLoadingRuntimeBridge(input: {
738
739
  viewState: EditorViewStateSnapshot;
739
740
  navigation: DocumentNavigationSnapshot;
740
741
  }): WordReviewEditorRuntime {
742
+ const inertLayoutFacet = createInertLayoutFacet();
741
743
  const emptyFieldSnapshot: FieldSnapshot = {
742
744
  totalCount: 0,
743
745
  supportedCount: 0,
@@ -772,7 +774,13 @@ function createLoadingRuntimeBridge(input: {
772
774
  getCanonicalDocument: () => input.sessionState.canonicalDocument,
773
775
  getSourcePackage: () => input.sessionState.sourcePackage,
774
776
  replaceText: () => undefined,
775
- applyActiveStoryTextCommand: () => undefined,
777
+ applyActiveStoryTextCommand: () => ({
778
+ kind: "rejected",
779
+ newRevisionToken: "",
780
+ blockedReasons: [
781
+ { code: "runtime_loading", message: "Document runtime is not ready yet." },
782
+ ],
783
+ }),
776
784
  dispatch: () => undefined,
777
785
  undo: () => undefined,
778
786
  redo: () => undefined,
@@ -801,6 +809,7 @@ function createLoadingRuntimeBridge(input: {
801
809
  setZoom: () => undefined,
802
810
  getPageLayoutSnapshot: () => null,
803
811
  getDocumentNavigationSnapshot: () => input.navigation,
812
+ layout: inertLayoutFacet,
804
813
  getCurrentLocation: () => null,
805
814
  getLocationForSelection: () => null,
806
815
  getLocationForAnchor: () => null,
@@ -60,6 +60,9 @@ export interface EditorSurfaceControllerProps {
60
60
  activeWorkflowWorkItemId?: string | null;
61
61
  activeWorkflowScopeIds?: readonly string[];
62
62
  workflowMetadata?: readonly WorkflowMetadataMarkup[];
63
+ dispatchRuntimeCommand?: (
64
+ command: import("../ui-tailwind/editor-surface/fast-text-edit-lane.ts").LaneRuntimeCommand,
65
+ ) => import("../api/public-types.ts").TextCommandAck;
63
66
  }
64
67
 
65
68
  export const EditorSurfaceController = forwardRef<
@@ -0,0 +1,235 @@
1
+ import type { WordReviewEditorChromePreset } from "../../api/public-types";
2
+ import type { SelectionToolKind } from "./selection-tool-types";
3
+
4
+ export type ChromeSurface = "top-toolbar" | "selection-tool";
5
+ export type ChromeDensity = "compact";
6
+ export type ToolbarChromePlacement = "inline" | "overflow" | "hidden";
7
+
8
+ export type ToolbarChromeItemId =
9
+ | "history"
10
+ | "text-style-selectors"
11
+ | "inline-formatting"
12
+ | "text-colors"
13
+ | "paragraph-alignment"
14
+ | "list-actions"
15
+ | "indentation"
16
+ | "list-continuation"
17
+ | "insert-actions"
18
+ | "update-actions"
19
+ | "scope-status"
20
+ | "story-breadcrumb"
21
+ | "sidebar-toggle"
22
+ | "comment"
23
+ | "tracked-changes-toggle"
24
+ | "workspace-mode"
25
+ | "zoom"
26
+ | "health"
27
+ | "export";
28
+
29
+ export interface ChromeRegistryEntryBase {
30
+ id: string;
31
+ surfaces: ReadonlyArray<ChromeSurface>;
32
+ group: string;
33
+ }
34
+
35
+ export interface SelectionToolRegistryEntry extends ChromeRegistryEntryBase {
36
+ id: SelectionToolKind;
37
+ surfaces: ["selection-tool"];
38
+ precedence: number;
39
+ }
40
+
41
+ export interface ToolbarChromeRegistryEntry extends ChromeRegistryEntryBase {
42
+ id: ToolbarChromeItemId;
43
+ surfaces: ["top-toolbar"];
44
+ presets: ReadonlyArray<WordReviewEditorChromePreset>;
45
+ fullPlacement: Exclude<ToolbarChromePlacement, "hidden">;
46
+ compactPlacement: ToolbarChromePlacement;
47
+ runtimeBehavior: "always" | "formatting" | "structure" | "comment";
48
+ scopeBehavior?: "default" | "scoped-only" | "hidden-when-scoped";
49
+ }
50
+
51
+ export const SELECTION_TOOL_REGISTRY: ReadonlyArray<SelectionToolRegistryEntry> = [
52
+ { id: "suggestion-review", surfaces: ["selection-tool"], group: "review", precedence: 10 },
53
+ { id: "comment-thread", surfaces: ["selection-tool"], group: "review", precedence: 20 },
54
+ { id: "workflow-task", surfaces: ["selection-tool"], group: "workflow", precedence: 30 },
55
+ { id: "structure-context", surfaces: ["selection-tool"], group: "structure", precedence: 40 },
56
+ { id: "formatting-inline", surfaces: ["selection-tool"], group: "formatting", precedence: 50 },
57
+ { id: "blocked-explainer", surfaces: ["selection-tool"], group: "blocked", precedence: 60 },
58
+ ];
59
+
60
+ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry> = [
61
+ {
62
+ id: "history",
63
+ surfaces: ["top-toolbar"],
64
+ group: "history",
65
+ presets: ["simple", "advanced", "review"],
66
+ fullPlacement: "inline",
67
+ compactPlacement: "inline",
68
+ runtimeBehavior: "always",
69
+ },
70
+ {
71
+ id: "text-style-selectors",
72
+ surfaces: ["top-toolbar"],
73
+ group: "text",
74
+ presets: ["advanced"],
75
+ fullPlacement: "inline",
76
+ compactPlacement: "overflow",
77
+ runtimeBehavior: "formatting",
78
+ },
79
+ {
80
+ id: "inline-formatting",
81
+ surfaces: ["top-toolbar"],
82
+ group: "text",
83
+ presets: ["simple", "advanced", "review"],
84
+ fullPlacement: "inline",
85
+ compactPlacement: "inline",
86
+ runtimeBehavior: "formatting",
87
+ },
88
+ {
89
+ id: "text-colors",
90
+ surfaces: ["top-toolbar"],
91
+ group: "text",
92
+ presets: ["simple", "advanced"],
93
+ fullPlacement: "inline",
94
+ compactPlacement: "inline",
95
+ runtimeBehavior: "formatting",
96
+ },
97
+ {
98
+ id: "paragraph-alignment",
99
+ surfaces: ["top-toolbar"],
100
+ group: "paragraph",
101
+ presets: ["simple", "advanced"],
102
+ fullPlacement: "inline",
103
+ compactPlacement: "inline",
104
+ runtimeBehavior: "formatting",
105
+ },
106
+ {
107
+ id: "list-actions",
108
+ surfaces: ["top-toolbar"],
109
+ group: "paragraph",
110
+ presets: ["simple", "advanced"],
111
+ fullPlacement: "inline",
112
+ compactPlacement: "overflow",
113
+ runtimeBehavior: "formatting",
114
+ },
115
+ {
116
+ id: "indentation",
117
+ surfaces: ["top-toolbar"],
118
+ group: "paragraph",
119
+ presets: ["simple", "advanced"],
120
+ fullPlacement: "inline",
121
+ compactPlacement: "overflow",
122
+ runtimeBehavior: "formatting",
123
+ },
124
+ {
125
+ id: "list-continuation",
126
+ surfaces: ["top-toolbar"],
127
+ group: "paragraph",
128
+ presets: ["simple", "advanced"],
129
+ fullPlacement: "inline",
130
+ compactPlacement: "overflow",
131
+ runtimeBehavior: "formatting",
132
+ },
133
+ {
134
+ id: "insert-actions",
135
+ surfaces: ["top-toolbar"],
136
+ group: "document",
137
+ presets: ["simple", "advanced"],
138
+ fullPlacement: "inline",
139
+ compactPlacement: "overflow",
140
+ runtimeBehavior: "structure",
141
+ scopeBehavior: "hidden-when-scoped",
142
+ },
143
+ {
144
+ id: "update-actions",
145
+ surfaces: ["top-toolbar"],
146
+ group: "document",
147
+ presets: ["advanced"],
148
+ fullPlacement: "inline",
149
+ compactPlacement: "overflow",
150
+ runtimeBehavior: "structure",
151
+ scopeBehavior: "hidden-when-scoped",
152
+ },
153
+ {
154
+ id: "scope-status",
155
+ surfaces: ["top-toolbar"],
156
+ group: "scope",
157
+ presets: ["simple", "advanced", "review"],
158
+ fullPlacement: "inline",
159
+ compactPlacement: "inline",
160
+ runtimeBehavior: "always",
161
+ scopeBehavior: "scoped-only",
162
+ },
163
+ {
164
+ id: "story-breadcrumb",
165
+ surfaces: ["top-toolbar"],
166
+ group: "scope",
167
+ presets: ["simple", "advanced", "review"],
168
+ fullPlacement: "inline",
169
+ compactPlacement: "inline",
170
+ runtimeBehavior: "always",
171
+ },
172
+ {
173
+ id: "sidebar-toggle",
174
+ surfaces: ["top-toolbar"],
175
+ group: "review",
176
+ presets: ["simple", "advanced", "review"],
177
+ fullPlacement: "inline",
178
+ compactPlacement: "inline",
179
+ runtimeBehavior: "always",
180
+ },
181
+ {
182
+ id: "comment",
183
+ surfaces: ["top-toolbar"],
184
+ group: "review",
185
+ presets: ["simple", "advanced", "review"],
186
+ fullPlacement: "inline",
187
+ compactPlacement: "inline",
188
+ runtimeBehavior: "comment",
189
+ },
190
+ {
191
+ id: "tracked-changes-toggle",
192
+ surfaces: ["top-toolbar"],
193
+ group: "review",
194
+ presets: ["advanced", "review"],
195
+ fullPlacement: "inline",
196
+ compactPlacement: "inline",
197
+ runtimeBehavior: "always",
198
+ },
199
+ {
200
+ id: "workspace-mode",
201
+ surfaces: ["top-toolbar"],
202
+ group: "view",
203
+ presets: ["simple", "advanced", "review"],
204
+ fullPlacement: "inline",
205
+ compactPlacement: "inline",
206
+ runtimeBehavior: "always",
207
+ },
208
+ {
209
+ id: "zoom",
210
+ surfaces: ["top-toolbar"],
211
+ group: "view",
212
+ presets: ["simple", "advanced", "review"],
213
+ fullPlacement: "inline",
214
+ compactPlacement: "inline",
215
+ runtimeBehavior: "always",
216
+ },
217
+ {
218
+ id: "health",
219
+ surfaces: ["top-toolbar"],
220
+ group: "status",
221
+ presets: ["simple", "advanced", "review"],
222
+ fullPlacement: "inline",
223
+ compactPlacement: "inline",
224
+ runtimeBehavior: "always",
225
+ },
226
+ {
227
+ id: "export",
228
+ surfaces: ["top-toolbar"],
229
+ group: "status",
230
+ presets: ["simple", "advanced", "review"],
231
+ fullPlacement: "inline",
232
+ compactPlacement: "inline",
233
+ runtimeBehavior: "always",
234
+ },
235
+ ];
@@ -0,0 +1,164 @@
1
+ import type {
2
+ ActiveListContext,
3
+ InteractionGuardSnapshot,
4
+ WorkflowScopeSnapshot,
5
+ WordReviewEditorChromePreset,
6
+ } from "../../api/public-types";
7
+ import type { SessionCapabilities } from "../../runtime/session-capabilities";
8
+ import {
9
+ TOOLBAR_CHROME_REGISTRY,
10
+ type ToolbarChromeItemId,
11
+ type ToolbarChromePlacement,
12
+ } from "./chrome-registry";
13
+ import type { SelectionToolKind } from "./selection-tool-types";
14
+
15
+ export interface ToolbarChromeItemPolicy {
16
+ visible: boolean;
17
+ enabled: boolean;
18
+ placement: ToolbarChromePlacement;
19
+ }
20
+
21
+ export interface ScopedChromePolicy {
22
+ density: "compact";
23
+ activeScopeId?: string;
24
+ activeWorkItemId?: string | null;
25
+ scopeStatusLabel?: string;
26
+ toolbar: Record<ToolbarChromeItemId, ToolbarChromeItemPolicy>;
27
+ selection: {
28
+ hiddenKinds: ReadonlyArray<SelectionToolKind>;
29
+ };
30
+ }
31
+
32
+ export interface ResolveScopedChromePolicyInput {
33
+ preset: WordReviewEditorChromePreset;
34
+ compactMode: boolean;
35
+ capabilities?: SessionCapabilities;
36
+ interactionGuardSnapshot?: InteractionGuardSnapshot;
37
+ workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
38
+ activeListContext?: ActiveListContext | null;
39
+ }
40
+
41
+ export function resolveScopedChromePolicy(
42
+ input: ResolveScopedChromePolicyInput,
43
+ ): ScopedChromePolicy {
44
+ const effectiveMode = input.interactionGuardSnapshot?.effectiveMode ?? "edit";
45
+ const canFormatText = Boolean(input.capabilities?.canEdit) && effectiveMode === "edit";
46
+ const canInsertStructural = Boolean(input.capabilities?.canEdit) && effectiveMode === "edit";
47
+ const canAddComment =
48
+ Boolean(input.capabilities?.canAddComment) &&
49
+ effectiveMode !== "view" &&
50
+ effectiveMode !== "blocked";
51
+ const activeScopeId = input.interactionGuardSnapshot?.matchedScopeId;
52
+ const activeWorkItemId = input.workflowScopeSnapshot?.activeWorkItemId ?? null;
53
+ const hasScopedContext = Boolean(activeScopeId || activeWorkItemId);
54
+ const scopeStatusLabel =
55
+ input.workflowScopeSnapshot?.activeWorkItem?.title ??
56
+ input.workflowScopeSnapshot?.scopes.find((scope) => scope.scopeId === activeScopeId)?.label ??
57
+ (activeScopeId && input.interactionGuardSnapshot?.matchedScopeMode
58
+ ? `Scoped ${input.interactionGuardSnapshot.matchedScopeMode}`
59
+ : undefined);
60
+
61
+ const toolbar = Object.fromEntries(
62
+ TOOLBAR_CHROME_REGISTRY.map((entry) => {
63
+ let visible = entry.presets.includes(input.preset);
64
+
65
+ if (visible) {
66
+ switch (entry.runtimeBehavior) {
67
+ case "formatting":
68
+ visible = canFormatText;
69
+ break;
70
+ case "structure":
71
+ visible = canInsertStructural;
72
+ break;
73
+ case "comment":
74
+ visible = canAddComment;
75
+ break;
76
+ default:
77
+ visible = true;
78
+ break;
79
+ }
80
+ }
81
+
82
+ if (visible && entry.id === "list-continuation") {
83
+ visible = Boolean(input.activeListContext);
84
+ }
85
+ if (visible && entry.id === "scope-status") {
86
+ visible = Boolean(scopeStatusLabel);
87
+ }
88
+
89
+ if (visible && entry.scopeBehavior === "scoped-only") {
90
+ visible = hasScopedContext;
91
+ }
92
+ if (visible && entry.scopeBehavior === "hidden-when-scoped") {
93
+ visible = !hasScopedContext;
94
+ }
95
+
96
+ const placement = visible
97
+ ? (input.compactMode ? entry.compactPlacement : entry.fullPlacement)
98
+ : "hidden";
99
+ const enabled =
100
+ visible &&
101
+ (entry.id !== "tracked-changes-toggle" || Boolean(input.capabilities?.trackChangesSupported)) &&
102
+ (entry.id !== "export" || Boolean(input.capabilities?.canExport));
103
+
104
+ return [
105
+ entry.id,
106
+ {
107
+ visible,
108
+ enabled,
109
+ placement,
110
+ } satisfies ToolbarChromeItemPolicy,
111
+ ];
112
+ }),
113
+ ) as Record<ToolbarChromeItemId, ToolbarChromeItemPolicy>;
114
+
115
+ const hiddenKinds = resolveHiddenSelectionToolKinds(effectiveMode);
116
+
117
+ return {
118
+ density: "compact",
119
+ ...(activeScopeId ? { activeScopeId } : {}),
120
+ activeWorkItemId,
121
+ ...(scopeStatusLabel ? { scopeStatusLabel } : {}),
122
+ toolbar,
123
+ selection: {
124
+ hiddenKinds,
125
+ },
126
+ };
127
+ }
128
+
129
+ export function isToolbarChromeItemVisible(
130
+ policy: ScopedChromePolicy,
131
+ itemId: ToolbarChromeItemId,
132
+ ): boolean {
133
+ return policy.toolbar[itemId]?.visible ?? false;
134
+ }
135
+
136
+ export function getToolbarChromePlacement(
137
+ policy: ScopedChromePolicy,
138
+ itemId: ToolbarChromeItemId,
139
+ ): ToolbarChromePlacement {
140
+ return policy.toolbar[itemId]?.placement ?? "hidden";
141
+ }
142
+
143
+ export function shouldRenderSelectionToolKind(
144
+ policy: ScopedChromePolicy | undefined,
145
+ kind: SelectionToolKind,
146
+ ): boolean {
147
+ return !policy?.selection.hiddenKinds.includes(kind);
148
+ }
149
+
150
+ function resolveHiddenSelectionToolKinds(
151
+ effectiveMode: InteractionGuardSnapshot["effectiveMode"] | "edit",
152
+ ): ReadonlyArray<SelectionToolKind> {
153
+ switch (effectiveMode) {
154
+ case "suggest":
155
+ return ["formatting-inline", "structure-context"];
156
+ case "comment":
157
+ return ["formatting-inline", "structure-context"];
158
+ case "view":
159
+ case "blocked":
160
+ return ["formatting-inline", "structure-context", "suggestion-review"];
161
+ default:
162
+ return [];
163
+ }
164
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  StyleCatalogSnapshot,
6
6
  } from "../../api/public-types";
7
7
  import type { SessionCapabilities } from "../../runtime/session-capabilities";
8
+ import type { ScopedChromePolicy } from "./scoped-chrome-policy";
8
9
  import type { SelectionToolResolverContext } from "./selection-tool-types";
9
10
 
10
11
  export interface SelectionToolResolverInput extends SelectionToolResolverContext {
@@ -16,4 +17,5 @@ export interface SelectionToolResolverInput extends SelectionToolResolverContext
16
17
  preferListStructureContext?: boolean;
17
18
  addCommentDisabledReason?: string;
18
19
  suppressedSuggestionRevisionId?: string | null;
20
+ scopedChromePolicy?: ScopedChromePolicy;
19
21
  }
@@ -17,27 +17,26 @@ import type {
17
17
  SuggestionReviewSelectionToolModel,
18
18
  WorkflowTaskSelectionToolModel,
19
19
  } from "./selection-tool-types";
20
+ import {
21
+ SELECTION_TOOL_REGISTRY,
22
+ type SelectionToolRegistryEntry,
23
+ } from "./chrome-registry";
24
+ import { shouldRenderSelectionToolKind } from "./scoped-chrome-policy";
20
25
 
21
26
  export function resolveActiveSelectionTool(
22
27
  input: SelectionToolResolverInput,
23
28
  ): ActiveSelectionToolModel | null {
24
- const suggestionTool = buildSuggestionReviewSelectionToolModel(input);
25
- const commentTool = buildCommentThreadSelectionToolModel(input);
26
- const workflowTool = buildWorkflowTaskSelectionToolModel(input);
27
- const structureTool = buildStructureContextSelectionToolModel(input);
28
- // Semantic targets beat generic range formatting so structure tools do not
29
- // leak preview text when the user is acting on a table, image, or object.
30
- const formattingTool = buildFormattingInlineSelectionToolModel(input);
31
- const blockedTool = buildBlockedExplainerSelectionToolModel(input);
32
- const resolvedTool =
33
- suggestionTool ??
34
- commentTool ??
35
- workflowTool ??
36
- structureTool ??
37
- formattingTool ??
38
- blockedTool;
39
-
40
- return resolvedTool;
29
+ for (const entry of SELECTION_TOOL_REGISTRY) {
30
+ if (!shouldRenderSelectionToolKind(input.scopedChromePolicy, entry.id)) {
31
+ continue;
32
+ }
33
+ const resolvedTool = buildSelectionToolFromRegistryEntry(entry, input);
34
+ if (resolvedTool) {
35
+ return resolvedTool;
36
+ }
37
+ }
38
+
39
+ return null;
41
40
  }
42
41
 
43
42
  export function buildFormattingInlineSelectionToolModel(
@@ -273,6 +272,26 @@ function selectionMatchesSuggestion(
273
272
  );
274
273
  }
275
274
 
275
+ function buildSelectionToolFromRegistryEntry(
276
+ entry: SelectionToolRegistryEntry,
277
+ input: SelectionToolResolverInput,
278
+ ): ActiveSelectionToolModel | null {
279
+ switch (entry.id) {
280
+ case "suggestion-review":
281
+ return buildSuggestionReviewSelectionToolModel(input);
282
+ case "comment-thread":
283
+ return buildCommentThreadSelectionToolModel(input);
284
+ case "workflow-task":
285
+ return buildWorkflowTaskSelectionToolModel(input);
286
+ case "structure-context":
287
+ return buildStructureContextSelectionToolModel(input);
288
+ case "formatting-inline":
289
+ return buildFormattingInlineSelectionToolModel(input);
290
+ case "blocked-explainer":
291
+ return buildBlockedExplainerSelectionToolModel(input);
292
+ }
293
+ }
294
+
276
295
  function findSelectionMatchingScope(
277
296
  activeRange: SelectionToolResolverInput["snapshot"]["selection"]["activeRange"] | null,
278
297
  activeStory: EditorStoryTarget,