@beyondwork/docx-react-component 1.0.89 → 1.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -17
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +1 -6
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +7 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +0 -5
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +38 -32
- package/src/ui-tailwind/theme/editor-theme.css +12 -25
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +7 -33
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +15 -27
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +18 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +4 -15
- package/src/ui-tailwind/workflow-scope-layers.ts +0 -70
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.
|
|
4
|
+
"version": "1.0.91",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import * as React from "react";
|
|
16
16
|
import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
|
|
17
|
-
import type {
|
|
17
|
+
import type { ScopeRailSegment } from "../../api/public-types.ts";
|
|
18
18
|
import type {
|
|
19
19
|
EditorRole,
|
|
20
20
|
EditorStoryTarget,
|
|
@@ -49,8 +49,6 @@ export interface TwChromeOverlayProps {
|
|
|
49
49
|
space?: OverlayCoordinateSpace;
|
|
50
50
|
/** Active scope id (for emphasis + rail tab sync). */
|
|
51
51
|
activeScopeId?: string | null;
|
|
52
|
-
/** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
|
|
53
|
-
visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
|
|
54
52
|
/**
|
|
55
53
|
* Click handler fired when the user clicks a scope rail stripe.
|
|
56
54
|
* P0 wires this to open the scope card (P1 ships the card layer).
|
|
@@ -215,7 +213,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
215
213
|
geometryFacet,
|
|
216
214
|
space,
|
|
217
215
|
activeScopeId,
|
|
218
|
-
visibleScopePostures,
|
|
219
216
|
onScopeStripeClick,
|
|
220
217
|
onScopeSegmentClick,
|
|
221
218
|
onScopeCardClose,
|
|
@@ -245,17 +242,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
245
242
|
mediaPreviews,
|
|
246
243
|
activeBandRibbonProps,
|
|
247
244
|
}) => {
|
|
248
|
-
const visibleScopeIds = React.useMemo(() => {
|
|
249
|
-
if (!visibleScopePostures) return undefined;
|
|
250
|
-
const ids = new Set<string>();
|
|
251
|
-
for (const segment of workflowFacet?.getAllRailSegments() ?? []) {
|
|
252
|
-
if (visibleScopePostures.has(segment.posture)) {
|
|
253
|
-
ids.add(segment.scopeId);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return ids;
|
|
257
|
-
}, [visibleScopePostures, workflowFacet]);
|
|
258
|
-
|
|
259
245
|
return (
|
|
260
246
|
<div
|
|
261
247
|
className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
|
|
@@ -282,7 +268,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
282
268
|
workflowFacet={workflowFacet}
|
|
283
269
|
space={space}
|
|
284
270
|
activeScopeId={activeScopeId}
|
|
285
|
-
visibleScopePostures={visibleScopePostures}
|
|
286
271
|
onStripeClick={onScopeStripeClick}
|
|
287
272
|
onSegmentClick={onScopeSegmentClick}
|
|
288
273
|
/>
|
|
@@ -290,7 +275,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
290
275
|
facet={facet}
|
|
291
276
|
workflowFacet={workflowFacet}
|
|
292
277
|
activeScopeId={activeScopeId ?? null}
|
|
293
|
-
visibleScopeIds={visibleScopeIds}
|
|
294
278
|
onClose={onScopeCardClose ?? noop}
|
|
295
279
|
onModeChange={onScopeCardModeChange ?? noopModeChange}
|
|
296
280
|
onIssueAction={onScopeCardIssueAction ?? noopIssueAction}
|
|
@@ -52,8 +52,6 @@ export interface TwScopeCardLayerProps {
|
|
|
52
52
|
*/
|
|
53
53
|
workflowFacet: WorkflowFacet | null;
|
|
54
54
|
activeScopeId: string | null;
|
|
55
|
-
/** Scope ids currently visible under the Workflow rail layer filters. */
|
|
56
|
-
visibleScopeIds?: ReadonlySet<string>;
|
|
57
55
|
onClose: () => void;
|
|
58
56
|
onModeChange: (scopeId: string, mode: WorkflowScopeMode) => void;
|
|
59
57
|
onIssueAction: (
|
|
@@ -93,7 +91,6 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
|
|
|
93
91
|
facet,
|
|
94
92
|
workflowFacet,
|
|
95
93
|
activeScopeId,
|
|
96
|
-
visibleScopeIds,
|
|
97
94
|
onClose,
|
|
98
95
|
onModeChange,
|
|
99
96
|
onIssueAction,
|
|
@@ -122,9 +119,7 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
|
|
|
122
119
|
// The effective scope is the pinned one if it still resolves to a
|
|
123
120
|
// model, else the active one. When a pinned scope disappears
|
|
124
121
|
// (e.g. the host cleared the overlay), drop the pin.
|
|
125
|
-
const models =
|
|
126
|
-
visibleScopeIds ? visibleScopeIds.has(model.scopeId) : true,
|
|
127
|
-
);
|
|
122
|
+
const models = workflowFacet?.getAllScopeCardModels() ?? [];
|
|
128
123
|
|
|
129
124
|
const pinnedModel = pinnedScopeId
|
|
130
125
|
? models.find((m) => m.scopeId === pinnedScopeId) ?? null
|
|
@@ -46,8 +46,6 @@ export interface TwScopeRailLayerProps {
|
|
|
46
46
|
railLaneWidthPx?: number;
|
|
47
47
|
/** Scope id that should render with the `active` emphasis. */
|
|
48
48
|
activeScopeId?: string | null;
|
|
49
|
-
/** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
|
|
50
|
-
visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
|
|
51
49
|
/**
|
|
52
50
|
* Fires when the user clicks the rail stripe — opens the scope card.
|
|
53
51
|
* P0 wires this directly; P1 replaces with card-layer-aware routing.
|
|
@@ -89,8 +87,8 @@ const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
|
|
|
89
87
|
// ---------------------------------------------------------------------------
|
|
90
88
|
|
|
91
89
|
const DEFAULT_RAIL_LANE_PX = 44;
|
|
92
|
-
const STRIPE_WIDTH_PX =
|
|
93
|
-
const LABEL_WIDTH_PX =
|
|
90
|
+
const STRIPE_WIDTH_PX = 4;
|
|
91
|
+
const LABEL_WIDTH_PX = 40;
|
|
94
92
|
const STACK_OFFSET_PX = 6;
|
|
95
93
|
|
|
96
94
|
export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
@@ -99,15 +97,12 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
99
97
|
space,
|
|
100
98
|
railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
|
|
101
99
|
activeScopeId,
|
|
102
|
-
visibleScopePostures,
|
|
103
100
|
onStripeClick,
|
|
104
101
|
onSegmentClick,
|
|
105
102
|
"data-testid": testId,
|
|
106
103
|
}) => {
|
|
107
104
|
const frame = geometryFacet.getRenderFrame() ?? null;
|
|
108
|
-
const segments =
|
|
109
|
-
visibleScopePostures ? visibleScopePostures.has(segment.posture) : true,
|
|
110
|
-
);
|
|
105
|
+
const segments = workflowFacet?.getAllRailSegments() ?? [];
|
|
111
106
|
|
|
112
107
|
if (!frame || segments.length === 0) {
|
|
113
108
|
return null;
|
|
@@ -228,21 +223,16 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
228
223
|
style={projectRectToOverlay(stripeRect, projectorSpace)}
|
|
229
224
|
/>
|
|
230
225
|
{/* Label pill — revealed on stripe hover via CSS. */}
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
tabIndex={-1}
|
|
234
|
-
className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken} ${
|
|
235
|
-
isActive ? "wre-scope-rail-label-active" : ""
|
|
236
|
-
}`}
|
|
226
|
+
<div
|
|
227
|
+
className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken}`}
|
|
237
228
|
data-scope-id={segment.scopeId}
|
|
238
229
|
data-posture={segment.posture}
|
|
239
|
-
aria-
|
|
240
|
-
onClick={handleActivate}
|
|
230
|
+
aria-hidden="true"
|
|
241
231
|
style={projectRectToOverlay(labelRect, projectorSpace)}
|
|
242
232
|
>
|
|
243
233
|
<span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
|
|
244
234
|
<span className="wre-scope-rail-label-text">{style.labelText}</span>
|
|
245
|
-
</
|
|
235
|
+
</div>
|
|
246
236
|
</React.Fragment>
|
|
247
237
|
);
|
|
248
238
|
})}
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
TwReviewRailFooter,
|
|
24
24
|
type TwReviewRailFooterProps,
|
|
25
25
|
} from "./tw-review-rail-footer";
|
|
26
|
-
import type { WorkflowScopeLayerKey } from "../workflow-scope-layers";
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Review rail with up to four tabs (Workflow / Comments / Changes / Health).
|
|
@@ -67,8 +66,6 @@ export interface TwReviewRailProps {
|
|
|
67
66
|
*/
|
|
68
67
|
scopeRailSegments?: readonly ScopeRailSegment[];
|
|
69
68
|
activeScopeId?: string | null;
|
|
70
|
-
workflowLayerFilters?: ReadonlySet<WorkflowScopeLayerKey>;
|
|
71
|
-
onWorkflowLayerFiltersChange?: (filters: ReadonlySet<WorkflowScopeLayerKey>) => void;
|
|
72
69
|
/**
|
|
73
70
|
* Optional host-provided Workflow-tab override. When supplied this
|
|
74
71
|
* ReactNode replaces the default TwWorkflowTab content while still using
|
|
@@ -265,8 +262,6 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
265
262
|
<TwWorkflowTab
|
|
266
263
|
segments={workflowSegments}
|
|
267
264
|
activeScopeId={props.activeScopeId ?? null}
|
|
268
|
-
enabledLayerFilters={props.workflowLayerFilters}
|
|
269
|
-
onEnabledLayerFiltersChange={props.onWorkflowLayerFiltersChange}
|
|
270
265
|
onOpenScope={props.onOpenScope}
|
|
271
266
|
/>
|
|
272
267
|
)}
|
|
@@ -10,13 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import React from "react";
|
|
12
12
|
import type { ScopeRailSegment, ScopeRailPosture } from "../../api/public-types";
|
|
13
|
-
import {
|
|
14
|
-
WORKFLOW_SCOPE_LAYER_FILTERS,
|
|
15
|
-
createDefaultWorkflowScopeLayerKeys,
|
|
16
|
-
isWorkflowScopePostureVisible,
|
|
17
|
-
toggleWorkflowScopeLayerKey,
|
|
18
|
-
type WorkflowScopeLayerKey,
|
|
19
|
-
} from "../workflow-scope-layers";
|
|
20
13
|
|
|
21
14
|
export interface TwWorkflowTabProps {
|
|
22
15
|
segments: readonly ScopeRailSegment[];
|
|
@@ -28,8 +21,6 @@ export interface TwWorkflowTabProps {
|
|
|
28
21
|
* matching overlay card. If omitted, focus sync is not wired.
|
|
29
22
|
*/
|
|
30
23
|
onActiveScopeChange?: (scopeId: string) => void;
|
|
31
|
-
enabledLayerFilters?: ReadonlySet<WorkflowScopeLayerKey>;
|
|
32
|
-
onEnabledLayerFiltersChange?: (filters: ReadonlySet<WorkflowScopeLayerKey>) => void;
|
|
33
24
|
}
|
|
34
25
|
|
|
35
26
|
const POSTURE_META: Record<
|
|
@@ -48,13 +39,26 @@ const POSTURE_META: Record<
|
|
|
48
39
|
const focusRingClass =
|
|
49
40
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
50
41
|
|
|
42
|
+
type ScopeFilterKey = "edit" | "suggest" | "comment" | "view" | "candidate" | "blocked";
|
|
43
|
+
|
|
44
|
+
const SCOPE_FILTERS: ReadonlyArray<{
|
|
45
|
+
key: ScopeFilterKey;
|
|
46
|
+
label: string;
|
|
47
|
+
postures: readonly ScopeRailPosture[];
|
|
48
|
+
}> = [
|
|
49
|
+
{ key: "edit", label: "Edit", postures: ["edit"] },
|
|
50
|
+
{ key: "suggest", label: "Suggest", postures: ["suggest"] },
|
|
51
|
+
{ key: "comment", label: "Comment", postures: ["comment"] },
|
|
52
|
+
{ key: "view", label: "Review", postures: ["view"] },
|
|
53
|
+
{ key: "candidate", label: "Scheduled", postures: ["candidate"] },
|
|
54
|
+
{ key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
|
|
55
|
+
];
|
|
56
|
+
|
|
51
57
|
export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
52
58
|
segments,
|
|
53
59
|
activeScopeId,
|
|
54
60
|
onOpenScope,
|
|
55
61
|
onActiveScopeChange,
|
|
56
|
-
enabledLayerFilters,
|
|
57
|
-
onEnabledLayerFiltersChange,
|
|
58
62
|
}) => {
|
|
59
63
|
// Dedupe by scopeId so a scope spanning multiple pages shows once.
|
|
60
64
|
const uniqueSegments = React.useMemo(() => {
|
|
@@ -67,31 +71,20 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
67
71
|
return Array.from(byScopeId.values()).sort(compareWorkflowSegments(activeScopeId ?? null));
|
|
68
72
|
}, [activeScopeId, segments]);
|
|
69
73
|
const [query, setQuery] = React.useState("");
|
|
70
|
-
const [
|
|
71
|
-
|
|
72
|
-
>(
|
|
73
|
-
createDefaultWorkflowScopeLayerKeys,
|
|
74
|
-
);
|
|
75
|
-
const activeEnabledFilters = enabledLayerFilters ?? uncontrolledEnabledFilters;
|
|
76
|
-
const setEnabledFilters = React.useCallback(
|
|
77
|
-
(next: ReadonlySet<WorkflowScopeLayerKey>) => {
|
|
78
|
-
if (!enabledLayerFilters) {
|
|
79
|
-
setUncontrolledEnabledFilters(next);
|
|
80
|
-
}
|
|
81
|
-
onEnabledLayerFiltersChange?.(next);
|
|
82
|
-
},
|
|
83
|
-
[enabledLayerFilters, onEnabledLayerFiltersChange],
|
|
74
|
+
const [enabledFilters, setEnabledFilters] = React.useState<ReadonlySet<ScopeFilterKey>>(
|
|
75
|
+
() => new Set(SCOPE_FILTERS.map((filter) => filter.key)),
|
|
84
76
|
);
|
|
85
77
|
const availableFilters = React.useMemo(() => {
|
|
86
78
|
const presentPostures = new Set(uniqueSegments.map((segment) => segment.posture));
|
|
87
|
-
return
|
|
79
|
+
return SCOPE_FILTERS.filter((filter) =>
|
|
88
80
|
filter.postures.some((posture) => presentPostures.has(posture)),
|
|
89
81
|
);
|
|
90
82
|
}, [uniqueSegments]);
|
|
91
83
|
const visibleSegments = React.useMemo(() => {
|
|
92
84
|
const normalizedQuery = normalizeScopeQuery(query);
|
|
93
85
|
return uniqueSegments.filter((segment) => {
|
|
94
|
-
|
|
86
|
+
const filterKey = filterKeyForPosture(segment.posture);
|
|
87
|
+
if (!enabledFilters.has(filterKey)) {
|
|
95
88
|
return false;
|
|
96
89
|
}
|
|
97
90
|
if (!normalizedQuery) {
|
|
@@ -99,7 +92,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
99
92
|
}
|
|
100
93
|
return scopeSearchText(segment).includes(normalizedQuery);
|
|
101
94
|
});
|
|
102
|
-
}, [
|
|
95
|
+
}, [enabledFilters, query, uniqueSegments]);
|
|
103
96
|
|
|
104
97
|
if (uniqueSegments.length === 0) {
|
|
105
98
|
return (
|
|
@@ -153,7 +146,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
153
146
|
role="group"
|
|
154
147
|
>
|
|
155
148
|
{availableFilters.map((filter) => {
|
|
156
|
-
const isEnabled =
|
|
149
|
+
const isEnabled = enabledFilters.has(filter.key);
|
|
157
150
|
return (
|
|
158
151
|
<button
|
|
159
152
|
key={filter.key}
|
|
@@ -167,9 +160,15 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
167
160
|
].join(" ")}
|
|
168
161
|
data-testid={`workflow-scope-filter-${filter.key}`}
|
|
169
162
|
onClick={() => {
|
|
170
|
-
setEnabledFilters(
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
setEnabledFilters((current) => {
|
|
164
|
+
const next = new Set(current);
|
|
165
|
+
if (next.has(filter.key)) {
|
|
166
|
+
next.delete(filter.key);
|
|
167
|
+
} else {
|
|
168
|
+
next.add(filter.key);
|
|
169
|
+
}
|
|
170
|
+
return next;
|
|
171
|
+
});
|
|
173
172
|
}}
|
|
174
173
|
>
|
|
175
174
|
{filter.label}
|
|
@@ -246,6 +245,13 @@ function compareWorkflowSegments(activeScopeId: string | null) {
|
|
|
246
245
|
};
|
|
247
246
|
}
|
|
248
247
|
|
|
248
|
+
function filterKeyForPosture(posture: ScopeRailPosture): ScopeFilterKey {
|
|
249
|
+
if (posture === "preserve-only" || posture === "blocked-import") {
|
|
250
|
+
return "blocked";
|
|
251
|
+
}
|
|
252
|
+
return posture;
|
|
253
|
+
}
|
|
254
|
+
|
|
249
255
|
function normalizeScopeQuery(value: string): string {
|
|
250
256
|
return value.trim().toLocaleLowerCase();
|
|
251
257
|
}
|
|
@@ -475,19 +475,19 @@
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
.wre-scope-rail-tint-accent {
|
|
478
|
-
background: color-mix(in srgb, var(--color-accent)
|
|
478
|
+
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
|
479
479
|
}
|
|
480
480
|
.wre-scope-rail-tint-warning {
|
|
481
|
-
background: color-mix(in srgb, var(--color-warning)
|
|
481
|
+
background: color-mix(in srgb, var(--color-warning) 14%, transparent);
|
|
482
482
|
}
|
|
483
483
|
.wre-scope-rail-tint-insert {
|
|
484
|
-
background: color-mix(in srgb, var(--color-insert)
|
|
484
|
+
background: color-mix(in srgb, var(--color-insert) 12%, transparent);
|
|
485
485
|
}
|
|
486
486
|
.wre-scope-rail-tint-secondary {
|
|
487
|
-
background: color-mix(in srgb, var(--color-secondary)
|
|
487
|
+
background: color-mix(in srgb, var(--color-secondary) 9%, transparent);
|
|
488
488
|
}
|
|
489
489
|
.wre-scope-rail-tint-danger {
|
|
490
|
-
background: color-mix(in srgb, var(--color-danger)
|
|
490
|
+
background: color-mix(in srgb, var(--color-danger) 14%, transparent);
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
/* §3.7 canonical scope families */
|
|
@@ -543,22 +543,22 @@
|
|
|
543
543
|
/*
|
|
544
544
|
* ─── Scope rail stripe ───
|
|
545
545
|
*
|
|
546
|
-
* The rail stripe is the rest-state representation of a scope: a
|
|
546
|
+
* The rail stripe is the rest-state representation of a scope: a 4px
|
|
547
547
|
* color stripe in the gutter lane. Posture color comes from the
|
|
548
548
|
* accent/warning/insert/secondary/danger tokens. Hover widens the
|
|
549
549
|
* stripe via transform (zero layout cost) and reveals the label pill.
|
|
550
550
|
*/
|
|
551
551
|
.wre-scope-rail-stripe {
|
|
552
552
|
position: absolute;
|
|
553
|
-
width:
|
|
554
|
-
border-radius:
|
|
553
|
+
width: 4px;
|
|
554
|
+
border-radius: 2px;
|
|
555
555
|
background: currentColor;
|
|
556
556
|
pointer-events: auto;
|
|
557
557
|
cursor: pointer;
|
|
558
558
|
z-index: 1;
|
|
559
559
|
transform-origin: left center;
|
|
560
560
|
transition: transform 120ms ease-out, opacity 120ms ease-out;
|
|
561
|
-
opacity: 0.
|
|
561
|
+
opacity: 0.75;
|
|
562
562
|
/* Reset button defaults. */
|
|
563
563
|
border: none;
|
|
564
564
|
padding: 0;
|
|
@@ -568,22 +568,16 @@
|
|
|
568
568
|
background-clip: padding-box;
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
-
.wre-scope-rail-stripe::before {
|
|
572
|
-
content: "";
|
|
573
|
-
position: absolute;
|
|
574
|
-
inset: -5px -10px;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
571
|
.wre-scope-rail-stripe:hover,
|
|
578
572
|
.wre-scope-rail-stripe:focus-visible {
|
|
579
|
-
transform: scaleX(1.
|
|
573
|
+
transform: scaleX(1.5);
|
|
580
574
|
opacity: 1;
|
|
581
575
|
outline: none;
|
|
582
576
|
}
|
|
583
577
|
|
|
584
578
|
.wre-scope-rail-stripe-active {
|
|
585
579
|
opacity: 1;
|
|
586
|
-
transform: scaleX(1.
|
|
580
|
+
transform: scaleX(1.75);
|
|
587
581
|
}
|
|
588
582
|
|
|
589
583
|
.wre-scope-rail-stripe.wre-scope-rail-label-accent { color: var(--color-accent); }
|
|
@@ -629,8 +623,6 @@
|
|
|
629
623
|
pointer-events: none;
|
|
630
624
|
transition: opacity 140ms ease-out, transform 140ms ease-out;
|
|
631
625
|
transform: translateX(-4px);
|
|
632
|
-
margin: 0;
|
|
633
|
-
font-family: inherit;
|
|
634
626
|
}
|
|
635
627
|
|
|
636
628
|
.wre-scope-rail-stripe:hover + .wre-scope-rail-label,
|
|
@@ -700,12 +692,7 @@
|
|
|
700
692
|
}
|
|
701
693
|
|
|
702
694
|
.wre-scope-rail-label-active {
|
|
703
|
-
|
|
704
|
-
pointer-events: auto;
|
|
705
|
-
transform: translateX(0);
|
|
706
|
-
box-shadow:
|
|
707
|
-
0 0 0 1px color-mix(in srgb, currentColor 42%, transparent),
|
|
708
|
-
0 8px 22px color-mix(in srgb, currentColor 14%, transparent);
|
|
695
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
|
|
709
696
|
}
|
|
710
697
|
|
|
711
698
|
.wre-scope-rail-icon {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Review-role actions here collapse what used to live in
|
|
10
10
|
* `TwReviewQueueBar` as a second strip — the review prev/next, counts,
|
|
11
11
|
* active-item label, accept/reject, markup-mode, and batch operations.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Workflow-role actions surface the scope posture menu plus work-item
|
|
13
|
+
* traversal + claim/skip/complete.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import React, { useState } from "react";
|
|
@@ -18,14 +18,12 @@ import * as Popover from "@radix-ui/react-popover";
|
|
|
18
18
|
import * as Toggle from "@radix-ui/react-toggle";
|
|
19
19
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
20
20
|
import {
|
|
21
|
-
BookmarkCheck,
|
|
22
21
|
Check,
|
|
23
22
|
CheckCheck,
|
|
24
23
|
ChevronLeft,
|
|
25
24
|
ChevronRight,
|
|
26
25
|
CircleOff,
|
|
27
26
|
FileDiff,
|
|
28
|
-
Flag,
|
|
29
27
|
Hand,
|
|
30
28
|
MessageSquare,
|
|
31
29
|
MessageSquareDot,
|
|
@@ -48,6 +46,7 @@ import type { ScopedChromePolicy } from "../../ui/headless/scoped-chrome-policy"
|
|
|
48
46
|
import type { ToolbarChromeItemId } from "../../ui/headless/chrome-registry";
|
|
49
47
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
50
48
|
import { ROLE_ACTION_SETS } from "../../ui/headless/role-action-sets";
|
|
49
|
+
import { TwDisplayModeSelector } from "../chrome/tw-display-mode-selector";
|
|
51
50
|
import { TwScopePostureMenu } from "./tw-scope-posture-menu";
|
|
52
51
|
|
|
53
52
|
/**
|
|
@@ -103,7 +102,7 @@ export interface TwRoleActionRegionProps {
|
|
|
103
102
|
onReviewSidebarTrackedChanges?: () => void;
|
|
104
103
|
onReviewSidebarComments?: () => void;
|
|
105
104
|
|
|
106
|
-
// Workflow
|
|
105
|
+
// Workflow role: assign authorable scope posture.
|
|
107
106
|
onMarkScopePosture?: (posture: ScopeRailPosture) => void;
|
|
108
107
|
|
|
109
108
|
// Review role
|
|
@@ -366,10 +365,11 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
|
|
|
366
365
|
);
|
|
367
366
|
case "review-markup-mode":
|
|
368
367
|
return (
|
|
369
|
-
<
|
|
370
|
-
|
|
368
|
+
<TwDisplayModeSelector
|
|
369
|
+
value={props.markupDisplay ?? "simple"}
|
|
371
370
|
onChange={(mode) => props.onReviewMarkupMode?.(mode)}
|
|
372
371
|
disabled={!props.onReviewMarkupMode}
|
|
372
|
+
data-testid="role-review-markup-mode"
|
|
373
373
|
/>
|
|
374
374
|
);
|
|
375
375
|
case "workflow-prev":
|
|
@@ -693,30 +693,4 @@ function ReviewActiveLabel({
|
|
|
693
693
|
);
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
-
function MarkupModeSelect(arg: {
|
|
697
|
-
mode: MarkupDisplayMode;
|
|
698
|
-
onChange: (mode: MarkupDisplayMode) => void;
|
|
699
|
-
disabled?: boolean;
|
|
700
|
-
}): React.JSX.Element {
|
|
701
|
-
const Icon = arg.mode === "clean" ? BookmarkCheck : arg.mode === "all" ? Flag : Rows3;
|
|
702
|
-
return (
|
|
703
|
-
<button
|
|
704
|
-
type="button"
|
|
705
|
-
aria-label={`Markup display: ${arg.mode}`}
|
|
706
|
-
disabled={arg.disabled}
|
|
707
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
708
|
-
onClick={() => {
|
|
709
|
-
const next: MarkupDisplayMode =
|
|
710
|
-
arg.mode === "all" ? "clean" : arg.mode === "clean" ? "simple" : "all";
|
|
711
|
-
arg.onChange(next);
|
|
712
|
-
}}
|
|
713
|
-
data-testid="role-review-markup-mode"
|
|
714
|
-
className="inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
|
|
715
|
-
>
|
|
716
|
-
<Icon className="h-3.5 w-3.5" />
|
|
717
|
-
<span className="capitalize">{arg.mode}</span>
|
|
718
|
-
</button>
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
696
|
export default TwRoleActionRegion;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scope posture menu — replaces the old "Mark section" button with a
|
|
3
|
-
* topnav dropdown listing the
|
|
4
|
-
*
|
|
5
|
-
* single "marked" flag.
|
|
3
|
+
* topnav dropdown listing the authorable `ScopeRailPosture` values so
|
|
4
|
+
* workflow operators can mark regions with an explicit workflow mode
|
|
5
|
+
* instead of a single "marked" flag.
|
|
6
6
|
*
|
|
7
7
|
* Per runtime-rendering-and-chrome-phase.md §6.4, the menu lives inline
|
|
8
|
-
* in the
|
|
8
|
+
* in the workflow role's primary action region (not in the review queue
|
|
9
9
|
* strip). Postures align 1:1 with the rail vocabulary so the rail
|
|
10
|
-
* updates visually as soon as the user picks one.
|
|
10
|
+
* updates visually as soon as the user picks one. Runtime-only postures
|
|
11
|
+
* like preserve-only and blocked-import still render in overlays, but
|
|
12
|
+
* they are not choices users can assign from this product menu.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import React, { useState } from "react";
|
|
@@ -17,7 +19,6 @@ import {
|
|
|
17
19
|
ChevronDown,
|
|
18
20
|
Eye,
|
|
19
21
|
Flag,
|
|
20
|
-
Lock,
|
|
21
22
|
MessageCircle,
|
|
22
23
|
Pencil,
|
|
23
24
|
Sparkles,
|
|
@@ -35,12 +36,12 @@ export interface TwScopePostureMenuProps {
|
|
|
35
36
|
"data-testid"?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
interface
|
|
39
|
+
export interface ScopePostureMenuEntry {
|
|
39
40
|
posture: ScopeRailPosture;
|
|
40
41
|
label: string;
|
|
41
42
|
hint: string;
|
|
42
43
|
icon: React.ComponentType<{ className?: string }>;
|
|
43
|
-
tone: "accent" | "warning" | "comment" | "secondary"
|
|
44
|
+
tone: "accent" | "warning" | "comment" | "secondary";
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -49,7 +50,7 @@ interface PostureEntry {
|
|
|
49
50
|
* glyphs via the `data-icon` attribute). Extract both into a single
|
|
50
51
|
* source of truth in a follow-up.
|
|
51
52
|
*/
|
|
52
|
-
const
|
|
53
|
+
export const SCOPE_POSTURE_MENU_ENTRIES: readonly ScopePostureMenuEntry[] = [
|
|
53
54
|
{
|
|
54
55
|
posture: "edit",
|
|
55
56
|
label: "Edit scope",
|
|
@@ -85,22 +86,11 @@ const POSTURE_ENTRIES: readonly PostureEntry[] = [
|
|
|
85
86
|
icon: Flag,
|
|
86
87
|
tone: "warning",
|
|
87
88
|
},
|
|
88
|
-
{
|
|
89
|
-
posture: "preserve-only",
|
|
90
|
-
label: "Preserve only",
|
|
91
|
-
hint: "Blocked — export-preserving only",
|
|
92
|
-
icon: Lock,
|
|
93
|
-
tone: "danger",
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
posture: "blocked-import",
|
|
97
|
-
label: "Blocked import",
|
|
98
|
-
hint: "Blocked — imported region is locked",
|
|
99
|
-
icon: Lock,
|
|
100
|
-
tone: "danger",
|
|
101
|
-
},
|
|
102
89
|
];
|
|
103
90
|
|
|
91
|
+
export const SCOPE_POSTURE_MENU_POSTURES: readonly ScopeRailPosture[] =
|
|
92
|
+
SCOPE_POSTURE_MENU_ENTRIES.map((entry) => entry.posture);
|
|
93
|
+
|
|
104
94
|
export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.Element {
|
|
105
95
|
const [open, setOpen] = useState(false);
|
|
106
96
|
|
|
@@ -131,7 +121,7 @@ export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.El
|
|
|
131
121
|
<div className="mb-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
132
122
|
Mark section with posture
|
|
133
123
|
</div>
|
|
134
|
-
{
|
|
124
|
+
{SCOPE_POSTURE_MENU_ENTRIES.map((entry) => (
|
|
135
125
|
<Popover.Close key={entry.posture} asChild>
|
|
136
126
|
<button
|
|
137
127
|
type="button"
|
|
@@ -163,7 +153,7 @@ export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.El
|
|
|
163
153
|
);
|
|
164
154
|
}
|
|
165
155
|
|
|
166
|
-
function toneClass(tone:
|
|
156
|
+
function toneClass(tone: ScopePostureMenuEntry["tone"]): string {
|
|
167
157
|
switch (tone) {
|
|
168
158
|
case "accent":
|
|
169
159
|
return "text-accent";
|
|
@@ -171,8 +161,6 @@ function toneClass(tone: PostureEntry["tone"]): string {
|
|
|
171
161
|
return "text-warning";
|
|
172
162
|
case "comment":
|
|
173
163
|
return "text-comment";
|
|
174
|
-
case "danger":
|
|
175
|
-
return "text-danger";
|
|
176
164
|
case "secondary":
|
|
177
165
|
default:
|
|
178
166
|
return "text-secondary";
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
type ScopedChromePolicy,
|
|
70
70
|
} from "../../ui/headless/scoped-chrome-policy";
|
|
71
71
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
72
|
+
import { TwDisplayModeSelector } from "../chrome/tw-display-mode-selector";
|
|
72
73
|
import { type MarkupDisplayMode } from "./tw-role-action-region";
|
|
73
74
|
import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
|
|
74
75
|
|
|
@@ -123,6 +124,8 @@ export interface TwToolbarProps {
|
|
|
123
124
|
onToggleSidebar?: () => void;
|
|
124
125
|
onZoomChange?: (level: ZoomLevel) => void;
|
|
125
126
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
127
|
+
/** Top-toolbar fallback for changing redline/comment display when review context band is not active. */
|
|
128
|
+
onMarkupDisplayChange?: (mode: MarkupDisplayMode) => void;
|
|
126
129
|
onRestartNumbering?: () => void;
|
|
127
130
|
onContinueNumbering?: () => void;
|
|
128
131
|
onUpdateFields?: () => void;
|
|
@@ -262,6 +265,10 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
262
265
|
const showTrackedChangesToggle =
|
|
263
266
|
isToolbarChromeItemVisible(scopedChromePolicy, "tracked-changes-toggle") &&
|
|
264
267
|
!isChromeItemOwnedByRoleRegion("tracked-changes-toggle", props.role);
|
|
268
|
+
const showMarkupDisplaySelector =
|
|
269
|
+
props.markupDisplay !== undefined &&
|
|
270
|
+
props.onMarkupDisplayChange !== undefined &&
|
|
271
|
+
!isChromeItemOwnedByRoleRegion("review-markup-mode", props.role);
|
|
265
272
|
const showRightClusterComment =
|
|
266
273
|
isToolbarChromeItemVisible(scopedChromePolicy, "comment") &&
|
|
267
274
|
!isChromeItemOwnedByRoleRegion("comment", props.role);
|
|
@@ -695,6 +702,17 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
695
702
|
</>
|
|
696
703
|
) : null}
|
|
697
704
|
|
|
705
|
+
{showMarkupDisplaySelector ? (
|
|
706
|
+
<>
|
|
707
|
+
<TwDisplayModeSelector
|
|
708
|
+
value={props.markupDisplay!}
|
|
709
|
+
onChange={(mode) => props.onMarkupDisplayChange?.(mode)}
|
|
710
|
+
data-testid="toolbar-display-mode-selector"
|
|
711
|
+
/>
|
|
712
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
713
|
+
</>
|
|
714
|
+
) : null}
|
|
715
|
+
|
|
698
716
|
{/* View mode toggle group: Canvas (clean, flowing) / Page (layout-sensitive) */}
|
|
699
717
|
{isToolbarChromeItemVisible(scopedChromePolicy, "workspace-mode") ? (
|
|
700
718
|
<ToggleGroup.Root
|
|
@@ -68,11 +68,6 @@ import { useLayoutFacetRenderSignal } from "./review-workspace/use-layout-facet-
|
|
|
68
68
|
import { useScrollRootCapture } from "./review-workspace/use-scroll-root-capture.ts";
|
|
69
69
|
import { usePmSurfaceCapture } from "./review-workspace/use-pm-surface-capture.ts";
|
|
70
70
|
import { TwReviewWorkspaceNavigator } from "./review-workspace/tw-review-workspace-navigator.tsx";
|
|
71
|
-
import {
|
|
72
|
-
createDefaultWorkflowScopeLayerKeys,
|
|
73
|
-
workflowScopePosturesForLayerKeys,
|
|
74
|
-
type WorkflowScopeLayerKey,
|
|
75
|
-
} from "./workflow-scope-layers";
|
|
76
71
|
|
|
77
72
|
export {
|
|
78
73
|
FRAME_PX_PER_TWIP_AT_96DPI,
|
|
@@ -179,13 +174,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
179
174
|
useRef<TwWorkspaceChromeHostController | null>(null);
|
|
180
175
|
const [shellModeOverride, setShellModeOverride] =
|
|
181
176
|
useState<ShellHeaderMode | null>(null);
|
|
182
|
-
const [workflowLayerFilters, setWorkflowLayerFilters] = useState<
|
|
183
|
-
ReadonlySet<WorkflowScopeLayerKey>
|
|
184
|
-
>(createDefaultWorkflowScopeLayerKeys);
|
|
185
|
-
const visibleScopePostures = useMemo(
|
|
186
|
-
() => workflowScopePosturesForLayerKeys(workflowLayerFilters),
|
|
187
|
-
[workflowLayerFilters],
|
|
188
|
-
);
|
|
189
177
|
// P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
|
|
190
178
|
// root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
|
|
191
179
|
// measure per-page rects and to reparent PM's DOM node across band
|
|
@@ -878,6 +866,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
878
866
|
dismissSelectionToolbar();
|
|
879
867
|
props.onShowTrackedChangesChange(show);
|
|
880
868
|
}}
|
|
869
|
+
onMarkupDisplayChange={(mode) => {
|
|
870
|
+
dismissSelectionToolbar();
|
|
871
|
+
props.onReviewMarkupModeChange?.(mode);
|
|
872
|
+
}}
|
|
881
873
|
role={viewState.editorRole}
|
|
882
874
|
reviewQueue={props.reviewQueue}
|
|
883
875
|
markupDisplay={markupDisplay}
|
|
@@ -1174,7 +1166,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1174
1166
|
onSetColumnWidth={props.onSetColumnWidth}
|
|
1175
1167
|
onSetRowHeight={props.onSetRowHeight}
|
|
1176
1168
|
activeScopeId={activeScopeId}
|
|
1177
|
-
visibleScopePostures={visibleScopePostures}
|
|
1178
1169
|
editorRole={viewState.editorRole}
|
|
1179
1170
|
scopeCardScopeTagEditor={props.scopeCardScopeTagEditor}
|
|
1180
1171
|
onScopeStripeClick={handleScopeStripeClick}
|
|
@@ -1281,8 +1272,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1281
1272
|
// `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
|
|
1282
1273
|
scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
|
|
1283
1274
|
activeScopeId,
|
|
1284
|
-
workflowLayerFilters,
|
|
1285
|
-
onWorkflowLayerFiltersChange: setWorkflowLayerFilters,
|
|
1286
1275
|
onOpenScope: (segment) => {
|
|
1287
1276
|
handleScopeStripeClick({ scopeId: segment.scopeId });
|
|
1288
1277
|
},
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { ScopeRailPosture } from "../api/public-types";
|
|
2
|
-
|
|
3
|
-
export type WorkflowScopeLayerKey =
|
|
4
|
-
| "edit"
|
|
5
|
-
| "suggest"
|
|
6
|
-
| "comment"
|
|
7
|
-
| "view"
|
|
8
|
-
| "candidate"
|
|
9
|
-
| "blocked";
|
|
10
|
-
|
|
11
|
-
export interface WorkflowScopeLayerFilter {
|
|
12
|
-
key: WorkflowScopeLayerKey;
|
|
13
|
-
label: string;
|
|
14
|
-
postures: readonly ScopeRailPosture[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const WORKFLOW_SCOPE_LAYER_FILTERS: readonly WorkflowScopeLayerFilter[] = [
|
|
18
|
-
{ key: "edit", label: "Edit", postures: ["edit"] },
|
|
19
|
-
{ key: "suggest", label: "Suggest", postures: ["suggest"] },
|
|
20
|
-
{ key: "comment", label: "Comment", postures: ["comment"] },
|
|
21
|
-
{ key: "view", label: "Review", postures: ["view"] },
|
|
22
|
-
{ key: "candidate", label: "Scheduled", postures: ["candidate"] },
|
|
23
|
-
{ key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
export function createDefaultWorkflowScopeLayerKeys(): ReadonlySet<WorkflowScopeLayerKey> {
|
|
27
|
-
return new Set(WORKFLOW_SCOPE_LAYER_FILTERS.map((filter) => filter.key));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function workflowScopeLayerKeyForPosture(
|
|
31
|
-
posture: ScopeRailPosture,
|
|
32
|
-
): WorkflowScopeLayerKey {
|
|
33
|
-
if (posture === "preserve-only" || posture === "blocked-import") {
|
|
34
|
-
return "blocked";
|
|
35
|
-
}
|
|
36
|
-
return posture;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function isWorkflowScopePostureVisible(
|
|
40
|
-
posture: ScopeRailPosture,
|
|
41
|
-
enabledLayers: ReadonlySet<WorkflowScopeLayerKey>,
|
|
42
|
-
): boolean {
|
|
43
|
-
return enabledLayers.has(workflowScopeLayerKeyForPosture(posture));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function workflowScopePosturesForLayerKeys(
|
|
47
|
-
enabledLayers: ReadonlySet<WorkflowScopeLayerKey>,
|
|
48
|
-
): ReadonlySet<ScopeRailPosture> {
|
|
49
|
-
const postures = new Set<ScopeRailPosture>();
|
|
50
|
-
for (const filter of WORKFLOW_SCOPE_LAYER_FILTERS) {
|
|
51
|
-
if (!enabledLayers.has(filter.key)) continue;
|
|
52
|
-
for (const posture of filter.postures) {
|
|
53
|
-
postures.add(posture);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return postures;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function toggleWorkflowScopeLayerKey(
|
|
60
|
-
current: ReadonlySet<WorkflowScopeLayerKey>,
|
|
61
|
-
key: WorkflowScopeLayerKey,
|
|
62
|
-
): ReadonlySet<WorkflowScopeLayerKey> {
|
|
63
|
-
const next = new Set(current);
|
|
64
|
-
if (next.has(key)) {
|
|
65
|
-
next.delete(key);
|
|
66
|
-
} else {
|
|
67
|
-
next.add(key);
|
|
68
|
-
}
|
|
69
|
-
return next;
|
|
70
|
-
}
|