@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.
- package/package.json +1 -1
- package/src/api/v3/_runtime-handle.ts +5 -0
- package/src/api/v3/ai/replacement.ts +82 -0
- package/src/api/v3/runtime/content.ts +3 -0
- package/src/api/v3/runtime/formatting.ts +64 -0
- package/src/core/commands/formatting-commands.ts +107 -0
- package/src/core/state/text-transaction.ts +11 -4
- package/src/runtime/document-runtime.ts +293 -27
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +12 -3
- package/src/runtime/scopes/audit-bundle.ts +2 -2
- package/src/runtime/scopes/compiler-service.ts +70 -0
- package/src/runtime/scopes/formatting/apply.ts +262 -0
- package/src/runtime/scopes/index.ts +12 -0
- package/src/runtime/scopes/replacement/propose.ts +2 -0
- package/src/runtime/scopes/scope-kinds/paragraph.ts +1 -0
- package/src/runtime/scopes/semantic-scope-types.ts +48 -4
- package/src/runtime/scopes/workflow-overlap.ts +9 -11
- package/src/shell/session-bootstrap.ts +1 -0
- package/src/ui/WordReviewEditor.tsx +277 -28
- package/src/ui/editor-command-bag.ts +11 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/headless/chrome-registry.ts +6 -6
- package/src/ui/headless/role-action-sets.ts +4 -10
- package/src/ui/headless/selection-tool-resolver.ts +11 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +1 -1
- package/src/ui-tailwind/chrome/tw-context-band.tsx +7 -7
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +13 -18
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +8 -5
- package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +100 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -40
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +9 -7
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +17 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +6 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +17 -7
- package/src/ui-tailwind/editor-surface/preserve-position.ts +61 -11
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +52 -6
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
- package/src/ui-tailwind/review-workspace/types.ts +2 -0
- package/src/ui-tailwind/theme/editor-theme.css +25 -12
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +13 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +6 -15
- package/src/ui-tailwind/tw-review-workspace.tsx +28 -18
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
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.
|
|
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 [
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 =
|
|
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(
|
|
164
|
-
|
|
165
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
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:
|
|
554
|
-
border-radius:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
157
|
+
/** Current chrome pin state, retained for host compatibility. */
|
|
159
158
|
chromePins?: ChromePinsState;
|
|
160
|
-
/** Called when
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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>
|