@beyondwork/docx-react-component 1.0.79 → 1.0.81

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +28 -21
  3. package/src/api/v3/ai/resolve.ts +13 -7
  4. package/src/api/v3/runtime/workflow.ts +0 -9
  5. package/src/api/v3/ui/chrome-composition.ts +10 -2
  6. package/src/core/commands/add-scope.ts +110 -84
  7. package/src/runtime/formatting/formatting-types.ts +16 -0
  8. package/src/runtime/formatting/revision-display.ts +16 -10
  9. package/src/runtime/scopes/compile-scope-bundle.ts +9 -1
  10. package/src/runtime/scopes/compile-scope.ts +16 -0
  11. package/src/runtime/scopes/enumerate-scopes.ts +116 -3
  12. package/src/runtime/scopes/replaceability.ts +16 -0
  13. package/src/runtime/scopes/replacement/apply.ts +13 -3
  14. package/src/runtime/scopes/resolve-reference.ts +5 -0
  15. package/src/runtime/scopes/scope-kinds/scope.ts +87 -0
  16. package/src/runtime/scopes/scope-range.ts +11 -0
  17. package/src/runtime/workflow/coordinator.ts +3 -6
  18. package/src/runtime/workflow/scope-writer.ts +5 -26
  19. package/src/ui/WordReviewEditor.tsx +62 -3
  20. package/src/ui/editor-shell-view.tsx +1 -0
  21. package/src/ui/headless/revision-decoration-model.ts +10 -0
  22. package/src/ui-tailwind/chrome/editor-action-registry.ts +153 -0
  23. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +2 -0
  24. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  25. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +15 -10
  26. package/src/ui-tailwind/review-workspace/types.ts +1 -0
  27. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +46 -6
  28. package/src/ui-tailwind/theme/editor-theme.css +10 -1
  29. package/src/ui-tailwind/tw-review-workspace.tsx +114 -14
@@ -66,11 +66,30 @@ export interface EditorActionDispatchContext {
66
66
  * `WordReviewEditorRef`; the surface is intentionally minimal now.
67
67
  */
68
68
  export interface EditorActionHostCallbacks {
69
+ // Command history
70
+ readonly onUndo?: () => void;
71
+ readonly onRedo?: () => void;
72
+
69
73
  // Clipboard (always available — browser-native fallback acceptable)
70
74
  readonly onCut?: () => void;
71
75
  readonly onCopy?: () => void;
72
76
  readonly onPaste?: () => void;
73
77
 
78
+ // Formatting / paragraph operations (plain text + table-cell targets)
79
+ readonly onToggleBold?: () => void;
80
+ readonly onToggleItalic?: () => void;
81
+ readonly onToggleUnderline?: () => void;
82
+ readonly onToggleStrikethrough?: () => void;
83
+ readonly onToggleBulletedList?: () => void;
84
+ readonly onToggleNumberedList?: () => void;
85
+ readonly onOutdent?: () => void;
86
+ readonly onIndent?: () => void;
87
+ readonly onSetAlignment?: (
88
+ alignment: "left" | "center" | "right" | "justify",
89
+ ) => void;
90
+ readonly onInsertTable?: () => void;
91
+ readonly onAddComment?: () => void;
92
+
74
93
  // Tracked-change operations (suggestion target)
75
94
  readonly onAcceptSuggestion?: () => void;
76
95
  readonly onRejectSuggestion?: () => void;
@@ -87,6 +106,7 @@ export interface EditorActionHostCallbacks {
87
106
  readonly onInsertColumnAfter?: () => void;
88
107
  readonly onDeleteRow?: () => void;
89
108
  readonly onDeleteColumn?: () => void;
109
+ readonly onDeleteTable?: () => void;
90
110
  readonly onMergeCells?: () => void;
91
111
  readonly onSplitCell?: () => void;
92
112
  readonly onOpenTableProperties?: () => void;
@@ -220,6 +240,24 @@ function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
220
240
  }
221
241
 
222
242
  export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
243
+ // -------- History --------
244
+ mk({
245
+ id: "undo",
246
+ label: "Undo",
247
+ shortcut: ["Mod", "Z"],
248
+ group: "misc",
249
+ targetKinds: ["plain-text", "table-cell"],
250
+ callback: "onUndo",
251
+ }),
252
+ mk({
253
+ id: "redo",
254
+ label: "Redo",
255
+ shortcut: ["Mod", "Shift", "Z"],
256
+ group: "misc",
257
+ targetKinds: ["plain-text", "table-cell"],
258
+ callback: "onRedo",
259
+ }),
260
+
223
261
  // -------- Clipboard (always available across every target kind) --------
224
262
  mk({
225
263
  id: "cut",
@@ -271,6 +309,113 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
271
309
  callback: "onPaste",
272
310
  }),
273
311
 
312
+ // -------- Formatting / paragraph --------
313
+ mk({
314
+ id: "format-bold",
315
+ label: "Bold",
316
+ shortcut: ["Mod", "B"],
317
+ group: "formatting",
318
+ targetKinds: ["plain-text", "table-cell"],
319
+ callback: "onToggleBold",
320
+ }),
321
+ mk({
322
+ id: "format-italic",
323
+ label: "Italic",
324
+ shortcut: ["Mod", "I"],
325
+ group: "formatting",
326
+ targetKinds: ["plain-text", "table-cell"],
327
+ callback: "onToggleItalic",
328
+ }),
329
+ mk({
330
+ id: "format-underline",
331
+ label: "Underline",
332
+ shortcut: ["Mod", "U"],
333
+ group: "formatting",
334
+ targetKinds: ["plain-text", "table-cell"],
335
+ callback: "onToggleUnderline",
336
+ }),
337
+ mk({
338
+ id: "format-strikethrough",
339
+ label: "Strikethrough",
340
+ group: "formatting",
341
+ targetKinds: ["plain-text", "table-cell"],
342
+ callback: "onToggleStrikethrough",
343
+ }),
344
+ mk({
345
+ id: "list-bulleted",
346
+ label: "Bulleted list",
347
+ group: "formatting",
348
+ targetKinds: ["plain-text", "table-cell"],
349
+ callback: "onToggleBulletedList",
350
+ }),
351
+ mk({
352
+ id: "list-numbered",
353
+ label: "Numbered list",
354
+ group: "formatting",
355
+ targetKinds: ["plain-text", "table-cell"],
356
+ callback: "onToggleNumberedList",
357
+ }),
358
+ mk({
359
+ id: "paragraph-outdent",
360
+ label: "Decrease indent",
361
+ group: "formatting",
362
+ targetKinds: ["plain-text", "table-cell"],
363
+ callback: "onOutdent",
364
+ }),
365
+ mk({
366
+ id: "paragraph-indent",
367
+ label: "Increase indent",
368
+ group: "formatting",
369
+ targetKinds: ["plain-text", "table-cell"],
370
+ callback: "onIndent",
371
+ }),
372
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
373
+ id: "align-left",
374
+ label: "Align left",
375
+ group: "formatting",
376
+ targetKinds: ["plain-text", "table-cell"],
377
+ callback: "onSetAlignment",
378
+ payload: "left",
379
+ }),
380
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
381
+ id: "align-center",
382
+ label: "Align center",
383
+ group: "formatting",
384
+ targetKinds: ["plain-text", "table-cell"],
385
+ callback: "onSetAlignment",
386
+ payload: "center",
387
+ }),
388
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
389
+ id: "align-right",
390
+ label: "Align right",
391
+ group: "formatting",
392
+ targetKinds: ["plain-text", "table-cell"],
393
+ callback: "onSetAlignment",
394
+ payload: "right",
395
+ }),
396
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
397
+ id: "align-justify",
398
+ label: "Justify",
399
+ group: "formatting",
400
+ targetKinds: ["plain-text", "table-cell"],
401
+ callback: "onSetAlignment",
402
+ payload: "justify",
403
+ }),
404
+ mk({
405
+ id: "insert-table",
406
+ label: "Insert table",
407
+ group: "table",
408
+ targetKinds: ["plain-text"],
409
+ callback: "onInsertTable",
410
+ }),
411
+ mk({
412
+ id: "comment-add",
413
+ label: "Add comment",
414
+ group: "comment",
415
+ targetKinds: ["plain-text", "table-cell"],
416
+ callback: "onAddComment",
417
+ }),
418
+
274
419
  // -------- Suggestion / tracked change --------
275
420
  mk({
276
421
  id: "accept-suggestion",
@@ -379,6 +524,14 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
379
524
  targetKinds: ["table-column", "table-whole"],
380
525
  callback: "onDeleteColumn",
381
526
  }),
527
+ // ── Whole-table destructive op ──
528
+ mk({
529
+ id: "table-delete-table",
530
+ label: "Delete table",
531
+ group: "table",
532
+ targetKinds: ["table-whole"],
533
+ callback: "onDeleteTable",
534
+ }),
382
535
  // ── Whole-table — properties / borders surface at any tier so
383
536
  // they are reachable from a single right-click in a cell ──
384
537
  mk({
@@ -75,6 +75,7 @@ export interface TwSelectionToolHostProps {
75
75
  onDistributeColumnsEvenly?: () => void;
76
76
  onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
77
77
  onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
78
+ onOpenTableMore?: (coords: { clientX: number; clientY: number }) => void;
78
79
  }
79
80
 
80
81
  /**
@@ -299,6 +300,7 @@ function renderTool(
299
300
  onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
300
301
  onSetTableAlignment={props.onSetTableAlignment}
301
302
  onSetCellVerticalAlign={props.onSetCellVerticalAlign}
303
+ onOpenTableMore={props.onOpenTableMore}
302
304
  />
303
305
  );
304
306
  case "comment-thread":
@@ -35,6 +35,7 @@ export interface TwSelectionToolStructureProps {
35
35
  onDistributeColumnsEvenly?: () => void;
36
36
  onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
37
37
  onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
38
+ onOpenTableMore?: (coords: { clientX: number; clientY: number }) => void;
38
39
  }
39
40
 
40
41
  export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
@@ -58,7 +59,10 @@ export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
58
59
  return (
59
60
  <TwTableContextToolbar
60
61
  disabled={!props.model.canMutate}
61
- tableContext={props.model.activeTable ?? null} tableStyles={props.model.tableStyles ?? []}
62
+ tableContext={props.model.activeTable ?? null}
63
+ tableStyles={props.model.tableStyles ?? []}
64
+ compact={true}
65
+ onOpenMore={props.onOpenTableMore}
62
66
  onSetTableStyle={props.onSetTableStyle}
63
67
  onAddRowBefore={props.onAddRowBefore}
64
68
  onAddRowAfter={props.onAddRowAfter}
@@ -58,6 +58,18 @@ function tableContextToKinds(
58
58
  }
59
59
  }
60
60
 
61
+ function mergeTableTierKinds(
62
+ kinds: readonly TargetKind[],
63
+ tableContext: TableStructureContextSnapshot | null | undefined,
64
+ ): readonly TargetKind[] {
65
+ if (!kinds.includes("table-cell") || !tableContext) return kinds;
66
+ const merged = [...kinds];
67
+ for (const k of tableContextToKinds(tableContext)) {
68
+ if (!merged.includes(k)) merged.push(k);
69
+ }
70
+ return merged;
71
+ }
72
+
61
73
  export interface ContextMenuRequest {
62
74
  readonly clientX: number;
63
75
  readonly clientY: number;
@@ -149,14 +161,7 @@ export function useContextMenuController(
149
161
  // is in a table cell. Cell-level ops carry every table-* kind
150
162
  // (so they are still visible) and tier-only ops (delete row /
151
163
  // delete column) gate on the matching kind.
152
- const tierKinds =
153
- baseKinds.includes("table-cell") && tableContext
154
- ? tableContextToKinds(tableContext)
155
- : [];
156
- const merged = [...baseKinds];
157
- for (const k of tierKinds) {
158
- if (!merged.includes(k)) merged.push(k);
159
- }
164
+ const merged = mergeTableTierKinds(baseKinds, tableContext);
160
165
  const entries = buildEntries(merged);
161
166
  // Progressive disclosure: if no actions resolve (every host callback
162
167
  // absent + no scope-anchor + etc), don't open an empty menu. Let
@@ -180,7 +185,7 @@ export function useContextMenuController(
180
185
  clientY: number;
181
186
  kinds: readonly TargetKind[];
182
187
  }) => {
183
- const entries = buildEntries(args.kinds);
188
+ const entries = buildEntries(mergeTableTierKinds(args.kinds, tableContext));
184
189
  if (entries.length === 0) return;
185
190
  setState({
186
191
  open: true,
@@ -189,7 +194,7 @@ export function useContextMenuController(
189
194
  entries,
190
195
  });
191
196
  },
192
- [buildEntries],
197
+ [buildEntries, tableContext],
193
198
  );
194
199
 
195
200
  // D.5.Nit1 — the return value is consumed directly by the caller
@@ -150,6 +150,7 @@ export interface TwReviewWorkspaceProps {
150
150
  interactionGuardSnapshot?: InteractionGuardSnapshot;
151
151
  chromePreset?: WordReviewEditorChromePreset;
152
152
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;
153
+ density?: "compact" | "standard" | "comfortable";
153
154
  /** P9g — live collab session for the `"collab"` chrome preset's top nav. */
154
155
  collabSession?: import("../../runtime/collab-session.ts").CollabSession;
155
156
  collabTransportStatus?: import("../../api/awareness-identity-types.ts").TransportStatus;
@@ -10,10 +10,18 @@ export type UseWorkspaceCompositionOptions = Pick<
10
10
  ChromeCompositionInput,
11
11
  | "chromePreset"
12
12
  | "chromeOptions"
13
+ | "chromeVisibility"
13
14
  | "reviewMode"
14
15
  | "role"
16
+ | "readOnly"
15
17
  | "markupDisplay"
18
+ | "activeRailTab"
19
+ | "railOpen"
20
+ | "pinnedRailTabs"
21
+ | "density"
22
+ | "containerWidth"
16
23
  | "diagnosticsSignal"
24
+ | "modeOverride"
17
25
  >;
18
26
 
19
27
  /**
@@ -49,11 +57,11 @@ export type UseWorkspaceCompositionOptions = Pick<
49
57
  * downstream. Review M6 (2026-04-22) flagged this as a silent perf
50
58
  * regression risk.
51
59
  *
52
- * `readOnly` is intentionally omitted from the option set: the
53
- * composition seam is independent of read-only posture today; Phase E
54
- * threads it through the interactionGuardSnapshot wiring once the rail
55
- * owns the full posture composition.
56
- */
60
+ * Product-lane note: this hook intentionally accepts the full set of
61
+ * mounted composition signals the resolver already models. Leaving these
62
+ * out makes `resolveChromeComposition()` look canonical in tests while the
63
+ * shipped editor silently falls back to stale defaults.
64
+ */
57
65
  export function useWorkspaceComposition(
58
66
  options: UseWorkspaceCompositionOptions,
59
67
  ): ReturnType<typeof resolveChromeComposition> {
@@ -61,10 +69,18 @@ export function useWorkspaceComposition(
61
69
  const {
62
70
  chromePreset,
63
71
  chromeOptions,
72
+ chromeVisibility,
64
73
  reviewMode,
65
74
  role,
75
+ readOnly,
66
76
  markupDisplay,
77
+ activeRailTab,
78
+ railOpen,
79
+ pinnedRailTabs,
80
+ density,
81
+ containerWidth,
67
82
  diagnosticsSignal,
83
+ modeOverride,
68
84
  } = options;
69
85
 
70
86
  return useMemo(
@@ -72,15 +88,39 @@ export function useWorkspaceComposition(
72
88
  const input: ChromeCompositionInput = {
73
89
  chromePreset,
74
90
  chromeOptions,
91
+ chromeVisibility,
75
92
  reviewMode,
76
93
  role,
94
+ readOnly,
77
95
  markupDisplay,
96
+ activeRailTab,
97
+ railOpen,
98
+ pinnedRailTabs,
99
+ density,
100
+ containerWidth,
78
101
  diagnosticsSignal,
102
+ modeOverride,
79
103
  };
80
104
  return ui
81
105
  ? ui.chrome.getComposition(input)
82
106
  : resolveChromeComposition(input);
83
107
  },
84
- [ui, chromePreset, chromeOptions, reviewMode, role, markupDisplay, diagnosticsSignal],
108
+ [
109
+ ui,
110
+ chromePreset,
111
+ chromeOptions,
112
+ chromeVisibility,
113
+ reviewMode,
114
+ role,
115
+ readOnly,
116
+ markupDisplay,
117
+ activeRailTab,
118
+ railOpen,
119
+ pinnedRailTabs,
120
+ density,
121
+ containerWidth,
122
+ diagnosticsSignal,
123
+ modeOverride,
124
+ ],
85
125
  );
86
126
  }
@@ -1075,7 +1075,16 @@
1075
1075
  }
1076
1076
 
1077
1077
  .wre-page-band:hover {
1078
- background-color: color-mix(in srgb, var(--color-bg-muted) 80%, var(--color-surface));
1078
+ /*
1079
+ * N3 audit (2026-04-24): the prior `color-mix(bg-muted 80%, surface 20%)`
1080
+ * blended two near-adjacent tokens — in dark mode (#17211C vs #182420)
1081
+ * the result moved ≤2 RGB units from the base, leaving no visible
1082
+ * hover affordance. `--color-bg-hover` is the token the design system
1083
+ * already dedicates to this signal (light #EAF6EF vs muted #F7FAF8;
1084
+ * dark #21342A vs muted #17211C) and resolves the dark-mode legibility
1085
+ * gap without regressing light-mode subtlety.
1086
+ */
1087
+ background-color: var(--color-bg-hover);
1079
1088
  }
1080
1089
 
1081
1090
  .wre-page-band[data-active="true"] {
@@ -43,7 +43,10 @@ import {
43
43
  import { TwContextBand } from "./chrome/tw-context-band";
44
44
  import { TwRoleActionRegion } from "./toolbar/tw-role-action-region";
45
45
  import { LocalSurfaceArbiterContext } from "./chrome/local-surface-arbiter";
46
- import { TwWorkspaceChromeHost } from "./chrome/tw-workspace-chrome-host";
46
+ import {
47
+ TwWorkspaceChromeHost,
48
+ type TwWorkspaceChromeHostController,
49
+ } from "./chrome/tw-workspace-chrome-host";
47
50
  import { TwChromeOverlay, TwPageStackOverlayLayer } from "./chrome-overlay";
48
51
  import { TwFloatingImageLayer } from "./page-stack/tw-floating-image-layer.tsx";
49
52
  import { shouldHidePageBorderForSelection } from "./review-workspace/paragraph-layout.ts";
@@ -85,16 +88,14 @@ export type {
85
88
  import type { EditorRole } from "../api/public-types.ts";
86
89
 
87
90
  // Default shell-header modes for the workspace's default composition.
88
- // Designsystem §6.1 prescribes a 4-mode switcher (edit / review / workflow
89
- // / more). "more" is kept in the layout for parity but marked disabled
90
- // until its handler is defined by refactor/11 Slice 7 + refactor/10
91
- // Slice 5 (Phase Q debug UX). Hosts that want a fully-wired 4-mode set
92
- // supply their own `shellHeader` prop.
91
+ // Designsystem §6.1 prescribes a 4-mode switcher; all four are reachable in
92
+ // the product path. "More" is a shell-owned diagnostics/search posture, so it
93
+ // uses `ChromeCompositionInput.modeOverride` instead of changing runtime role.
93
94
  const DEFAULT_WORKSPACE_SHELL_MODES: readonly ShellHeaderModeOption[] = [
94
95
  { id: "edit", label: "Edit" },
95
96
  { id: "review", label: "Review" },
96
97
  { id: "workflow", label: "Workflow" },
97
- { id: "more", label: "More", disabled: true },
98
+ { id: "more", label: "More" },
98
99
  ];
99
100
 
100
101
  function editorRoleToShellMode(role: EditorRole): ShellHeaderMode {
@@ -121,6 +122,47 @@ function shellModeToEditorRole(mode: ShellHeaderMode): EditorRole | null {
121
122
  }
122
123
  }
123
124
 
125
+ function TwMoreContextBandContent(props: {
126
+ onOpenDiagnostics: () => void;
127
+ onOpenCompatibility: () => void;
128
+ onOpenOutline: () => void;
129
+ onOpenSearch?: () => void;
130
+ }): React.JSX.Element {
131
+ return (
132
+ <div
133
+ className="flex min-w-0 items-center gap-1"
134
+ data-testid="more-context-band-content"
135
+ >
136
+ <ContextBandButton label="Diagnostics" onClick={props.onOpenDiagnostics} />
137
+ <ContextBandButton label="Compatibility" onClick={props.onOpenCompatibility} />
138
+ <ContextBandButton label="Outline" onClick={props.onOpenOutline} />
139
+ <ContextBandButton
140
+ label="Search"
141
+ onClick={props.onOpenSearch}
142
+ disabled={!props.onOpenSearch}
143
+ />
144
+ </div>
145
+ );
146
+ }
147
+
148
+ function ContextBandButton(props: {
149
+ label: string;
150
+ onClick?: () => void;
151
+ disabled?: boolean;
152
+ }): React.JSX.Element {
153
+ return (
154
+ <button
155
+ type="button"
156
+ disabled={props.disabled}
157
+ onMouseDown={preserveEditorSelectionMouseDown}
158
+ onClick={props.onClick}
159
+ className="inline-flex h-7 items-center rounded-[var(--radius-md)] px-2.5 text-xs font-medium text-secondary transition-colors hover:bg-hover focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] disabled:cursor-not-allowed disabled:opacity-40"
160
+ >
161
+ {props.label}
162
+ </button>
163
+ );
164
+ }
165
+
124
166
  export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
125
167
  const props = {
126
168
  ...inputProps,
@@ -128,6 +170,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
128
170
  } as TwReviewWorkspaceProps & EditorCommandBag;
129
171
  const { snapshot, viewState } = props;
130
172
  const selectionToolbarRootRef = useRef<HTMLDivElement>(null);
173
+ const workspaceChromeControllerRef =
174
+ useRef<TwWorkspaceChromeHostController | null>(null);
175
+ const [shellModeOverride, setShellModeOverride] =
176
+ useState<ShellHeaderMode | null>(null);
131
177
  // P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
132
178
  // root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
133
179
  // measure per-page rects and to reparent PM's DOM node across band
@@ -382,18 +428,46 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
382
428
  props.onSetParagraphTabStops,
383
429
  ]);
384
430
 
431
+ const setWorkspaceChromeController = useCallback(
432
+ (instance: TwWorkspaceChromeHostController | null) => {
433
+ workspaceChromeControllerRef.current = instance;
434
+ const ref = props.chromeControllerRef;
435
+ if (!ref) return;
436
+ if (typeof ref === "function") {
437
+ ref(instance);
438
+ return;
439
+ }
440
+ (ref as React.MutableRefObject<TwWorkspaceChromeHostController | null>).current =
441
+ instance;
442
+ },
443
+ [props.chromeControllerRef],
444
+ );
445
+
446
+ const openTableMoreMenu = useCallback(
447
+ (coords: { clientX: number; clientY: number }) => {
448
+ workspaceChromeControllerRef.current?.openWithKinds({
449
+ ...coords,
450
+ kinds: ["table-cell"],
451
+ });
452
+ },
453
+ [],
454
+ );
455
+
385
456
  // Audit §2.4 — the shell header is ALWAYS present in default composition.
386
457
  // When the host does not supply a pre-assembled shell node, fall back to
387
458
  // a default TwShellHeader wired to the workspace's editor-role state so
388
459
  // the mode tabs actually change the active role instead of being
389
- // decorative. The "more" tab is included for layout parity with §6.1
390
- // but disabled until its handler is defined (Phase Q debug UX / Slice 7).
460
+ // decorative. The "more" tab is shell-owned diagnostics/search posture:
461
+ // it drives composition mode via `modeOverride` and does not mutate the
462
+ // runtime editor role.
391
463
  // Host-supplied shells continue to win (back-compat).
392
464
  const defaultShellModes: readonly ShellHeaderModeOption[] =
393
465
  DEFAULT_WORKSPACE_SHELL_MODES;
394
466
  const defaultShellActiveMode: ShellHeaderMode = editorRoleToShellMode(
395
467
  viewState.editorRole,
396
468
  );
469
+ const activeShellMode: ShellHeaderMode =
470
+ shellModeOverride === "more" ? "more" : defaultShellActiveMode;
397
471
  // coord-11 §21 — host-supplied shellHeader wins; otherwise the default
398
472
  // TwShellHeader mounts only when `chromeVisibility.shellHeader === true`
399
473
  // (default for every preset except `selection`). The `selection` preset
@@ -406,8 +480,15 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
406
480
  ? (
407
481
  <TwShellHeader
408
482
  modes={defaultShellModes}
409
- activeMode={defaultShellActiveMode}
483
+ activeMode={activeShellMode}
410
484
  onModeChange={(mode) => {
485
+ if (mode === "more") {
486
+ setShellModeOverride("more");
487
+ setReviewRailOpen(true);
488
+ props.onActiveRailTabChange?.("health");
489
+ return;
490
+ }
491
+ setShellModeOverride(null);
411
492
  const nextRole = shellModeToEditorRole(mode);
412
493
  if (nextRole !== null && nextRole !== viewState.editorRole) {
413
494
  props.onEditorRoleChange?.(nextRole);
@@ -444,10 +525,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
444
525
  const composition = useWorkspaceComposition({
445
526
  chromePreset,
446
527
  chromeOptions: props.chromeOptions,
528
+ chromeVisibility: props.chromeVisibility,
447
529
  reviewMode: props.reviewMode,
448
530
  role: viewState.editorRole,
531
+ readOnly: snapshot.readOnly,
449
532
  markupDisplay: props.markupDisplay,
533
+ activeRailTab: props.activeRailTab,
534
+ railOpen: reviewRailOpen,
535
+ density: props.density,
536
+ containerWidth: viewportWidth,
450
537
  diagnosticsSignal,
538
+ modeOverride: shellModeOverride === "more" ? "more" : undefined,
451
539
  });
452
540
  const showHealthRailTab = composition.rail.visibleTabs.has("health");
453
541
 
@@ -481,7 +569,20 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
481
569
  */}
482
570
  {chromeVisibility.toolbar ? (
483
571
  <TwContextBand mode={composition.mode}>
484
- {viewState.editorRole ? (
572
+ {composition.mode === "more" ? (
573
+ <TwMoreContextBandContent
574
+ onOpenDiagnostics={() => {
575
+ setReviewRailOpen(true);
576
+ props.onActiveRailTabChange?.("health");
577
+ }}
578
+ onOpenCompatibility={() => {
579
+ setReviewRailOpen(true);
580
+ props.onActiveRailTabChange?.("health");
581
+ }}
582
+ onOpenOutline={() => setNavOpen(true)}
583
+ onOpenSearch={props.reviewRailFooter?.onSearch}
584
+ />
585
+ ) : viewState.editorRole ? (
485
586
  <TwRoleActionRegion
486
587
  role={viewState.editorRole}
487
588
  policy={scopedChromePolicy}
@@ -574,9 +675,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
574
675
  <TwWorkspaceChromeHost
575
676
  mode={composition.mode}
576
677
  editorActionHost={props.editorActionHost}
577
- {...(props.chromeControllerRef
578
- ? { controllerRef: props.chromeControllerRef }
579
- : {})}
678
+ controllerRef={setWorkspaceChromeController}
580
679
  {...(props.commandPaletteDisabled !== undefined
581
680
  ? { paletteDisabled: props.commandPaletteDisabled }
582
681
  : {})}
@@ -875,6 +974,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
875
974
  onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
876
975
  onSetTableAlignment={props.onSetTableAlignment}
877
976
  onSetCellVerticalAlign={props.onSetCellVerticalAlign}
977
+ onOpenTableMore={openTableMoreMenu}
878
978
  chromePins={viewState.chromePins}
879
979
  onChromePinChange={props.onChromePinChange}
880
980
  />