@beyondwork/docx-react-component 1.0.89 → 1.0.91

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.89",
4
+ "version": "1.0.91",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -14,7 +14,7 @@
14
14
 
15
15
  import * as React from "react";
16
16
  import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
17
- import type { ScopeRailPosture, ScopeRailSegment } from "../../api/public-types.ts";
17
+ import type { ScopeRailSegment } from "../../api/public-types.ts";
18
18
  import type {
19
19
  EditorRole,
20
20
  EditorStoryTarget,
@@ -49,8 +49,6 @@ export interface TwChromeOverlayProps {
49
49
  space?: OverlayCoordinateSpace;
50
50
  /** Active scope id (for emphasis + rail tab sync). */
51
51
  activeScopeId?: string | null;
52
- /** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
53
- visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
54
52
  /**
55
53
  * Click handler fired when the user clicks a scope rail stripe.
56
54
  * P0 wires this to open the scope card (P1 ships the card layer).
@@ -215,7 +213,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
215
213
  geometryFacet,
216
214
  space,
217
215
  activeScopeId,
218
- visibleScopePostures,
219
216
  onScopeStripeClick,
220
217
  onScopeSegmentClick,
221
218
  onScopeCardClose,
@@ -245,17 +242,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
245
242
  mediaPreviews,
246
243
  activeBandRibbonProps,
247
244
  }) => {
248
- const visibleScopeIds = React.useMemo(() => {
249
- if (!visibleScopePostures) return undefined;
250
- const ids = new Set<string>();
251
- for (const segment of workflowFacet?.getAllRailSegments() ?? []) {
252
- if (visibleScopePostures.has(segment.posture)) {
253
- ids.add(segment.scopeId);
254
- }
255
- }
256
- return ids;
257
- }, [visibleScopePostures, workflowFacet]);
258
-
259
245
  return (
260
246
  <div
261
247
  className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
@@ -282,7 +268,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
282
268
  workflowFacet={workflowFacet}
283
269
  space={space}
284
270
  activeScopeId={activeScopeId}
285
- visibleScopePostures={visibleScopePostures}
286
271
  onStripeClick={onScopeStripeClick}
287
272
  onSegmentClick={onScopeSegmentClick}
288
273
  />
@@ -290,7 +275,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
290
275
  facet={facet}
291
276
  workflowFacet={workflowFacet}
292
277
  activeScopeId={activeScopeId ?? null}
293
- visibleScopeIds={visibleScopeIds}
294
278
  onClose={onScopeCardClose ?? noop}
295
279
  onModeChange={onScopeCardModeChange ?? noopModeChange}
296
280
  onIssueAction={onScopeCardIssueAction ?? noopIssueAction}
@@ -52,8 +52,6 @@ export interface TwScopeCardLayerProps {
52
52
  */
53
53
  workflowFacet: WorkflowFacet | null;
54
54
  activeScopeId: string | null;
55
- /** Scope ids currently visible under the Workflow rail layer filters. */
56
- visibleScopeIds?: ReadonlySet<string>;
57
55
  onClose: () => void;
58
56
  onModeChange: (scopeId: string, mode: WorkflowScopeMode) => void;
59
57
  onIssueAction: (
@@ -93,7 +91,6 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
93
91
  facet,
94
92
  workflowFacet,
95
93
  activeScopeId,
96
- visibleScopeIds,
97
94
  onClose,
98
95
  onModeChange,
99
96
  onIssueAction,
@@ -122,9 +119,7 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
122
119
  // The effective scope is the pinned one if it still resolves to a
123
120
  // model, else the active one. When a pinned scope disappears
124
121
  // (e.g. the host cleared the overlay), drop the pin.
125
- const models = (workflowFacet?.getAllScopeCardModels() ?? []).filter((model) =>
126
- visibleScopeIds ? visibleScopeIds.has(model.scopeId) : true,
127
- );
122
+ const models = workflowFacet?.getAllScopeCardModels() ?? [];
128
123
 
129
124
  const pinnedModel = pinnedScopeId
130
125
  ? models.find((m) => m.scopeId === pinnedScopeId) ?? null
@@ -46,8 +46,6 @@ export interface TwScopeRailLayerProps {
46
46
  railLaneWidthPx?: number;
47
47
  /** Scope id that should render with the `active` emphasis. */
48
48
  activeScopeId?: string | null;
49
- /** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
50
- visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
51
49
  /**
52
50
  * Fires when the user clicks the rail stripe — opens the scope card.
53
51
  * P0 wires this directly; P1 replaces with card-layer-aware routing.
@@ -89,8 +87,8 @@ const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
89
87
  // ---------------------------------------------------------------------------
90
88
 
91
89
  const DEFAULT_RAIL_LANE_PX = 44;
92
- const STRIPE_WIDTH_PX = 6;
93
- const LABEL_WIDTH_PX = 58;
90
+ const STRIPE_WIDTH_PX = 4;
91
+ const LABEL_WIDTH_PX = 40;
94
92
  const STACK_OFFSET_PX = 6;
95
93
 
96
94
  export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
@@ -99,15 +97,12 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
99
97
  space,
100
98
  railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
101
99
  activeScopeId,
102
- visibleScopePostures,
103
100
  onStripeClick,
104
101
  onSegmentClick,
105
102
  "data-testid": testId,
106
103
  }) => {
107
104
  const frame = geometryFacet.getRenderFrame() ?? null;
108
- const segments = (workflowFacet?.getAllRailSegments() ?? []).filter((segment) =>
109
- visibleScopePostures ? visibleScopePostures.has(segment.posture) : true,
110
- );
105
+ const segments = workflowFacet?.getAllRailSegments() ?? [];
111
106
 
112
107
  if (!frame || segments.length === 0) {
113
108
  return null;
@@ -228,21 +223,16 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
228
223
  style={projectRectToOverlay(stripeRect, projectorSpace)}
229
224
  />
230
225
  {/* Label pill — revealed on stripe hover via CSS. */}
231
- <button
232
- type="button"
233
- tabIndex={-1}
234
- className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken} ${
235
- isActive ? "wre-scope-rail-label-active" : ""
236
- }`}
226
+ <div
227
+ className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken}`}
237
228
  data-scope-id={segment.scopeId}
238
229
  data-posture={segment.posture}
239
- aria-label={`${style.labelText}${segment.label ? `: ${segment.label}` : ""}`}
240
- onClick={handleActivate}
230
+ aria-hidden="true"
241
231
  style={projectRectToOverlay(labelRect, projectorSpace)}
242
232
  >
243
233
  <span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
244
234
  <span className="wre-scope-rail-label-text">{style.labelText}</span>
245
- </button>
235
+ </div>
246
236
  </React.Fragment>
247
237
  );
248
238
  })}
@@ -23,7 +23,6 @@ import {
23
23
  TwReviewRailFooter,
24
24
  type TwReviewRailFooterProps,
25
25
  } from "./tw-review-rail-footer";
26
- import type { WorkflowScopeLayerKey } from "../workflow-scope-layers";
27
26
 
28
27
  /**
29
28
  * Review rail with up to four tabs (Workflow / Comments / Changes / Health).
@@ -67,8 +66,6 @@ export interface TwReviewRailProps {
67
66
  */
68
67
  scopeRailSegments?: readonly ScopeRailSegment[];
69
68
  activeScopeId?: string | null;
70
- workflowLayerFilters?: ReadonlySet<WorkflowScopeLayerKey>;
71
- onWorkflowLayerFiltersChange?: (filters: ReadonlySet<WorkflowScopeLayerKey>) => void;
72
69
  /**
73
70
  * Optional host-provided Workflow-tab override. When supplied this
74
71
  * ReactNode replaces the default TwWorkflowTab content while still using
@@ -265,8 +262,6 @@ export function TwReviewRail(props: TwReviewRailProps) {
265
262
  <TwWorkflowTab
266
263
  segments={workflowSegments}
267
264
  activeScopeId={props.activeScopeId ?? null}
268
- enabledLayerFilters={props.workflowLayerFilters}
269
- onEnabledLayerFiltersChange={props.onWorkflowLayerFiltersChange}
270
265
  onOpenScope={props.onOpenScope}
271
266
  />
272
267
  )}
@@ -10,13 +10,6 @@
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";
20
13
 
21
14
  export interface TwWorkflowTabProps {
22
15
  segments: readonly ScopeRailSegment[];
@@ -28,8 +21,6 @@ export interface TwWorkflowTabProps {
28
21
  * matching overlay card. If omitted, focus sync is not wired.
29
22
  */
30
23
  onActiveScopeChange?: (scopeId: string) => void;
31
- enabledLayerFilters?: ReadonlySet<WorkflowScopeLayerKey>;
32
- onEnabledLayerFiltersChange?: (filters: ReadonlySet<WorkflowScopeLayerKey>) => void;
33
24
  }
34
25
 
35
26
  const POSTURE_META: Record<
@@ -48,13 +39,26 @@ const POSTURE_META: Record<
48
39
  const focusRingClass =
49
40
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
50
41
 
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
+
51
57
  export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
52
58
  segments,
53
59
  activeScopeId,
54
60
  onOpenScope,
55
61
  onActiveScopeChange,
56
- enabledLayerFilters,
57
- onEnabledLayerFiltersChange,
58
62
  }) => {
59
63
  // Dedupe by scopeId so a scope spanning multiple pages shows once.
60
64
  const uniqueSegments = React.useMemo(() => {
@@ -67,31 +71,20 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
67
71
  return Array.from(byScopeId.values()).sort(compareWorkflowSegments(activeScopeId ?? null));
68
72
  }, [activeScopeId, segments]);
69
73
  const [query, setQuery] = React.useState("");
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],
74
+ const [enabledFilters, setEnabledFilters] = React.useState<ReadonlySet<ScopeFilterKey>>(
75
+ () => new Set(SCOPE_FILTERS.map((filter) => filter.key)),
84
76
  );
85
77
  const availableFilters = React.useMemo(() => {
86
78
  const presentPostures = new Set(uniqueSegments.map((segment) => segment.posture));
87
- return WORKFLOW_SCOPE_LAYER_FILTERS.filter((filter) =>
79
+ return SCOPE_FILTERS.filter((filter) =>
88
80
  filter.postures.some((posture) => presentPostures.has(posture)),
89
81
  );
90
82
  }, [uniqueSegments]);
91
83
  const visibleSegments = React.useMemo(() => {
92
84
  const normalizedQuery = normalizeScopeQuery(query);
93
85
  return uniqueSegments.filter((segment) => {
94
- if (!isWorkflowScopePostureVisible(segment.posture, activeEnabledFilters)) {
86
+ const filterKey = filterKeyForPosture(segment.posture);
87
+ if (!enabledFilters.has(filterKey)) {
95
88
  return false;
96
89
  }
97
90
  if (!normalizedQuery) {
@@ -99,7 +92,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
99
92
  }
100
93
  return scopeSearchText(segment).includes(normalizedQuery);
101
94
  });
102
- }, [activeEnabledFilters, query, uniqueSegments]);
95
+ }, [enabledFilters, query, uniqueSegments]);
103
96
 
104
97
  if (uniqueSegments.length === 0) {
105
98
  return (
@@ -153,7 +146,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
153
146
  role="group"
154
147
  >
155
148
  {availableFilters.map((filter) => {
156
- const isEnabled = activeEnabledFilters.has(filter.key);
149
+ const isEnabled = enabledFilters.has(filter.key);
157
150
  return (
158
151
  <button
159
152
  key={filter.key}
@@ -167,9 +160,15 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
167
160
  ].join(" ")}
168
161
  data-testid={`workflow-scope-filter-${filter.key}`}
169
162
  onClick={() => {
170
- setEnabledFilters(
171
- toggleWorkflowScopeLayerKey(activeEnabledFilters, filter.key),
172
- );
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
+ });
173
172
  }}
174
173
  >
175
174
  {filter.label}
@@ -246,6 +245,13 @@ function compareWorkflowSegments(activeScopeId: string | null) {
246
245
  };
247
246
  }
248
247
 
248
+ function filterKeyForPosture(posture: ScopeRailPosture): ScopeFilterKey {
249
+ if (posture === "preserve-only" || posture === "blocked-import") {
250
+ return "blocked";
251
+ }
252
+ return posture;
253
+ }
254
+
249
255
  function normalizeScopeQuery(value: string): string {
250
256
  return value.trim().toLocaleLowerCase();
251
257
  }
@@ -475,19 +475,19 @@
475
475
  }
476
476
 
477
477
  .wre-scope-rail-tint-accent {
478
- background: color-mix(in srgb, var(--color-accent) 20%, transparent);
478
+ background: color-mix(in srgb, var(--color-accent) 12%, transparent);
479
479
  }
480
480
  .wre-scope-rail-tint-warning {
481
- background: color-mix(in srgb, var(--color-warning) 23%, transparent);
481
+ background: color-mix(in srgb, var(--color-warning) 14%, transparent);
482
482
  }
483
483
  .wre-scope-rail-tint-insert {
484
- background: color-mix(in srgb, var(--color-insert) 20%, transparent);
484
+ background: color-mix(in srgb, var(--color-insert) 12%, transparent);
485
485
  }
486
486
  .wre-scope-rail-tint-secondary {
487
- background: color-mix(in srgb, var(--color-secondary) 16%, transparent);
487
+ background: color-mix(in srgb, var(--color-secondary) 9%, transparent);
488
488
  }
489
489
  .wre-scope-rail-tint-danger {
490
- background: color-mix(in srgb, var(--color-danger) 24%, transparent);
490
+ background: color-mix(in srgb, var(--color-danger) 14%, 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 6px
546
+ * The rail stripe is the rest-state representation of a scope: a 4px
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: 6px;
554
- border-radius: 999px;
553
+ width: 4px;
554
+ border-radius: 2px;
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.9;
561
+ opacity: 0.75;
562
562
  /* Reset button defaults. */
563
563
  border: none;
564
564
  padding: 0;
@@ -568,22 +568,16 @@
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
-
577
571
  .wre-scope-rail-stripe:hover,
578
572
  .wre-scope-rail-stripe:focus-visible {
579
- transform: scaleX(1.45);
573
+ transform: scaleX(1.5);
580
574
  opacity: 1;
581
575
  outline: none;
582
576
  }
583
577
 
584
578
  .wre-scope-rail-stripe-active {
585
579
  opacity: 1;
586
- transform: scaleX(1.6);
580
+ transform: scaleX(1.75);
587
581
  }
588
582
 
589
583
  .wre-scope-rail-stripe.wre-scope-rail-label-accent { color: var(--color-accent); }
@@ -629,8 +623,6 @@
629
623
  pointer-events: none;
630
624
  transition: opacity 140ms ease-out, transform 140ms ease-out;
631
625
  transform: translateX(-4px);
632
- margin: 0;
633
- font-family: inherit;
634
626
  }
635
627
 
636
628
  .wre-scope-rail-stripe:hover + .wre-scope-rail-label,
@@ -700,12 +692,7 @@
700
692
  }
701
693
 
702
694
  .wre-scope-rail-label-active {
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);
695
+ box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
709
696
  }
710
697
 
711
698
  .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,14 +18,12 @@ 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
23
  ChevronLeft,
25
24
  ChevronRight,
26
25
  CircleOff,
27
26
  FileDiff,
28
- Flag,
29
27
  Hand,
30
28
  MessageSquare,
31
29
  MessageSquareDot,
@@ -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
@@ -366,10 +365,11 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
366
365
  );
367
366
  case "review-markup-mode":
368
367
  return (
369
- <MarkupModeSelect
370
- mode={props.markupDisplay ?? "simple"}
368
+ <TwDisplayModeSelector
369
+ value={props.markupDisplay ?? "simple"}
371
370
  onChange={(mode) => props.onReviewMarkupMode?.(mode)}
372
371
  disabled={!props.onReviewMarkupMode}
372
+ data-testid="role-review-markup-mode"
373
373
  />
374
374
  );
375
375
  case "workflow-prev":
@@ -693,30 +693,4 @@ function ReviewActiveLabel({
693
693
  );
694
694
  }
695
695
 
696
- function MarkupModeSelect(arg: {
697
- mode: MarkupDisplayMode;
698
- onChange: (mode: MarkupDisplayMode) => void;
699
- disabled?: boolean;
700
- }): React.JSX.Element {
701
- const Icon = arg.mode === "clean" ? BookmarkCheck : arg.mode === "all" ? Flag : Rows3;
702
- return (
703
- <button
704
- type="button"
705
- aria-label={`Markup display: ${arg.mode}`}
706
- disabled={arg.disabled}
707
- onMouseDown={preserveEditorSelectionMouseDown}
708
- onClick={() => {
709
- const next: MarkupDisplayMode =
710
- arg.mode === "all" ? "clean" : arg.mode === "clean" ? "simple" : "all";
711
- arg.onChange(next);
712
- }}
713
- data-testid="role-review-markup-mode"
714
- 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"
715
- >
716
- <Icon className="h-3.5 w-3.5" />
717
- <span className="capitalize">{arg.mode}</span>
718
- </button>
719
- );
720
- }
721
-
722
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,6 +69,7 @@ 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
74
  import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
74
75
 
@@ -123,6 +124,8 @@ export interface TwToolbarProps {
123
124
  onToggleSidebar?: () => void;
124
125
  onZoomChange?: (level: ZoomLevel) => void;
125
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;
126
129
  onRestartNumbering?: () => void;
127
130
  onContinueNumbering?: () => void;
128
131
  onUpdateFields?: () => void;
@@ -262,6 +265,10 @@ export function TwToolbar(props: TwToolbarProps) {
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
@@ -68,11 +68,6 @@ 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";
76
71
 
77
72
  export {
78
73
  FRAME_PX_PER_TWIP_AT_96DPI,
@@ -179,13 +174,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
179
174
  useRef<TwWorkspaceChromeHostController | null>(null);
180
175
  const [shellModeOverride, setShellModeOverride] =
181
176
  useState<ShellHeaderMode | null>(null);
182
- const [workflowLayerFilters, setWorkflowLayerFilters] = useState<
183
- ReadonlySet<WorkflowScopeLayerKey>
184
- >(createDefaultWorkflowScopeLayerKeys);
185
- const visibleScopePostures = useMemo(
186
- () => workflowScopePosturesForLayerKeys(workflowLayerFilters),
187
- [workflowLayerFilters],
188
- );
189
177
  // P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
190
178
  // root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
191
179
  // measure per-page rects and to reparent PM's DOM node across band
@@ -878,6 +866,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
878
866
  dismissSelectionToolbar();
879
867
  props.onShowTrackedChangesChange(show);
880
868
  }}
869
+ onMarkupDisplayChange={(mode) => {
870
+ dismissSelectionToolbar();
871
+ props.onReviewMarkupModeChange?.(mode);
872
+ }}
881
873
  role={viewState.editorRole}
882
874
  reviewQueue={props.reviewQueue}
883
875
  markupDisplay={markupDisplay}
@@ -1174,7 +1166,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1174
1166
  onSetColumnWidth={props.onSetColumnWidth}
1175
1167
  onSetRowHeight={props.onSetRowHeight}
1176
1168
  activeScopeId={activeScopeId}
1177
- visibleScopePostures={visibleScopePostures}
1178
1169
  editorRole={viewState.editorRole}
1179
1170
  scopeCardScopeTagEditor={props.scopeCardScopeTagEditor}
1180
1171
  onScopeStripeClick={handleScopeStripeClick}
@@ -1281,8 +1272,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1281
1272
  // `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
1282
1273
  scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
1283
1274
  activeScopeId,
1284
- workflowLayerFilters,
1285
- onWorkflowLayerFiltersChange: setWorkflowLayerFilters,
1286
1275
  onOpenScope: (segment) => {
1287
1276
  handleScopeStripeClick({ scopeId: segment.scopeId });
1288
1277
  },
@@ -1,70 +0,0 @@
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
- }