@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +41 -31
- package/src/api/public-types.ts +496 -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 +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- 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-fields.ts +10 -3
- 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-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- 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 +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -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 +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -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 +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -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/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -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/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -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 +680 -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 +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -0,0 +1,680 @@
|
|
|
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 * as Toggle from "@radix-ui/react-toggle";
|
|
19
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
20
|
+
import {
|
|
21
|
+
BookmarkCheck,
|
|
22
|
+
Check,
|
|
23
|
+
CheckCheck,
|
|
24
|
+
ChevronDown,
|
|
25
|
+
ChevronLeft,
|
|
26
|
+
ChevronRight,
|
|
27
|
+
CircleOff,
|
|
28
|
+
Eye,
|
|
29
|
+
EyeOff,
|
|
30
|
+
FileDiff,
|
|
31
|
+
Flag,
|
|
32
|
+
Hand,
|
|
33
|
+
MessageSquare,
|
|
34
|
+
MessageSquareDot,
|
|
35
|
+
MessageSquareText,
|
|
36
|
+
Rows3,
|
|
37
|
+
SkipForward,
|
|
38
|
+
Target,
|
|
39
|
+
X,
|
|
40
|
+
XCircle,
|
|
41
|
+
} from "lucide-react";
|
|
42
|
+
|
|
43
|
+
import type {
|
|
44
|
+
EditorRole,
|
|
45
|
+
ReviewQueueSnapshot,
|
|
46
|
+
ScopeRailPosture,
|
|
47
|
+
} from "../../api/public-types";
|
|
48
|
+
import type { SessionCapabilities } from "../../runtime/session-capabilities";
|
|
49
|
+
import type { ScopedChromePolicy } from "../../ui/headless/scoped-chrome-policy";
|
|
50
|
+
import type { ToolbarChromeItemId } from "../../ui/headless/chrome-registry";
|
|
51
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
52
|
+
import { ROLE_ACTION_SETS } from "../chrome/role-action-sets";
|
|
53
|
+
import { TwScopePostureMenu } from "./tw-scope-posture-menu";
|
|
54
|
+
|
|
55
|
+
export type MarkupDisplayMode = "clean" | "simple" | "all";
|
|
56
|
+
|
|
57
|
+
export interface WorkflowWorkItemSnapshot {
|
|
58
|
+
workItemId: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
status?: string;
|
|
61
|
+
hasPrevious: boolean;
|
|
62
|
+
hasNext: boolean;
|
|
63
|
+
canClaim: boolean;
|
|
64
|
+
canComplete: boolean;
|
|
65
|
+
canSkip: boolean;
|
|
66
|
+
canMarkBlocked: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TwRoleActionRegionProps {
|
|
70
|
+
role: EditorRole;
|
|
71
|
+
policy: ScopedChromePolicy;
|
|
72
|
+
compactMode: boolean;
|
|
73
|
+
/** Review-queue snapshot for the review role. */
|
|
74
|
+
reviewQueue?: ReviewQueueSnapshot;
|
|
75
|
+
/** Active workflow work item for the workflow role. */
|
|
76
|
+
workflowItem?: WorkflowWorkItemSnapshot | null;
|
|
77
|
+
markupDisplay?: MarkupDisplayMode;
|
|
78
|
+
|
|
79
|
+
// Shared: editor + review role
|
|
80
|
+
canAddComment?: boolean;
|
|
81
|
+
showTrackedChanges?: boolean;
|
|
82
|
+
onAddComment?: () => void;
|
|
83
|
+
onShowTrackedChangesChange?: (show: boolean) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Session capabilities used to gate the role-region tracked-changes
|
|
86
|
+
* toggle (mirrors the right-cluster gate at tw-toolbar.tsx). When
|
|
87
|
+
* `capabilities.trackChangesSupported` is false, the toggle is disabled.
|
|
88
|
+
*/
|
|
89
|
+
capabilities?: SessionCapabilities;
|
|
90
|
+
|
|
91
|
+
// Review sidebar panel (optional — hidden when not provided)
|
|
92
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
93
|
+
onReviewSidebarComments?: () => void;
|
|
94
|
+
|
|
95
|
+
// Workflow + review role: scope posture menu
|
|
96
|
+
onMarkScopePosture?: (posture: ScopeRailPosture) => void;
|
|
97
|
+
|
|
98
|
+
// Review role
|
|
99
|
+
onReviewPrev?: () => void;
|
|
100
|
+
onReviewNext?: () => void;
|
|
101
|
+
onReviewAccept?: () => void;
|
|
102
|
+
onReviewReject?: () => void;
|
|
103
|
+
onReviewAcceptAll?: () => void;
|
|
104
|
+
onReviewRejectAll?: () => void;
|
|
105
|
+
onReviewMarkupMode?: (mode: MarkupDisplayMode) => void;
|
|
106
|
+
|
|
107
|
+
// Workflow role
|
|
108
|
+
onWorkflowPrev?: () => void;
|
|
109
|
+
onWorkflowNext?: () => void;
|
|
110
|
+
onWorkflowMarkComplete?: () => void;
|
|
111
|
+
onWorkflowClaim?: () => void;
|
|
112
|
+
onWorkflowSkip?: () => void;
|
|
113
|
+
onWorkflowMarkBlocked?: () => void;
|
|
114
|
+
onWorkflowJumpToScope?: () => void;
|
|
115
|
+
|
|
116
|
+
"data-testid"?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function TwRoleActionRegion(
|
|
120
|
+
props: TwRoleActionRegionProps,
|
|
121
|
+
): React.JSX.Element | null {
|
|
122
|
+
const order = ROLE_ACTION_SETS[props.role];
|
|
123
|
+
const inlineIds: ToolbarChromeItemId[] = [];
|
|
124
|
+
const overflowIds: ToolbarChromeItemId[] = [];
|
|
125
|
+
|
|
126
|
+
for (const id of order) {
|
|
127
|
+
const entry = props.policy.toolbar[id];
|
|
128
|
+
if (!entry?.visible) continue;
|
|
129
|
+
if (entry.placement === "overflow") {
|
|
130
|
+
overflowIds.push(id);
|
|
131
|
+
} else if (entry.placement === "inline") {
|
|
132
|
+
inlineIds.push(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (inlineIds.length === 0 && overflowIds.length === 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
className="flex items-center gap-1 border-l border-r border-border/40 px-1.5"
|
|
143
|
+
data-testid={props["data-testid"] ?? `role-action-region-${props.role}`}
|
|
144
|
+
data-role={props.role}
|
|
145
|
+
>
|
|
146
|
+
{inlineIds.map((id) => (
|
|
147
|
+
<RoleActionButton
|
|
148
|
+
key={id}
|
|
149
|
+
id={id}
|
|
150
|
+
compact={props.compactMode}
|
|
151
|
+
props={props}
|
|
152
|
+
/>
|
|
153
|
+
))}
|
|
154
|
+
{overflowIds.length > 0 ? (
|
|
155
|
+
<RoleActionOverflow ids={overflowIds} props={props} />
|
|
156
|
+
) : null}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface RoleActionButtonProps {
|
|
162
|
+
id: ToolbarChromeItemId;
|
|
163
|
+
compact: boolean;
|
|
164
|
+
props: TwRoleActionRegionProps;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null {
|
|
168
|
+
const { id, props } = arg;
|
|
169
|
+
const focusRingClass =
|
|
170
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
171
|
+
switch (id) {
|
|
172
|
+
case "comment":
|
|
173
|
+
return (
|
|
174
|
+
<Tooltip.Root>
|
|
175
|
+
<Tooltip.Trigger asChild>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
aria-label="Add comment"
|
|
179
|
+
disabled={!props.canAddComment || !props.onAddComment}
|
|
180
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
181
|
+
onClick={props.onAddComment}
|
|
182
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 outline-none ${focusRingClass}`}
|
|
183
|
+
data-testid="role-add-comment"
|
|
184
|
+
>
|
|
185
|
+
<MessageSquare className="h-3.5 w-3.5" />
|
|
186
|
+
</button>
|
|
187
|
+
</Tooltip.Trigger>
|
|
188
|
+
<Tooltip.Portal>
|
|
189
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
190
|
+
Add comment
|
|
191
|
+
</Tooltip.Content>
|
|
192
|
+
</Tooltip.Portal>
|
|
193
|
+
</Tooltip.Root>
|
|
194
|
+
);
|
|
195
|
+
case "tracked-changes-toggle":
|
|
196
|
+
return (
|
|
197
|
+
<Tooltip.Root>
|
|
198
|
+
<Tooltip.Trigger asChild>
|
|
199
|
+
<Toggle.Root
|
|
200
|
+
pressed={props.showTrackedChanges ?? false}
|
|
201
|
+
onPressedChange={(v) => props.onShowTrackedChangesChange?.(v)}
|
|
202
|
+
disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
|
|
203
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
204
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
205
|
+
data-testid="role-tracked-changes-toggle"
|
|
206
|
+
>
|
|
207
|
+
{(props.showTrackedChanges ?? false) ? (
|
|
208
|
+
<Eye className="h-3.5 w-3.5" />
|
|
209
|
+
) : (
|
|
210
|
+
<EyeOff className="h-3.5 w-3.5" />
|
|
211
|
+
)}
|
|
212
|
+
</Toggle.Root>
|
|
213
|
+
</Tooltip.Trigger>
|
|
214
|
+
<Tooltip.Portal>
|
|
215
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
216
|
+
{(props.showTrackedChanges ?? false) ? "Hide tracked changes" : "Show tracked changes"}
|
|
217
|
+
</Tooltip.Content>
|
|
218
|
+
</Tooltip.Portal>
|
|
219
|
+
</Tooltip.Root>
|
|
220
|
+
);
|
|
221
|
+
case "review-sidebar-tracked-changes":
|
|
222
|
+
return (
|
|
223
|
+
<Tooltip.Root>
|
|
224
|
+
<Tooltip.Trigger asChild>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
aria-label="Show tracked changes in sidebar"
|
|
228
|
+
disabled={!props.onReviewSidebarTrackedChanges}
|
|
229
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
230
|
+
onClick={props.onReviewSidebarTrackedChanges}
|
|
231
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 outline-none ${focusRingClass}`}
|
|
232
|
+
data-testid="role-sidebar-tracked-changes"
|
|
233
|
+
>
|
|
234
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
235
|
+
</button>
|
|
236
|
+
</Tooltip.Trigger>
|
|
237
|
+
<Tooltip.Portal>
|
|
238
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
239
|
+
Tracked changes panel
|
|
240
|
+
</Tooltip.Content>
|
|
241
|
+
</Tooltip.Portal>
|
|
242
|
+
</Tooltip.Root>
|
|
243
|
+
);
|
|
244
|
+
case "review-sidebar-comments":
|
|
245
|
+
return (
|
|
246
|
+
<Tooltip.Root>
|
|
247
|
+
<Tooltip.Trigger asChild>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
aria-label="Show comments in sidebar"
|
|
251
|
+
disabled={!props.onReviewSidebarComments}
|
|
252
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
253
|
+
onClick={props.onReviewSidebarComments}
|
|
254
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 outline-none ${focusRingClass}`}
|
|
255
|
+
data-testid="role-sidebar-comments"
|
|
256
|
+
>
|
|
257
|
+
<MessageSquareDot className="h-3.5 w-3.5" />
|
|
258
|
+
</button>
|
|
259
|
+
</Tooltip.Trigger>
|
|
260
|
+
<Tooltip.Portal>
|
|
261
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
262
|
+
Comments panel
|
|
263
|
+
</Tooltip.Content>
|
|
264
|
+
</Tooltip.Portal>
|
|
265
|
+
</Tooltip.Root>
|
|
266
|
+
);
|
|
267
|
+
case "editor-scope-posture-menu":
|
|
268
|
+
return (
|
|
269
|
+
<TwScopePostureMenu
|
|
270
|
+
onSelect={(posture) => props.onMarkScopePosture?.(posture)}
|
|
271
|
+
disabled={!props.onMarkScopePosture}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
case "review-queue-prev":
|
|
275
|
+
return (
|
|
276
|
+
<IconTextButton
|
|
277
|
+
icon={ChevronLeft}
|
|
278
|
+
text="Prev"
|
|
279
|
+
ariaLabel="Previous review item"
|
|
280
|
+
disabled={(props.reviewQueue?.totalCount ?? 0) === 0 || !props.onReviewPrev}
|
|
281
|
+
onClick={props.onReviewPrev}
|
|
282
|
+
data-testid="role-review-prev"
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
case "review-queue-next":
|
|
286
|
+
return (
|
|
287
|
+
<IconTextButton
|
|
288
|
+
icon={ChevronRight}
|
|
289
|
+
iconAfter
|
|
290
|
+
text="Next"
|
|
291
|
+
ariaLabel="Next review item"
|
|
292
|
+
disabled={(props.reviewQueue?.totalCount ?? 0) === 0 || !props.onReviewNext}
|
|
293
|
+
onClick={props.onReviewNext}
|
|
294
|
+
data-testid="role-review-next"
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
case "review-queue-counts":
|
|
298
|
+
return props.reviewQueue ? <ReviewQueueCounts queue={props.reviewQueue} /> : null;
|
|
299
|
+
case "review-queue-active-label":
|
|
300
|
+
return props.reviewQueue ? <ReviewActiveLabel queue={props.reviewQueue} /> : null;
|
|
301
|
+
case "review-accept":
|
|
302
|
+
return (
|
|
303
|
+
<IconTextButton
|
|
304
|
+
icon={Check}
|
|
305
|
+
text="Accept"
|
|
306
|
+
tone="accent"
|
|
307
|
+
ariaLabel="Accept current change"
|
|
308
|
+
disabled={!props.onReviewAccept}
|
|
309
|
+
onClick={props.onReviewAccept}
|
|
310
|
+
data-testid="role-review-accept"
|
|
311
|
+
/>
|
|
312
|
+
);
|
|
313
|
+
case "review-reject":
|
|
314
|
+
return (
|
|
315
|
+
<IconTextButton
|
|
316
|
+
icon={X}
|
|
317
|
+
text="Reject"
|
|
318
|
+
tone="danger"
|
|
319
|
+
ariaLabel="Reject current change"
|
|
320
|
+
disabled={!props.onReviewReject}
|
|
321
|
+
onClick={props.onReviewReject}
|
|
322
|
+
data-testid="role-review-reject"
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
case "review-markup-mode":
|
|
326
|
+
return (
|
|
327
|
+
<MarkupModeSelect
|
|
328
|
+
mode={props.markupDisplay ?? "simple"}
|
|
329
|
+
onChange={(mode) => props.onReviewMarkupMode?.(mode)}
|
|
330
|
+
disabled={!props.onReviewMarkupMode}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
case "workflow-prev":
|
|
334
|
+
return (
|
|
335
|
+
<IconTextButton
|
|
336
|
+
icon={ChevronLeft}
|
|
337
|
+
text="Prev"
|
|
338
|
+
ariaLabel="Previous work item"
|
|
339
|
+
disabled={!(props.workflowItem?.hasPrevious && props.onWorkflowPrev)}
|
|
340
|
+
onClick={props.onWorkflowPrev}
|
|
341
|
+
data-testid="role-workflow-prev"
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
case "workflow-next":
|
|
345
|
+
return (
|
|
346
|
+
<IconTextButton
|
|
347
|
+
icon={ChevronRight}
|
|
348
|
+
iconAfter
|
|
349
|
+
text="Next"
|
|
350
|
+
ariaLabel="Next work item"
|
|
351
|
+
disabled={!(props.workflowItem?.hasNext && props.onWorkflowNext)}
|
|
352
|
+
onClick={props.onWorkflowNext}
|
|
353
|
+
data-testid="role-workflow-next"
|
|
354
|
+
/>
|
|
355
|
+
);
|
|
356
|
+
case "workflow-mark-complete":
|
|
357
|
+
return (
|
|
358
|
+
<IconTextButton
|
|
359
|
+
icon={CheckCheck}
|
|
360
|
+
text="Complete"
|
|
361
|
+
tone="accent"
|
|
362
|
+
ariaLabel="Mark work item complete"
|
|
363
|
+
disabled={!(props.workflowItem?.canComplete && props.onWorkflowMarkComplete)}
|
|
364
|
+
onClick={props.onWorkflowMarkComplete}
|
|
365
|
+
data-testid="role-workflow-complete"
|
|
366
|
+
/>
|
|
367
|
+
);
|
|
368
|
+
case "workflow-claim":
|
|
369
|
+
return (
|
|
370
|
+
<IconTextButton
|
|
371
|
+
icon={Hand}
|
|
372
|
+
text="Claim"
|
|
373
|
+
ariaLabel="Claim work item"
|
|
374
|
+
disabled={!(props.workflowItem?.canClaim && props.onWorkflowClaim)}
|
|
375
|
+
onClick={props.onWorkflowClaim}
|
|
376
|
+
data-testid="role-workflow-claim"
|
|
377
|
+
/>
|
|
378
|
+
);
|
|
379
|
+
default:
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
interface RoleActionOverflowProps {
|
|
385
|
+
ids: ToolbarChromeItemId[];
|
|
386
|
+
props: TwRoleActionRegionProps;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function RoleActionOverflow({
|
|
390
|
+
ids,
|
|
391
|
+
props,
|
|
392
|
+
}: RoleActionOverflowProps): React.JSX.Element {
|
|
393
|
+
const [open, setOpen] = useState(false);
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
397
|
+
<Popover.Trigger asChild>
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
aria-label="More role actions"
|
|
401
|
+
aria-expanded={open}
|
|
402
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
403
|
+
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"
|
|
404
|
+
data-testid="role-action-overflow-trigger"
|
|
405
|
+
>
|
|
406
|
+
More
|
|
407
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
408
|
+
</button>
|
|
409
|
+
</Popover.Trigger>
|
|
410
|
+
<Popover.Portal>
|
|
411
|
+
<Popover.Content
|
|
412
|
+
className="z-50 w-[220px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
|
|
413
|
+
sideOffset={8}
|
|
414
|
+
align="start"
|
|
415
|
+
data-testid="role-action-overflow-content"
|
|
416
|
+
>
|
|
417
|
+
{ids.map((id) => (
|
|
418
|
+
<OverflowAction key={id} id={id} props={props} onClose={() => setOpen(false)} />
|
|
419
|
+
))}
|
|
420
|
+
</Popover.Content>
|
|
421
|
+
</Popover.Portal>
|
|
422
|
+
</Popover.Root>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function OverflowAction(arg: {
|
|
427
|
+
id: ToolbarChromeItemId;
|
|
428
|
+
props: TwRoleActionRegionProps;
|
|
429
|
+
onClose: () => void;
|
|
430
|
+
}): React.JSX.Element | null {
|
|
431
|
+
const { id, props, onClose } = arg;
|
|
432
|
+
switch (id) {
|
|
433
|
+
case "review-accept-all":
|
|
434
|
+
return (
|
|
435
|
+
<MenuRow
|
|
436
|
+
icon={CheckCheck}
|
|
437
|
+
label="Accept all in scope"
|
|
438
|
+
ariaLabel="Accept all reviewable changes in scope"
|
|
439
|
+
disabled={!props.onReviewAcceptAll}
|
|
440
|
+
onClick={() => {
|
|
441
|
+
props.onReviewAcceptAll?.();
|
|
442
|
+
onClose();
|
|
443
|
+
}}
|
|
444
|
+
/>
|
|
445
|
+
);
|
|
446
|
+
case "review-reject-all":
|
|
447
|
+
return (
|
|
448
|
+
<MenuRow
|
|
449
|
+
icon={XCircle}
|
|
450
|
+
label="Reject all in scope"
|
|
451
|
+
ariaLabel="Reject all reviewable changes in scope"
|
|
452
|
+
disabled={!props.onReviewRejectAll}
|
|
453
|
+
onClick={() => {
|
|
454
|
+
props.onReviewRejectAll?.();
|
|
455
|
+
onClose();
|
|
456
|
+
}}
|
|
457
|
+
/>
|
|
458
|
+
);
|
|
459
|
+
case "review-markup-mode":
|
|
460
|
+
return (
|
|
461
|
+
<MenuRow
|
|
462
|
+
icon={Rows3}
|
|
463
|
+
label={`Markup: ${props.markupDisplay ?? "simple"}`}
|
|
464
|
+
ariaLabel="Change markup display mode"
|
|
465
|
+
disabled={!props.onReviewMarkupMode}
|
|
466
|
+
onClick={() => {
|
|
467
|
+
const next: MarkupDisplayMode =
|
|
468
|
+
props.markupDisplay === "all"
|
|
469
|
+
? "clean"
|
|
470
|
+
: props.markupDisplay === "simple"
|
|
471
|
+
? "all"
|
|
472
|
+
: "simple";
|
|
473
|
+
props.onReviewMarkupMode?.(next);
|
|
474
|
+
onClose();
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
477
|
+
);
|
|
478
|
+
case "workflow-skip":
|
|
479
|
+
return (
|
|
480
|
+
<MenuRow
|
|
481
|
+
icon={SkipForward}
|
|
482
|
+
label="Skip work item"
|
|
483
|
+
ariaLabel="Skip work item"
|
|
484
|
+
disabled={!(props.workflowItem?.canSkip && props.onWorkflowSkip)}
|
|
485
|
+
onClick={() => {
|
|
486
|
+
props.onWorkflowSkip?.();
|
|
487
|
+
onClose();
|
|
488
|
+
}}
|
|
489
|
+
/>
|
|
490
|
+
);
|
|
491
|
+
case "workflow-mark-blocked":
|
|
492
|
+
return (
|
|
493
|
+
<MenuRow
|
|
494
|
+
icon={CircleOff}
|
|
495
|
+
label="Mark blocked"
|
|
496
|
+
ariaLabel="Mark work item blocked"
|
|
497
|
+
disabled={!(props.workflowItem?.canMarkBlocked && props.onWorkflowMarkBlocked)}
|
|
498
|
+
onClick={() => {
|
|
499
|
+
props.onWorkflowMarkBlocked?.();
|
|
500
|
+
onClose();
|
|
501
|
+
}}
|
|
502
|
+
/>
|
|
503
|
+
);
|
|
504
|
+
case "workflow-jump-to-scope":
|
|
505
|
+
return (
|
|
506
|
+
<MenuRow
|
|
507
|
+
icon={Target}
|
|
508
|
+
label="Jump to scope"
|
|
509
|
+
ariaLabel="Jump to workflow scope"
|
|
510
|
+
disabled={!props.onWorkflowJumpToScope}
|
|
511
|
+
onClick={() => {
|
|
512
|
+
props.onWorkflowJumpToScope?.();
|
|
513
|
+
onClose();
|
|
514
|
+
}}
|
|
515
|
+
/>
|
|
516
|
+
);
|
|
517
|
+
case "review-queue-counts":
|
|
518
|
+
return props.reviewQueue ? (
|
|
519
|
+
<div className="px-2 py-1.5">
|
|
520
|
+
<ReviewQueueCounts queue={props.reviewQueue} compact />
|
|
521
|
+
</div>
|
|
522
|
+
) : null;
|
|
523
|
+
case "review-queue-active-label":
|
|
524
|
+
return props.reviewQueue ? (
|
|
525
|
+
<div className="px-2 py-1.5">
|
|
526
|
+
<ReviewActiveLabel queue={props.reviewQueue} />
|
|
527
|
+
</div>
|
|
528
|
+
) : null;
|
|
529
|
+
default:
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function IconTextButton(arg: {
|
|
535
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
536
|
+
iconAfter?: boolean;
|
|
537
|
+
text: string;
|
|
538
|
+
ariaLabel: string;
|
|
539
|
+
tone?: "default" | "accent" | "danger";
|
|
540
|
+
disabled?: boolean;
|
|
541
|
+
onClick?: () => void;
|
|
542
|
+
"data-testid"?: string;
|
|
543
|
+
}): React.JSX.Element {
|
|
544
|
+
const Icon = arg.icon;
|
|
545
|
+
const tone = arg.tone ?? "default";
|
|
546
|
+
const toneClasses =
|
|
547
|
+
tone === "accent"
|
|
548
|
+
? "text-accent hover:bg-accent/10"
|
|
549
|
+
: tone === "danger"
|
|
550
|
+
? "text-danger hover:bg-danger/10"
|
|
551
|
+
: "text-secondary hover:bg-surface";
|
|
552
|
+
return (
|
|
553
|
+
<button
|
|
554
|
+
type="button"
|
|
555
|
+
aria-label={arg.ariaLabel}
|
|
556
|
+
disabled={arg.disabled}
|
|
557
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
558
|
+
onClick={arg.onClick}
|
|
559
|
+
data-testid={arg["data-testid"]}
|
|
560
|
+
className={[
|
|
561
|
+
"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",
|
|
562
|
+
toneClasses,
|
|
563
|
+
].join(" ")}
|
|
564
|
+
>
|
|
565
|
+
{!arg.iconAfter ? <Icon className="h-3.5 w-3.5" /> : null}
|
|
566
|
+
<span>{arg.text}</span>
|
|
567
|
+
{arg.iconAfter ? <Icon className="h-3.5 w-3.5" /> : null}
|
|
568
|
+
</button>
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function MenuRow(arg: {
|
|
573
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
574
|
+
label: string;
|
|
575
|
+
ariaLabel: string;
|
|
576
|
+
disabled?: boolean;
|
|
577
|
+
onClick: () => void;
|
|
578
|
+
}): React.JSX.Element {
|
|
579
|
+
const Icon = arg.icon;
|
|
580
|
+
return (
|
|
581
|
+
<button
|
|
582
|
+
type="button"
|
|
583
|
+
aria-label={arg.ariaLabel}
|
|
584
|
+
disabled={arg.disabled}
|
|
585
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
586
|
+
onClick={arg.onClick}
|
|
587
|
+
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"
|
|
588
|
+
>
|
|
589
|
+
<Icon className="h-3.5 w-3.5 text-secondary" />
|
|
590
|
+
<span>{arg.label}</span>
|
|
591
|
+
</button>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function ReviewQueueCounts({
|
|
596
|
+
queue,
|
|
597
|
+
compact,
|
|
598
|
+
}: {
|
|
599
|
+
queue: ReviewQueueSnapshot;
|
|
600
|
+
compact?: boolean;
|
|
601
|
+
}): React.JSX.Element {
|
|
602
|
+
return (
|
|
603
|
+
<span
|
|
604
|
+
className={[
|
|
605
|
+
"inline-flex items-center gap-1.5 text-[10px] text-secondary",
|
|
606
|
+
compact ? "px-0" : "px-1",
|
|
607
|
+
].join(" ")}
|
|
608
|
+
data-testid="role-review-counts"
|
|
609
|
+
>
|
|
610
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-surface px-1.5 py-0.5 font-medium">
|
|
611
|
+
<MessageSquareText className="h-3 w-3 text-comment" />
|
|
612
|
+
{queue.openCount} open
|
|
613
|
+
</span>
|
|
614
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-surface px-1.5 py-0.5 font-medium">
|
|
615
|
+
<Rows3 className="h-3 w-3 text-accent" />
|
|
616
|
+
{queue.totalCount} total
|
|
617
|
+
</span>
|
|
618
|
+
</span>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function ReviewActiveLabel({
|
|
623
|
+
queue,
|
|
624
|
+
}: {
|
|
625
|
+
queue: ReviewQueueSnapshot;
|
|
626
|
+
}): React.JSX.Element {
|
|
627
|
+
const active = queue.items[queue.activeIndex] ?? null;
|
|
628
|
+
const label =
|
|
629
|
+
active?.label ??
|
|
630
|
+
(queue.totalCount > 0 ? "Review queue" : "No review items");
|
|
631
|
+
const kind =
|
|
632
|
+
active?.kind === "comment"
|
|
633
|
+
? "Comment"
|
|
634
|
+
: active?.kind === "change"
|
|
635
|
+
? "Redline"
|
|
636
|
+
: active?.kind === "section_mark"
|
|
637
|
+
? "Tag"
|
|
638
|
+
: null;
|
|
639
|
+
return (
|
|
640
|
+
<span
|
|
641
|
+
className="inline-flex min-w-0 items-center gap-1.5 truncate text-[11px] text-primary"
|
|
642
|
+
data-testid="role-review-active-label"
|
|
643
|
+
>
|
|
644
|
+
{kind ? (
|
|
645
|
+
<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">
|
|
646
|
+
{kind}
|
|
647
|
+
</span>
|
|
648
|
+
) : null}
|
|
649
|
+
<span className="truncate">{label}</span>
|
|
650
|
+
</span>
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function MarkupModeSelect(arg: {
|
|
655
|
+
mode: MarkupDisplayMode;
|
|
656
|
+
onChange: (mode: MarkupDisplayMode) => void;
|
|
657
|
+
disabled?: boolean;
|
|
658
|
+
}): React.JSX.Element {
|
|
659
|
+
const Icon = arg.mode === "clean" ? BookmarkCheck : arg.mode === "all" ? Flag : Rows3;
|
|
660
|
+
return (
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
aria-label={`Markup display: ${arg.mode}`}
|
|
664
|
+
disabled={arg.disabled}
|
|
665
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
666
|
+
onClick={() => {
|
|
667
|
+
const next: MarkupDisplayMode =
|
|
668
|
+
arg.mode === "all" ? "clean" : arg.mode === "clean" ? "simple" : "all";
|
|
669
|
+
arg.onChange(next);
|
|
670
|
+
}}
|
|
671
|
+
data-testid="role-review-markup-mode"
|
|
672
|
+
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"
|
|
673
|
+
>
|
|
674
|
+
<Icon className="h-3.5 w-3.5" />
|
|
675
|
+
<span className="capitalize">{arg.mode}</span>
|
|
676
|
+
</button>
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export default TwRoleActionRegion;
|