@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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +319 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +815 -55
  6. package/src/io/export/serialize-main-document.ts +2 -11
  7. package/src/io/export/serialize-numbering.ts +1 -2
  8. package/src/io/export/serialize-tables.ts +74 -0
  9. package/src/io/export/table-properties-xml.ts +139 -4
  10. package/src/io/normalize/normalize-text.ts +15 -0
  11. package/src/io/ooxml/parse-footnotes.ts +60 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  13. package/src/io/ooxml/parse-main-document.ts +137 -0
  14. package/src/io/ooxml/parse-tables.ts +249 -0
  15. package/src/model/canonical-document.ts +34 -0
  16. package/src/runtime/document-layout.ts +4 -2
  17. package/src/runtime/document-navigation.ts +1 -1
  18. package/src/runtime/document-runtime.ts +114 -0
  19. package/src/runtime/layout/default-page-format.ts +96 -0
  20. package/src/runtime/layout/index.ts +45 -0
  21. package/src/runtime/layout/inert-layout-facet.ts +14 -0
  22. package/src/runtime/layout/layout-engine-instance.ts +33 -23
  23. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  24. package/src/runtime/layout/page-format-catalog.ts +233 -0
  25. package/src/runtime/layout/page-graph.ts +19 -0
  26. package/src/runtime/layout/paginated-layout-engine.ts +142 -9
  27. package/src/runtime/layout/project-block-fragments.ts +91 -0
  28. package/src/runtime/layout/public-facet.ts +709 -16
  29. package/src/runtime/layout/table-render-plan.ts +229 -0
  30. package/src/runtime/render/block-fragment-projection.ts +35 -0
  31. package/src/runtime/render/decoration-resolver.ts +189 -0
  32. package/src/runtime/render/index.ts +57 -0
  33. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  34. package/src/runtime/render/render-frame-types.ts +317 -0
  35. package/src/runtime/render/render-kernel.ts +755 -0
  36. package/src/runtime/view-state.ts +67 -0
  37. package/src/runtime/workflow-markup.ts +1 -5
  38. package/src/runtime/workflow-rail-segments.ts +280 -0
  39. package/src/ui/WordReviewEditor.tsx +84 -15
  40. package/src/ui/editor-shell-view.tsx +6 -0
  41. package/src/ui/headless/chrome-registry.ts +280 -14
  42. package/src/ui/headless/scoped-chrome-policy.ts +20 -1
  43. package/src/ui/headless/selection-tool-types.ts +10 -0
  44. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  45. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  46. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  47. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  48. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  49. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  52. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  53. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  54. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  55. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  56. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  57. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  58. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  59. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  60. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
  61. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  62. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
  63. package/src/ui-tailwind/index.ts +33 -0
  64. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  65. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  66. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  68. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  69. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  70. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  71. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  72. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  73. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
  74. 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;