@beyondwork/docx-react-component 1.0.88 → 1.0.90

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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/api/v3/_runtime-handle.ts +5 -0
  3. package/src/api/v3/ai/replacement.ts +82 -0
  4. package/src/api/v3/runtime/content.ts +3 -0
  5. package/src/api/v3/runtime/formatting.ts +64 -0
  6. package/src/core/commands/formatting-commands.ts +107 -0
  7. package/src/core/state/text-transaction.ts +11 -4
  8. package/src/runtime/document-runtime.ts +51 -0
  9. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  10. package/src/runtime/scopes/action-validation.ts +12 -3
  11. package/src/runtime/scopes/audit-bundle.ts +2 -2
  12. package/src/runtime/scopes/compiler-service.ts +70 -0
  13. package/src/runtime/scopes/formatting/apply.ts +262 -0
  14. package/src/runtime/scopes/index.ts +12 -0
  15. package/src/runtime/scopes/replacement/propose.ts +2 -0
  16. package/src/runtime/scopes/scope-kinds/paragraph.ts +1 -0
  17. package/src/runtime/scopes/semantic-scope-types.ts +48 -4
  18. package/src/runtime/scopes/workflow-overlap.ts +9 -11
  19. package/src/shell/session-bootstrap.ts +1 -0
  20. package/src/ui/WordReviewEditor.tsx +277 -28
  21. package/src/ui/editor-command-bag.ts +11 -0
  22. package/src/ui/editor-shell-view.tsx +10 -0
  23. package/src/ui/headless/chrome-registry.ts +6 -6
  24. package/src/ui/headless/role-action-sets.ts +4 -10
  25. package/src/ui/headless/selection-tool-resolver.ts +11 -0
  26. package/src/ui-tailwind/chrome/editor-action-registry.ts +1 -1
  27. package/src/ui-tailwind/chrome/tw-context-band.tsx +7 -7
  28. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +13 -18
  29. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +8 -5
  30. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +100 -0
  31. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -40
  32. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +9 -7
  33. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +17 -1
  34. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +6 -1
  35. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +17 -7
  36. package/src/ui-tailwind/editor-surface/preserve-position.ts +30 -5
  37. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
  38. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
  39. package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
  40. package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
  41. package/src/ui-tailwind/review-workspace/types.ts +2 -0
  42. package/src/ui-tailwind/theme/editor-theme.css +25 -12
  43. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +20 -37
  44. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +15 -27
  45. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +24 -15
  46. package/src/ui-tailwind/tw-review-workspace.tsx +32 -18
  47. package/src/ui-tailwind/workflow-scope-layers.ts +70 -0
@@ -10,6 +10,13 @@
10
10
 
11
11
  import React from "react";
12
12
  import type { ScopeRailSegment, ScopeRailPosture } from "../../api/public-types";
13
+ import {
14
+ WORKFLOW_SCOPE_LAYER_FILTERS,
15
+ createDefaultWorkflowScopeLayerKeys,
16
+ isWorkflowScopePostureVisible,
17
+ toggleWorkflowScopeLayerKey,
18
+ type WorkflowScopeLayerKey,
19
+ } from "../workflow-scope-layers";
13
20
 
14
21
  export interface TwWorkflowTabProps {
15
22
  segments: readonly ScopeRailSegment[];
@@ -21,6 +28,8 @@ export interface TwWorkflowTabProps {
21
28
  * matching overlay card. If omitted, focus sync is not wired.
22
29
  */
23
30
  onActiveScopeChange?: (scopeId: string) => void;
31
+ enabledLayerFilters?: ReadonlySet<WorkflowScopeLayerKey>;
32
+ onEnabledLayerFiltersChange?: (filters: ReadonlySet<WorkflowScopeLayerKey>) => void;
24
33
  }
25
34
 
26
35
  const POSTURE_META: Record<
@@ -39,26 +48,13 @@ const POSTURE_META: Record<
39
48
  const focusRingClass =
40
49
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
41
50
 
42
- type ScopeFilterKey = "edit" | "suggest" | "comment" | "view" | "candidate" | "blocked";
43
-
44
- const SCOPE_FILTERS: ReadonlyArray<{
45
- key: ScopeFilterKey;
46
- label: string;
47
- postures: readonly ScopeRailPosture[];
48
- }> = [
49
- { key: "edit", label: "Edit", postures: ["edit"] },
50
- { key: "suggest", label: "Suggest", postures: ["suggest"] },
51
- { key: "comment", label: "Comment", postures: ["comment"] },
52
- { key: "view", label: "Review", postures: ["view"] },
53
- { key: "candidate", label: "Scheduled", postures: ["candidate"] },
54
- { key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
55
- ];
56
-
57
51
  export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
58
52
  segments,
59
53
  activeScopeId,
60
54
  onOpenScope,
61
55
  onActiveScopeChange,
56
+ enabledLayerFilters,
57
+ onEnabledLayerFiltersChange,
62
58
  }) => {
63
59
  // Dedupe by scopeId so a scope spanning multiple pages shows once.
64
60
  const uniqueSegments = React.useMemo(() => {
@@ -71,20 +67,31 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
71
67
  return Array.from(byScopeId.values()).sort(compareWorkflowSegments(activeScopeId ?? null));
72
68
  }, [activeScopeId, segments]);
73
69
  const [query, setQuery] = React.useState("");
74
- const [enabledFilters, setEnabledFilters] = React.useState<ReadonlySet<ScopeFilterKey>>(
75
- () => new Set(SCOPE_FILTERS.map((filter) => filter.key)),
70
+ const [uncontrolledEnabledFilters, setUncontrolledEnabledFilters] = React.useState<
71
+ ReadonlySet<WorkflowScopeLayerKey>
72
+ >(
73
+ createDefaultWorkflowScopeLayerKeys,
74
+ );
75
+ const activeEnabledFilters = enabledLayerFilters ?? uncontrolledEnabledFilters;
76
+ const setEnabledFilters = React.useCallback(
77
+ (next: ReadonlySet<WorkflowScopeLayerKey>) => {
78
+ if (!enabledLayerFilters) {
79
+ setUncontrolledEnabledFilters(next);
80
+ }
81
+ onEnabledLayerFiltersChange?.(next);
82
+ },
83
+ [enabledLayerFilters, onEnabledLayerFiltersChange],
76
84
  );
77
85
  const availableFilters = React.useMemo(() => {
78
86
  const presentPostures = new Set(uniqueSegments.map((segment) => segment.posture));
79
- return SCOPE_FILTERS.filter((filter) =>
87
+ return WORKFLOW_SCOPE_LAYER_FILTERS.filter((filter) =>
80
88
  filter.postures.some((posture) => presentPostures.has(posture)),
81
89
  );
82
90
  }, [uniqueSegments]);
83
91
  const visibleSegments = React.useMemo(() => {
84
92
  const normalizedQuery = normalizeScopeQuery(query);
85
93
  return uniqueSegments.filter((segment) => {
86
- const filterKey = filterKeyForPosture(segment.posture);
87
- if (!enabledFilters.has(filterKey)) {
94
+ if (!isWorkflowScopePostureVisible(segment.posture, activeEnabledFilters)) {
88
95
  return false;
89
96
  }
90
97
  if (!normalizedQuery) {
@@ -92,7 +99,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
92
99
  }
93
100
  return scopeSearchText(segment).includes(normalizedQuery);
94
101
  });
95
- }, [enabledFilters, query, uniqueSegments]);
102
+ }, [activeEnabledFilters, query, uniqueSegments]);
96
103
 
97
104
  if (uniqueSegments.length === 0) {
98
105
  return (
@@ -146,7 +153,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
146
153
  role="group"
147
154
  >
148
155
  {availableFilters.map((filter) => {
149
- const isEnabled = enabledFilters.has(filter.key);
156
+ const isEnabled = activeEnabledFilters.has(filter.key);
150
157
  return (
151
158
  <button
152
159
  key={filter.key}
@@ -160,15 +167,9 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
160
167
  ].join(" ")}
161
168
  data-testid={`workflow-scope-filter-${filter.key}`}
162
169
  onClick={() => {
163
- setEnabledFilters((current) => {
164
- const next = new Set(current);
165
- if (next.has(filter.key)) {
166
- next.delete(filter.key);
167
- } else {
168
- next.add(filter.key);
169
- }
170
- return next;
171
- });
170
+ setEnabledFilters(
171
+ toggleWorkflowScopeLayerKey(activeEnabledFilters, filter.key),
172
+ );
172
173
  }}
173
174
  >
174
175
  {filter.label}
@@ -245,13 +246,6 @@ function compareWorkflowSegments(activeScopeId: string | null) {
245
246
  };
246
247
  }
247
248
 
248
- function filterKeyForPosture(posture: ScopeRailPosture): ScopeFilterKey {
249
- if (posture === "preserve-only" || posture === "blocked-import") {
250
- return "blocked";
251
- }
252
- return posture;
253
- }
254
-
255
249
  function normalizeScopeQuery(value: string): string {
256
250
  return value.trim().toLocaleLowerCase();
257
251
  }
@@ -136,6 +136,8 @@ export interface TwReviewWorkspaceProps {
136
136
  searchLabel?: string;
137
137
  helpLabel?: string;
138
138
  };
139
+ /** Opens the built-in inline find surface from More/Search and command palette. */
140
+ onOpenInlineFind?: () => void;
139
141
  document: ReactNode;
140
142
  workspaceMode: WorkspaceMode;
141
143
  zoomLevel?: ZoomLevel;
@@ -475,19 +475,19 @@
475
475
  }
476
476
 
477
477
  .wre-scope-rail-tint-accent {
478
- background: color-mix(in srgb, var(--color-accent) 12%, transparent);
478
+ background: color-mix(in srgb, var(--color-accent) 20%, transparent);
479
479
  }
480
480
  .wre-scope-rail-tint-warning {
481
- background: color-mix(in srgb, var(--color-warning) 14%, transparent);
481
+ background: color-mix(in srgb, var(--color-warning) 23%, transparent);
482
482
  }
483
483
  .wre-scope-rail-tint-insert {
484
- background: color-mix(in srgb, var(--color-insert) 12%, transparent);
484
+ background: color-mix(in srgb, var(--color-insert) 20%, transparent);
485
485
  }
486
486
  .wre-scope-rail-tint-secondary {
487
- background: color-mix(in srgb, var(--color-secondary) 9%, transparent);
487
+ background: color-mix(in srgb, var(--color-secondary) 16%, transparent);
488
488
  }
489
489
  .wre-scope-rail-tint-danger {
490
- background: color-mix(in srgb, var(--color-danger) 14%, transparent);
490
+ background: color-mix(in srgb, var(--color-danger) 24%, transparent);
491
491
  }
492
492
 
493
493
  /* §3.7 canonical scope families */
@@ -543,22 +543,22 @@
543
543
  /*
544
544
  * ─── Scope rail stripe ───
545
545
  *
546
- * The rail stripe is the rest-state representation of a scope: a 4px
546
+ * The rail stripe is the rest-state representation of a scope: a 6px
547
547
  * color stripe in the gutter lane. Posture color comes from the
548
548
  * accent/warning/insert/secondary/danger tokens. Hover widens the
549
549
  * stripe via transform (zero layout cost) and reveals the label pill.
550
550
  */
551
551
  .wre-scope-rail-stripe {
552
552
  position: absolute;
553
- width: 4px;
554
- border-radius: 2px;
553
+ width: 6px;
554
+ border-radius: 999px;
555
555
  background: currentColor;
556
556
  pointer-events: auto;
557
557
  cursor: pointer;
558
558
  z-index: 1;
559
559
  transform-origin: left center;
560
560
  transition: transform 120ms ease-out, opacity 120ms ease-out;
561
- opacity: 0.75;
561
+ opacity: 0.9;
562
562
  /* Reset button defaults. */
563
563
  border: none;
564
564
  padding: 0;
@@ -568,16 +568,22 @@
568
568
  background-clip: padding-box;
569
569
  }
570
570
 
571
+ .wre-scope-rail-stripe::before {
572
+ content: "";
573
+ position: absolute;
574
+ inset: -5px -10px;
575
+ }
576
+
571
577
  .wre-scope-rail-stripe:hover,
572
578
  .wre-scope-rail-stripe:focus-visible {
573
- transform: scaleX(1.5);
579
+ transform: scaleX(1.45);
574
580
  opacity: 1;
575
581
  outline: none;
576
582
  }
577
583
 
578
584
  .wre-scope-rail-stripe-active {
579
585
  opacity: 1;
580
- transform: scaleX(1.75);
586
+ transform: scaleX(1.6);
581
587
  }
582
588
 
583
589
  .wre-scope-rail-stripe.wre-scope-rail-label-accent { color: var(--color-accent); }
@@ -623,6 +629,8 @@
623
629
  pointer-events: none;
624
630
  transition: opacity 140ms ease-out, transform 140ms ease-out;
625
631
  transform: translateX(-4px);
632
+ margin: 0;
633
+ font-family: inherit;
626
634
  }
627
635
 
628
636
  .wre-scope-rail-stripe:hover + .wre-scope-rail-label,
@@ -692,7 +700,12 @@
692
700
  }
693
701
 
694
702
  .wre-scope-rail-label-active {
695
- box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
703
+ opacity: 1;
704
+ pointer-events: auto;
705
+ transform: translateX(0);
706
+ box-shadow:
707
+ 0 0 0 1px color-mix(in srgb, currentColor 42%, transparent),
708
+ 0 8px 22px color-mix(in srgb, currentColor 14%, transparent);
696
709
  }
697
710
 
698
711
  .wre-scope-rail-icon {
@@ -9,8 +9,8 @@
9
9
  * Review-role actions here collapse what used to live in
10
10
  * `TwReviewQueueBar` as a second strip — the review prev/next, counts,
11
11
  * active-item label, accept/reject, markup-mode, and batch operations.
12
- * Editor-role actions surface the scope posture menu. Workflow-role
13
- * actions surface work-item traversal + claim/skip/complete.
12
+ * Workflow-role actions surface the scope posture menu plus work-item
13
+ * traversal + claim/skip/complete.
14
14
  */
15
15
 
16
16
  import React, { useState } from "react";
@@ -18,19 +18,17 @@ import * as Popover from "@radix-ui/react-popover";
18
18
  import * as Toggle from "@radix-ui/react-toggle";
19
19
  import * as Tooltip from "@radix-ui/react-tooltip";
20
20
  import {
21
- BookmarkCheck,
22
21
  Check,
23
22
  CheckCheck,
24
- ChevronDown,
25
23
  ChevronLeft,
26
24
  ChevronRight,
27
25
  CircleOff,
28
26
  FileDiff,
29
- Flag,
30
27
  Hand,
31
28
  MessageSquare,
32
29
  MessageSquareDot,
33
30
  MessageSquareText,
31
+ MoreHorizontal,
34
32
  Rows3,
35
33
  SkipForward,
36
34
  Target,
@@ -48,6 +46,7 @@ import type { ScopedChromePolicy } from "../../ui/headless/scoped-chrome-policy"
48
46
  import type { ToolbarChromeItemId } from "../../ui/headless/chrome-registry";
49
47
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
50
48
  import { ROLE_ACTION_SETS } from "../../ui/headless/role-action-sets";
49
+ import { TwDisplayModeSelector } from "../chrome/tw-display-mode-selector";
51
50
  import { TwScopePostureMenu } from "./tw-scope-posture-menu";
52
51
 
53
52
  /**
@@ -103,7 +102,7 @@ export interface TwRoleActionRegionProps {
103
102
  onReviewSidebarTrackedChanges?: () => void;
104
103
  onReviewSidebarComments?: () => void;
105
104
 
106
- // Workflow + review role: scope posture menu
105
+ // Workflow role: assign authorable scope posture.
107
106
  onMarkScopePosture?: (posture: ScopeRailPosture) => void;
108
107
 
109
108
  // Review role
@@ -185,6 +184,15 @@ function isRoleActionRenderable(
185
184
  case "review-accept-all":
186
185
  case "review-reject-all":
187
186
  return reviewQueueTotal > 0;
187
+ case "workflow-prev":
188
+ case "workflow-next":
189
+ case "workflow-mark-complete":
190
+ case "workflow-claim":
191
+ case "workflow-skip":
192
+ case "workflow-mark-blocked":
193
+ return props.workflowItem !== undefined;
194
+ case "workflow-jump-to-scope":
195
+ return props.workflowItem !== undefined || props.onWorkflowJumpToScope !== undefined;
188
196
  default:
189
197
  return true;
190
198
  }
@@ -357,10 +365,11 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
357
365
  );
358
366
  case "review-markup-mode":
359
367
  return (
360
- <MarkupModeSelect
361
- mode={props.markupDisplay ?? "simple"}
368
+ <TwDisplayModeSelector
369
+ value={props.markupDisplay ?? "simple"}
362
370
  onChange={(mode) => props.onReviewMarkupMode?.(mode)}
363
371
  disabled={!props.onReviewMarkupMode}
372
+ data-testid="role-review-markup-mode"
364
373
  />
365
374
  );
366
375
  case "workflow-prev":
@@ -433,11 +442,11 @@ function RoleActionOverflow({
433
442
  aria-label="More role actions"
434
443
  aria-expanded={open}
435
444
  onMouseDown={preserveEditorSelectionMouseDown}
436
- className="inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
445
+ title="More role actions"
446
+ className="inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-canvas text-primary transition-colors hover:bg-surface outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
437
447
  data-testid="role-action-overflow-trigger"
438
448
  >
439
- More
440
- <ChevronDown className="h-3.5 w-3.5 text-tertiary" />
449
+ <MoreHorizontal className="h-3.5 w-3.5 text-tertiary" aria-hidden="true" />
441
450
  </button>
442
451
  </Popover.Trigger>
443
452
  <Popover.Portal>
@@ -684,30 +693,4 @@ function ReviewActiveLabel({
684
693
  );
685
694
  }
686
695
 
687
- function MarkupModeSelect(arg: {
688
- mode: MarkupDisplayMode;
689
- onChange: (mode: MarkupDisplayMode) => void;
690
- disabled?: boolean;
691
- }): React.JSX.Element {
692
- const Icon = arg.mode === "clean" ? BookmarkCheck : arg.mode === "all" ? Flag : Rows3;
693
- return (
694
- <button
695
- type="button"
696
- aria-label={`Markup display: ${arg.mode}`}
697
- disabled={arg.disabled}
698
- onMouseDown={preserveEditorSelectionMouseDown}
699
- onClick={() => {
700
- const next: MarkupDisplayMode =
701
- arg.mode === "all" ? "clean" : arg.mode === "clean" ? "simple" : "all";
702
- arg.onChange(next);
703
- }}
704
- data-testid="role-review-markup-mode"
705
- className="inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
706
- >
707
- <Icon className="h-3.5 w-3.5" />
708
- <span className="capitalize">{arg.mode}</span>
709
- </button>
710
- );
711
- }
712
-
713
696
  export default TwRoleActionRegion;
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Scope posture menu — replaces the old "Mark section" button with a
3
- * topnav dropdown listing the seven `ScopeRailPosture` values so
4
- * editors can mark regions with an explicit workflow mode instead of a
5
- * single "marked" flag.
3
+ * topnav dropdown listing the authorable `ScopeRailPosture` values so
4
+ * workflow operators can mark regions with an explicit workflow mode
5
+ * instead of a single "marked" flag.
6
6
  *
7
7
  * Per runtime-rendering-and-chrome-phase.md §6.4, the menu lives inline
8
- * in the editor role's primary action region (not in the review queue
8
+ * in the workflow role's primary action region (not in the review queue
9
9
  * strip). Postures align 1:1 with the rail vocabulary so the rail
10
- * updates visually as soon as the user picks one.
10
+ * updates visually as soon as the user picks one. Runtime-only postures
11
+ * like preserve-only and blocked-import still render in overlays, but
12
+ * they are not choices users can assign from this product menu.
11
13
  */
12
14
 
13
15
  import React, { useState } from "react";
@@ -17,7 +19,6 @@ import {
17
19
  ChevronDown,
18
20
  Eye,
19
21
  Flag,
20
- Lock,
21
22
  MessageCircle,
22
23
  Pencil,
23
24
  Sparkles,
@@ -35,12 +36,12 @@ export interface TwScopePostureMenuProps {
35
36
  "data-testid"?: string;
36
37
  }
37
38
 
38
- interface PostureEntry {
39
+ export interface ScopePostureMenuEntry {
39
40
  posture: ScopeRailPosture;
40
41
  label: string;
41
42
  hint: string;
42
43
  icon: React.ComponentType<{ className?: string }>;
43
- tone: "accent" | "warning" | "comment" | "secondary" | "danger";
44
+ tone: "accent" | "warning" | "comment" | "secondary";
44
45
  }
45
46
 
46
47
  /**
@@ -49,7 +50,7 @@ interface PostureEntry {
49
50
  * glyphs via the `data-icon` attribute). Extract both into a single
50
51
  * source of truth in a follow-up.
51
52
  */
52
- const POSTURE_ENTRIES: readonly PostureEntry[] = [
53
+ export const SCOPE_POSTURE_MENU_ENTRIES: readonly ScopePostureMenuEntry[] = [
53
54
  {
54
55
  posture: "edit",
55
56
  label: "Edit scope",
@@ -85,22 +86,11 @@ const POSTURE_ENTRIES: readonly PostureEntry[] = [
85
86
  icon: Flag,
86
87
  tone: "warning",
87
88
  },
88
- {
89
- posture: "preserve-only",
90
- label: "Preserve only",
91
- hint: "Blocked — export-preserving only",
92
- icon: Lock,
93
- tone: "danger",
94
- },
95
- {
96
- posture: "blocked-import",
97
- label: "Blocked import",
98
- hint: "Blocked — imported region is locked",
99
- icon: Lock,
100
- tone: "danger",
101
- },
102
89
  ];
103
90
 
91
+ export const SCOPE_POSTURE_MENU_POSTURES: readonly ScopeRailPosture[] =
92
+ SCOPE_POSTURE_MENU_ENTRIES.map((entry) => entry.posture);
93
+
104
94
  export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.Element {
105
95
  const [open, setOpen] = useState(false);
106
96
 
@@ -131,7 +121,7 @@ export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.El
131
121
  <div className="mb-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
132
122
  Mark section with posture
133
123
  </div>
134
- {POSTURE_ENTRIES.map((entry) => (
124
+ {SCOPE_POSTURE_MENU_ENTRIES.map((entry) => (
135
125
  <Popover.Close key={entry.posture} asChild>
136
126
  <button
137
127
  type="button"
@@ -163,7 +153,7 @@ export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.El
163
153
  );
164
154
  }
165
155
 
166
- function toneClass(tone: PostureEntry["tone"]): string {
156
+ function toneClass(tone: ScopePostureMenuEntry["tone"]): string {
167
157
  switch (tone) {
168
158
  case "accent":
169
159
  return "text-accent";
@@ -171,8 +161,6 @@ function toneClass(tone: PostureEntry["tone"]): string {
171
161
  return "text-warning";
172
162
  case "comment":
173
163
  return "text-comment";
174
- case "danger":
175
- return "text-danger";
176
164
  case "secondary":
177
165
  default:
178
166
  return "text-secondary";
@@ -69,8 +69,8 @@ import {
69
69
  type ScopedChromePolicy,
70
70
  } from "../../ui/headless/scoped-chrome-policy";
71
71
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
72
+ import { TwDisplayModeSelector } from "../chrome/tw-display-mode-selector";
72
73
  import { type MarkupDisplayMode } from "./tw-role-action-region";
73
- import { TwDetachHandle } from "../chrome/tw-detach-handle";
74
74
  import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
75
75
 
76
76
  export interface TwToolbarProps {
@@ -124,6 +124,8 @@ export interface TwToolbarProps {
124
124
  onToggleSidebar?: () => void;
125
125
  onZoomChange?: (level: ZoomLevel) => void;
126
126
  onShowTrackedChangesChange: (show: boolean) => void;
127
+ /** Top-toolbar fallback for changing redline/comment display when review context band is not active. */
128
+ onMarkupDisplayChange?: (mode: MarkupDisplayMode) => void;
127
129
  onRestartNumbering?: () => void;
128
130
  onContinueNumbering?: () => void;
129
131
  onUpdateFields?: () => void;
@@ -155,9 +157,9 @@ export interface TwToolbarProps {
155
157
  onReviewNext?: () => void;
156
158
  onReviewAccept?: () => void;
157
159
  onReviewReject?: () => void;
158
- /** Current chrome pin state; when supplied enables the topnav detach handle. */
160
+ /** Current chrome pin state, retained for host compatibility. */
159
161
  chromePins?: ChromePinsState;
160
- /** Called when the user detaches or re-attaches the topnav. */
162
+ /** Called when a host-supported chrome surface changes placement. */
161
163
  onChromePinChange?: (surface: ChromePinSurface, pin: PinState | null) => void;
162
164
 
163
165
  /**
@@ -255,13 +257,18 @@ export function TwToolbar(props: TwToolbarProps) {
255
257
  });
256
258
  const showStyleSelectors = isToolbarChromeItemVisible(scopedChromePolicy, "text-style-selectors");
257
259
  const showInlineFormatting = isToolbarChromeItemVisible(scopedChromePolicy, "inline-formatting");
258
- const showAdvancedFormatting = preset === "advanced" && showInlineFormatting;
260
+ const showAdvancedFormatting =
261
+ showInlineFormatting && (preset === "advanced" || props.role === "editor");
259
262
  const showTextColors = isToolbarChromeItemVisible(scopedChromePolicy, "text-colors");
260
263
  const showParagraphAlignment = isToolbarChromeItemVisible(scopedChromePolicy, "paragraph-alignment");
261
264
  const showInsertMenu = isToolbarChromeItemVisible(scopedChromePolicy, "insert-actions");
262
265
  const showTrackedChangesToggle =
263
266
  isToolbarChromeItemVisible(scopedChromePolicy, "tracked-changes-toggle") &&
264
267
  !isChromeItemOwnedByRoleRegion("tracked-changes-toggle", props.role);
268
+ const showMarkupDisplaySelector =
269
+ props.markupDisplay !== undefined &&
270
+ props.onMarkupDisplayChange !== undefined &&
271
+ !isChromeItemOwnedByRoleRegion("review-markup-mode", props.role);
265
272
  const showRightClusterComment =
266
273
  isToolbarChromeItemVisible(scopedChromePolicy, "comment") &&
267
274
  !isChromeItemOwnedByRoleRegion("comment", props.role);
@@ -695,6 +702,17 @@ export function TwToolbar(props: TwToolbarProps) {
695
702
  </>
696
703
  ) : null}
697
704
 
705
+ {showMarkupDisplaySelector ? (
706
+ <>
707
+ <TwDisplayModeSelector
708
+ value={props.markupDisplay!}
709
+ onChange={(mode) => props.onMarkupDisplayChange?.(mode)}
710
+ data-testid="toolbar-display-mode-selector"
711
+ />
712
+ <div className="mx-1 h-4 w-px bg-border" />
713
+ </>
714
+ ) : null}
715
+
698
716
  {/* View mode toggle group: Canvas (clean, flowing) / Page (layout-sensitive) */}
699
717
  {isToolbarChromeItemVisible(scopedChromePolicy, "workspace-mode") ? (
700
718
  <ToggleGroup.Root
@@ -891,14 +909,6 @@ export function TwToolbar(props: TwToolbarProps) {
891
909
  />
892
910
  ) : null}
893
911
 
894
- {props.onChromePinChange ? (
895
- <TwDetachHandle
896
- surface="topnav"
897
- pin={props.chromePins?.topnav}
898
- onChange={props.onChromePinChange}
899
- label="Detach toolbar"
900
- />
901
- ) : null}
902
912
  </div>
903
913
  </header>
904
914
  );
@@ -1158,10 +1168,9 @@ function ToolbarCompactOverflow(props: {
1158
1168
  aria-expanded={open}
1159
1169
  onMouseDown={preserveEditorSelectionMouseDown}
1160
1170
  onClick={() => setOpen((value) => !value)}
1161
- className={`inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none ${focusRingClass}`}
1171
+ className={`inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-canvas text-primary transition-colors hover:bg-surface outline-none ${focusRingClass}`}
1162
1172
  >
1163
- More
1164
- <ChevronDown className="h-3.5 w-3.5 text-tertiary" />
1173
+ <MoreHorizontal className="h-3.5 w-3.5" />
1165
1174
  </button>
1166
1175
  </Tooltip.Trigger>
1167
1176
  <Tooltip.Portal>