@beyondwork/docx-react-component 1.0.37 → 1.0.38
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/public-types.ts +319 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +1 -2
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/model/canonical-document.ts +34 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +114 -0
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +45 -0
- package/src/runtime/layout/inert-layout-facet.ts +14 -0
- package/src/runtime/layout/layout-engine-instance.ts +33 -23
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +142 -9
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +709 -16
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +84 -15
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/headless/chrome-registry.ts +280 -14
- package/src/ui/headless/scoped-chrome-policy.ts +20 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-scoped inline action region rendered inside `TwToolbar`.
|
|
3
|
+
*
|
|
4
|
+
* Per runtime-rendering-and-chrome-phase.md §6.4 the top chrome carries
|
|
5
|
+
* a role-scoped action set between the left formatting cluster and the
|
|
6
|
+
* right view cluster. This component renders the primary actions for
|
|
7
|
+
* the active role, driving them from host-supplied callbacks.
|
|
8
|
+
*
|
|
9
|
+
* Review-role actions here collapse what used to live in
|
|
10
|
+
* `TwReviewQueueBar` as a second strip — the review prev/next, counts,
|
|
11
|
+
* active-item label, accept/reject, markup-mode, and batch operations.
|
|
12
|
+
* Editor-role actions surface the scope posture menu. Workflow-role
|
|
13
|
+
* actions surface work-item traversal + claim/skip/complete.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useState } from "react";
|
|
17
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
18
|
+
import {
|
|
19
|
+
BookmarkCheck,
|
|
20
|
+
Check,
|
|
21
|
+
CheckCheck,
|
|
22
|
+
ChevronDown,
|
|
23
|
+
ChevronLeft,
|
|
24
|
+
ChevronRight,
|
|
25
|
+
CircleOff,
|
|
26
|
+
Flag,
|
|
27
|
+
Hand,
|
|
28
|
+
MessageSquareText,
|
|
29
|
+
Rows3,
|
|
30
|
+
SkipForward,
|
|
31
|
+
Target,
|
|
32
|
+
X,
|
|
33
|
+
XCircle,
|
|
34
|
+
} from "lucide-react";
|
|
35
|
+
|
|
36
|
+
import type {
|
|
37
|
+
EditorRole,
|
|
38
|
+
ReviewQueueSnapshot,
|
|
39
|
+
ScopeRailPosture,
|
|
40
|
+
} from "../../api/public-types";
|
|
41
|
+
import type { ScopedChromePolicy } from "../../ui/headless/scoped-chrome-policy";
|
|
42
|
+
import type { ToolbarChromeItemId } from "../../ui/headless/chrome-registry";
|
|
43
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
44
|
+
import { ROLE_ACTION_SETS } from "../chrome/role-action-sets";
|
|
45
|
+
import { TwScopePostureMenu } from "./tw-scope-posture-menu";
|
|
46
|
+
|
|
47
|
+
export type MarkupDisplayMode = "clean" | "simple" | "all";
|
|
48
|
+
|
|
49
|
+
export interface WorkflowWorkItemSnapshot {
|
|
50
|
+
workItemId: string;
|
|
51
|
+
title?: string;
|
|
52
|
+
status?: string;
|
|
53
|
+
hasPrevious: boolean;
|
|
54
|
+
hasNext: boolean;
|
|
55
|
+
canClaim: boolean;
|
|
56
|
+
canComplete: boolean;
|
|
57
|
+
canSkip: boolean;
|
|
58
|
+
canMarkBlocked: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TwRoleActionRegionProps {
|
|
62
|
+
role: EditorRole;
|
|
63
|
+
policy: ScopedChromePolicy;
|
|
64
|
+
compactMode: boolean;
|
|
65
|
+
/** Review-queue snapshot for the review role. */
|
|
66
|
+
reviewQueue?: ReviewQueueSnapshot;
|
|
67
|
+
/** Active workflow work item for the workflow role. */
|
|
68
|
+
workflowItem?: WorkflowWorkItemSnapshot | null;
|
|
69
|
+
markupDisplay?: MarkupDisplayMode;
|
|
70
|
+
|
|
71
|
+
// Editor role
|
|
72
|
+
onMarkScopePosture?: (posture: ScopeRailPosture) => void;
|
|
73
|
+
|
|
74
|
+
// Review role
|
|
75
|
+
onReviewPrev?: () => void;
|
|
76
|
+
onReviewNext?: () => void;
|
|
77
|
+
onReviewAccept?: () => void;
|
|
78
|
+
onReviewReject?: () => void;
|
|
79
|
+
onReviewAcceptAll?: () => void;
|
|
80
|
+
onReviewRejectAll?: () => void;
|
|
81
|
+
onReviewMarkupMode?: (mode: MarkupDisplayMode) => void;
|
|
82
|
+
|
|
83
|
+
// Workflow role
|
|
84
|
+
onWorkflowPrev?: () => void;
|
|
85
|
+
onWorkflowNext?: () => void;
|
|
86
|
+
onWorkflowMarkComplete?: () => void;
|
|
87
|
+
onWorkflowClaim?: () => void;
|
|
88
|
+
onWorkflowSkip?: () => void;
|
|
89
|
+
onWorkflowMarkBlocked?: () => void;
|
|
90
|
+
onWorkflowJumpToScope?: () => void;
|
|
91
|
+
|
|
92
|
+
"data-testid"?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function TwRoleActionRegion(
|
|
96
|
+
props: TwRoleActionRegionProps,
|
|
97
|
+
): React.JSX.Element | null {
|
|
98
|
+
const order = ROLE_ACTION_SETS[props.role];
|
|
99
|
+
const inlineIds: ToolbarChromeItemId[] = [];
|
|
100
|
+
const overflowIds: ToolbarChromeItemId[] = [];
|
|
101
|
+
|
|
102
|
+
for (const id of order) {
|
|
103
|
+
const entry = props.policy.toolbar[id];
|
|
104
|
+
if (!entry?.visible) continue;
|
|
105
|
+
if (entry.placement === "overflow") {
|
|
106
|
+
overflowIds.push(id);
|
|
107
|
+
} else if (entry.placement === "inline") {
|
|
108
|
+
inlineIds.push(id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (inlineIds.length === 0 && overflowIds.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
className="flex items-center gap-1 border-l border-r border-border/40 px-1.5"
|
|
119
|
+
data-testid={props["data-testid"] ?? `role-action-region-${props.role}`}
|
|
120
|
+
data-role={props.role}
|
|
121
|
+
>
|
|
122
|
+
{inlineIds.map((id) => (
|
|
123
|
+
<RoleActionButton
|
|
124
|
+
key={id}
|
|
125
|
+
id={id}
|
|
126
|
+
compact={props.compactMode}
|
|
127
|
+
props={props}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
{overflowIds.length > 0 ? (
|
|
131
|
+
<RoleActionOverflow ids={overflowIds} props={props} />
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface RoleActionButtonProps {
|
|
138
|
+
id: ToolbarChromeItemId;
|
|
139
|
+
compact: boolean;
|
|
140
|
+
props: TwRoleActionRegionProps;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null {
|
|
144
|
+
const { id, props } = arg;
|
|
145
|
+
switch (id) {
|
|
146
|
+
case "editor-scope-posture-menu":
|
|
147
|
+
return (
|
|
148
|
+
<TwScopePostureMenu
|
|
149
|
+
onSelect={(posture) => props.onMarkScopePosture?.(posture)}
|
|
150
|
+
disabled={!props.onMarkScopePosture}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
case "review-queue-prev":
|
|
154
|
+
return (
|
|
155
|
+
<IconTextButton
|
|
156
|
+
icon={ChevronLeft}
|
|
157
|
+
text="Prev"
|
|
158
|
+
ariaLabel="Previous review item"
|
|
159
|
+
disabled={(props.reviewQueue?.totalCount ?? 0) === 0 || !props.onReviewPrev}
|
|
160
|
+
onClick={props.onReviewPrev}
|
|
161
|
+
data-testid="role-review-prev"
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
case "review-queue-next":
|
|
165
|
+
return (
|
|
166
|
+
<IconTextButton
|
|
167
|
+
icon={ChevronRight}
|
|
168
|
+
iconAfter
|
|
169
|
+
text="Next"
|
|
170
|
+
ariaLabel="Next review item"
|
|
171
|
+
disabled={(props.reviewQueue?.totalCount ?? 0) === 0 || !props.onReviewNext}
|
|
172
|
+
onClick={props.onReviewNext}
|
|
173
|
+
data-testid="role-review-next"
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
case "review-queue-counts":
|
|
177
|
+
return props.reviewQueue ? <ReviewQueueCounts queue={props.reviewQueue} /> : null;
|
|
178
|
+
case "review-queue-active-label":
|
|
179
|
+
return props.reviewQueue ? <ReviewActiveLabel queue={props.reviewQueue} /> : null;
|
|
180
|
+
case "review-accept":
|
|
181
|
+
return (
|
|
182
|
+
<IconTextButton
|
|
183
|
+
icon={Check}
|
|
184
|
+
text="Accept"
|
|
185
|
+
tone="accent"
|
|
186
|
+
ariaLabel="Accept current change"
|
|
187
|
+
disabled={!props.onReviewAccept}
|
|
188
|
+
onClick={props.onReviewAccept}
|
|
189
|
+
data-testid="role-review-accept"
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
case "review-reject":
|
|
193
|
+
return (
|
|
194
|
+
<IconTextButton
|
|
195
|
+
icon={X}
|
|
196
|
+
text="Reject"
|
|
197
|
+
tone="danger"
|
|
198
|
+
ariaLabel="Reject current change"
|
|
199
|
+
disabled={!props.onReviewReject}
|
|
200
|
+
onClick={props.onReviewReject}
|
|
201
|
+
data-testid="role-review-reject"
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
case "review-markup-mode":
|
|
205
|
+
return (
|
|
206
|
+
<MarkupModeSelect
|
|
207
|
+
mode={props.markupDisplay ?? "simple"}
|
|
208
|
+
onChange={(mode) => props.onReviewMarkupMode?.(mode)}
|
|
209
|
+
disabled={!props.onReviewMarkupMode}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
case "workflow-prev":
|
|
213
|
+
return (
|
|
214
|
+
<IconTextButton
|
|
215
|
+
icon={ChevronLeft}
|
|
216
|
+
text="Prev"
|
|
217
|
+
ariaLabel="Previous work item"
|
|
218
|
+
disabled={!(props.workflowItem?.hasPrevious && props.onWorkflowPrev)}
|
|
219
|
+
onClick={props.onWorkflowPrev}
|
|
220
|
+
data-testid="role-workflow-prev"
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
case "workflow-next":
|
|
224
|
+
return (
|
|
225
|
+
<IconTextButton
|
|
226
|
+
icon={ChevronRight}
|
|
227
|
+
iconAfter
|
|
228
|
+
text="Next"
|
|
229
|
+
ariaLabel="Next work item"
|
|
230
|
+
disabled={!(props.workflowItem?.hasNext && props.onWorkflowNext)}
|
|
231
|
+
onClick={props.onWorkflowNext}
|
|
232
|
+
data-testid="role-workflow-next"
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
case "workflow-mark-complete":
|
|
236
|
+
return (
|
|
237
|
+
<IconTextButton
|
|
238
|
+
icon={CheckCheck}
|
|
239
|
+
text="Complete"
|
|
240
|
+
tone="accent"
|
|
241
|
+
ariaLabel="Mark work item complete"
|
|
242
|
+
disabled={!(props.workflowItem?.canComplete && props.onWorkflowMarkComplete)}
|
|
243
|
+
onClick={props.onWorkflowMarkComplete}
|
|
244
|
+
data-testid="role-workflow-complete"
|
|
245
|
+
/>
|
|
246
|
+
);
|
|
247
|
+
case "workflow-claim":
|
|
248
|
+
return (
|
|
249
|
+
<IconTextButton
|
|
250
|
+
icon={Hand}
|
|
251
|
+
text="Claim"
|
|
252
|
+
ariaLabel="Claim work item"
|
|
253
|
+
disabled={!(props.workflowItem?.canClaim && props.onWorkflowClaim)}
|
|
254
|
+
onClick={props.onWorkflowClaim}
|
|
255
|
+
data-testid="role-workflow-claim"
|
|
256
|
+
/>
|
|
257
|
+
);
|
|
258
|
+
default:
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface RoleActionOverflowProps {
|
|
264
|
+
ids: ToolbarChromeItemId[];
|
|
265
|
+
props: TwRoleActionRegionProps;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function RoleActionOverflow({
|
|
269
|
+
ids,
|
|
270
|
+
props,
|
|
271
|
+
}: RoleActionOverflowProps): React.JSX.Element {
|
|
272
|
+
const [open, setOpen] = useState(false);
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
276
|
+
<Popover.Trigger asChild>
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
aria-label="More role actions"
|
|
280
|
+
aria-expanded={open}
|
|
281
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
282
|
+
className="inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
|
|
283
|
+
data-testid="role-action-overflow-trigger"
|
|
284
|
+
>
|
|
285
|
+
More
|
|
286
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
287
|
+
</button>
|
|
288
|
+
</Popover.Trigger>
|
|
289
|
+
<Popover.Portal>
|
|
290
|
+
<Popover.Content
|
|
291
|
+
className="z-50 w-[220px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
|
|
292
|
+
sideOffset={8}
|
|
293
|
+
align="start"
|
|
294
|
+
data-testid="role-action-overflow-content"
|
|
295
|
+
>
|
|
296
|
+
{ids.map((id) => (
|
|
297
|
+
<OverflowAction key={id} id={id} props={props} onClose={() => setOpen(false)} />
|
|
298
|
+
))}
|
|
299
|
+
</Popover.Content>
|
|
300
|
+
</Popover.Portal>
|
|
301
|
+
</Popover.Root>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function OverflowAction(arg: {
|
|
306
|
+
id: ToolbarChromeItemId;
|
|
307
|
+
props: TwRoleActionRegionProps;
|
|
308
|
+
onClose: () => void;
|
|
309
|
+
}): React.JSX.Element | null {
|
|
310
|
+
const { id, props, onClose } = arg;
|
|
311
|
+
switch (id) {
|
|
312
|
+
case "review-accept-all":
|
|
313
|
+
return (
|
|
314
|
+
<MenuRow
|
|
315
|
+
icon={CheckCheck}
|
|
316
|
+
label="Accept all in scope"
|
|
317
|
+
ariaLabel="Accept all reviewable changes in scope"
|
|
318
|
+
disabled={!props.onReviewAcceptAll}
|
|
319
|
+
onClick={() => {
|
|
320
|
+
props.onReviewAcceptAll?.();
|
|
321
|
+
onClose();
|
|
322
|
+
}}
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
case "review-reject-all":
|
|
326
|
+
return (
|
|
327
|
+
<MenuRow
|
|
328
|
+
icon={XCircle}
|
|
329
|
+
label="Reject all in scope"
|
|
330
|
+
ariaLabel="Reject all reviewable changes in scope"
|
|
331
|
+
disabled={!props.onReviewRejectAll}
|
|
332
|
+
onClick={() => {
|
|
333
|
+
props.onReviewRejectAll?.();
|
|
334
|
+
onClose();
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
case "review-markup-mode":
|
|
339
|
+
return (
|
|
340
|
+
<MenuRow
|
|
341
|
+
icon={Rows3}
|
|
342
|
+
label={`Markup: ${props.markupDisplay ?? "simple"}`}
|
|
343
|
+
ariaLabel="Change markup display mode"
|
|
344
|
+
disabled={!props.onReviewMarkupMode}
|
|
345
|
+
onClick={() => {
|
|
346
|
+
const next: MarkupDisplayMode =
|
|
347
|
+
props.markupDisplay === "all"
|
|
348
|
+
? "clean"
|
|
349
|
+
: props.markupDisplay === "simple"
|
|
350
|
+
? "all"
|
|
351
|
+
: "simple";
|
|
352
|
+
props.onReviewMarkupMode?.(next);
|
|
353
|
+
onClose();
|
|
354
|
+
}}
|
|
355
|
+
/>
|
|
356
|
+
);
|
|
357
|
+
case "workflow-skip":
|
|
358
|
+
return (
|
|
359
|
+
<MenuRow
|
|
360
|
+
icon={SkipForward}
|
|
361
|
+
label="Skip work item"
|
|
362
|
+
ariaLabel="Skip work item"
|
|
363
|
+
disabled={!(props.workflowItem?.canSkip && props.onWorkflowSkip)}
|
|
364
|
+
onClick={() => {
|
|
365
|
+
props.onWorkflowSkip?.();
|
|
366
|
+
onClose();
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
);
|
|
370
|
+
case "workflow-mark-blocked":
|
|
371
|
+
return (
|
|
372
|
+
<MenuRow
|
|
373
|
+
icon={CircleOff}
|
|
374
|
+
label="Mark blocked"
|
|
375
|
+
ariaLabel="Mark work item blocked"
|
|
376
|
+
disabled={!(props.workflowItem?.canMarkBlocked && props.onWorkflowMarkBlocked)}
|
|
377
|
+
onClick={() => {
|
|
378
|
+
props.onWorkflowMarkBlocked?.();
|
|
379
|
+
onClose();
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
case "workflow-jump-to-scope":
|
|
384
|
+
return (
|
|
385
|
+
<MenuRow
|
|
386
|
+
icon={Target}
|
|
387
|
+
label="Jump to scope"
|
|
388
|
+
ariaLabel="Jump to workflow scope"
|
|
389
|
+
disabled={!props.onWorkflowJumpToScope}
|
|
390
|
+
onClick={() => {
|
|
391
|
+
props.onWorkflowJumpToScope?.();
|
|
392
|
+
onClose();
|
|
393
|
+
}}
|
|
394
|
+
/>
|
|
395
|
+
);
|
|
396
|
+
case "review-queue-counts":
|
|
397
|
+
return props.reviewQueue ? (
|
|
398
|
+
<div className="px-2 py-1.5">
|
|
399
|
+
<ReviewQueueCounts queue={props.reviewQueue} compact />
|
|
400
|
+
</div>
|
|
401
|
+
) : null;
|
|
402
|
+
case "review-queue-active-label":
|
|
403
|
+
return props.reviewQueue ? (
|
|
404
|
+
<div className="px-2 py-1.5">
|
|
405
|
+
<ReviewActiveLabel queue={props.reviewQueue} />
|
|
406
|
+
</div>
|
|
407
|
+
) : null;
|
|
408
|
+
default:
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function IconTextButton(arg: {
|
|
414
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
415
|
+
iconAfter?: boolean;
|
|
416
|
+
text: string;
|
|
417
|
+
ariaLabel: string;
|
|
418
|
+
tone?: "default" | "accent" | "danger";
|
|
419
|
+
disabled?: boolean;
|
|
420
|
+
onClick?: () => void;
|
|
421
|
+
"data-testid"?: string;
|
|
422
|
+
}): React.JSX.Element {
|
|
423
|
+
const Icon = arg.icon;
|
|
424
|
+
const tone = arg.tone ?? "default";
|
|
425
|
+
const toneClasses =
|
|
426
|
+
tone === "accent"
|
|
427
|
+
? "text-accent hover:bg-accent/10"
|
|
428
|
+
: tone === "danger"
|
|
429
|
+
? "text-danger hover:bg-danger/10"
|
|
430
|
+
: "text-secondary hover:bg-surface";
|
|
431
|
+
return (
|
|
432
|
+
<button
|
|
433
|
+
type="button"
|
|
434
|
+
aria-label={arg.ariaLabel}
|
|
435
|
+
disabled={arg.disabled}
|
|
436
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
437
|
+
onClick={arg.onClick}
|
|
438
|
+
data-testid={arg["data-testid"]}
|
|
439
|
+
className={[
|
|
440
|
+
"inline-flex h-6 items-center gap-1 rounded-md border border-transparent px-1.5 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas",
|
|
441
|
+
toneClasses,
|
|
442
|
+
].join(" ")}
|
|
443
|
+
>
|
|
444
|
+
{!arg.iconAfter ? <Icon className="h-3.5 w-3.5" /> : null}
|
|
445
|
+
<span>{arg.text}</span>
|
|
446
|
+
{arg.iconAfter ? <Icon className="h-3.5 w-3.5" /> : null}
|
|
447
|
+
</button>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function MenuRow(arg: {
|
|
452
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
453
|
+
label: string;
|
|
454
|
+
ariaLabel: string;
|
|
455
|
+
disabled?: boolean;
|
|
456
|
+
onClick: () => void;
|
|
457
|
+
}): React.JSX.Element {
|
|
458
|
+
const Icon = arg.icon;
|
|
459
|
+
return (
|
|
460
|
+
<button
|
|
461
|
+
type="button"
|
|
462
|
+
aria-label={arg.ariaLabel}
|
|
463
|
+
disabled={arg.disabled}
|
|
464
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
465
|
+
onClick={arg.onClick}
|
|
466
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[11px] font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-none focus-visible:bg-surface"
|
|
467
|
+
>
|
|
468
|
+
<Icon className="h-3.5 w-3.5 text-secondary" />
|
|
469
|
+
<span>{arg.label}</span>
|
|
470
|
+
</button>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function ReviewQueueCounts({
|
|
475
|
+
queue,
|
|
476
|
+
compact,
|
|
477
|
+
}: {
|
|
478
|
+
queue: ReviewQueueSnapshot;
|
|
479
|
+
compact?: boolean;
|
|
480
|
+
}): React.JSX.Element {
|
|
481
|
+
return (
|
|
482
|
+
<span
|
|
483
|
+
className={[
|
|
484
|
+
"inline-flex items-center gap-1.5 text-[10px] text-secondary",
|
|
485
|
+
compact ? "px-0" : "px-1",
|
|
486
|
+
].join(" ")}
|
|
487
|
+
data-testid="role-review-counts"
|
|
488
|
+
>
|
|
489
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-surface px-1.5 py-0.5 font-medium">
|
|
490
|
+
<MessageSquareText className="h-3 w-3 text-comment" />
|
|
491
|
+
{queue.openCount} open
|
|
492
|
+
</span>
|
|
493
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-surface px-1.5 py-0.5 font-medium">
|
|
494
|
+
<Rows3 className="h-3 w-3 text-accent" />
|
|
495
|
+
{queue.totalCount} total
|
|
496
|
+
</span>
|
|
497
|
+
</span>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function ReviewActiveLabel({
|
|
502
|
+
queue,
|
|
503
|
+
}: {
|
|
504
|
+
queue: ReviewQueueSnapshot;
|
|
505
|
+
}): React.JSX.Element {
|
|
506
|
+
const active = queue.items[queue.activeIndex] ?? null;
|
|
507
|
+
const label =
|
|
508
|
+
active?.label ??
|
|
509
|
+
(queue.totalCount > 0 ? "Review queue" : "No review items");
|
|
510
|
+
const kind =
|
|
511
|
+
active?.kind === "comment"
|
|
512
|
+
? "Comment"
|
|
513
|
+
: active?.kind === "change"
|
|
514
|
+
? "Redline"
|
|
515
|
+
: active?.kind === "section_mark"
|
|
516
|
+
? "Tag"
|
|
517
|
+
: null;
|
|
518
|
+
return (
|
|
519
|
+
<span
|
|
520
|
+
className="inline-flex min-w-0 items-center gap-1.5 truncate text-[11px] text-primary"
|
|
521
|
+
data-testid="role-review-active-label"
|
|
522
|
+
>
|
|
523
|
+
{kind ? (
|
|
524
|
+
<span className="shrink-0 rounded-full bg-canvas px-1.5 py-0.5 text-[9px] font-medium text-secondary ring-1 ring-border/50">
|
|
525
|
+
{kind}
|
|
526
|
+
</span>
|
|
527
|
+
) : null}
|
|
528
|
+
<span className="truncate">{label}</span>
|
|
529
|
+
</span>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function MarkupModeSelect(arg: {
|
|
534
|
+
mode: MarkupDisplayMode;
|
|
535
|
+
onChange: (mode: MarkupDisplayMode) => void;
|
|
536
|
+
disabled?: boolean;
|
|
537
|
+
}): React.JSX.Element {
|
|
538
|
+
const Icon = arg.mode === "clean" ? BookmarkCheck : arg.mode === "all" ? Flag : Rows3;
|
|
539
|
+
return (
|
|
540
|
+
<button
|
|
541
|
+
type="button"
|
|
542
|
+
aria-label={`Markup display: ${arg.mode}`}
|
|
543
|
+
disabled={arg.disabled}
|
|
544
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
545
|
+
onClick={() => {
|
|
546
|
+
const next: MarkupDisplayMode =
|
|
547
|
+
arg.mode === "all" ? "clean" : arg.mode === "clean" ? "simple" : "all";
|
|
548
|
+
arg.onChange(next);
|
|
549
|
+
}}
|
|
550
|
+
data-testid="role-review-markup-mode"
|
|
551
|
+
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"
|
|
552
|
+
>
|
|
553
|
+
<Icon className="h-3.5 w-3.5" />
|
|
554
|
+
<span className="capitalize">{arg.mode}</span>
|
|
555
|
+
</button>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export default TwRoleActionRegion;
|