@beyondwork/docx-react-component 1.0.88 → 1.0.89

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 (46) 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 +13 -4
  44. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +6 -15
  45. package/src/ui-tailwind/tw-review-workspace.tsx +28 -18
  46. 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 {
@@ -21,7 +21,6 @@ import {
21
21
  BookmarkCheck,
22
22
  Check,
23
23
  CheckCheck,
24
- ChevronDown,
25
24
  ChevronLeft,
26
25
  ChevronRight,
27
26
  CircleOff,
@@ -31,6 +30,7 @@ import {
31
30
  MessageSquare,
32
31
  MessageSquareDot,
33
32
  MessageSquareText,
33
+ MoreHorizontal,
34
34
  Rows3,
35
35
  SkipForward,
36
36
  Target,
@@ -185,6 +185,15 @@ function isRoleActionRenderable(
185
185
  case "review-accept-all":
186
186
  case "review-reject-all":
187
187
  return reviewQueueTotal > 0;
188
+ case "workflow-prev":
189
+ case "workflow-next":
190
+ case "workflow-mark-complete":
191
+ case "workflow-claim":
192
+ case "workflow-skip":
193
+ case "workflow-mark-blocked":
194
+ return props.workflowItem !== undefined;
195
+ case "workflow-jump-to-scope":
196
+ return props.workflowItem !== undefined || props.onWorkflowJumpToScope !== undefined;
188
197
  default:
189
198
  return true;
190
199
  }
@@ -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>
@@ -70,7 +70,6 @@ import {
70
70
  } from "../../ui/headless/scoped-chrome-policy";
71
71
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
72
72
  import { type MarkupDisplayMode } from "./tw-role-action-region";
73
- import { TwDetachHandle } from "../chrome/tw-detach-handle";
74
73
  import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
75
74
 
76
75
  export interface TwToolbarProps {
@@ -155,9 +154,9 @@ export interface TwToolbarProps {
155
154
  onReviewNext?: () => void;
156
155
  onReviewAccept?: () => void;
157
156
  onReviewReject?: () => void;
158
- /** Current chrome pin state; when supplied enables the topnav detach handle. */
157
+ /** Current chrome pin state, retained for host compatibility. */
159
158
  chromePins?: ChromePinsState;
160
- /** Called when the user detaches or re-attaches the topnav. */
159
+ /** Called when a host-supported chrome surface changes placement. */
161
160
  onChromePinChange?: (surface: ChromePinSurface, pin: PinState | null) => void;
162
161
 
163
162
  /**
@@ -255,7 +254,8 @@ export function TwToolbar(props: TwToolbarProps) {
255
254
  });
256
255
  const showStyleSelectors = isToolbarChromeItemVisible(scopedChromePolicy, "text-style-selectors");
257
256
  const showInlineFormatting = isToolbarChromeItemVisible(scopedChromePolicy, "inline-formatting");
258
- const showAdvancedFormatting = preset === "advanced" && showInlineFormatting;
257
+ const showAdvancedFormatting =
258
+ showInlineFormatting && (preset === "advanced" || props.role === "editor");
259
259
  const showTextColors = isToolbarChromeItemVisible(scopedChromePolicy, "text-colors");
260
260
  const showParagraphAlignment = isToolbarChromeItemVisible(scopedChromePolicy, "paragraph-alignment");
261
261
  const showInsertMenu = isToolbarChromeItemVisible(scopedChromePolicy, "insert-actions");
@@ -891,14 +891,6 @@ export function TwToolbar(props: TwToolbarProps) {
891
891
  />
892
892
  ) : null}
893
893
 
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
894
  </div>
903
895
  </header>
904
896
  );
@@ -1158,10 +1150,9 @@ function ToolbarCompactOverflow(props: {
1158
1150
  aria-expanded={open}
1159
1151
  onMouseDown={preserveEditorSelectionMouseDown}
1160
1152
  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}`}
1153
+ 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
1154
  >
1163
- More
1164
- <ChevronDown className="h-3.5 w-3.5 text-tertiary" />
1155
+ <MoreHorizontal className="h-3.5 w-3.5" />
1165
1156
  </button>
1166
1157
  </Tooltip.Trigger>
1167
1158
  <Tooltip.Portal>
@@ -68,6 +68,11 @@ import { useLayoutFacetRenderSignal } from "./review-workspace/use-layout-facet-
68
68
  import { useScrollRootCapture } from "./review-workspace/use-scroll-root-capture.ts";
69
69
  import { usePmSurfaceCapture } from "./review-workspace/use-pm-surface-capture.ts";
70
70
  import { TwReviewWorkspaceNavigator } from "./review-workspace/tw-review-workspace-navigator.tsx";
71
+ import {
72
+ createDefaultWorkflowScopeLayerKeys,
73
+ workflowScopePosturesForLayerKeys,
74
+ type WorkflowScopeLayerKey,
75
+ } from "./workflow-scope-layers";
71
76
 
72
77
  export {
73
78
  FRAME_PX_PER_TWIP_AT_96DPI,
@@ -174,6 +179,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
174
179
  useRef<TwWorkspaceChromeHostController | null>(null);
175
180
  const [shellModeOverride, setShellModeOverride] =
176
181
  useState<ShellHeaderMode | null>(null);
182
+ const [workflowLayerFilters, setWorkflowLayerFilters] = useState<
183
+ ReadonlySet<WorkflowScopeLayerKey>
184
+ >(createDefaultWorkflowScopeLayerKeys);
185
+ const visibleScopePostures = useMemo(
186
+ () => workflowScopePosturesForLayerKeys(workflowLayerFilters),
187
+ [workflowLayerFilters],
188
+ );
177
189
  // P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
178
190
  // root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
179
191
  // measure per-page rects and to reparent PM's DOM node across band
@@ -533,12 +545,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
533
545
  )
534
546
  : null;
535
547
 
536
- // Audit §2.5 — context band mounts as a composition-level sibling of
537
- // the toolbar so the workspace row becomes
538
- // [toolbar left cluster] · [context band] · [toolbar right cluster]
539
- // and the band can render mode-owned content (Phase B.3 additive mount;
540
- // Phase D migrates role-action contents into the band and retires the
541
- // legacy inline role region inside TwToolbar).
548
+ // Audit §2.5 — context band mounts as a composition-level sibling below
549
+ // the shell header only when the active mode has meaningful mode-owned
550
+ // content. Plain editor mode keeps its authoring controls in the toolbar
551
+ // and intentionally does not render an empty "Edit" ribbon.
542
552
  //
543
553
  // `resolveChromeComposition` is pure but each call allocates new Sets +
544
554
  // arrays. `TwReviewWorkspace` re-renders on every PM transaction (the
@@ -573,6 +583,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
573
583
  modeOverride: shellModeOverride === "more" ? "more" : undefined,
574
584
  });
575
585
  const showHealthRailTab = composition.rail.visibleTabs.has("health");
586
+ const showContextBand =
587
+ chromeVisibility.toolbar &&
588
+ (composition.mode === "more" || viewState.editorRole !== "editor");
576
589
 
577
590
  const arbiter = useWorkspaceArbiter();
578
591
 
@@ -591,18 +604,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
591
604
  * decoupling this branch.
592
605
  */}
593
606
  {/*
594
- * Chrome Closure Pass · Task 1 (designsystem.md §6.3) — the
595
- * role-action region used to render inline in the toolbar's
596
- * center subregion. It now lives inside `TwContextBand` so the
597
- * workspace row reads
598
- * [shell header] · [TwContextBand carrying role actions] · [TwToolbar (formatting only)]
599
- * matching §8.8.1's "Workspace context row / mode-owned band"
600
- * row. Callbacks below mirror what was previously threaded
601
- * through `ChromePresetToolbar` to `TwRoleActionRegion`; the
602
- * toolbar still receives `role` for `isChromeItemOwnedByRoleRegion`
603
- * deferral but no longer renders the region itself.
607
+ * Chrome Closure Pass · Task 1 (designsystem.md §6.3) — review,
608
+ * workflow, and More mode actions live in `TwContextBand`; editor
609
+ * mode stays toolbar-only unless a future pass adds real contextual
610
+ * authoring content.
604
611
  */}
605
- {chromeVisibility.toolbar ? (
612
+ {showContextBand ? (
606
613
  <TwContextBand mode={composition.mode}>
607
614
  {composition.mode === "more" ? (
608
615
  <TwMoreContextBandContent
@@ -615,7 +622,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
615
622
  props.onActiveRailTabChange?.("health");
616
623
  }}
617
624
  onOpenOutline={() => setNavOpen(true)}
618
- onOpenSearch={props.reviewRailFooter?.onSearch}
625
+ onOpenSearch={props.onOpenInlineFind ?? props.reviewRailFooter?.onSearch}
619
626
  />
620
627
  ) : viewState.editorRole ? (
621
628
  <TwRoleActionRegion
@@ -1167,6 +1174,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1167
1174
  onSetColumnWidth={props.onSetColumnWidth}
1168
1175
  onSetRowHeight={props.onSetRowHeight}
1169
1176
  activeScopeId={activeScopeId}
1177
+ visibleScopePostures={visibleScopePostures}
1170
1178
  editorRole={viewState.editorRole}
1171
1179
  scopeCardScopeTagEditor={props.scopeCardScopeTagEditor}
1172
1180
  onScopeStripeClick={handleScopeStripeClick}
@@ -1273,6 +1281,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1273
1281
  // `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
1274
1282
  scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
1275
1283
  activeScopeId,
1284
+ workflowLayerFilters,
1285
+ onWorkflowLayerFiltersChange: setWorkflowLayerFilters,
1276
1286
  onOpenScope: (segment) => {
1277
1287
  handleScopeStripeClick({ scopeId: segment.scopeId });
1278
1288
  },
@@ -0,0 +1,70 @@
1
+ import type { ScopeRailPosture } from "../api/public-types";
2
+
3
+ export type WorkflowScopeLayerKey =
4
+ | "edit"
5
+ | "suggest"
6
+ | "comment"
7
+ | "view"
8
+ | "candidate"
9
+ | "blocked";
10
+
11
+ export interface WorkflowScopeLayerFilter {
12
+ key: WorkflowScopeLayerKey;
13
+ label: string;
14
+ postures: readonly ScopeRailPosture[];
15
+ }
16
+
17
+ export const WORKFLOW_SCOPE_LAYER_FILTERS: readonly WorkflowScopeLayerFilter[] = [
18
+ { key: "edit", label: "Edit", postures: ["edit"] },
19
+ { key: "suggest", label: "Suggest", postures: ["suggest"] },
20
+ { key: "comment", label: "Comment", postures: ["comment"] },
21
+ { key: "view", label: "Review", postures: ["view"] },
22
+ { key: "candidate", label: "Scheduled", postures: ["candidate"] },
23
+ { key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
24
+ ];
25
+
26
+ export function createDefaultWorkflowScopeLayerKeys(): ReadonlySet<WorkflowScopeLayerKey> {
27
+ return new Set(WORKFLOW_SCOPE_LAYER_FILTERS.map((filter) => filter.key));
28
+ }
29
+
30
+ export function workflowScopeLayerKeyForPosture(
31
+ posture: ScopeRailPosture,
32
+ ): WorkflowScopeLayerKey {
33
+ if (posture === "preserve-only" || posture === "blocked-import") {
34
+ return "blocked";
35
+ }
36
+ return posture;
37
+ }
38
+
39
+ export function isWorkflowScopePostureVisible(
40
+ posture: ScopeRailPosture,
41
+ enabledLayers: ReadonlySet<WorkflowScopeLayerKey>,
42
+ ): boolean {
43
+ return enabledLayers.has(workflowScopeLayerKeyForPosture(posture));
44
+ }
45
+
46
+ export function workflowScopePosturesForLayerKeys(
47
+ enabledLayers: ReadonlySet<WorkflowScopeLayerKey>,
48
+ ): ReadonlySet<ScopeRailPosture> {
49
+ const postures = new Set<ScopeRailPosture>();
50
+ for (const filter of WORKFLOW_SCOPE_LAYER_FILTERS) {
51
+ if (!enabledLayers.has(filter.key)) continue;
52
+ for (const posture of filter.postures) {
53
+ postures.add(posture);
54
+ }
55
+ }
56
+ return postures;
57
+ }
58
+
59
+ export function toggleWorkflowScopeLayerKey(
60
+ current: ReadonlySet<WorkflowScopeLayerKey>,
61
+ key: WorkflowScopeLayerKey,
62
+ ): ReadonlySet<WorkflowScopeLayerKey> {
63
+ const next = new Set(current);
64
+ if (next.has(key)) {
65
+ next.delete(key);
66
+ } else {
67
+ next.add(key);
68
+ }
69
+ return next;
70
+ }