@beyondwork/docx-react-component 1.0.85 → 1.0.86

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.85",
4
+ "version": "1.0.86",
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": [
@@ -1062,7 +1062,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1062
1062
  } = props;
1063
1063
 
1064
1064
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
1065
- const [showTrackedChanges, setShowTrackedChanges] = useState(() => suggestionsEnabled);
1066
1065
  const [localMarkupDisplay, setLocalMarkupDisplay] =
1067
1066
  useState<WorkflowMarkupMode | null>(() => suggestionsEnabled ? "all" : null);
1068
1067
  const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
@@ -1368,6 +1367,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1368
1367
  const isPageWorkspace = viewState.workspaceMode === "page";
1369
1368
  const liveMarkupDisplay = localMarkupDisplay ??
1370
1369
  __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
1370
+ const trackedChangesAuthoringEnabled = viewState.documentMode === "suggesting";
1371
+ const showTrackedChanges = toWorkflowMarkupMode(liveMarkupDisplay) !== "clean";
1371
1372
  const documentNavigation = useRuntimeValue(
1372
1373
  runtime
1373
1374
  ? {
@@ -1510,16 +1511,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1510
1511
  const setReviewMarkupMode = useCallback((mode: MarkupDisplay) => {
1511
1512
  const workflowMode = toWorkflowMarkupMode(mode);
1512
1513
  setLocalMarkupDisplay(workflowMode);
1513
- setShowTrackedChanges(workflowMode !== "clean");
1514
1514
  api.ui?.viewport.setLocalMarkupMode(workflowMode);
1515
1515
  }, [api]);
1516
1516
 
1517
1517
  const setTrackedChangesAuthoring = useCallback((enabled: boolean) => {
1518
- const workflowMode: WorkflowMarkupMode = enabled ? "all" : "clean";
1519
- setShowTrackedChanges(enabled);
1520
- setLocalMarkupDisplay(workflowMode);
1521
1518
  api.runtime.document.setMode(enabled ? "suggesting" : "editing");
1522
- api.ui?.viewport.setLocalMarkupMode(workflowMode);
1519
+ if (enabled) {
1520
+ setLocalMarkupDisplay("all");
1521
+ api.ui?.viewport.setLocalMarkupMode("all");
1522
+ }
1523
1523
  }, [api]);
1524
1524
 
1525
1525
  useEffect(() => {
@@ -1527,7 +1527,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1527
1527
  setTrackedChangesAuthoring(true);
1528
1528
  return;
1529
1529
  }
1530
- setShowTrackedChanges(false);
1531
1530
  api.runtime.document.setMode("editing");
1532
1531
  }, [api, setTrackedChangesAuthoring, suggestionsEnabled]);
1533
1532
 
@@ -2467,13 +2466,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2467
2466
 
2468
2467
  function addReviewComment(): string | null {
2469
2468
  try {
2469
+ const currentSnapshot = activeRuntime.getRenderSnapshot();
2470
2470
  const { commentId } = activeRuntime.addComment({
2471
- anchor: resolveCommentCommandAnchor(snapshot),
2471
+ anchor: resolveCommentCommandAnchor(currentSnapshot),
2472
2472
  body: "",
2473
2473
  authorId: currentUser.userId,
2474
2474
  snapToSafeBoundary: true,
2475
2475
  });
2476
2476
  activeRuntime.openComment(commentId);
2477
+ setActiveReviewQueueItemId(`comment:${commentId}`);
2477
2478
  setActiveRailTab("comments");
2478
2479
  return commentId;
2479
2480
  } catch {
@@ -2814,11 +2815,49 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2814
2815
  }, [
2815
2816
  activeRuntime,
2816
2817
  activeSelectionTool,
2818
+ addReviewComment,
2817
2819
  currentUser.userId,
2818
2820
  dismissSelectionToolbar,
2819
2821
  focusDocumentSurface,
2820
2822
  ]);
2821
2823
 
2824
+ const acceptActiveSuggestion = useCallback(() => {
2825
+ if (activeSelectionTool?.kind !== "suggestion-review") {
2826
+ return;
2827
+ }
2828
+ for (const changeId of activeSelectionTool.changeIds) {
2829
+ activeRuntime.acceptChange(changeId);
2830
+ }
2831
+ dismissSelectionToolbar("chrome-action");
2832
+ }, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
2833
+
2834
+ const rejectActiveSuggestion = useCallback(() => {
2835
+ if (activeSelectionTool?.kind !== "suggestion-review") {
2836
+ return;
2837
+ }
2838
+ for (const changeId of activeSelectionTool.changeIds) {
2839
+ activeRuntime.rejectChange(changeId);
2840
+ }
2841
+ dismissSelectionToolbar("chrome-action");
2842
+ }, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
2843
+
2844
+ const editActiveSuggestion = useCallback(() => {
2845
+ if (activeSelectionTool?.kind !== "suggestion-review") {
2846
+ return;
2847
+ }
2848
+ setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
2849
+ const activeSuggestion = suggestionsSnapshot.suggestions.find(
2850
+ (suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
2851
+ );
2852
+ if (activeSuggestion) {
2853
+ applyRuntimeSelection(
2854
+ activeRuntime,
2855
+ createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
2856
+ );
2857
+ }
2858
+ setSelectionToolbarFocusWithin(true);
2859
+ }, [activeRuntime, activeSelectionTool, suggestionsSnapshot.suggestions]);
2860
+
2822
2861
  const handleSelectionToolbarAnchorChange = useCallback(
2823
2862
  (nextAnchor: SelectionToolbarAnchor | null) => {
2824
2863
  setSelectionToolbarAnchor((current) =>
@@ -3209,6 +3248,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3209
3248
  focusAnchor(revision.anchor, revision.storyTarget);
3210
3249
  setActiveRailTab("changes");
3211
3250
  },
3251
+ onReplyToRevision: (revision: typeof snapshot.trackedChanges.revisions[number]) => {
3252
+ const linkedThread = activeRuntime.ensureCommentThreadForChange(
3253
+ revision.revisionId,
3254
+ currentUser.userId,
3255
+ );
3256
+ if (!linkedThread?.commentId) {
3257
+ return;
3258
+ }
3259
+ setActiveRevisionId(revision.revisionId);
3260
+ focusAnchor(revision.anchor, revision.storyTarget);
3261
+ activeRuntime.openComment(linkedThread.commentId);
3262
+ setActiveRailTab("comments");
3263
+ },
3212
3264
  onAcceptRevision: (revisionId: string) => {
3213
3265
  activeRuntime.acceptChange(revisionId);
3214
3266
  setActiveRailTab("changes");
@@ -3237,6 +3289,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3237
3289
  onActiveRailTabChange: setActiveRailTab,
3238
3290
  onShowTrackedChangesChange: setTrackedChangesAuthoring,
3239
3291
  onReviewMarkupModeChange: setReviewMarkupMode,
3292
+ onChromePinChange: (surface, pin) => activeRuntime.setChromePin(surface, pin),
3240
3293
  onToggleBold: () =>
3241
3294
  applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
3242
3295
  onToggleItalic: () =>
@@ -3449,6 +3502,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3449
3502
  onInsertPageBreak: commands.onInsertPageBreak,
3450
3503
  onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
3451
3504
  onInsertTable: commands.onInsertTable,
3505
+ onInsertImage: commands.onInsertImage
3506
+ ? () => requestImageInsertFromPicker(commands.onInsertImage)
3507
+ : undefined,
3452
3508
  onAddComment: commands.onAddComment,
3453
3509
  onFindRequested: onFindRequested
3454
3510
  ? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
@@ -3592,6 +3648,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3592
3648
  activeRailTab={activeRailTab}
3593
3649
  activeCommentId={snapshot.comments.activeCommentId}
3594
3650
  activeRevisionId={activeRevisionId}
3651
+ trackedChangesAuthoringEnabled={trackedChangesAuthoringEnabled}
3595
3652
  showTrackedChanges={showTrackedChanges}
3596
3653
  workflowScopeSnapshot={workflowScopeSnapshot}
3597
3654
  layoutFacet={activeRuntime.layout}
@@ -3626,35 +3683,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3626
3683
  onAddCommentFromSelection={addSelectionToolbarComment}
3627
3684
  onAddCommentFromSuggestion={addSelectionToolbarComment}
3628
3685
  onAcceptSuggestion={activeSelectionTool?.kind === "suggestion-review"
3629
- ? () => {
3630
- for (const changeId of activeSelectionTool.changeIds) {
3631
- activeRuntime.acceptChange(changeId);
3632
- }
3633
- dismissSelectionToolbar("chrome-action");
3634
- }
3686
+ ? acceptActiveSuggestion
3635
3687
  : undefined}
3636
3688
  onRejectSuggestion={activeSelectionTool?.kind === "suggestion-review"
3637
- ? () => {
3638
- for (const changeId of activeSelectionTool.changeIds) {
3639
- activeRuntime.rejectChange(changeId);
3640
- }
3641
- dismissSelectionToolbar("chrome-action");
3642
- }
3689
+ ? rejectActiveSuggestion
3643
3690
  : undefined}
3644
3691
  onEditSuggestion={activeSelectionTool?.kind === "suggestion-review"
3645
- ? () => {
3646
- setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
3647
- const activeSuggestion = suggestionsSnapshot.suggestions.find(
3648
- (suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
3649
- );
3650
- if (activeSuggestion) {
3651
- applyRuntimeSelection(
3652
- activeRuntime,
3653
- createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
3654
- );
3655
- }
3656
- setSelectionToolbarFocusWithin(true);
3657
- }
3692
+ ? editActiveSuggestion
3658
3693
  : undefined}
3659
3694
  onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
3660
3695
  onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
@@ -4296,6 +4331,42 @@ function resolveCommentCommandAnchor(
4296
4331
  : selection.activeRange;
4297
4332
  }
4298
4333
 
4334
+ function requestImageInsertFromPicker(
4335
+ onInsertImage: ((options: InsertImageOptions) => void) | undefined,
4336
+ ): void {
4337
+ const document = globalThis.document;
4338
+ if (!onInsertImage || typeof document?.createElement !== "function" || !document.body) {
4339
+ return;
4340
+ }
4341
+
4342
+ const input = document.createElement("input");
4343
+ input.type = "file";
4344
+ input.accept = "image/png,image/jpeg,image/gif";
4345
+ input.style.position = "fixed";
4346
+ input.style.left = "-9999px";
4347
+ input.style.top = "-9999px";
4348
+ input.addEventListener("change", () => {
4349
+ const file = input.files?.[0];
4350
+ if (!file) {
4351
+ input.remove();
4352
+ return;
4353
+ }
4354
+ void file.arrayBuffer()
4355
+ .then((buffer) => {
4356
+ onInsertImage({
4357
+ data: new Uint8Array(buffer),
4358
+ mimeType: file.type || "image/png",
4359
+ altText: file.name,
4360
+ });
4361
+ })
4362
+ .finally(() => {
4363
+ input.remove();
4364
+ });
4365
+ }, { once: true });
4366
+ document.body.appendChild(input);
4367
+ input.click();
4368
+ }
4369
+
4299
4370
  function resolveCollapsedCommentRange(
4300
4371
  surface: RuntimeRenderSnapshot["surface"],
4301
4372
  selection: RuntimeRenderSnapshot["selection"],
@@ -1,6 +1,7 @@
1
1
  import { useMemo, useRef } from "react";
2
2
 
3
3
  import type {
4
+ ChromePinSurface,
4
5
  CommentSidebarThreadSnapshot,
5
6
  EditorStoryTarget,
6
7
  FormattingAlignment,
@@ -9,6 +10,7 @@ import type {
9
10
  SectionBreakType,
10
11
  SectionLayoutPatch,
11
12
  SectionPageNumberingPatch,
13
+ PinState,
12
14
  TrackedChangeEntrySnapshot,
13
15
  ZoomLevel,
14
16
  WorkspaceMode,
@@ -24,6 +26,7 @@ export interface EditorCommandBag {
24
26
  onActiveRailTabChange(value: ReviewRailTab): void;
25
27
  onShowTrackedChangesChange(show: boolean): void;
26
28
  onReviewMarkupModeChange?(mode: MarkupDisplay): void;
29
+ onChromePinChange?(surface: ChromePinSurface, pin: PinState | null): void;
27
30
  onUndo(): void;
28
31
  onRedo(): void;
29
32
  onSetParagraphStyle?(styleId: string): void;
@@ -85,6 +88,7 @@ export interface EditorCommandBag {
85
88
  onAddReply?(commentId: string, body: string): void;
86
89
  onEditBody?(commentId: string, body: string): void;
87
90
  onOpenRevision(revision: TrackedChangeEntrySnapshot): void;
91
+ onReplyToRevision?(revision: TrackedChangeEntrySnapshot): void;
88
92
  onAcceptRevision(revisionId: string): void;
89
93
  onRejectRevision(revisionId: string): void;
90
94
  onAcceptAllChanges(): void;
@@ -55,6 +55,7 @@ export interface EditorShellViewProps {
55
55
  activeRailTab: ReviewRailTab;
56
56
  activeCommentId?: string;
57
57
  activeRevisionId?: string;
58
+ trackedChangesAuthoringEnabled?: boolean;
58
59
  showTrackedChanges: boolean;
59
60
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
60
61
  /**
@@ -3,16 +3,17 @@
3
3
  * contextual actions exist, keyed by `TargetKind`.
4
4
  *
5
5
  * Consumed by three access routes so DESIGN-EDITOR.md §6.4 holds by
6
- * construction ("right-click cannot be richer than the floating surface;
7
- * cannot duplicate command trees"):
6
+ * construction for general editor actions:
8
7
  *
9
8
  * 1. Right-click context menu (via `build-context-menu-entries.ts`)
10
9
  * 2. Inline "More…" affordance on reduced floating toolbars (Phase D)
11
10
  * 3. Command palette groups (Phase C.δ)
12
11
  *
13
- * All three routes produce the same actions from this registry — they
14
- * cannot diverge. This is the load-bearing decision that makes the
15
- * floating-menu demotion legal.
12
+ * All three routes produce the same general actions from this registry.
13
+ * Suggestion-specific review actions are intentionally absent for now:
14
+ * they require an exact revision/suggestion id payload, so they live on
15
+ * the floating suggestion card and Changes rail until the context-menu
16
+ * target resolver can supply that payload.
16
17
  *
17
18
  * Perf discipline: pure data + pure filter helpers. No DOM reads, no
18
19
  * runtime calls, no observers. Actions are dispatched through host-
@@ -106,10 +107,6 @@ export interface EditorActionHostCallbacks {
106
107
  readonly onPrintRequested?: () => void;
107
108
  readonly onGoToRequested?: () => void;
108
109
 
109
- // Tracked-change operations (suggestion target)
110
- readonly onAcceptSuggestion?: () => void;
111
- readonly onRejectSuggestion?: () => void;
112
-
113
110
  // Comment operations (comment-anchor target)
114
111
  readonly onResolveComment?: () => void;
115
112
  readonly onReplyToComment?: () => void;
@@ -576,7 +573,7 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
576
573
  mkImportant({
577
574
  id: "insert-image",
578
575
  label: "Insert image…",
579
- description: "Needs a host file picker before it can run.",
576
+ description: "Choose an image file and insert it at the caret.",
580
577
  group: "misc",
581
578
  targetKinds: [],
582
579
  callback: "onInsertImage",
@@ -636,22 +633,6 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
636
633
  callback: "onPrintRequested",
637
634
  }),
638
635
 
639
- // -------- Suggestion / tracked change --------
640
- mk({
641
- id: "accept-suggestion",
642
- label: "Accept suggestion",
643
- group: "suggestion",
644
- targetKinds: ["suggestion"],
645
- callback: "onAcceptSuggestion",
646
- }),
647
- mk({
648
- id: "reject-suggestion",
649
- label: "Reject suggestion",
650
- group: "suggestion",
651
- targetKinds: ["suggestion"],
652
- callback: "onRejectSuggestion",
653
- }),
654
-
655
636
  // -------- Comment --------
656
637
  mk({
657
638
  id: "resolve-comment",
@@ -43,6 +43,10 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
43
43
  const { surface, pin, onChange, label } = props;
44
44
  const isDetached = pin?.detached ?? false;
45
45
  const offset = pin?.offset ?? { x: 0, y: 0 };
46
+ const dragHandleTestId =
47
+ surface === "selectionTier" ? "selection-tool-drag-handle" : `${surface}-detach-drag-handle`;
48
+ const toggleTestId =
49
+ surface === "selectionTier" ? "selection-tool-attach-toggle" : `${surface}-detach-toggle`;
46
50
  const dragState = useRef<
47
51
  | {
48
52
  startX: number;
@@ -117,7 +121,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
117
121
  <button
118
122
  type="button"
119
123
  aria-label={isDetached ? "Drag floating menu" : "Drag to float menu"}
120
- data-testid={`${surface}-detach-drag-handle`}
124
+ data-testid={dragHandleTestId}
121
125
  className="inline-flex h-6 items-center justify-center rounded-md border border-transparent px-1.5 text-tertiary transition-colors hover:border-border/60 hover:bg-surface hover:text-primary"
122
126
  onMouseDown={beginDrag}
123
127
  >
@@ -133,7 +137,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
133
137
  type="button"
134
138
  aria-label={isDetached ? "Dock menu" : "Float menu"}
135
139
  aria-pressed={isDetached}
136
- data-testid={`${surface}-detach-toggle`}
140
+ data-testid={toggleTestId}
137
141
  className="inline-flex h-6 items-center rounded-md border border-border/60 px-2 text-[10px] font-medium text-secondary transition-colors hover:bg-surface hover:text-primary"
138
142
  onMouseDown={preserveEditorSelectionMouseDown}
139
143
  onClick={toggle}
@@ -153,6 +153,7 @@ function CommentThreadCard(props: {
153
153
  }, [presentation]);
154
154
  const leadEntry = thread.entries[0];
155
155
  const isDraftThread = thread.status === "open" && thread.entryCount === 1 && isEmptyCommentBody(leadEntry?.body);
156
+ const isLinkedRevisionThread = thread.linkedRevisionId != null;
156
157
  const isOwnComment = props.currentUserId != null && leadEntry?.authorId === props.currentUserId;
157
158
  const canEdit = isOwnComment && thread.status === "open" && props.onEditBody != null;
158
159
  const hasNoBody = isEmptyCommentBody(leadEntry?.body);
@@ -205,6 +206,7 @@ function CommentThreadCard(props: {
205
206
  {formatCommentDate(thread.createdAt)}
206
207
  </span>
207
208
  <span className="flex-1" />
209
+ {isLinkedRevisionThread ? <StatusBadge label="tracked change" tone="revision" /> : null}
208
210
  {isDraftThread ? <StatusBadge label="draft" tone="draft" /> : null}
209
211
  {thread.status === "resolved" ? <StatusBadge label="resolved" tone="resolved" /> : null}
210
212
  </div>
@@ -222,7 +224,9 @@ function CommentThreadCard(props: {
222
224
  body={leadEntry?.body ?? ""}
223
225
  autoFocus={isActive && hasNoBody}
224
226
  onSave={(newBody) => props.onEditBody?.(thread.commentId, newBody)}
225
- label={isDraftThread ? "New comment" : undefined}
227
+ label={isDraftThread
228
+ ? (isLinkedRevisionThread ? "Tracked change discussion" : "New comment")
229
+ : undefined}
226
230
  />
227
231
  ) : presentation ? (
228
232
  <CommentMarkdownRenderer
@@ -247,7 +251,7 @@ function CommentThreadCard(props: {
247
251
  props.onOpenComment?.(thread);
248
252
  }}
249
253
  >
250
- New comment
254
+ {isLinkedRevisionThread ? "Tracked change discussion" : "New comment"}
251
255
  </p>
252
256
  ) : null}
253
257
 
@@ -494,11 +498,12 @@ function formatCommentDate(raw: string): string {
494
498
  }
495
499
  }
496
500
 
497
- function StatusBadge(props: { label: string; tone: "resolved" | "detached" | "draft" }) {
501
+ function StatusBadge(props: { label: string; tone: "resolved" | "detached" | "draft" | "revision" }) {
498
502
  const styles: Record<string, string> = {
499
503
  resolved: "text-insert bg-insert-soft",
500
504
  detached: "text-comment bg-warning-soft",
501
505
  draft: "text-secondary bg-subtle",
506
+ revision: "text-accent bg-accent-soft",
502
507
  };
503
508
  return (
504
509
  <span
@@ -111,6 +111,7 @@ export interface TwReviewRailProps {
111
111
  onAddReply?: (commentId: string, body: string) => void;
112
112
  onEditBody?: (commentId: string, body: string) => void;
113
113
  onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
114
+ onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
114
115
  onAcceptRevision?: (revisionId: string) => void;
115
116
  onRejectRevision?: (revisionId: string) => void;
116
117
  onAcceptAllChanges?: () => void;
@@ -285,6 +286,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
285
286
  markupDisplay={props.markupDisplay}
286
287
  activeRevisionId={props.activeRevisionId}
287
288
  onOpenRevision={props.onOpenRevision}
289
+ onReplyToRevision={props.onReplyToRevision}
288
290
  onAcceptRevision={props.onAcceptRevision}
289
291
  onRejectRevision={props.onRejectRevision}
290
292
  onAcceptAllChanges={props.onAcceptAllChanges}
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Check, X } from "lucide-react";
2
+ import { Check, MessageSquare, X } from "lucide-react";
3
3
 
4
4
  import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
5
5
  import { selectVisibleRevisions } from "../../ui/shared/revision-filters";
@@ -10,6 +10,7 @@ export interface TwRevisionSidebarProps {
10
10
  markupDisplay: MarkupDisplay;
11
11
  activeRevisionId?: string;
12
12
  onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
13
+ onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
13
14
  onAcceptRevision?: (revisionId: string) => void;
14
15
  onRejectRevision?: (revisionId: string) => void;
15
16
  onAcceptAllChanges?: () => void;
@@ -120,46 +121,61 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
120
121
  const isActive = activeRevisionId === rev.revisionId;
121
122
 
122
123
  return (
123
- <button
124
+ <div
124
125
  key={rev.revisionId}
125
- type="button"
126
- className={`w-full text-left flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
127
- onClick={() => props.onOpenRevision?.(rev)}
126
+ className={`w-full text-left flex rounded-md bg-surface/90 transition-colors ring-1 ring-border ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
128
127
  >
129
128
  <div className={`w-0.5 shrink-0 rounded-l-md ${
130
129
  rev.kind === "insertion" ? "bg-insert"
131
130
  : rev.kind === "deletion" ? "bg-danger"
132
131
  : "bg-tertiary"
133
132
  }`} />
134
- <div className="p-2 flex-1 min-w-0">
135
- <div className="mb-0.5 flex items-start justify-between gap-2">
136
- <span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
137
- <RevisionBadge status={rev.status} actionability={rev.actionability} />
138
- </div>
139
- <p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
140
- {rev.excerpt ? (
141
- <p className={`text-[11px] ${
142
- rev.kind === "insertion" ? "text-insert"
143
- : rev.kind === "deletion" ? "text-danger line-through"
144
- : "text-secondary"
145
- }`}>
146
- {rev.excerpt}
147
- </p>
148
- ) : (
149
- <p className="text-[11px] text-secondary">{rev.label}</p>
150
- )}
151
- {rev.detail ? (
152
- <p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
153
- ) : null}
154
- <div className="mt-2 flex gap-1.5">
133
+ <div className="flex-1 min-w-0">
134
+ <button
135
+ type="button"
136
+ className={`w-full p-2 text-left ${focusRingClass}`}
137
+ onClick={() => props.onOpenRevision?.(rev)}
138
+ >
139
+ <div className="mb-0.5 flex items-start justify-between gap-2">
140
+ <span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
141
+ <RevisionBadge status={rev.status} actionability={rev.actionability} />
142
+ </div>
143
+ <p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
144
+ {rev.excerpt ? (
145
+ <p className={`text-[11px] ${
146
+ rev.kind === "insertion" ? "text-insert"
147
+ : rev.kind === "deletion" ? "text-danger line-through"
148
+ : "text-secondary"
149
+ }`}>
150
+ {rev.excerpt}
151
+ </p>
152
+ ) : (
153
+ <p className="text-[11px] text-secondary">{rev.label}</p>
154
+ )}
155
+ {rev.detail ? (
156
+ <p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
157
+ ) : null}
158
+ </button>
159
+ <div className="flex flex-wrap gap-1.5 px-2 pb-2">
160
+ {props.onReplyToRevision ? (
161
+ <button
162
+ type="button"
163
+ className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-secondary hover:bg-canvas transition-colors"
164
+ onClick={() => props.onReplyToRevision?.(rev)}
165
+ >
166
+ <MessageSquare className="h-3 w-3" />
167
+ {rev.replyCount && rev.replyCount > 0
168
+ ? `Reply ${rev.replyCount}`
169
+ : "Reply"}
170
+ </button>
171
+ ) : null}
155
172
  {rev.actionability === "actionable" ? (
156
173
  <>
157
174
  <button
158
175
  type="button"
159
176
  disabled={!rev.canAccept || rev.status === "accepted"}
160
177
  className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
161
- onClick={(e) => {
162
- e.stopPropagation();
178
+ onClick={() => {
163
179
  props.onAcceptRevision?.(rev.revisionId);
164
180
  }}
165
181
  >
@@ -169,8 +185,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
169
185
  type="button"
170
186
  disabled={!rev.canReject || rev.status === "rejected"}
171
187
  className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
172
- onClick={(e) => {
173
- e.stopPropagation();
188
+ onClick={() => {
174
189
  props.onRejectRevision?.(rev.revisionId);
175
190
  }}
176
191
  >
@@ -182,7 +197,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
182
197
  )}
183
198
  </div>
184
199
  </div>
185
- </button>
200
+ </div>
186
201
  );
187
202
  })}
188
203
  </div>
@@ -36,6 +36,24 @@ const POSTURE_META: Record<
36
36
  "blocked-import": { chip: "BLOCKED REGION", kind: "danger" },
37
37
  };
38
38
 
39
+ const focusRingClass =
40
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
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
+
39
57
  export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
40
58
  segments,
41
59
  activeScopeId,
@@ -43,13 +61,38 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
43
61
  onActiveScopeChange,
44
62
  }) => {
45
63
  // Dedupe by scopeId so a scope spanning multiple pages shows once.
46
- const byScopeId = new Map<string, ScopeRailSegment>();
47
- for (const segment of segments) {
48
- if (!byScopeId.has(segment.scopeId)) {
49
- byScopeId.set(segment.scopeId, segment);
64
+ const uniqueSegments = React.useMemo(() => {
65
+ const byScopeId = new Map<string, ScopeRailSegment>();
66
+ for (const segment of segments) {
67
+ if (!byScopeId.has(segment.scopeId)) {
68
+ byScopeId.set(segment.scopeId, segment);
69
+ }
50
70
  }
51
- }
52
- const uniqueSegments = Array.from(byScopeId.values());
71
+ return Array.from(byScopeId.values()).sort(compareWorkflowSegments(activeScopeId ?? null));
72
+ }, [activeScopeId, segments]);
73
+ const [query, setQuery] = React.useState("");
74
+ const [enabledFilters, setEnabledFilters] = React.useState<ReadonlySet<ScopeFilterKey>>(
75
+ () => new Set(SCOPE_FILTERS.map((filter) => filter.key)),
76
+ );
77
+ const availableFilters = React.useMemo(() => {
78
+ const presentPostures = new Set(uniqueSegments.map((segment) => segment.posture));
79
+ return SCOPE_FILTERS.filter((filter) =>
80
+ filter.postures.some((posture) => presentPostures.has(posture)),
81
+ );
82
+ }, [uniqueSegments]);
83
+ const visibleSegments = React.useMemo(() => {
84
+ const normalizedQuery = normalizeScopeQuery(query);
85
+ return uniqueSegments.filter((segment) => {
86
+ const filterKey = filterKeyForPosture(segment.posture);
87
+ if (!enabledFilters.has(filterKey)) {
88
+ return false;
89
+ }
90
+ if (!normalizedQuery) {
91
+ return true;
92
+ }
93
+ return scopeSearchText(segment).includes(normalizedQuery);
94
+ });
95
+ }, [enabledFilters, query, uniqueSegments]);
53
96
 
54
97
  if (uniqueSegments.length === 0) {
55
98
  return (
@@ -73,9 +116,79 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
73
116
  <div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-accent">
74
117
  Document Intelligence
75
118
  </div>
76
- <div className="text-[15px] font-semibold text-primary">Workflow Scopes</div>
119
+ <div className="flex items-baseline justify-between gap-3">
120
+ <div className="text-[15px] font-semibold text-primary">Workflow Scopes</div>
121
+ <div
122
+ className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary"
123
+ data-testid="workflow-scope-count"
124
+ >
125
+ {visibleSegments.length}/{uniqueSegments.length} shown
126
+ </div>
127
+ </div>
77
128
  </div>
78
- {uniqueSegments.map((segment) => {
129
+
130
+ <div
131
+ className="wre-workflow-tab-controls rounded-lg border border-border bg-surface/55 p-2"
132
+ data-testid="workflow-scope-controls"
133
+ >
134
+ <input
135
+ aria-label="Search workflow scopes"
136
+ className={`h-8 w-full rounded-md border border-border bg-canvas px-2 text-[12px] text-primary placeholder:text-tertiary ${focusRingClass}`}
137
+ placeholder="Search scope, page, section..."
138
+ type="search"
139
+ value={query}
140
+ onChange={(event) => setQuery(event.currentTarget.value)}
141
+ />
142
+ {availableFilters.length > 1 ? (
143
+ <div
144
+ aria-label="Workflow scope layers"
145
+ className="mt-2 flex flex-wrap gap-1"
146
+ role="group"
147
+ >
148
+ {availableFilters.map((filter) => {
149
+ const isEnabled = enabledFilters.has(filter.key);
150
+ return (
151
+ <button
152
+ key={filter.key}
153
+ type="button"
154
+ aria-pressed={isEnabled}
155
+ className={[
156
+ "rounded-full border px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.1em] transition-colors",
157
+ isEnabled
158
+ ? "border-accent/50 bg-accent/10 text-accent"
159
+ : "border-border bg-canvas text-tertiary hover:text-secondary",
160
+ ].join(" ")}
161
+ data-testid={`workflow-scope-filter-${filter.key}`}
162
+ 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
+ });
172
+ }}
173
+ >
174
+ {filter.label}
175
+ </button>
176
+ );
177
+ })}
178
+ </div>
179
+ ) : null}
180
+ </div>
181
+
182
+ {visibleSegments.length === 0 ? (
183
+ <div
184
+ className="rounded-md border border-dashed border-border bg-canvas/50 p-3 text-[11px] text-tertiary"
185
+ data-testid="workflow-scope-filter-empty"
186
+ >
187
+ No workflow scopes match the current search or layer filters.
188
+ </div>
189
+ ) : null}
190
+
191
+ {visibleSegments.map((segment) => {
79
192
  const meta = POSTURE_META[segment.posture];
80
193
  const isActive = activeScopeId === segment.scopeId || segment.isActiveWorkItem;
81
194
  return (
@@ -115,4 +228,42 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
115
228
  );
116
229
  };
117
230
 
231
+ function compareWorkflowSegments(activeScopeId: string | null) {
232
+ return (left: ScopeRailSegment, right: ScopeRailSegment): number => {
233
+ const leftActive = left.scopeId === activeScopeId || left.isActiveWorkItem;
234
+ const rightActive = right.scopeId === activeScopeId || right.isActiveWorkItem;
235
+ if (leftActive !== rightActive) {
236
+ return leftActive ? -1 : 1;
237
+ }
238
+ if (left.pageIndex !== right.pageIndex) {
239
+ return left.pageIndex - right.pageIndex;
240
+ }
241
+ if (left.sectionIndex !== right.sectionIndex) {
242
+ return left.sectionIndex - right.sectionIndex;
243
+ }
244
+ return (left.label || left.scopeId).localeCompare(right.label || right.scopeId);
245
+ };
246
+ }
247
+
248
+ function filterKeyForPosture(posture: ScopeRailPosture): ScopeFilterKey {
249
+ if (posture === "preserve-only" || posture === "blocked-import") {
250
+ return "blocked";
251
+ }
252
+ return posture;
253
+ }
254
+
255
+ function normalizeScopeQuery(value: string): string {
256
+ return value.trim().toLocaleLowerCase();
257
+ }
258
+
259
+ function scopeSearchText(segment: ScopeRailSegment): string {
260
+ return normalizeScopeQuery([
261
+ segment.label,
262
+ segment.scopeId,
263
+ segment.posture,
264
+ `page ${segment.pageIndex + 1}`,
265
+ `section ${segment.sectionIndex + 1}`,
266
+ ].filter(Boolean).join(" "));
267
+ }
268
+
118
269
  export default TwWorkflowTab;
@@ -145,6 +145,9 @@ export interface TwReviewWorkspaceProps {
145
145
  activeRailTab: ReviewRailTab;
146
146
  activeCommentId?: string;
147
147
  activeRevisionId?: string;
148
+ /** Authoring mode toggle state: whether new edits are recorded as tracked changes. */
149
+ trackedChangesAuthoringEnabled?: boolean;
150
+ /** Visual markup state: whether tracked-change decorations are currently shown. */
148
151
  showTrackedChanges: boolean;
149
152
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
150
153
  interactionGuardSnapshot?: InteractionGuardSnapshot;
@@ -286,6 +289,7 @@ export interface TwReviewWorkspaceProps {
286
289
  onAddReply?: (commentId: string, body: string) => void;
287
290
  onEditBody?: (commentId: string, body: string) => void;
288
291
  onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
292
+ onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
289
293
  onAcceptRevision?: (revisionId: string) => void;
290
294
  onRejectRevision?: (revisionId: string) => void;
291
295
  onAcceptAllChanges?: () => void;
@@ -25,8 +25,6 @@ import {
25
25
  ChevronLeft,
26
26
  ChevronRight,
27
27
  CircleOff,
28
- Eye,
29
- EyeOff,
30
28
  FileDiff,
31
29
  Flag,
32
30
  Hand,
@@ -226,33 +224,33 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
226
224
  </Tooltip.Portal>
227
225
  </Tooltip.Root>
228
226
  );
229
- case "tracked-changes-toggle":
227
+ case "tracked-changes-toggle": {
228
+ const trackChangesLabel = (props.showTrackedChanges ?? false)
229
+ ? "Stop tracking changes"
230
+ : "Start tracking changes";
230
231
  return (
231
232
  <Tooltip.Root>
232
233
  <Tooltip.Trigger asChild>
233
234
  <Toggle.Root
234
235
  pressed={props.showTrackedChanges ?? false}
235
236
  onPressedChange={(v) => props.onShowTrackedChangesChange?.(v)}
236
- aria-label={(props.showTrackedChanges ?? false) ? "Turn off tracked changes" : "Turn on tracked changes"}
237
+ aria-label={trackChangesLabel}
237
238
  disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
238
239
  onMouseDown={preserveEditorSelectionMouseDown}
239
240
  className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
240
241
  data-testid="role-tracked-changes-toggle"
241
242
  >
242
- {(props.showTrackedChanges ?? false) ? (
243
- <Eye className="h-3.5 w-3.5" />
244
- ) : (
245
- <EyeOff className="h-3.5 w-3.5" />
246
- )}
243
+ <FileDiff className="h-3.5 w-3.5" />
247
244
  </Toggle.Root>
248
245
  </Tooltip.Trigger>
249
246
  <Tooltip.Portal>
250
247
  <Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
251
- {(props.showTrackedChanges ?? false) ? "Turn off tracked changes" : "Turn on tracked changes"}
248
+ {trackChangesLabel}
252
249
  </Tooltip.Content>
253
250
  </Tooltip.Portal>
254
251
  </Tooltip.Root>
255
252
  );
253
+ }
256
254
  case "review-sidebar-tracked-changes":
257
255
  return (
258
256
  <Tooltip.Root>
@@ -15,8 +15,7 @@ import {
15
15
  Bold,
16
16
  ChevronDown,
17
17
  Download,
18
- Eye,
19
- EyeOff,
18
+ FileDiff,
20
19
  FileText,
21
20
  Highlighter,
22
21
  ImagePlus,
@@ -89,7 +88,7 @@ export interface TwToolbarProps {
89
88
  formattingState?: FormattingStateSnapshot;
90
89
  activeListContext?: ActiveListContext | null;
91
90
  styleCatalog?: StyleCatalogSnapshot;
92
- /** Display toggle for tracked change decorations (not a runtime mutation toggle). */
91
+ /** Authoring toggle for recording new edits as tracked changes. */
93
92
  showTrackedChanges: boolean;
94
93
  /** Active story target — shows a breadcrumb when editing a secondary story. */
95
94
  activeStory?: EditorStoryTarget;
@@ -378,7 +377,7 @@ export function TwToolbar(props: TwToolbarProps) {
378
377
  label="Bold"
379
378
  shortcut="⌘B"
380
379
  active={props.formattingState?.bold ?? false}
381
- disabled={!canEdit}
380
+ disabled={!canEdit || !props.onToggleBold}
382
381
  onClick={props.onToggleBold}
383
382
  />
384
383
  <TwToolbarIconButton
@@ -386,7 +385,7 @@ export function TwToolbar(props: TwToolbarProps) {
386
385
  label="Italic"
387
386
  shortcut="⌘I"
388
387
  active={props.formattingState?.italic ?? false}
389
- disabled={!canEdit}
388
+ disabled={!canEdit || !props.onToggleItalic}
390
389
  onClick={props.onToggleItalic}
391
390
  />
392
391
  <TwToolbarIconButton
@@ -394,12 +393,17 @@ export function TwToolbar(props: TwToolbarProps) {
394
393
  label="Underline"
395
394
  shortcut="⌘U"
396
395
  active={props.formattingState?.underline ?? false}
397
- disabled={!canEdit}
396
+ disabled={!canEdit || !props.onToggleUnderline}
398
397
  onClick={props.onToggleUnderline}
399
398
  />
400
399
  {showAdvancedFormatting ? (
401
400
  <ToolbarFormattingOverflow
402
- disabled={!canEdit}
401
+ disabled={
402
+ !canEdit ||
403
+ (!props.onToggleStrikethrough &&
404
+ !props.onToggleSuperscript &&
405
+ !props.onToggleSubscript)
406
+ }
403
407
  formattingState={props.formattingState}
404
408
  onToggleStrikethrough={props.onToggleStrikethrough}
405
409
  onToggleSuperscript={props.onToggleSuperscript}
@@ -452,14 +456,14 @@ export function TwToolbar(props: TwToolbarProps) {
452
456
  icon={List}
453
457
  label="Bulleted list"
454
458
  active={Boolean(props.activeListContext && !props.activeListContext.isOrdered)}
455
- disabled={!canEdit}
459
+ disabled={!canEdit || !props.onToggleBulletedList}
456
460
  onClick={props.onToggleBulletedList}
457
461
  />
458
462
  <TwToolbarIconButton
459
463
  icon={Rows3}
460
464
  label="Numbered list"
461
465
  active={Boolean(props.activeListContext?.isOrdered)}
462
- disabled={!canEdit}
466
+ disabled={!canEdit || !props.onToggleNumberedList}
463
467
  onClick={props.onToggleNumberedList}
464
468
  />
465
469
  </>
@@ -469,13 +473,13 @@ export function TwToolbar(props: TwToolbarProps) {
469
473
  <TwToolbarIconButton
470
474
  icon={Outdent}
471
475
  label="Outdent"
472
- disabled={!canEdit}
476
+ disabled={!canEdit || !props.onOutdent}
473
477
  onClick={props.onOutdent}
474
478
  />
475
479
  <TwToolbarIconButton
476
480
  icon={Indent}
477
481
  label="Indent"
478
- disabled={!canEdit}
482
+ disabled={!canEdit || !props.onIndent}
479
483
  onClick={props.onIndent}
480
484
  />
481
485
  </>
@@ -669,12 +673,12 @@ export function TwToolbar(props: TwToolbarProps) {
669
673
  <Toggle.Root
670
674
  pressed={props.showTrackedChanges}
671
675
  onPressedChange={props.onShowTrackedChangesChange}
672
- aria-label={props.showTrackedChanges ? "Turn off tracked changes" : "Turn on tracked changes"}
676
+ aria-label={props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
673
677
  disabled={caps ? !caps.trackChangesSupported : false}
674
678
  onMouseDown={preserveEditorSelectionMouseDown}
675
679
  className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
676
680
  >
677
- {props.showTrackedChanges ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
681
+ <FileDiff className="h-3.5 w-3.5" />
678
682
  </Toggle.Root>
679
683
  </Tooltip.Trigger>
680
684
  <Tooltip.Portal>
@@ -682,7 +686,7 @@ export function TwToolbar(props: TwToolbarProps) {
682
686
  className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
683
687
  sideOffset={6}
684
688
  >
685
- {props.showTrackedChanges ? "Turn off tracked changes" : "Turn on tracked changes"}
689
+ {props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
686
690
  </Tooltip.Content>
687
691
  </Tooltip.Portal>
688
692
  </Tooltip.Root>
@@ -1420,7 +1424,7 @@ function ToolbarFormattingOverflow(props: {
1420
1424
  <ToolbarPopoverActionButton
1421
1425
  active={props.formattingState?.strikethrough ?? false}
1422
1426
  ariaLabel="Strikethrough"
1423
- disabled={props.disabled}
1427
+ disabled={props.disabled || !props.onToggleStrikethrough}
1424
1428
  icon={<Strikethrough className="h-3.5 w-3.5" />}
1425
1429
  onClick={() => {
1426
1430
  props.onToggleStrikethrough?.();
@@ -1430,7 +1434,7 @@ function ToolbarFormattingOverflow(props: {
1430
1434
  <ToolbarPopoverActionButton
1431
1435
  active={props.formattingState?.superscript ?? false}
1432
1436
  ariaLabel="Superscript"
1433
- disabled={props.disabled}
1437
+ disabled={props.disabled || !props.onToggleSuperscript}
1434
1438
  icon={<Superscript className="h-3.5 w-3.5" />}
1435
1439
  onClick={() => {
1436
1440
  props.onToggleSuperscript?.();
@@ -1440,7 +1444,7 @@ function ToolbarFormattingOverflow(props: {
1440
1444
  <ToolbarPopoverActionButton
1441
1445
  active={props.formattingState?.subscript ?? false}
1442
1446
  ariaLabel="Subscript"
1443
- disabled={props.disabled}
1447
+ disabled={props.disabled || !props.onToggleSubscript}
1444
1448
  icon={<Subscript className="h-3.5 w-3.5" />}
1445
1449
  onClick={() => {
1446
1450
  props.onToggleSubscript?.();
@@ -184,6 +184,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
184
184
  const caps = props.capabilities;
185
185
  const isPageWorkspace = props.workspaceMode === "page";
186
186
  const markupDisplay = props.markupDisplay;
187
+ const trackedChangesAuthoringEnabled =
188
+ props.trackedChangesAuthoringEnabled ?? props.showTrackedChanges;
187
189
  const [navOpen, setNavOpen] = useState(false);
188
190
  const handleOpenPageModeStory = useCallback(
189
191
  (target: EditorStoryTarget) => {
@@ -609,7 +611,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
609
611
  toolbarInteractionPolicy?.canAddComment ??
610
612
  (caps ? caps.canAddComment : false)
611
613
  }
612
- showTrackedChanges={props.showTrackedChanges}
614
+ showTrackedChanges={trackedChangesAuthoringEnabled}
613
615
  capabilities={caps}
614
616
  onAddComment={
615
617
  props.onAddComment
@@ -744,7 +746,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
744
746
  formattingState={props.formattingState}
745
747
  activeListContext={props.activeListContext}
746
748
  styleCatalog={props.styleCatalog}
747
- showTrackedChanges={props.showTrackedChanges}
749
+ showTrackedChanges={trackedChangesAuthoringEnabled}
748
750
  showSidebarToggle={responsiveChrome.showSidebarToggle}
749
751
  isSidebarOpen={reviewRailOpen}
750
752
  onUndo={runWithSelectionToolbarDismiss(props.onUndo)}
@@ -1243,6 +1245,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1243
1245
  onAddReply: props.onAddReply,
1244
1246
  onEditBody: props.onEditBody,
1245
1247
  onOpenRevision: props.onOpenRevision,
1248
+ onReplyToRevision: props.onReplyToRevision,
1246
1249
  onAcceptRevision: props.onAcceptRevision,
1247
1250
  onRejectRevision: props.onRejectRevision,
1248
1251
  onAcceptAllChanges: props.onAcceptAllChanges,
@@ -1251,6 +1254,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1251
1254
  // Layer-06 workflow facet. Layout facet no longer exposes
1252
1255
  // `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
1253
1256
  scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
1257
+ activeScopeId,
1258
+ onOpenScope: (segment) => {
1259
+ handleScopeStripeClick({ scopeId: segment.scopeId });
1260
+ },
1254
1261
  workflowTab: props.reviewRailWorkflowTab,
1255
1262
  workflowCount: props.reviewRailWorkflowCount,
1256
1263
  workflowScopesTitle: props.reviewRailWorkflowScopesTitle,