@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
@@ -14,6 +14,7 @@ import * as Tooltip from "@radix-ui/react-tooltip";
14
14
  import { ChevronLeft, ChevronRight, List } from "lucide-react";
15
15
 
16
16
  import type {
17
+ ActiveListContext,
17
18
  CommentSidebarThreadSnapshot,
18
19
  DocumentNavigationSnapshot,
19
20
  EditorViewStateSnapshot,
@@ -22,12 +23,16 @@ import type {
22
23
  HeaderFooterLinkPatch,
23
24
  InteractionGuardSnapshot,
24
25
  InsertImageOptions,
26
+ RuntimeContextAnalyticsSnapshot,
25
27
  RuntimeRenderSnapshot,
28
+ ReviewQueueSnapshot,
26
29
  SectionPageNumberingPatch,
27
30
  SectionBreakType,
28
31
  StyleCatalogSnapshot,
29
32
  SurfaceBlockSnapshot,
30
33
  TrackedChangeEntrySnapshot,
34
+ WordReviewEditorChromeOptions,
35
+ WordReviewEditorChromePreset,
31
36
  WordReviewEditorChromeVisibility,
32
37
  WorkflowScopeSnapshot,
33
38
  WorkspaceMode,
@@ -48,24 +53,26 @@ import {
48
53
  import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
49
54
  import type { SessionCapabilities } from "../runtime/session-capabilities";
50
55
  import type {
51
- SelectionToolbarAnchor,
52
- SelectionToolbarModel,
53
- SuggestionCardModel,
54
- } from "../ui/headless/selection-toolbar-model";
56
+ ActiveSelectionToolModel,
57
+ SelectionToolAnchor,
58
+ } from "../ui/headless/selection-tool-types";
55
59
  import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
56
60
  import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
57
61
  import { preserveEditorSelectionMouseDown } from "../ui/headless/preserve-editor-selection";
58
62
  import { TwAlertBanner } from "./chrome/tw-alert-banner";
59
- import { TwImageContextToolbar, type ActiveImageContext } from "./chrome/tw-image-context-toolbar";
60
63
  import { TwLayoutPanel } from "./chrome/tw-layout-panel";
61
- import { TwObjectContextToolbar, type ActiveObjectContext } from "./chrome/tw-object-context-toolbar";
62
64
  import { TwPageRuler } from "./chrome/tw-page-ruler";
63
- import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
64
- import { TwSuggestionCard } from "./chrome/tw-suggestion-card";
65
- import { TwTableContextToolbar } from "./chrome/tw-table-context-toolbar";
65
+ import { ChromePresetToolbar } from "./chrome/chrome-preset-toolbar";
66
+ import {
67
+ resolveChromePreset,
68
+ resolveChromePresetOptions,
69
+ resolveChromeVisibilityForPreset,
70
+ } from "./chrome/chrome-preset-model";
71
+ import { TwReviewQueueBar } from "./chrome/review-queue-bar";
72
+ import { TwSelectionToolHost } from "./chrome/tw-selection-tool-host";
66
73
  import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
67
74
  import { TwStatusBar } from "./status/tw-status-bar";
68
- import { TwToolbar, type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
75
+ import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
69
76
 
70
77
  export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
71
78
 
@@ -80,6 +87,7 @@ export interface TwReviewWorkspaceProps {
80
87
  workspaceMode: WorkspaceMode;
81
88
  zoomLevel?: ZoomLevel;
82
89
  formattingState?: FormattingStateSnapshot;
90
+ activeListContext?: ActiveListContext | null;
83
91
  styleCatalog?: StyleCatalogSnapshot;
84
92
  activeRailTab: ReviewRailTab;
85
93
  activeCommentId?: string;
@@ -87,10 +95,15 @@ export interface TwReviewWorkspaceProps {
87
95
  showTrackedChanges: boolean;
88
96
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
89
97
  interactionGuardSnapshot?: InteractionGuardSnapshot;
98
+ chromePreset?: WordReviewEditorChromePreset;
99
+ chromeOptions?: Partial<WordReviewEditorChromeOptions>;
100
+ reviewQueue?: ReviewQueueSnapshot;
101
+ documentContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
102
+ selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
103
+ currentScopeContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
90
104
  commands: EditorCommandBag;
91
- selectionToolbar?: SelectionToolbarModel | null;
92
- suggestionCard?: SuggestionCardModel | null;
93
- selectionToolbarAnchor?: SelectionToolbarAnchor | null;
105
+ activeSelectionTool?: ActiveSelectionToolModel | null;
106
+ selectionToolAnchor?: SelectionToolAnchor | null;
94
107
  documentNavigation?: DocumentNavigationSnapshot;
95
108
  onWorkspaceModeChange?: (value: WorkspaceMode) => void;
96
109
  onZoomChange?: (level: ZoomLevel) => void;
@@ -112,6 +125,8 @@ export interface TwReviewWorkspaceProps {
112
125
  onSetTextColor?: (color: string) => void;
113
126
  onSetHighlightColor?: (color: string | null) => void;
114
127
  onSetAlignment?: (alignment: FormattingAlignment) => void;
128
+ onToggleBulletedList?: () => void;
129
+ onToggleNumberedList?: () => void;
115
130
  onOutdent?: () => void;
116
131
  onIndent?: () => void;
117
132
  onAddComment?: () => void;
@@ -130,8 +145,6 @@ export interface TwReviewWorkspaceProps {
130
145
  onMergeCells?: () => void;
131
146
  onSplitCell?: () => void;
132
147
  onSetCellBackground?: (color: string) => void;
133
- activeImageContext?: ActiveImageContext | null;
134
- activeObjectContext?: ActiveObjectContext | null;
135
148
  onSetImageLayout?: (
136
149
  mediaId: string,
137
150
  dimensions: { widthEmu: number; heightEmu: number },
@@ -205,6 +218,11 @@ export interface TwReviewWorkspaceProps {
205
218
  onSetParagraphTabStops?: (tabStops: Array<{ pos: number; val?: string; leader?: string }>) => void;
206
219
  onRestartNumbering?: () => void;
207
220
  onContinueNumbering?: () => void;
221
+ onUpdateFields?: () => void;
222
+ onUpdateTableOfContents?: () => void;
223
+ onGoToPreviousReviewItem?: () => void;
224
+ onGoToNextReviewItem?: () => void;
225
+ onMarkSectionForReview?: () => void;
208
226
  onNavigateHeading?: (headingId: string) => void;
209
227
  chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
210
228
  }
@@ -223,6 +241,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
223
241
  const [layoutToolsOpen, setLayoutToolsOpen] = useState(false);
224
242
  const zoomLevel = props.zoomLevel ?? 100;
225
243
  const zoomScale = typeof zoomLevel === "number" ? zoomLevel / 100 : 1;
244
+ const pageZoomBucket =
245
+ !isPageWorkspace ? undefined : zoomScale < 1 ? "low" : zoomScale > 1 ? "high" : "base";
246
+ const chromePreset = resolveChromePreset(props.chromePreset, props.reviewMode);
247
+ const chromeOptions = resolveChromePresetOptions(chromePreset, props.chromeOptions);
226
248
  const preserveOnlyCount = caps?.preserveOnlyCount ??
227
249
  snapshot.compatibility.featureEntries.filter(
228
250
  (entry) => entry.featureClass === "preserve-only",
@@ -231,16 +253,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
231
253
  props.interactionGuardSnapshot?.blockedReasons ??
232
254
  props.workflowScopeSnapshot?.blockedReasons ??
233
255
  [];
234
- const chromeVisibility: ReviewWorkspaceChromeVisibility = {
235
- toolbar: true,
236
- alerts: true,
237
- selectionOverlay: true,
238
- contextToolbars: true,
239
- pageChrome: true,
240
- statusBar: true,
241
- reviewRail: true,
242
- ...props.chromeVisibility,
243
- };
256
+ const chromeVisibility = resolveChromeVisibilityForPreset({
257
+ chromePreset,
258
+ chromeOptions,
259
+ chromeVisibility: props.chromeVisibility,
260
+ });
244
261
  const showReviewRail = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
245
262
  const headings = props.documentNavigation?.headings ?? [];
246
263
  const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
@@ -249,21 +266,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
249
266
  viewState.selection.activeRange.kind === "node"
250
267
  ? viewState.selection.activeRange.at
251
268
  : viewState.selection.head;
269
+ const shouldResolveActiveParagraphLayout =
270
+ isPageWorkspace &&
271
+ chromeVisibility.pageChrome &&
272
+ layoutToolsOpen;
252
273
  const activeParagraphLayout = useMemo(
253
- () => resolveActiveParagraphLayout(snapshot.surface, selectionPosition),
254
- [selectionPosition, snapshot.surface],
255
- );
256
- const isTableContext = Boolean(
257
- props.formattingState?.breadcrumb.some((item) => item.kind === "table" || item.kind === "table_cell" || item.kind === "table_row"),
274
+ () =>
275
+ shouldResolveActiveParagraphLayout
276
+ ? resolveActiveParagraphLayout(snapshot.surface, selectionPosition)
277
+ : null,
278
+ [selectionPosition, shouldResolveActiveParagraphLayout, snapshot.surface],
258
279
  );
259
- const contextualSurface =
260
- props.activeImageContext
261
- ? "image"
262
- : props.activeObjectContext
263
- ? "object"
264
- : isTableContext
265
- ? "table"
266
- : null;
267
280
  const pageChromeModel = useMemo(
268
281
  () =>
269
282
  buildPageChromeModel(
@@ -275,21 +288,32 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
275
288
  [props.documentNavigation, snapshot.activeStory, snapshot.pageLayout, snapshot.surface],
276
289
  );
277
290
  const selectionToolbarPlacement = resolveSelectionToolbarPlacement(
278
- props.selectionToolbarAnchor,
291
+ props.selectionToolAnchor,
279
292
  selectionToolbarRootRef.current,
280
293
  zoomScale,
281
294
  );
295
+ const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
296
+ const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
297
+ const gatedSelectionTool = useMemo(() => {
298
+ if (!props.activeSelectionTool) {
299
+ return null;
300
+ }
301
+ if (props.activeSelectionTool.kind === "structure-context" && !chromeVisibility.contextToolbars) {
302
+ return null;
303
+ }
304
+ return props.activeSelectionTool;
305
+ }, [props.activeSelectionTool, chromeVisibility.contextToolbars]);
282
306
  const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
283
307
  const pageShellMetrics = useMemo(
284
308
  () => buildPageShellMetrics(snapshot.pageLayout),
285
309
  [snapshot.pageLayout],
286
310
  );
311
+ const headerBandLabel = resolvePageBandLabel("header", snapshot.activeStory);
312
+ const footerBandLabel = resolvePageBandLabel("footer", snapshot.activeStory);
287
313
  const hidePageBorderForActiveEditing =
288
314
  isPageWorkspace &&
289
315
  snapshot.activeStory.kind === "main" &&
290
316
  shouldHidePageBorderForSelection(viewState.selection);
291
- const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
292
- const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
293
317
  const pageChromeReadOnly =
294
318
  snapshot.readOnly ||
295
319
  snapshot.activeStory.kind !== "main" ||
@@ -332,99 +356,126 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
332
356
  return (
333
357
  <Tooltip.Provider delayDuration={400}>
334
358
  <div className="flex h-full flex-col bg-canvas text-primary">
335
- {chromeVisibility.toolbar ? <TwToolbar
336
- sourceLabel={snapshot.sourceLabel}
337
- capabilities={caps}
338
- compatibility={snapshot.compatibility}
339
- warnings={snapshot.warnings}
340
- interactionPolicy={toolbarInteractionPolicy}
341
- workspaceMode={props.workspaceMode}
342
- zoomLevel={props.zoomLevel}
343
- formattingState={props.formattingState}
344
- styleCatalog={props.styleCatalog}
345
- showTrackedChanges={props.showTrackedChanges}
346
- onUndo={runWithSelectionToolbarDismiss(props.onUndo)}
347
- onRedo={runWithSelectionToolbarDismiss(props.onRedo)}
348
- onSetParagraphStyle={props.onSetParagraphStyle
349
- ? (styleId) => {
350
- dismissSelectionToolbar();
351
- props.onSetParagraphStyle?.(styleId);
352
- }
353
- : undefined}
354
- onToggleBold={runWithSelectionToolbarDismiss(props.onToggleBold)}
355
- onToggleItalic={runWithSelectionToolbarDismiss(props.onToggleItalic)}
356
- onToggleUnderline={runWithSelectionToolbarDismiss(props.onToggleUnderline)}
357
- onToggleStrikethrough={runWithSelectionToolbarDismiss(props.onToggleStrikethrough)}
358
- onToggleSuperscript={runWithSelectionToolbarDismiss(props.onToggleSuperscript)}
359
- onToggleSubscript={runWithSelectionToolbarDismiss(props.onToggleSubscript)}
360
- onSetFontFamily={props.onSetFontFamily
361
- ? (fontFamily) => {
362
- dismissSelectionToolbar();
363
- props.onSetFontFamily?.(fontFamily);
364
- }
365
- : undefined}
366
- onSetFontSize={props.onSetFontSize
367
- ? (fontSize) => {
368
- dismissSelectionToolbar();
369
- props.onSetFontSize?.(fontSize);
370
- }
371
- : undefined}
372
- onSetTextColor={props.onSetTextColor
373
- ? (color) => {
374
- dismissSelectionToolbar();
375
- props.onSetTextColor?.(color);
376
- }
377
- : undefined}
378
- onSetHighlightColor={props.onSetHighlightColor
379
- ? (color) => {
380
- dismissSelectionToolbar();
381
- props.onSetHighlightColor?.(color);
382
- }
383
- : undefined}
384
- onSetAlignment={props.onSetAlignment
385
- ? (alignment) => {
386
- dismissSelectionToolbar();
387
- props.onSetAlignment?.(alignment);
388
- }
389
- : undefined}
390
- onOutdent={runWithSelectionToolbarDismiss(props.onOutdent)}
391
- onIndent={runWithSelectionToolbarDismiss(props.onIndent)}
392
- onAddComment={runWithSelectionToolbarDismiss(props.onAddComment)}
393
- onInsertPageBreak={runWithSelectionToolbarDismiss(props.onInsertPageBreak)}
394
- onInsertTable={runWithSelectionToolbarDismiss(props.onInsertTable)}
395
- onInsertSectionBreak={props.onInsertSectionBreak
396
- ? (type) => {
397
- dismissSelectionToolbar();
398
- props.onInsertSectionBreak?.(type);
399
- }
400
- : undefined}
401
- onInsertImage={props.onInsertImage
402
- ? (options) => {
403
- dismissSelectionToolbar();
404
- props.onInsertImage?.(options);
405
- }
406
- : undefined}
407
- onExport={runWithSelectionToolbarDismiss(props.onExport)}
408
- activeStory={snapshot.activeStory}
409
- onCloseStory={props.onCloseStory
410
- ? runWithSelectionToolbarDismiss(props.onCloseStory)
411
- : undefined}
412
- onWorkspaceModeChange={(value) => {
413
- dismissSelectionToolbar();
414
- props.onWorkspaceModeChange(value);
415
- }}
416
- onZoomChange={props.onZoomChange
417
- ? (level) => {
418
- dismissSelectionToolbar();
419
- props.onZoomChange?.(level);
420
- }
421
- : undefined}
422
- onShowTrackedChangesChange={(show) => {
423
- dismissSelectionToolbar();
424
- props.onShowTrackedChangesChange(show);
425
- }}
426
- blockedReasons={blockedReasons}
427
- /> : null}
359
+ {chromeVisibility.toolbar ? (
360
+ <div className="px-3 pt-3">
361
+ <ChromePresetToolbar
362
+ chromePreset={chromePreset}
363
+ capabilities={caps}
364
+ compatibility={snapshot.compatibility}
365
+ warnings={snapshot.warnings}
366
+ blockedReasons={blockedReasons}
367
+ showDiagnosticsChrome={chromeVisibility.alerts}
368
+ interactionPolicy={toolbarInteractionPolicy}
369
+ workspaceMode={props.workspaceMode}
370
+ zoomLevel={props.zoomLevel}
371
+ formattingState={props.formattingState}
372
+ activeListContext={props.activeListContext}
373
+ styleCatalog={props.styleCatalog}
374
+ showTrackedChanges={props.showTrackedChanges}
375
+ onUndo={runWithSelectionToolbarDismiss(props.onUndo)}
376
+ onRedo={runWithSelectionToolbarDismiss(props.onRedo)}
377
+ onSetParagraphStyle={props.onSetParagraphStyle
378
+ ? (styleId) => {
379
+ dismissSelectionToolbar();
380
+ props.onSetParagraphStyle?.(styleId);
381
+ }
382
+ : undefined}
383
+ onToggleBold={runWithSelectionToolbarDismiss(props.onToggleBold)}
384
+ onToggleItalic={runWithSelectionToolbarDismiss(props.onToggleItalic)}
385
+ onToggleUnderline={runWithSelectionToolbarDismiss(props.onToggleUnderline)}
386
+ onToggleStrikethrough={runWithSelectionToolbarDismiss(props.onToggleStrikethrough)}
387
+ onToggleSuperscript={runWithSelectionToolbarDismiss(props.onToggleSuperscript)}
388
+ onToggleSubscript={runWithSelectionToolbarDismiss(props.onToggleSubscript)}
389
+ onSetFontFamily={props.onSetFontFamily
390
+ ? (fontFamily) => {
391
+ dismissSelectionToolbar();
392
+ props.onSetFontFamily?.(fontFamily);
393
+ }
394
+ : undefined}
395
+ onSetFontSize={props.onSetFontSize
396
+ ? (fontSize) => {
397
+ dismissSelectionToolbar();
398
+ props.onSetFontSize?.(fontSize);
399
+ }
400
+ : undefined}
401
+ onSetTextColor={props.onSetTextColor
402
+ ? (color) => {
403
+ dismissSelectionToolbar();
404
+ props.onSetTextColor?.(color);
405
+ }
406
+ : undefined}
407
+ onSetHighlightColor={props.onSetHighlightColor
408
+ ? (color) => {
409
+ dismissSelectionToolbar();
410
+ props.onSetHighlightColor?.(color);
411
+ }
412
+ : undefined}
413
+ onSetAlignment={props.onSetAlignment
414
+ ? (alignment) => {
415
+ dismissSelectionToolbar();
416
+ props.onSetAlignment?.(alignment);
417
+ }
418
+ : undefined}
419
+ onToggleBulletedList={runWithSelectionToolbarDismiss(props.onToggleBulletedList)}
420
+ onToggleNumberedList={runWithSelectionToolbarDismiss(props.onToggleNumberedList)}
421
+ onOutdent={runWithSelectionToolbarDismiss(props.onOutdent)}
422
+ onIndent={runWithSelectionToolbarDismiss(props.onIndent)}
423
+ onAddComment={runWithSelectionToolbarDismiss(props.onAddComment)}
424
+ onInsertPageBreak={runWithSelectionToolbarDismiss(props.onInsertPageBreak)}
425
+ onInsertTable={runWithSelectionToolbarDismiss(props.onInsertTable)}
426
+ onInsertSectionBreak={props.onInsertSectionBreak
427
+ ? (type) => {
428
+ dismissSelectionToolbar();
429
+ props.onInsertSectionBreak?.(type);
430
+ }
431
+ : undefined}
432
+ onInsertImage={props.onInsertImage
433
+ ? (options) => {
434
+ dismissSelectionToolbar();
435
+ props.onInsertImage?.(options);
436
+ }
437
+ : undefined}
438
+ onExport={runWithSelectionToolbarDismiss(props.onExport)}
439
+ activeStory={snapshot.activeStory}
440
+ onCloseStory={props.onCloseStory
441
+ ? runWithSelectionToolbarDismiss(props.onCloseStory)
442
+ : undefined}
443
+ onWorkspaceModeChange={(value) => {
444
+ dismissSelectionToolbar();
445
+ props.onWorkspaceModeChange(value);
446
+ }}
447
+ onZoomChange={props.onZoomChange
448
+ ? (level) => {
449
+ dismissSelectionToolbar();
450
+ props.onZoomChange?.(level);
451
+ }
452
+ : undefined}
453
+ onRestartNumbering={runWithSelectionToolbarDismiss(props.onRestartNumbering)}
454
+ onContinueNumbering={runWithSelectionToolbarDismiss(props.onContinueNumbering)}
455
+ onUpdateFields={runWithSelectionToolbarDismiss(props.onUpdateFields)}
456
+ onUpdateTableOfContents={runWithSelectionToolbarDismiss(props.onUpdateTableOfContents)}
457
+ onShowTrackedChangesChange={(show) => {
458
+ dismissSelectionToolbar();
459
+ props.onShowTrackedChangesChange(show);
460
+ }}
461
+ />
462
+ </div>
463
+ ) : null}
464
+
465
+ {chromePreset === "review" && chromeOptions.showReviewQueueBar && props.reviewQueue ? (
466
+ <TwReviewQueueBar
467
+ queue={props.reviewQueue}
468
+ onPrevious={props.onGoToPreviousReviewItem
469
+ ? runWithSelectionToolbarDismiss(props.onGoToPreviousReviewItem)
470
+ : undefined}
471
+ onNext={props.onGoToNextReviewItem
472
+ ? runWithSelectionToolbarDismiss(props.onGoToNextReviewItem)
473
+ : undefined}
474
+ onMarkSection={chromeOptions.showSectionTagAction && props.onMarkSectionForReview
475
+ ? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
476
+ : undefined}
477
+ />
478
+ ) : null}
428
479
 
429
480
  {chromeVisibility.alerts ? <TwAlertBanner
430
481
  snapshot={snapshot}
@@ -527,16 +578,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
527
578
  {/* Document column */}
528
579
  <div className="flex flex-1 flex-col min-w-0">
529
580
  <div
530
- className={`flex-1 overflow-y-auto ${isPageWorkspace ? "bg-surface" : "bg-canvas"}`}
581
+ className="flex-1 overflow-y-auto bg-surface"
531
582
  data-wre-scroll-root="true"
532
583
  >
533
584
  <div
534
585
  ref={selectionToolbarRootRef}
535
- className={`mx-auto min-h-full ${
586
+ className={`mx-auto min-h-full w-full ${
536
587
  isPageWorkspace
537
588
  ? "wre-page-chrome wre-page-surface relative max-w-[840px] my-8 overflow-hidden"
538
- : "wre-canvas-surface relative bg-canvas"
589
+ : "wre-canvas-surface relative my-8 overflow-hidden"
539
590
  }`}
591
+ data-zoom-bucket={pageZoomBucket}
540
592
  style={isPageWorkspace && zoomScale !== 1 ? { transform: `scale(${zoomScale})`, transformOrigin: "top center" } : undefined}
541
593
  >
542
594
  {isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
@@ -686,139 +738,47 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
686
738
  />
687
739
  </div>
688
740
  ) : null}
689
- {chromeVisibility.contextToolbars && contextualSurface ? (
690
- <div className="px-5 pt-3 space-y-3">
691
- {contextualSurface === "table" ? (
692
- <TwTableContextToolbar
693
- disabled={!allowLocalChromeMutations}
694
- tableStyles={props.styleCatalog?.tables ?? []}
695
- onSetTableStyle={props.onSetTableStyle
696
- ? (styleId) => {
697
- dismissSelectionToolbar();
698
- props.onSetTableStyle?.(styleId);
699
- }
700
- : undefined}
701
- onAddRowBefore={runWithSelectionToolbarDismiss(props.onAddRowBefore)}
702
- onAddRowAfter={runWithSelectionToolbarDismiss(props.onAddRowAfter)}
703
- onAddColumnBefore={runWithSelectionToolbarDismiss(props.onAddColumnBefore)}
704
- onAddColumnAfter={runWithSelectionToolbarDismiss(props.onAddColumnAfter)}
705
- onDeleteRow={runWithSelectionToolbarDismiss(props.onDeleteRow)}
706
- onDeleteColumn={runWithSelectionToolbarDismiss(props.onDeleteColumn)}
707
- onDeleteTable={runWithSelectionToolbarDismiss(props.onDeleteTable)}
708
- onMergeCells={runWithSelectionToolbarDismiss(props.onMergeCells)}
709
- onSplitCell={runWithSelectionToolbarDismiss(props.onSplitCell)}
710
- onSetCellBackground={props.onSetCellBackground
711
- ? (color) => {
712
- dismissSelectionToolbar();
713
- props.onSetCellBackground?.(color);
714
- }
715
- : undefined}
716
- />
717
- ) : null}
718
- {contextualSurface === "image" && props.activeImageContext ? (
719
- <TwImageContextToolbar
720
- activeImage={props.activeImageContext}
721
- disabled={!allowLocalChromeMutations}
722
- onSetImageLayout={props.onSetImageLayout
723
- ? (mediaId, dimensions) => {
724
- dismissSelectionToolbar();
725
- props.onSetImageLayout?.(mediaId, dimensions);
726
- }
727
- : undefined}
728
- onSetImageFrame={props.onSetImageFrame
729
- ? (mediaId, offsets) => {
730
- dismissSelectionToolbar();
731
- props.onSetImageFrame?.(mediaId, offsets);
732
- }
733
- : undefined}
734
- />
735
- ) : null}
736
- {contextualSurface === "object" && props.activeObjectContext ? (
737
- <TwObjectContextToolbar activeObject={props.activeObjectContext} />
738
- ) : null}
739
- </div>
740
- ) : null}
741
- {chromeVisibility.selectionOverlay && props.suggestionCard && selectionToolbarPlacement ? (
742
- <div className="pointer-events-none absolute inset-0 z-20" data-testid="suggestion-card-overlay">
743
- <div
744
- className="pointer-events-auto absolute"
745
- data-placement={selectionToolbarPlacement.placement}
746
- style={selectionToolbarPlacement.style}
747
- >
748
- <TwSuggestionCard
749
- model={props.suggestionCard}
750
- onFocusCapture={props.onSelectionToolbarFocusCapture}
751
- onBlurCapture={props.onSelectionToolbarBlurCapture}
752
- onAccept={props.onAcceptSuggestion}
753
- onReject={props.onRejectSuggestion}
754
- onEditSuggestion={props.onEditSuggestion}
755
- onAddComment={props.onAddCommentFromSuggestion ?? props.onAddComment}
756
- />
757
- </div>
758
- </div>
759
- ) : null}
760
- {chromeVisibility.selectionOverlay && props.suggestionCard && !selectionToolbarPlacement ? (
761
- <div
762
- className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
763
- data-testid="suggestion-card-fallback"
764
- >
765
- <div className="pointer-events-auto" data-placement="fallback">
766
- <TwSuggestionCard
767
- model={props.suggestionCard}
768
- onFocusCapture={props.onSelectionToolbarFocusCapture}
769
- onBlurCapture={props.onSelectionToolbarBlurCapture}
770
- onAccept={props.onAcceptSuggestion}
771
- onReject={props.onRejectSuggestion}
772
- onEditSuggestion={props.onEditSuggestion}
773
- onAddComment={props.onAddCommentFromSuggestion ?? props.onAddComment}
774
- />
775
- </div>
776
- </div>
777
- ) : null}
778
- {chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && selectionToolbarPlacement ? (
779
- <div className="pointer-events-none absolute inset-0 z-20" data-testid="selection-toolbar-overlay">
780
- <div
781
- className="pointer-events-auto absolute"
782
- data-placement={selectionToolbarPlacement.placement}
783
- style={selectionToolbarPlacement.style}
784
- >
785
- <TwSelectionToolbar
786
- ref={props.selectionToolbarRef}
787
- model={props.selectionToolbar}
788
- disabledReason={props.selectionToolbar.disabledReason}
789
- onFocusCapture={props.onSelectionToolbarFocusCapture}
790
- onBlurCapture={props.onSelectionToolbarBlurCapture}
791
- onToggleBold={props.onToggleBold}
792
- onToggleItalic={props.onToggleItalic}
793
- onToggleUnderline={props.onToggleUnderline}
794
- onSetTextColor={props.onSetSelectionTextColor}
795
- onSetHighlightColor={props.onSetSelectionHighlightColor}
796
- onAddComment={props.onAddCommentFromSelection ?? props.onAddComment}
797
- />
798
- </div>
799
- </div>
800
- ) : null}
801
- {chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && !selectionToolbarPlacement ? (
802
- <div
803
- className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
804
- data-testid="selection-toolbar-fallback"
805
- >
806
- <div className="pointer-events-auto" data-placement="fallback">
807
- <TwSelectionToolbar
808
- ref={props.selectionToolbarRef}
809
- model={props.selectionToolbar}
810
- disabledReason={props.selectionToolbar.disabledReason}
811
- onFocusCapture={props.onSelectionToolbarFocusCapture}
812
- onBlurCapture={props.onSelectionToolbarBlurCapture}
813
- onToggleBold={props.onToggleBold}
814
- onToggleItalic={props.onToggleItalic}
815
- onToggleUnderline={props.onToggleUnderline}
816
- onSetTextColor={props.onSetSelectionTextColor}
817
- onSetHighlightColor={props.onSetSelectionHighlightColor}
818
- onAddComment={props.onAddCommentFromSelection ?? props.onAddComment}
819
- />
820
- </div>
821
- </div>
741
+ {chromeVisibility.selectionOverlay && gatedSelectionTool ? (
742
+ <TwSelectionToolHost
743
+ tool={gatedSelectionTool}
744
+ contextAnalytics={
745
+ chromeVisibility.contextAnalytics
746
+ ? props.selectionContextAnalytics
747
+ : null
748
+ }
749
+ placement={selectionToolbarPlacement}
750
+ rootRef={props.selectionToolbarRef}
751
+ onFocusCapture={props.onSelectionToolbarFocusCapture}
752
+ onBlurCapture={props.onSelectionToolbarBlurCapture}
753
+ onToggleBold={props.onToggleBold}
754
+ onToggleItalic={props.onToggleItalic}
755
+ onToggleUnderline={props.onToggleUnderline}
756
+ onSetTextColor={props.onSetSelectionTextColor}
757
+ onSetHighlightColor={props.onSetSelectionHighlightColor}
758
+ onAddComment={
759
+ gatedSelectionTool.kind === "suggestion-review"
760
+ ? (props.onAddCommentFromSuggestion ?? props.onAddComment)
761
+ : (props.onAddCommentFromSelection ?? props.onAddComment)
762
+ }
763
+ onAcceptSuggestion={props.onAcceptSuggestion}
764
+ onRejectSuggestion={props.onRejectSuggestion}
765
+ onEditSuggestion={props.onEditSuggestion}
766
+ onSetTableStyle={props.onSetTableStyle}
767
+ onAddRowBefore={props.onAddRowBefore}
768
+ onAddRowAfter={props.onAddRowAfter}
769
+ onAddColumnBefore={props.onAddColumnBefore}
770
+ onAddColumnAfter={props.onAddColumnAfter}
771
+ onDeleteRow={props.onDeleteRow}
772
+ onDeleteColumn={props.onDeleteColumn}
773
+ onDeleteTable={props.onDeleteTable}
774
+ onMergeCells={props.onMergeCells}
775
+ onSplitCell={props.onSplitCell}
776
+ onSetCellBackground={props.onSetCellBackground}
777
+ onSetImageLayout={props.onSetImageLayout}
778
+ onSetImageFrame={props.onSetImageFrame}
779
+ onRestartNumbering={props.onRestartNumbering}
780
+ onContinueNumbering={props.onContinueNumbering}
781
+ />
822
782
  ) : null}
823
783
  <div
824
784
  className={isPageWorkspace ? "relative" : undefined}
@@ -860,10 +820,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
860
820
  {isPageWorkspace && chromeVisibility.pageChrome ? (
861
821
  <div
862
822
  data-testid="page-header-band"
863
- className="relative z-10 flex items-center justify-between border-b border-dashed border-border/60 px-4 text-[11px] text-secondary"
823
+ className="relative z-10 flex items-center justify-between border-b border-border/50 bg-surface/45 px-4 text-[11px] text-secondary backdrop-blur-[1px]"
864
824
  style={pageShellMetrics.headerBandStyle}
865
825
  >
866
- <span className="uppercase tracking-[0.12em] text-tertiary">Header</span>
826
+ <span className="uppercase tracking-[0.12em] text-tertiary">{headerBandLabel}</span>
867
827
  {snapshot.pageLayout?.headerVariants[0] ? (
868
828
  <button
869
829
  type="button"
@@ -890,10 +850,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
890
850
  {isPageWorkspace && chromeVisibility.pageChrome ? (
891
851
  <div
892
852
  data-testid="page-footer-band"
893
- className="relative z-10 flex items-center justify-between border-t border-dashed border-border/60 px-4 text-[11px] text-secondary"
853
+ className="relative z-10 flex items-center justify-between border-t border-border/50 bg-surface/45 px-4 text-[11px] text-secondary backdrop-blur-[1px]"
894
854
  style={pageShellMetrics.footerBandStyle}
895
855
  >
896
- <span className="uppercase tracking-[0.12em] text-tertiary">Footer</span>
856
+ <span className="uppercase tracking-[0.12em] text-tertiary">{footerBandLabel}</span>
897
857
  {snapshot.pageLayout?.footerVariants[0] ? (
898
858
  <button
899
859
  type="button"
@@ -920,6 +880,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
920
880
  commentCount={snapshot.comments.totalCount}
921
881
  changeCount={snapshot.trackedChanges.totalCount}
922
882
  sessionId={snapshot.sessionId}
883
+ contextAnalytics={
884
+ chromeVisibility.contextAnalytics
885
+ ? props.documentContextAnalytics
886
+ : null
887
+ }
923
888
  />
924
889
  ) : null}
925
890
  </div>
@@ -933,6 +898,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
933
898
  compatibility={snapshot.compatibility}
934
899
  warnings={snapshot.warnings}
935
900
  markupDisplay={markupDisplay}
901
+ contextAnalytics={
902
+ chromeVisibility.contextAnalytics
903
+ ? props.currentScopeContextAnalytics
904
+ : null
905
+ }
936
906
  activeCommentId={props.activeCommentId}
937
907
  activeRevisionId={props.activeRevisionId}
938
908
  onActiveTabChange={props.onActiveRailTabChange}
@@ -971,22 +941,68 @@ function resolveActiveParagraphLayout(
971
941
  rightIndent: number;
972
942
  firstLineOffset: number;
973
943
  tabStops: Array<{ pos: number; val?: string; leader?: string }>;
944
+ indentationReadOnly?: boolean;
945
+ tabStopsReadOnly?: boolean;
974
946
  } | null {
975
947
  const paragraph = surface ? findActiveParagraph(surface.blocks, position) : null;
976
948
  if (!paragraph) {
977
949
  return null;
978
950
  }
951
+ const resolvedIndentation = paragraph.resolvedNumbering?.geometry.indentation;
952
+ const resolvedTabStops = paragraph.resolvedNumbering?.geometry.tabStops;
953
+ const indentation = resolvedIndentation ?? paragraph.indentation;
954
+ const tabStops = resolvedTabStops ?? paragraph.tabStops;
979
955
 
980
956
  return {
981
- leftIndent: paragraph.indentation?.left ?? 0,
982
- rightIndent: paragraph.indentation?.right ?? 0,
957
+ leftIndent: indentation?.left ?? 0,
958
+ rightIndent: indentation?.right ?? 0,
983
959
  firstLineOffset:
984
- paragraph.indentation?.firstLine ??
985
- (paragraph.indentation?.hanging ? -paragraph.indentation.hanging : 0),
986
- tabStops: paragraph.tabStops ? [...paragraph.tabStops] : [],
960
+ indentation?.firstLine ??
961
+ (indentation?.hanging ? -indentation.hanging : 0),
962
+ tabStops: tabStops ? [...tabStops] : [],
963
+ indentationReadOnly:
964
+ Boolean(resolvedIndentation) &&
965
+ !areIndentationsEqual(resolvedIndentation, paragraph.indentation),
966
+ tabStopsReadOnly:
967
+ Boolean(resolvedTabStops) &&
968
+ !areTabStopsEqual(resolvedTabStops, paragraph.tabStops),
987
969
  };
988
970
  }
989
971
 
972
+ function areIndentationsEqual(
973
+ left:
974
+ | Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"]
975
+ | undefined,
976
+ right:
977
+ | Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"]
978
+ | undefined,
979
+ ): boolean {
980
+ return (
981
+ left?.left === right?.left &&
982
+ left?.right === right?.right &&
983
+ left?.firstLine === right?.firstLine &&
984
+ left?.hanging === right?.hanging
985
+ );
986
+ }
987
+
988
+ function areTabStopsEqual(
989
+ left: ReadonlyArray<{ pos: number; val?: string; leader?: string }> | undefined,
990
+ right: ReadonlyArray<{ pos: number; val?: string; leader?: string }> | undefined,
991
+ ): boolean {
992
+ if (!left?.length && !right?.length) {
993
+ return true;
994
+ }
995
+ if (!left || !right || left.length !== right.length) {
996
+ return false;
997
+ }
998
+ return left.every(
999
+ (tabStop, index) =>
1000
+ tabStop.pos === right[index]?.pos &&
1001
+ tabStop.val === right[index]?.val &&
1002
+ tabStop.leader === right[index]?.leader,
1003
+ );
1004
+ }
1005
+
990
1006
  function findActiveParagraph(
991
1007
  blocks: readonly SurfaceBlockSnapshot[],
992
1008
  position: number,
@@ -1120,6 +1136,9 @@ function buildPageShellMetrics(
1120
1136
  },
1121
1137
  pageFrameStyle: {
1122
1138
  backgroundColor: "var(--color-page-bg)",
1139
+ borderRadius: "8px",
1140
+ boxShadow: "0 24px 48px -32px rgba(15, 23, 42, 0.38), 0 8px 20px -18px rgba(15, 23, 42, 0.22)",
1141
+ border: "1px solid rgba(148, 163, 184, 0.2)",
1123
1142
  },
1124
1143
  headerBandStyle: {
1125
1144
  minHeight: `${headerBandHeightPx}px`,
@@ -1130,6 +1149,30 @@ function buildPageShellMetrics(
1130
1149
  };
1131
1150
  }
1132
1151
 
1152
+ function resolvePageBandLabel(
1153
+ region: "header" | "footer",
1154
+ activeStory: RuntimeRenderSnapshot["activeStory"],
1155
+ ): string {
1156
+ const regionLabel = region === "header" ? "header" : "footer";
1157
+ let label: string;
1158
+ if (activeStory.kind !== region) {
1159
+ label = region === "header" ? "Header" : "Footer";
1160
+ } else {
1161
+ switch (activeStory.variant) {
1162
+ case "first":
1163
+ label = `First page ${regionLabel}`;
1164
+ break;
1165
+ case "even":
1166
+ label = `Even page ${regionLabel}`;
1167
+ break;
1168
+ default:
1169
+ label = `Default ${regionLabel}`;
1170
+ break;
1171
+ }
1172
+ }
1173
+ return label;
1174
+ }
1175
+
1133
1176
  function buildLineNumberMarkers(
1134
1177
  blocks: readonly SurfaceBlockSnapshot[],
1135
1178
  pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
@@ -1316,7 +1359,7 @@ function createInsetValue(spaceTwips: number | undefined, percent: number): stri
1316
1359
  }
1317
1360
 
1318
1361
  function resolveSelectionToolbarPlacement(
1319
- anchor: SelectionToolbarAnchor | null | undefined,
1362
+ anchor: SelectionToolAnchor | null | undefined,
1320
1363
  root: HTMLDivElement | null,
1321
1364
  zoomScale: number,
1322
1365
  ): { placement: "right" | "left" | "above" | "below"; style: CSSProperties } | null {