@beyondwork/docx-react-component 1.0.87 → 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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/api/v3/_runtime-handle.ts +5 -0
  3. package/src/api/v3/ai/replacement.ts +82 -0
  4. package/src/api/v3/runtime/content.ts +3 -0
  5. package/src/api/v3/runtime/formatting.ts +64 -0
  6. package/src/core/commands/formatting-commands.ts +107 -0
  7. package/src/core/state/text-transaction.ts +11 -4
  8. package/src/runtime/document-runtime.ts +293 -27
  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 +61 -11
  37. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
  38. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +52 -6
  39. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
  40. package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
  41. package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
  42. package/src/ui-tailwind/review-workspace/types.ts +2 -0
  43. package/src/ui-tailwind/theme/editor-theme.css +25 -12
  44. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +13 -4
  45. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +6 -15
  46. package/src/ui-tailwind/tw-review-workspace.tsx +28 -18
  47. package/src/ui-tailwind/workflow-scope-layers.ts +70 -0
@@ -137,7 +137,23 @@ export function restoreScrollAnchor(
137
137
  anchor: ScrollAnchor | null,
138
138
  options?: FindScrollAnchorOptions,
139
139
  ): void {
140
- if (!root || !anchor) return;
140
+ const targetScrollTop = resolveScrollTopForAnchor(root, anchor, options);
141
+ if (root && targetScrollTop !== null) {
142
+ root.scrollTop = targetScrollTop;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Resolve the `scrollTop` that would restore `anchor` without mutating the
148
+ * scroll container. Used by the edit-path preservation guard so it can reject
149
+ * unsafe restores before writing scroll state.
150
+ */
151
+ export function resolveScrollTopForAnchor(
152
+ root: HTMLElement | null,
153
+ anchor: ScrollAnchor | null,
154
+ options?: FindScrollAnchorOptions,
155
+ ): number | null {
156
+ if (!root || !anchor) return null;
141
157
 
142
158
  if (options?.geometryFacet && !options.prepaintFallback) {
143
159
  const geometry = options.geometryFacet.getBlock(anchor.blockId);
@@ -150,15 +166,14 @@ export function restoreScrollAnchor(
150
166
  // newScrollTop = newBlockTop + offsetWithinBlock.
151
167
  // Matches the DOM-path formula below (round-trip verified by
152
168
  // `test/ui/mode-toggle-scroll-anchor.test.ts`).
153
- root.scrollTop = rect.topPx + anchor.offsetWithinBlock;
154
- return;
169
+ return rect.topPx + anchor.offsetWithinBlock;
155
170
  }
156
171
  // No block match through facet; fall through to DOM path.
157
172
  }
158
173
 
159
174
  const selector = `[data-block-id="${cssEscape(anchor.blockId)}"]`;
160
175
  const block = root.querySelector<HTMLElement>(selector);
161
- if (!block) return;
176
+ if (!block) return null;
162
177
  // Cold-open / pre-paint DOM fallback — same rationale as
163
178
  // findScrollAnchor's fallback above.
164
179
  // geometry:allow-dom-fallback
@@ -171,7 +186,7 @@ export function restoreScrollAnchor(
171
186
  // capture time). Scrolling by `delta` shifts rects by `-delta`, so
172
187
  // solve for delta: delta = blockRect.top - rootRect.top + offsetWithinBlock.
173
188
  const delta = blockRect.top - rootRect.top + anchor.offsetWithinBlock;
174
- root.scrollTop = root.scrollTop + delta;
189
+ return root.scrollTop + delta;
175
190
  }
176
191
 
177
192
  /**
@@ -72,6 +72,7 @@ import { createPredictedTxGate } from "./predicted-tx-gate";
72
72
  import { replaceStatePreservingPosition } from "./preserve-position";
73
73
  import {
74
74
  createScopeTagRegistry,
75
+ storyTargetsEqual,
75
76
  type ScopeTagRegistry,
76
77
  } from "../../api/public-types";
77
78
  import { hasBailIfCrossedTagInRange } from "./predicted-tag-preflight";
@@ -92,6 +93,24 @@ import { chartNodeViews } from "./chart-node-view.tsx";
92
93
  import type { SelectionToolbarAnchor } from "../../ui/headless/selection-toolbar-model";
93
94
  import type { MediaPreviewDescriptor } from "./pm-state-from-snapshot";
94
95
 
96
+ type RebuildScrollAnchorPolicy = "bounded-same-story";
97
+
98
+ const BOUNDED_TYPING_SCROLL_RESTORE_MAX_DELTA_PX = 256;
99
+
100
+ export function shouldPreserveScrollAnchorForRebuild(options: {
101
+ policy: RebuildScrollAnchorPolicy | null;
102
+ view: Pick<EditorView, "hasFocus"> | null;
103
+ geometryFacet?: import("../../runtime/geometry/index.ts").GeometryFacet;
104
+ previousStory: EditorStoryTarget | null;
105
+ nextStory: EditorStoryTarget;
106
+ }): boolean {
107
+ if (options.policy !== "bounded-same-story") return false;
108
+ if (!options.view?.hasFocus()) return false;
109
+ if (!options.geometryFacet) return false;
110
+ if (!options.previousStory) return false;
111
+ return storyTargetsEqual(options.previousStory, options.nextStory);
112
+ }
113
+
95
114
  /**
96
115
  * Build page-break widget decorations from the layout facet's current page
97
116
  * graph. Returns `[]` when the facet is unavailable, the active story is
@@ -435,6 +454,8 @@ export const TwProseMirrorSurface = forwardRef<
435
454
  const sessionRef = useRef<import("./local-edit-session-state").LocalEditSessionState | null>(null);
436
455
  const laneRef = useRef<import("./fast-text-edit-lane").FastTextEditLane | null>(null);
437
456
  const equivalentAckLedgerRef = useRef<Map<string, string>>(new Map());
457
+ const pendingRebuildScrollAnchorRef = useRef<RebuildScrollAnchorPolicy | null>(null);
458
+ const lastBuiltStoryRef = useRef<EditorStoryTarget | null>(null);
438
459
  const selectionToolbarFrameRef = useRef<number | null>(null);
439
460
  const lastSelectionToolbarMeasurementRef = useRef<{
440
461
  key: string | null;
@@ -766,6 +787,7 @@ export const TwProseMirrorSurface = forwardRef<
766
787
  if (!props.dispatchRuntimeCommand || !sessionRef.current) {
767
788
  laneRef.current = null;
768
789
  equivalentAckLedgerRef.current.clear();
790
+ pendingRebuildScrollAnchorRef.current = null;
769
791
  return;
770
792
  }
771
793
  // Wave 1 Slice E1/E2 — lane observability.
@@ -811,6 +833,7 @@ export const TwProseMirrorSurface = forwardRef<
811
833
  );
812
834
  },
813
835
  onEquivalentAck: (ack) => {
836
+ pendingRebuildScrollAnchorRef.current = null;
814
837
  if (
815
838
  ack.opId &&
816
839
  ack.newRevisionToken &&
@@ -821,16 +844,22 @@ export const TwProseMirrorSurface = forwardRef<
821
844
  }
822
845
  equivalentAckLedgerRef.current.clear();
823
846
  },
824
- onAdjustedAck: () => {
847
+ onAdjustedAck: (ack) => {
825
848
  // Adjusted path: allow the rebuild effect to run (it will call
826
849
  // view.updateState with the canonical snapshot).
827
850
  equivalentAckLedgerRef.current.clear();
851
+ pendingRebuildScrollAnchorRef.current =
852
+ getTextCommandRefreshClass(ack) === "surface-only"
853
+ ? "bounded-same-story"
854
+ : null;
828
855
  },
829
856
  onRejectedAck: () => {
830
857
  equivalentAckLedgerRef.current.clear();
858
+ pendingRebuildScrollAnchorRef.current = null;
831
859
  },
832
860
  onStructuralDivergence: () => {
833
861
  equivalentAckLedgerRef.current.clear();
862
+ pendingRebuildScrollAnchorRef.current = null;
834
863
  },
835
864
  });
836
865
  }, [props.dispatchRuntimeCommand, scopeTagRegistry]);
@@ -839,6 +868,7 @@ export const TwProseMirrorSurface = forwardRef<
839
868
  if (!mountRef.current || !surface) return;
840
869
 
841
870
  if (viewRef.current && documentBuildKeyRef.current === documentBuildKey) {
871
+ pendingRebuildScrollAnchorRef.current = null;
842
872
  return;
843
873
  }
844
874
 
@@ -866,6 +896,8 @@ export const TwProseMirrorSurface = forwardRef<
866
896
  documentBuildKeyRef.current = documentBuildKey;
867
897
  applyDecorationProps(viewRef.current, positionMapRef.current);
868
898
  equivalentAckLedgerRef.current.delete(snapshot.revisionToken);
899
+ pendingRebuildScrollAnchorRef.current = null;
900
+ lastBuiltStoryRef.current = snapshot.activeStory;
869
901
  if (pendingTypingProbeRef.current) {
870
902
  finishPerfProbe(pendingTypingProbeRef.current);
871
903
  pendingTypingProbeRef.current = null;
@@ -921,24 +953,38 @@ export const TwProseMirrorSurface = forwardRef<
921
953
  // AFTER, so PM's internal selection-change events during the
922
954
  // swap are swallowed by the selection-sync plugin.
923
955
  //
924
- // Scroll-anchor preservation (`preserveScrollAnchor: true`) is
925
- // currently OFF by default after the 2026-04-24 jump-to-top
926
- // regression report (see hotfix commit). Re-enable under a
927
- // diagnosed-safe codepath only; the capture/restore helpers
928
- // remain tested and ready.
956
+ // Scroll-anchor preservation is narrowly re-enabled only when a
957
+ // predicted edit receives a `surface-only` adjusted ack and the
958
+ // replacement stays in the same focused story with a live geometry
959
+ // facet. `maxScrollDeltaPx` is the guardrail: if the anchor target
960
+ // would move by more than the small local-edit budget, the helper
961
+ // refuses that exact target but restores the captured scrollTop so
962
+ // a PM/browser-origin top jump is not accepted as the final state.
929
963
  //
930
964
  // Ordering invariant is regression-guarded by
931
965
  // `test/ui-tailwind/editor-surface/preserve-position-ordering.test.ts`.
966
+ const scrollAnchorPolicy = pendingRebuildScrollAnchorRef.current;
967
+ pendingRebuildScrollAnchorRef.current = null;
968
+ const preserveScrollAnchor = shouldPreserveScrollAnchorForRebuild({
969
+ policy: scrollAnchorPolicy,
970
+ view: viewRef.current,
971
+ geometryFacet: props.geometryFacet,
972
+ previousStory: lastBuiltStoryRef.current,
973
+ nextStory: snapshot.activeStory,
974
+ });
932
975
  replaceStatePreservingPosition(
933
976
  {
934
977
  view: viewRef.current,
935
978
  geometryFacet: props.geometryFacet,
936
979
  suppressionRef: suppressSelectionEchoRef,
980
+ preserveScrollAnchor,
981
+ maxScrollDeltaPx: BOUNDED_TYPING_SCROLL_RESTORE_MAX_DELTA_PX,
937
982
  },
938
983
  state,
939
984
  );
940
985
  }
941
986
  documentBuildKeyRef.current = documentBuildKey;
987
+ lastBuiltStoryRef.current = snapshot.activeStory;
942
988
  applyDecorationProps(viewRef.current, positionMap);
943
989
 
944
990
  if (activeSearchRef.current) {
@@ -30,6 +30,7 @@ interface OpenVerticalMerge {
30
30
  col: number;
31
31
  colSpan: number;
32
32
  continuedThisRow: boolean;
33
+ hasMaterializedRowSpan: boolean;
33
34
  layout: TableCellLayout;
34
35
  }
35
36
 
@@ -159,7 +160,9 @@ function computeTableLayout(tableNode: PMNode): TableCellLayout[][] {
159
160
  if (verticalMerge === "continue") {
160
161
  const owner = findVerticalMergeOwner(openVerticalMerges, column, colSpan);
161
162
  if (owner) {
162
- owner.layout.rowSpan += 1;
163
+ if (!owner.hasMaterializedRowSpan) {
164
+ owner.layout.rowSpan += 1;
165
+ }
163
166
  owner.continuedThisRow = true;
164
167
  layoutRow.push({
165
168
  cellIndex,
@@ -189,6 +192,7 @@ function computeTableLayout(tableNode: PMNode): TableCellLayout[][] {
189
192
  col: column,
190
193
  colSpan,
191
194
  continuedThisRow: true,
195
+ hasMaterializedRowSpan: explicitRowSpan > 1,
192
196
  layout,
193
197
  });
194
198
  }
@@ -23,6 +23,7 @@ import {
23
23
  TwReviewRailFooter,
24
24
  type TwReviewRailFooterProps,
25
25
  } from "./tw-review-rail-footer";
26
+ import type { WorkflowScopeLayerKey } from "../workflow-scope-layers";
26
27
 
27
28
  /**
28
29
  * Review rail with up to four tabs (Workflow / Comments / Changes / Health).
@@ -66,6 +67,8 @@ export interface TwReviewRailProps {
66
67
  */
67
68
  scopeRailSegments?: readonly ScopeRailSegment[];
68
69
  activeScopeId?: string | null;
70
+ workflowLayerFilters?: ReadonlySet<WorkflowScopeLayerKey>;
71
+ onWorkflowLayerFiltersChange?: (filters: ReadonlySet<WorkflowScopeLayerKey>) => void;
69
72
  /**
70
73
  * Optional host-provided Workflow-tab override. When supplied this
71
74
  * ReactNode replaces the default TwWorkflowTab content while still using
@@ -262,6 +265,8 @@ export function TwReviewRail(props: TwReviewRailProps) {
262
265
  <TwWorkflowTab
263
266
  segments={workflowSegments}
264
267
  activeScopeId={props.activeScopeId ?? null}
268
+ enabledLayerFilters={props.workflowLayerFilters}
269
+ onEnabledLayerFiltersChange={props.onWorkflowLayerFiltersChange}
265
270
  onOpenScope={props.onOpenScope}
266
271
  />
267
272
  )}
@@ -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>