@beyondwork/docx-react-component 1.0.88 → 1.0.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +51 -0
- 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 +30 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
- 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
|
@@ -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>
|
|
@@ -68,6 +68,11 @@ import { useLayoutFacetRenderSignal } from "./review-workspace/use-layout-facet-
|
|
|
68
68
|
import { useScrollRootCapture } from "./review-workspace/use-scroll-root-capture.ts";
|
|
69
69
|
import { usePmSurfaceCapture } from "./review-workspace/use-pm-surface-capture.ts";
|
|
70
70
|
import { TwReviewWorkspaceNavigator } from "./review-workspace/tw-review-workspace-navigator.tsx";
|
|
71
|
+
import {
|
|
72
|
+
createDefaultWorkflowScopeLayerKeys,
|
|
73
|
+
workflowScopePosturesForLayerKeys,
|
|
74
|
+
type WorkflowScopeLayerKey,
|
|
75
|
+
} from "./workflow-scope-layers";
|
|
71
76
|
|
|
72
77
|
export {
|
|
73
78
|
FRAME_PX_PER_TWIP_AT_96DPI,
|
|
@@ -174,6 +179,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
174
179
|
useRef<TwWorkspaceChromeHostController | null>(null);
|
|
175
180
|
const [shellModeOverride, setShellModeOverride] =
|
|
176
181
|
useState<ShellHeaderMode | null>(null);
|
|
182
|
+
const [workflowLayerFilters, setWorkflowLayerFilters] = useState<
|
|
183
|
+
ReadonlySet<WorkflowScopeLayerKey>
|
|
184
|
+
>(createDefaultWorkflowScopeLayerKeys);
|
|
185
|
+
const visibleScopePostures = useMemo(
|
|
186
|
+
() => workflowScopePosturesForLayerKeys(workflowLayerFilters),
|
|
187
|
+
[workflowLayerFilters],
|
|
188
|
+
);
|
|
177
189
|
// P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
|
|
178
190
|
// root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
|
|
179
191
|
// measure per-page rects and to reparent PM's DOM node across band
|
|
@@ -533,12 +545,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
533
545
|
)
|
|
534
546
|
: null;
|
|
535
547
|
|
|
536
|
-
// Audit §2.5 — context band mounts as a composition-level sibling
|
|
537
|
-
// the
|
|
538
|
-
//
|
|
539
|
-
// and
|
|
540
|
-
// Phase D migrates role-action contents into the band and retires the
|
|
541
|
-
// legacy inline role region inside TwToolbar).
|
|
548
|
+
// Audit §2.5 — context band mounts as a composition-level sibling below
|
|
549
|
+
// the shell header only when the active mode has meaningful mode-owned
|
|
550
|
+
// content. Plain editor mode keeps its authoring controls in the toolbar
|
|
551
|
+
// and intentionally does not render an empty "Edit" ribbon.
|
|
542
552
|
//
|
|
543
553
|
// `resolveChromeComposition` is pure but each call allocates new Sets +
|
|
544
554
|
// arrays. `TwReviewWorkspace` re-renders on every PM transaction (the
|
|
@@ -573,6 +583,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
573
583
|
modeOverride: shellModeOverride === "more" ? "more" : undefined,
|
|
574
584
|
});
|
|
575
585
|
const showHealthRailTab = composition.rail.visibleTabs.has("health");
|
|
586
|
+
const showContextBand =
|
|
587
|
+
chromeVisibility.toolbar &&
|
|
588
|
+
(composition.mode === "more" || viewState.editorRole !== "editor");
|
|
576
589
|
|
|
577
590
|
const arbiter = useWorkspaceArbiter();
|
|
578
591
|
|
|
@@ -591,18 +604,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
591
604
|
* decoupling this branch.
|
|
592
605
|
*/}
|
|
593
606
|
{/*
|
|
594
|
-
* Chrome Closure Pass · Task 1 (designsystem.md §6.3) —
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* [shell header] · [TwContextBand carrying role actions] · [TwToolbar (formatting only)]
|
|
599
|
-
* matching §8.8.1's "Workspace context row / mode-owned band"
|
|
600
|
-
* row. Callbacks below mirror what was previously threaded
|
|
601
|
-
* through `ChromePresetToolbar` to `TwRoleActionRegion`; the
|
|
602
|
-
* toolbar still receives `role` for `isChromeItemOwnedByRoleRegion`
|
|
603
|
-
* deferral but no longer renders the region itself.
|
|
607
|
+
* Chrome Closure Pass · Task 1 (designsystem.md §6.3) — review,
|
|
608
|
+
* workflow, and More mode actions live in `TwContextBand`; editor
|
|
609
|
+
* mode stays toolbar-only unless a future pass adds real contextual
|
|
610
|
+
* authoring content.
|
|
604
611
|
*/}
|
|
605
|
-
{
|
|
612
|
+
{showContextBand ? (
|
|
606
613
|
<TwContextBand mode={composition.mode}>
|
|
607
614
|
{composition.mode === "more" ? (
|
|
608
615
|
<TwMoreContextBandContent
|
|
@@ -615,7 +622,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
615
622
|
props.onActiveRailTabChange?.("health");
|
|
616
623
|
}}
|
|
617
624
|
onOpenOutline={() => setNavOpen(true)}
|
|
618
|
-
onOpenSearch={props.reviewRailFooter?.onSearch}
|
|
625
|
+
onOpenSearch={props.onOpenInlineFind ?? props.reviewRailFooter?.onSearch}
|
|
619
626
|
/>
|
|
620
627
|
) : viewState.editorRole ? (
|
|
621
628
|
<TwRoleActionRegion
|
|
@@ -1167,6 +1174,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1167
1174
|
onSetColumnWidth={props.onSetColumnWidth}
|
|
1168
1175
|
onSetRowHeight={props.onSetRowHeight}
|
|
1169
1176
|
activeScopeId={activeScopeId}
|
|
1177
|
+
visibleScopePostures={visibleScopePostures}
|
|
1170
1178
|
editorRole={viewState.editorRole}
|
|
1171
1179
|
scopeCardScopeTagEditor={props.scopeCardScopeTagEditor}
|
|
1172
1180
|
onScopeStripeClick={handleScopeStripeClick}
|
|
@@ -1273,6 +1281,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1273
1281
|
// `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
|
|
1274
1282
|
scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
|
|
1275
1283
|
activeScopeId,
|
|
1284
|
+
workflowLayerFilters,
|
|
1285
|
+
onWorkflowLayerFiltersChange: setWorkflowLayerFilters,
|
|
1276
1286
|
onOpenScope: (segment) => {
|
|
1277
1287
|
handleScopeStripeClick({ scopeId: segment.scopeId });
|
|
1278
1288
|
},
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ScopeRailPosture } from "../api/public-types";
|
|
2
|
+
|
|
3
|
+
export type WorkflowScopeLayerKey =
|
|
4
|
+
| "edit"
|
|
5
|
+
| "suggest"
|
|
6
|
+
| "comment"
|
|
7
|
+
| "view"
|
|
8
|
+
| "candidate"
|
|
9
|
+
| "blocked";
|
|
10
|
+
|
|
11
|
+
export interface WorkflowScopeLayerFilter {
|
|
12
|
+
key: WorkflowScopeLayerKey;
|
|
13
|
+
label: string;
|
|
14
|
+
postures: readonly ScopeRailPosture[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const WORKFLOW_SCOPE_LAYER_FILTERS: readonly WorkflowScopeLayerFilter[] = [
|
|
18
|
+
{ key: "edit", label: "Edit", postures: ["edit"] },
|
|
19
|
+
{ key: "suggest", label: "Suggest", postures: ["suggest"] },
|
|
20
|
+
{ key: "comment", label: "Comment", postures: ["comment"] },
|
|
21
|
+
{ key: "view", label: "Review", postures: ["view"] },
|
|
22
|
+
{ key: "candidate", label: "Scheduled", postures: ["candidate"] },
|
|
23
|
+
{ key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function createDefaultWorkflowScopeLayerKeys(): ReadonlySet<WorkflowScopeLayerKey> {
|
|
27
|
+
return new Set(WORKFLOW_SCOPE_LAYER_FILTERS.map((filter) => filter.key));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function workflowScopeLayerKeyForPosture(
|
|
31
|
+
posture: ScopeRailPosture,
|
|
32
|
+
): WorkflowScopeLayerKey {
|
|
33
|
+
if (posture === "preserve-only" || posture === "blocked-import") {
|
|
34
|
+
return "blocked";
|
|
35
|
+
}
|
|
36
|
+
return posture;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isWorkflowScopePostureVisible(
|
|
40
|
+
posture: ScopeRailPosture,
|
|
41
|
+
enabledLayers: ReadonlySet<WorkflowScopeLayerKey>,
|
|
42
|
+
): boolean {
|
|
43
|
+
return enabledLayers.has(workflowScopeLayerKeyForPosture(posture));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workflowScopePosturesForLayerKeys(
|
|
47
|
+
enabledLayers: ReadonlySet<WorkflowScopeLayerKey>,
|
|
48
|
+
): ReadonlySet<ScopeRailPosture> {
|
|
49
|
+
const postures = new Set<ScopeRailPosture>();
|
|
50
|
+
for (const filter of WORKFLOW_SCOPE_LAYER_FILTERS) {
|
|
51
|
+
if (!enabledLayers.has(filter.key)) continue;
|
|
52
|
+
for (const posture of filter.postures) {
|
|
53
|
+
postures.add(posture);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return postures;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function toggleWorkflowScopeLayerKey(
|
|
60
|
+
current: ReadonlySet<WorkflowScopeLayerKey>,
|
|
61
|
+
key: WorkflowScopeLayerKey,
|
|
62
|
+
): ReadonlySet<WorkflowScopeLayerKey> {
|
|
63
|
+
const next = new Set(current);
|
|
64
|
+
if (next.has(key)) {
|
|
65
|
+
next.delete(key);
|
|
66
|
+
} else {
|
|
67
|
+
next.add(key);
|
|
68
|
+
}
|
|
69
|
+
return next;
|
|
70
|
+
}
|