@beyondwork/docx-react-component 1.0.21 → 1.0.23

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 (33) hide show
  1. package/README.md +763 -38
  2. package/package.json +25 -36
  3. package/src/api/public-types.ts +66 -1
  4. package/src/core/commands/index.ts +574 -5
  5. package/src/index.ts +5 -0
  6. package/src/io/docx-session.ts +181 -2
  7. package/src/io/export/serialize-main-document.ts +21 -1
  8. package/src/io/normalize/normalize-text.ts +4 -0
  9. package/src/io/ooxml/parse-main-document.ts +88 -7
  10. package/src/model/canonical-document.ts +22 -0
  11. package/src/review/store/revision-store.ts +1 -0
  12. package/src/review/store/revision-types.ts +2 -0
  13. package/src/runtime/document-runtime.ts +503 -51
  14. package/src/runtime/session-capabilities.ts +6 -5
  15. package/src/runtime/surface-projection.ts +2 -0
  16. package/src/runtime/table-schema.ts +2 -0
  17. package/src/runtime/workflow-markup.ts +5 -1
  18. package/src/ui/WordReviewEditor.tsx +661 -132
  19. package/src/ui/editor-runtime-boundary.ts +10 -1
  20. package/src/ui/editor-shell-view.tsx +8 -0
  21. package/src/ui/editor-surface-controller.tsx +5 -0
  22. package/src/ui/headless/selection-toolbar-model.ts +12 -0
  23. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  24. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
  25. package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
  26. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
  27. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
  29. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
  30. package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
  31. package/src/ui-tailwind/theme/editor-theme.css +10 -0
  32. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
  33. package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
@@ -33,6 +33,58 @@ interface OpenExplicitRowSpan {
33
33
  remainingRows: number;
34
34
  }
35
35
 
36
+ function readRowPadding(node: PMNode, side: "gridBefore" | "gridAfter"): number {
37
+ const value = node.attrs[side];
38
+ return typeof value === "number" && value > 0 ? value : 0;
39
+ }
40
+
41
+ function sumGridColumns(
42
+ gridColumns: readonly number[],
43
+ start: number,
44
+ count: number,
45
+ ): number {
46
+ let total = 0;
47
+ for (let index = start; index < start + count; index += 1) {
48
+ total += gridColumns[index] ?? 0;
49
+ }
50
+ return total;
51
+ }
52
+
53
+ function removePaddingCells(rowElement: HTMLTableRowElement): void {
54
+ for (const cell of Array.from(rowElement.cells)) {
55
+ if (cell.hasAttribute("data-row-padding")) {
56
+ cell.remove();
57
+ }
58
+ }
59
+ }
60
+
61
+ function createPaddingCell(colSpan: number, widthTwips: number): HTMLTableCellElement {
62
+ const cell = document.createElement("td");
63
+ cell.setAttribute("data-row-padding", "true");
64
+ cell.setAttribute("aria-hidden", "true");
65
+ cell.colSpan = Math.max(1, colSpan);
66
+ cell.style.border = "none";
67
+ cell.style.padding = "0";
68
+ cell.style.background = "transparent";
69
+ if (widthTwips > 0) {
70
+ cell.style.width = `${widthTwips / 20}pt`;
71
+ }
72
+ return cell;
73
+ }
74
+
75
+ function nodesAreOnlyRowPadding(nodes: ArrayLike<Node> & { item?(index: number): Node | null }): boolean {
76
+ for (let index = 0; index < nodes.length; index += 1) {
77
+ const node = nodes.item ? nodes.item(index) : nodes[index];
78
+ if (!node) {
79
+ continue;
80
+ }
81
+ if (!(node instanceof HTMLElement) || !node.hasAttribute("data-row-padding")) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+
36
88
  function resolveRenderedColspan(node: PMNode): number {
37
89
  const colspan = node.attrs.colspan as number | undefined;
38
90
  if (typeof colspan === "number" && colspan > 1) {
@@ -172,6 +224,7 @@ function computeTableLayout(tableNode: PMNode): TableCellLayout[][] {
172
224
  function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode: PMNode): void {
173
225
  const rowLayouts = computeTableLayout(tableNode);
174
226
  const rowElements = Array.from(tableBody.rows);
227
+ const gridColumns = Array.isArray(tableNode.attrs.gridColumns) ? tableNode.attrs.gridColumns as number[] : [];
175
228
 
176
229
  for (let rowIndex = 0; rowIndex < rowLayouts.length; rowIndex += 1) {
177
230
  const rowLayout = rowLayouts[rowIndex];
@@ -180,6 +233,7 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
180
233
  continue;
181
234
  }
182
235
 
236
+ removePaddingCells(rowElement);
183
237
  const cellElements = Array.from(rowElement.cells);
184
238
  for (const cellLayout of rowLayout) {
185
239
  const element = cellElements[cellLayout.cellIndex];
@@ -199,6 +253,22 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
199
253
  element.removeAttribute("data-vertical-merge-hidden");
200
254
  }
201
255
  }
256
+
257
+ const rowNode = tableNode.child(rowIndex);
258
+ const gridBefore = readRowPadding(rowNode, "gridBefore");
259
+ const gridAfter = readRowPadding(rowNode, "gridAfter");
260
+ if (gridBefore > 0) {
261
+ rowElement.insertBefore(
262
+ createPaddingCell(gridBefore, sumGridColumns(gridColumns, 0, gridBefore)),
263
+ rowElement.firstChild,
264
+ );
265
+ }
266
+ if (gridAfter > 0) {
267
+ const start = Math.max(0, gridColumns.length - gridAfter);
268
+ rowElement.appendChild(
269
+ createPaddingCell(gridAfter, sumGridColumns(gridColumns, start, gridAfter)),
270
+ );
271
+ }
202
272
  }
203
273
  }
204
274
 
@@ -253,7 +323,18 @@ export class TableNodeView {
253
323
  }
254
324
 
255
325
  ignoreMutation(record: ViewMutationRecord): boolean {
256
- return record.type === "attributes" && record.target === this.dom;
326
+ if (record.type === "attributes") {
327
+ return record.target === this.dom
328
+ || (record.target instanceof HTMLElement && record.target.hasAttribute("data-row-padding"));
329
+ }
330
+ if (record.type === "childList" && this.dom.contains(record.target)) {
331
+ const addedNodes = record.addedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
332
+ const removedNodes = record.removedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
333
+ const addedOkay = (addedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(addedNodes);
334
+ const removedOkay = (removedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(removedNodes);
335
+ return addedOkay && removedOkay;
336
+ }
337
+ return false;
257
338
  }
258
339
 
259
340
  private scheduleLayoutSync(): void {
@@ -22,7 +22,10 @@ export function TwStatusBar(props: TwStatusBarProps) {
22
22
  : "Ready";
23
23
 
24
24
  return (
25
- <footer className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary">
25
+ <footer
26
+ data-testid="status-bar"
27
+ className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary"
28
+ >
26
29
  <span className="flex items-center gap-1.5">
27
30
  <span
28
31
  className={`inline-block h-1.5 w-1.5 rounded-full ${
@@ -323,6 +323,16 @@
323
323
  background: var(--wre-workflow-rail-color, var(--color-border-strong));
324
324
  }
325
325
 
326
+ .prosemirror-surface .ProseMirror .wre-workflow-rail-active::before {
327
+ width: 0.3125rem;
328
+ opacity: 1;
329
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 30%, transparent);
330
+ }
331
+
332
+ .prosemirror-surface .ProseMirror .wre-workflow-inline-active {
333
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 28%, transparent);
334
+ }
335
+
326
336
  .prosemirror-surface .ProseMirror .wre-workflow-rail-edit {
327
337
  --wre-workflow-rail-color: var(--color-accent);
328
338
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
@@ -62,6 +62,7 @@ export interface TwToolbarProps {
62
62
  compatibility?: CompatibilityPanelSnapshot;
63
63
  warnings?: EditorWarning[];
64
64
  blockedReasons?: WorkflowBlockedCommandReason[];
65
+ interactionPolicy?: ToolbarInteractionPolicy;
65
66
  workspaceMode: WorkspaceMode;
66
67
  zoomLevel?: ZoomLevel;
67
68
  formattingState?: FormattingStateSnapshot;
@@ -99,6 +100,13 @@ export interface TwToolbarProps {
99
100
  onShowTrackedChangesChange: (show: boolean) => void;
100
101
  }
101
102
 
103
+ export interface ToolbarInteractionPolicy {
104
+ mode: "edit" | "suggest" | "comment" | "view" | "blocked";
105
+ canFormatText: boolean;
106
+ canInsertStructural: boolean;
107
+ canAddComment: boolean;
108
+ }
109
+
102
110
  export function getSupportedZoomPresets(): ReadonlyArray<number> {
103
111
  return [75, 100, 125, 150];
104
112
  }
@@ -123,7 +131,9 @@ export function TwToolbar(props: TwToolbarProps) {
123
131
  const isPageMode = workspaceMode === "page";
124
132
  const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
125
133
  const zoomLevel = props.zoomLevel ?? 100;
126
- const canEdit = caps ? caps.canEdit : false;
134
+ const canEdit = props.interactionPolicy?.canFormatText ?? (caps ? caps.canEdit : false);
135
+ const canInsertStructural = props.interactionPolicy?.canInsertStructural ?? canEdit;
136
+ const canAddComment = props.interactionPolicy?.canAddComment ?? (caps ? caps.canAddComment : false);
127
137
  const zoomLabel =
128
138
  typeof zoomLevel === "number"
129
139
  ? `${zoomLevel}%`
@@ -173,14 +183,14 @@ export function TwToolbar(props: TwToolbarProps) {
173
183
  icon={Bold}
174
184
  label="Bold"
175
185
  active={props.formattingState?.bold ?? false}
176
- disabled={caps ? !caps.canEdit : true}
186
+ disabled={!canEdit}
177
187
  onClick={props.onToggleBold}
178
188
  />
179
189
  <TwToolbarIconButton
180
190
  icon={Italic}
181
191
  label="Italic"
182
192
  active={props.formattingState?.italic ?? false}
183
- disabled={caps ? !caps.canEdit : true}
193
+ disabled={!canEdit}
184
194
  onClick={props.onToggleItalic}
185
195
  />
186
196
  <TwToolbarIconButton
@@ -238,7 +248,7 @@ export function TwToolbar(props: TwToolbarProps) {
238
248
  onClick={props.onIndent}
239
249
  />
240
250
  <ToolbarInsertMenu
241
- disabled={!canEdit}
251
+ disabled={!canInsertStructural}
242
252
  onInsertImage={props.onInsertImage}
243
253
  onInsertPageBreak={props.onInsertPageBreak}
244
254
  onInsertSectionBreak={props.onInsertSectionBreak}
@@ -275,7 +285,7 @@ export function TwToolbar(props: TwToolbarProps) {
275
285
  <TwToolbarIconButton
276
286
  icon={MessageSquare}
277
287
  label="Add comment"
278
- disabled={caps ? !caps.canAddComment : true}
288
+ disabled={!canAddComment}
279
289
  emphasis
280
290
  onClick={props.onAddComment}
281
291
  />
@@ -554,6 +564,8 @@ function ToolbarParagraphStyleSelect(props: {
554
564
  >
555
565
  <Select.Trigger
556
566
  aria-label="Paragraph style"
567
+ aria-disabled={props.disabled || undefined}
568
+ data-disabled={props.disabled ? "" : undefined}
557
569
  onMouseDown={preserveEditorSelectionMouseDown}
558
570
  className={`inline-flex h-7 min-w-[8.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2.5 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
559
571
  >
@@ -601,6 +613,8 @@ function ToolbarFontFamilySelect(props: {
601
613
  >
602
614
  <Select.Trigger
603
615
  aria-label="Font family"
616
+ aria-disabled={props.disabled || undefined}
617
+ data-disabled={props.disabled ? "" : undefined}
604
618
  onMouseDown={preserveEditorSelectionMouseDown}
605
619
  className={`inline-flex h-7 min-w-[7rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
606
620
  >
@@ -649,6 +663,8 @@ function ToolbarFontSizeSelect(props: {
649
663
  >
650
664
  <Select.Trigger
651
665
  aria-label="Font size"
666
+ aria-disabled={props.disabled || undefined}
667
+ data-disabled={props.disabled ? "" : undefined}
652
668
  onMouseDown={preserveEditorSelectionMouseDown}
653
669
  className={`inline-flex h-7 min-w-[4rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
654
670
  >
@@ -28,6 +28,7 @@ import type {
28
28
  StyleCatalogSnapshot,
29
29
  SurfaceBlockSnapshot,
30
30
  TrackedChangeEntrySnapshot,
31
+ WordReviewEditorChromeVisibility,
31
32
  WorkflowScopeSnapshot,
32
33
  WorkspaceMode,
33
34
  ZoomLevel,
@@ -49,6 +50,7 @@ import type { SessionCapabilities } from "../runtime/session-capabilities";
49
50
  import type {
50
51
  SelectionToolbarAnchor,
51
52
  SelectionToolbarModel,
53
+ SuggestionCardModel,
52
54
  } from "../ui/headless/selection-toolbar-model";
53
55
  import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
54
56
  import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
@@ -59,10 +61,13 @@ import { TwLayoutPanel } from "./chrome/tw-layout-panel";
59
61
  import { TwObjectContextToolbar, type ActiveObjectContext } from "./chrome/tw-object-context-toolbar";
60
62
  import { TwPageRuler } from "./chrome/tw-page-ruler";
61
63
  import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
64
+ import { TwSuggestionCard } from "./chrome/tw-suggestion-card";
62
65
  import { TwTableContextToolbar } from "./chrome/tw-table-context-toolbar";
63
66
  import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
64
67
  import { TwStatusBar } from "./status/tw-status-bar";
65
- import { TwToolbar } from "./toolbar/tw-toolbar";
68
+ import { TwToolbar, type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
69
+
70
+ export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
66
71
 
67
72
  export interface TwReviewWorkspaceProps {
68
73
  snapshot: RuntimeRenderSnapshot;
@@ -84,6 +89,7 @@ export interface TwReviewWorkspaceProps {
84
89
  interactionGuardSnapshot?: InteractionGuardSnapshot;
85
90
  commands: EditorCommandBag;
86
91
  selectionToolbar?: SelectionToolbarModel | null;
92
+ suggestionCard?: SuggestionCardModel | null;
87
93
  selectionToolbarAnchor?: SelectionToolbarAnchor | null;
88
94
  documentNavigation?: DocumentNavigationSnapshot;
89
95
  onWorkspaceModeChange?: (value: WorkspaceMode) => void;
@@ -170,6 +176,10 @@ export interface TwReviewWorkspaceProps {
170
176
  onAddCommentFromSelection?: () => void;
171
177
  onExport?: () => void;
172
178
  onDismissSelectionToolbar?: () => void;
179
+ onAcceptSuggestion?: () => void;
180
+ onRejectSuggestion?: () => void;
181
+ onEditSuggestion?: () => void;
182
+ onAddCommentFromSuggestion?: () => void;
173
183
  onSelectionToolbarFocusCapture?: FocusEventHandler<HTMLDivElement>;
174
184
  onSelectionToolbarBlurCapture?: FocusEventHandler<HTMLDivElement>;
175
185
  selectionToolbarRef?: Ref<HTMLDivElement>;
@@ -196,6 +206,7 @@ export interface TwReviewWorkspaceProps {
196
206
  onRestartNumbering?: () => void;
197
207
  onContinueNumbering?: () => void;
198
208
  onNavigateHeading?: (headingId: string) => void;
209
+ chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
199
210
  }
200
211
 
201
212
  export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
@@ -220,7 +231,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
220
231
  props.interactionGuardSnapshot?.blockedReasons ??
221
232
  props.workflowScopeSnapshot?.blockedReasons ??
222
233
  [];
223
- const showReviewRail = caps?.reviewRailVisible ?? true;
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
+ };
244
+ const showReviewRail = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
224
245
  const headings = props.documentNavigation?.headings ?? [];
225
246
  const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
226
247
  const footerVariant = snapshot.pageLayout?.footerVariants[0]?.variant ?? "default";
@@ -267,6 +288,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
267
288
  isPageWorkspace &&
268
289
  snapshot.activeStory.kind === "main" &&
269
290
  shouldHidePageBorderForSelection(viewState.selection);
291
+ const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
292
+ const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
293
+ const pageChromeReadOnly =
294
+ snapshot.readOnly ||
295
+ snapshot.activeStory.kind !== "main" ||
296
+ effectiveSelectionMode !== "edit";
297
+ const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
298
+ ? {
299
+ mode: effectiveSelectionMode,
300
+ canFormatText: caps.canEdit && effectiveSelectionMode === "edit",
301
+ canInsertStructural: caps.canEdit && effectiveSelectionMode === "edit",
302
+ canAddComment:
303
+ caps.canAddComment &&
304
+ effectiveSelectionMode !== "view" &&
305
+ effectiveSelectionMode !== "blocked",
306
+ }
307
+ : undefined;
270
308
 
271
309
  useEffect(() => {
272
310
  recordPerfSample("workspace.chrome");
@@ -294,11 +332,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
294
332
  return (
295
333
  <Tooltip.Provider delayDuration={400}>
296
334
  <div className="flex h-full flex-col bg-canvas text-primary">
297
- <TwToolbar
335
+ {chromeVisibility.toolbar ? <TwToolbar
298
336
  sourceLabel={snapshot.sourceLabel}
299
337
  capabilities={caps}
300
338
  compatibility={snapshot.compatibility}
301
339
  warnings={snapshot.warnings}
340
+ interactionPolicy={toolbarInteractionPolicy}
302
341
  workspaceMode={props.workspaceMode}
303
342
  zoomLevel={props.zoomLevel}
304
343
  formattingState={props.formattingState}
@@ -385,17 +424,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
385
424
  props.onShowTrackedChangesChange(show);
386
425
  }}
387
426
  blockedReasons={blockedReasons}
388
- />
427
+ /> : null}
389
428
 
390
- <TwAlertBanner
429
+ {chromeVisibility.alerts ? <TwAlertBanner
391
430
  snapshot={snapshot}
392
431
  preserveOnlyCount={preserveOnlyCount}
393
432
  workflowBlockedReasons={blockedReasons}
394
- />
433
+ /> : null}
395
434
 
396
435
  <div className="flex flex-1 min-h-0">
397
436
  {/* Collapsible document navigator — page mode only */}
398
- {isPageWorkspace ? (
437
+ {isPageWorkspace && chromeVisibility.pageChrome ? (
399
438
  <aside
400
439
  aria-label="Document navigator"
401
440
  className={`shrink-0 border-r border-border bg-surface transition-[width] duration-200 ${
@@ -459,7 +498,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
459
498
  ) : null}
460
499
 
461
500
  {/* Navigator expand toggle — page mode only when collapsed */}
462
- {isPageWorkspace && !navOpen ? (
501
+ {isPageWorkspace && chromeVisibility.pageChrome && !navOpen ? (
463
502
  <div className="shrink-0 flex items-start pt-2 pl-1">
464
503
  <Tooltip.Root>
465
504
  <Tooltip.Trigger asChild>
@@ -500,7 +539,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
500
539
  }`}
501
540
  style={isPageWorkspace && zoomScale !== 1 ? { transform: `scale(${zoomScale})`, transformOrigin: "top center" } : undefined}
502
541
  >
503
- {isPageWorkspace && snapshot.pageLayout ? (
542
+ {isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
504
543
  <div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
505
544
  <div className="flex flex-wrap items-center justify-between gap-2">
506
545
  <div className="flex flex-wrap items-center gap-2 text-xs text-secondary">
@@ -531,7 +570,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
531
570
  <button
532
571
  type="button"
533
572
  aria-label="Link header to previous"
534
- disabled={!props.onSetHeaderFooterLink}
573
+ disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
535
574
  onMouseDown={preserveEditorSelectionMouseDown}
536
575
  onClick={() => {
537
576
  dismissSelectionToolbar();
@@ -548,7 +587,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
548
587
  <button
549
588
  type="button"
550
589
  aria-label="Link footer to previous"
551
- disabled={!props.onSetHeaderFooterLink}
590
+ disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
552
591
  onMouseDown={preserveEditorSelectionMouseDown}
553
592
  onClick={() => {
554
593
  dismissSelectionToolbar();
@@ -582,13 +621,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
582
621
  </div>
583
622
  </div>
584
623
  ) : null}
585
- {isPageWorkspace && snapshot.pageLayout && layoutToolsOpen ? (
624
+ {isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout && layoutToolsOpen ? (
586
625
  <div className="px-5 pt-3">
587
626
  <TwPageRuler
588
627
  pageLayout={snapshot.pageLayout}
589
628
  viewState={viewState}
590
629
  paragraphLayout={activeParagraphLayout}
591
- readOnly={snapshot.readOnly}
630
+ readOnly={pageChromeReadOnly}
592
631
  onReturnToBody={props.onCloseStory
593
632
  ? runWithSelectionToolbarDismiss(props.onCloseStory)
594
633
  : () => undefined}
@@ -619,7 +658,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
619
658
  />
620
659
  <TwLayoutPanel
621
660
  pageLayout={snapshot.pageLayout}
622
- readOnly={snapshot.readOnly || snapshot.activeStory.kind !== "main"}
661
+ readOnly={pageChromeReadOnly}
623
662
  onInsertSectionBreak={props.onInsertSectionBreak
624
663
  ? (type) => {
625
664
  dismissSelectionToolbar();
@@ -647,11 +686,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
647
686
  />
648
687
  </div>
649
688
  ) : null}
650
- {contextualSurface ? (
689
+ {chromeVisibility.contextToolbars && contextualSurface ? (
651
690
  <div className="px-5 pt-3 space-y-3">
652
691
  {contextualSurface === "table" ? (
653
692
  <TwTableContextToolbar
654
- disabled={!caps?.canEdit}
693
+ disabled={!allowLocalChromeMutations}
655
694
  tableStyles={props.styleCatalog?.tables ?? []}
656
695
  onSetTableStyle={props.onSetTableStyle
657
696
  ? (styleId) => {
@@ -679,7 +718,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
679
718
  {contextualSurface === "image" && props.activeImageContext ? (
680
719
  <TwImageContextToolbar
681
720
  activeImage={props.activeImageContext}
682
- disabled={!caps?.canEdit}
721
+ disabled={!allowLocalChromeMutations}
683
722
  onSetImageLayout={props.onSetImageLayout
684
723
  ? (mediaId, dimensions) => {
685
724
  dismissSelectionToolbar();
@@ -699,7 +738,44 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
699
738
  ) : null}
700
739
  </div>
701
740
  ) : null}
702
- {props.selectionToolbar && selectionToolbarPlacement ? (
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 ? (
703
779
  <div className="pointer-events-none absolute inset-0 z-20" data-testid="selection-toolbar-overlay">
704
780
  <div
705
781
  className="pointer-events-auto absolute"
@@ -722,7 +798,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
722
798
  </div>
723
799
  </div>
724
800
  ) : null}
725
- {props.selectionToolbar && !selectionToolbarPlacement ? (
801
+ {chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && !selectionToolbarPlacement ? (
726
802
  <div
727
803
  className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
728
804
  data-testid="selection-toolbar-fallback"
@@ -748,7 +824,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
748
824
  className={isPageWorkspace ? "relative" : undefined}
749
825
  data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
750
826
  >
751
- {isPageWorkspace && pageChromeModel.lineNumberingEnabled ? (
827
+ {isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
752
828
  <div
753
829
  aria-hidden="true"
754
830
  className="pointer-events-none absolute inset-y-0 left-0 z-10"
@@ -767,7 +843,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
767
843
  </div>
768
844
  ) : null}
769
845
  <div
770
- className={isPageWorkspace && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
846
+ className={isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
771
847
  style={isPageWorkspace ? pageShellMetrics.contentInsetStyle : undefined}
772
848
  >
773
849
  <div
@@ -781,7 +857,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
781
857
  }
782
858
  : pageChromeModel.documentGridStyle}
783
859
  >
784
- {isPageWorkspace ? (
860
+ {isPageWorkspace && chromeVisibility.pageChrome ? (
785
861
  <div
786
862
  data-testid="page-header-band"
787
863
  className="relative z-10 flex items-center justify-between border-b border-dashed border-border/60 px-4 text-[11px] text-secondary"
@@ -800,7 +876,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
800
876
  ) : null}
801
877
  </div>
802
878
  ) : null}
803
- {isPageWorkspace && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
879
+ {isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
804
880
  <div
805
881
  aria-hidden="true"
806
882
  className="pointer-events-none absolute inset-0 z-0 rounded-[2px]"
@@ -811,7 +887,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
811
887
  <div className={isPageWorkspace ? "relative z-10" : undefined}>
812
888
  {props.document}
813
889
  </div>
814
- {isPageWorkspace ? (
890
+ {isPageWorkspace && chromeVisibility.pageChrome ? (
815
891
  <div
816
892
  data-testid="page-footer-band"
817
893
  className="relative z-10 flex items-center justify-between border-t border-dashed border-border/60 px-4 text-[11px] text-secondary"
@@ -836,14 +912,16 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
836
912
  </div>
837
913
  </div>
838
914
 
839
- <TwStatusBar
840
- isDirty={snapshot.isDirty}
841
- isExportBlocked={snapshot.compatibility.blockExport}
842
- preserveOnlyCount={preserveOnlyCount}
843
- commentCount={snapshot.comments.totalCount}
844
- changeCount={snapshot.trackedChanges.totalCount}
845
- sessionId={snapshot.sessionId}
846
- />
915
+ {chromeVisibility.statusBar ? (
916
+ <TwStatusBar
917
+ isDirty={snapshot.isDirty}
918
+ isExportBlocked={snapshot.compatibility.blockExport}
919
+ preserveOnlyCount={preserveOnlyCount}
920
+ commentCount={snapshot.comments.totalCount}
921
+ changeCount={snapshot.trackedChanges.totalCount}
922
+ sessionId={snapshot.sessionId}
923
+ />
924
+ ) : null}
847
925
  </div>
848
926
 
849
927
  {/* Review rail — hidden in editing mode unless toggled */}