@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,150 @@
1
+ import React, { type ReactNode } from "react";
2
+
3
+ /**
4
+ * TwRailCard — shared editorial card primitive consumed by the runtime review
5
+ * rail tabs (Workflow, Comments, Changes).
6
+ *
7
+ * The card is tone-aware: the tone drives a 3px left edge rule, the eyebrow
8
+ * color, and the optional progress-bar fill via CSS custom properties declared
9
+ * in `src/ui-tailwind/theme/editor-theme.css` under `.wre-rail-card`.
10
+ *
11
+ * This primitive deliberately stays host-agnostic — no workflow, comment, or
12
+ * revision coupling. Consumers pass display fields and an optional footer
13
+ * slot so the card can host accept/reject, reply composer, or resolve actions
14
+ * without the primitive owning any domain logic.
15
+ */
16
+
17
+ export type RailCardTone =
18
+ | "neutral"
19
+ | "inReview"
20
+ | "blocked"
21
+ | "scheduled"
22
+ | "resolved";
23
+
24
+ export interface RailCardAvatar {
25
+ initials: string;
26
+ color?: string;
27
+ alt?: string;
28
+ }
29
+
30
+ export interface RailCardProgress {
31
+ value: number;
32
+ total?: number;
33
+ }
34
+
35
+ export interface RailCardCounter {
36
+ label: string;
37
+ value: string;
38
+ }
39
+
40
+ export interface TwRailCardProps {
41
+ tone: RailCardTone;
42
+ eyebrow: string;
43
+ title: string;
44
+ detail?: string;
45
+ leadingIcon?: ReactNode;
46
+ avatars?: readonly RailCardAvatar[];
47
+ avatarOverflowCount?: number;
48
+ counter?: RailCardCounter;
49
+ progress?: RailCardProgress;
50
+ footer?: ReactNode;
51
+ onClick?: () => void;
52
+ onSelect?: () => void;
53
+ isActive?: boolean;
54
+ dataTestId?: string;
55
+ }
56
+
57
+ export function TwRailCard(props: TwRailCardProps) {
58
+ const {
59
+ tone,
60
+ eyebrow,
61
+ title,
62
+ detail,
63
+ leadingIcon,
64
+ avatars,
65
+ avatarOverflowCount,
66
+ counter,
67
+ progress,
68
+ footer,
69
+ onClick,
70
+ onSelect,
71
+ isActive,
72
+ dataTestId,
73
+ } = props;
74
+
75
+ const handleClick = onClick || onSelect;
76
+ const tag: "article" | "button" = handleClick ? "button" : "article";
77
+
78
+ const clamped = progress
79
+ ? Math.max(0, Math.min(1, progress.total && progress.total > 0 ? progress.value / progress.total : progress.value))
80
+ : 0;
81
+
82
+ const commonProps: Record<string, unknown> = {
83
+ className: "wre-rail-card block w-full text-left",
84
+ "data-tone": tone,
85
+ "data-active": isActive ? "true" : "false",
86
+ "data-testid": dataTestId,
87
+ };
88
+
89
+ if (handleClick) {
90
+ commonProps.onClick = handleClick;
91
+ commonProps.type = "button";
92
+ }
93
+
94
+ return React.createElement(
95
+ tag,
96
+ commonProps,
97
+ <>
98
+ {counter ? (
99
+ <span className="wre-rail-card__counter" aria-label={counter.label} title={counter.label}>
100
+ {counter.value}
101
+ </span>
102
+ ) : null}
103
+
104
+ <span className="wre-rail-card__eyebrow">
105
+ {leadingIcon ? <span aria-hidden="true">{leadingIcon}</span> : null}
106
+ {eyebrow}
107
+ </span>
108
+
109
+ <p className="wre-rail-card__title">{title}</p>
110
+
111
+ {detail ? <p className="wre-rail-card__detail">{detail}</p> : null}
112
+
113
+ {avatars && avatars.length > 0 ? (
114
+ <span className="wre-rail-card__avatars" aria-hidden={avatars.every((a) => !a.alt) ? "true" : undefined}>
115
+ {avatars.map((avatar, index) => (
116
+ <span
117
+ key={`${avatar.initials}-${index}`}
118
+ className="wre-rail-card__avatar"
119
+ style={avatar.color ? { background: avatar.color } : undefined}
120
+ title={avatar.alt}
121
+ aria-label={avatar.alt}
122
+ >
123
+ {avatar.initials}
124
+ </span>
125
+ ))}
126
+ {avatarOverflowCount && avatarOverflowCount > 0 ? (
127
+ <span className="wre-rail-card__avatar-counter">+{avatarOverflowCount}</span>
128
+ ) : null}
129
+ </span>
130
+ ) : null}
131
+
132
+ {footer ? <div className="wre-rail-card__footer">{footer}</div> : null}
133
+
134
+ {progress ? (
135
+ <span
136
+ className="wre-rail-card__progress"
137
+ role="progressbar"
138
+ aria-valuemin={0}
139
+ aria-valuemax={progress.total ?? 1}
140
+ aria-valuenow={progress.value}
141
+ >
142
+ <span
143
+ className="wre-rail-card__progress-fill"
144
+ style={{ width: `${Math.round(clamped * 100)}%` }}
145
+ />
146
+ </span>
147
+ ) : null}
148
+ </>,
149
+ );
150
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { HelpCircle, Search } from "lucide-react";
3
+
4
+ /**
5
+ * Thin pinned footer rendered at the bottom of the review rail. The footer
6
+ * auto-hides when neither `onSearch` nor `helpHref` is supplied, so existing
7
+ * hosts that do not opt in see no change (progressive disclosure, DESIGN.md
8
+ * §5.4).
9
+ */
10
+ export interface TwReviewRailFooterProps {
11
+ onSearch?: () => void;
12
+ helpHref?: string;
13
+ helpLabel?: string;
14
+ searchLabel?: string;
15
+ }
16
+
17
+ const focusRingClass =
18
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
19
+
20
+ export function TwReviewRailFooter(props: TwReviewRailFooterProps) {
21
+ const searchLabel = props.searchLabel ?? "SEARCH";
22
+ const helpLabel = props.helpLabel ?? "HELP";
23
+
24
+ if (!props.onSearch && !props.helpHref) {
25
+ return null;
26
+ }
27
+
28
+ return (
29
+ <footer className="flex h-10 shrink-0 items-center gap-2 border-t border-border/60 px-3">
30
+ {props.onSearch ? (
31
+ <button
32
+ type="button"
33
+ onClick={props.onSearch}
34
+ className={`inline-flex items-center gap-1.5 rounded-sm px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-tertiary transition-colors hover:text-secondary ${focusRingClass}`}
35
+ >
36
+ <Search aria-hidden="true" className="h-3 w-3" />
37
+ <span>{searchLabel}</span>
38
+ </button>
39
+ ) : null}
40
+
41
+ {props.helpHref ? (
42
+ <a
43
+ href={props.helpHref}
44
+ className={`inline-flex items-center gap-1.5 rounded-sm px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-tertiary transition-colors hover:text-secondary ${focusRingClass}`}
45
+ >
46
+ <HelpCircle aria-hidden="true" className="h-3 w-3" />
47
+ <span>{helpLabel}</span>
48
+ </a>
49
+ ) : null}
50
+ </footer>
51
+ );
52
+ }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { type ReactNode } from "react";
2
2
 
3
3
  import * as Tabs from "@radix-ui/react-tabs";
4
4
  import * as ScrollArea from "@radix-ui/react-scroll-area";
@@ -12,11 +12,35 @@ import type {
12
12
  TrackedChangesSnapshot,
13
13
  TrackedChangeEntrySnapshot,
14
14
  } from "../../api/public-types";
15
+ import type { ScopeRailSegment } from "../../runtime/layout";
15
16
  import type { MarkupDisplay } from "../../ui/headless/comment-decoration-model";
16
17
  import { TwCommentSidebar } from "./tw-comment-sidebar";
17
18
  import { TwRevisionSidebar } from "./tw-revision-sidebar";
19
+ import { TwWorkflowTab } from "./tw-workflow-tab";
20
+ import {
21
+ TwReviewRailFooter,
22
+ type TwReviewRailFooterProps,
23
+ } from "./tw-review-rail-footer";
18
24
 
19
- export type ReviewRailTab = "comments" | "changes";
25
+ /**
26
+ * Review rail with three tabs (Comments / Changes / Workflow) that matches
27
+ * the editorial reference mock while preserving the shipped R3a surface.
28
+ *
29
+ * The Workflow tab reads `scopeRailSegments` from the runtime facet — that
30
+ * stays the default path. For hosts that need to override the Workflow card
31
+ * content (CLM, BW, agent integrations that derive their own model), pass
32
+ * `workflowTab` as a ReactNode and it will replace the built-in projection.
33
+ * Either way policy stays host-side; the runtime rail remains projection-only.
34
+ *
35
+ * Optional editorial surface (harness-pass §4.5):
36
+ * - `intelligenceHeader={true}` turns on the DOCUMENT INTELLIGENCE header +
37
+ * underline-active chip style. Off by default so existing pill-tab
38
+ * consumers see no change.
39
+ * - `workflowScopesTitle?: string` overrides the header title.
40
+ * - `railFooter?: TwReviewRailFooterProps` mounts the SEARCH / HELP footer.
41
+ */
42
+
43
+ export type ReviewRailTab = "comments" | "changes" | "workflow";
20
44
 
21
45
  export interface TwReviewRailProps {
22
46
  activeTab: ReviewRailTab;
@@ -30,6 +54,34 @@ export interface TwReviewRailProps {
30
54
  contextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
31
55
  activeCommentId?: string;
32
56
  activeRevisionId?: string;
57
+ /**
58
+ * Scope rail segments used by the Workflow tab. Consumers typically
59
+ * pass `ref.layout.getAllScopeRailSegments()` here. When omitted the
60
+ * Workflow tab renders an empty state (unless `workflowTab` is set).
61
+ */
62
+ scopeRailSegments?: readonly ScopeRailSegment[];
63
+ activeScopeId?: string | null;
64
+ /**
65
+ * Optional host-provided Workflow-tab override. When supplied this
66
+ * ReactNode replaces the default TwWorkflowTab content while still using
67
+ * the same tab chrome. Hosts retain ownership of workflow/CLM policy.
68
+ */
69
+ workflowTab?: ReactNode;
70
+ /** Count badge shown next to the Workflow trigger. Defaults to segment count. */
71
+ workflowCount?: number;
72
+ /** Editorial header title — defaults to "Review" or tab-aware copy. */
73
+ workflowScopesTitle?: string;
74
+ /** Editorial header eyebrow — defaults to "DOCUMENT INTELLIGENCE". */
75
+ intelligenceEyebrow?: string;
76
+ /**
77
+ * Turn on the editorial DOCUMENT INTELLIGENCE header + underline-active
78
+ * tab chip. Defaults to `false` so existing pill-tab callers see no
79
+ * change.
80
+ */
81
+ intelligenceHeader?: boolean;
82
+ /** Utility footer with SEARCH / HELP links. Hides when unset. */
83
+ railFooter?: TwReviewRailFooterProps;
84
+
33
85
  onActiveTabChange: (tab: ReviewRailTab) => void;
34
86
  onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
35
87
  onResolveComment?: (commentId: string) => void;
@@ -41,22 +93,38 @@ export interface TwReviewRailProps {
41
93
  onRejectRevision?: (revisionId: string) => void;
42
94
  onAcceptAllChanges?: () => void;
43
95
  onRejectAllChanges?: () => void;
96
+ onOpenScope?: (segment: ScopeRailSegment) => void;
44
97
  }
45
98
 
46
99
  const focusRingClass =
47
100
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
48
101
 
102
+ const PILL_TRIGGER_CLASS = `flex-1 rounded-lg px-3 py-1.5 text-xs font-medium text-tertiary transition-colors data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-[0_1px_0_var(--color-shadow)] outline-none ${focusRingClass}`;
103
+
49
104
  export function TwReviewRail(props: TwReviewRailProps) {
50
105
  const variant = props.variant ?? "docked";
106
+ const editorial = props.intelligenceHeader ?? false;
107
+ const intelligenceEyebrow =
108
+ props.intelligenceEyebrow ?? "DOCUMENT INTELLIGENCE";
109
+ const headerTitle = resolveHeaderTitle(props);
110
+ const workflowSegments = props.scopeRailSegments ?? [];
111
+ const workflowCount =
112
+ props.workflowCount ?? (props.workflowTab ? undefined : workflowSegments.length);
113
+ const widthClass = editorial ? "w-[360px]" : "w-[336px]";
114
+ const drawerWidthClass = editorial
115
+ ? "w-[min(360px,calc(100vw-1rem))]"
116
+ : "w-[min(336px,calc(100vw-1rem))]";
117
+
51
118
  return (
52
119
  <aside
53
120
  aria-label="Review rail"
54
121
  data-wre-drawer={variant === "drawer" ? "true" : "false"}
122
+ data-editorial-header={editorial ? "true" : "false"}
55
123
  className={[
56
124
  "flex flex-col border-l border-border/60 bg-[var(--color-sidebar-tint)]",
57
125
  variant === "drawer"
58
- ? "h-full w-[min(336px,calc(100vw-1rem))] max-w-full shrink-0 shadow-[0_18px_40px_-22px_var(--color-shadow-strong)]"
59
- : "w-[336px] shrink-0",
126
+ ? `h-full ${drawerWidthClass} max-w-full shrink-0 shadow-[var(--shadow-float)]`
127
+ : `${widthClass} shrink-0`,
60
128
  ].join(" ")}
61
129
  >
62
130
  <Tabs.Root
@@ -64,25 +132,84 @@ export function TwReviewRail(props: TwReviewRailProps) {
64
132
  onValueChange={(v: string) => props.onActiveTabChange(v as ReviewRailTab)}
65
133
  className="flex flex-1 flex-col min-h-0"
66
134
  >
67
- <Tabs.List className="flex shrink-0 border-b border-border/60 px-3 py-2">
135
+ {editorial ? (
136
+ <header className="shrink-0 px-4 pt-[18px] pb-3">
137
+ <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-tertiary">
138
+ {intelligenceEyebrow}
139
+ </p>
140
+ <p className="mt-1 text-[16px] font-semibold text-primary">
141
+ {headerTitle}
142
+ </p>
143
+ </header>
144
+ ) : null}
145
+
146
+ <Tabs.List
147
+ className={
148
+ editorial
149
+ ? "flex shrink-0 items-center gap-2 border-b border-border/60 px-2"
150
+ : "flex shrink-0 border-b border-border/60 px-3 py-2"
151
+ }
152
+ aria-label="Review rail sections"
153
+ >
154
+ <Tabs.Trigger
155
+ value="workflow"
156
+ className={
157
+ editorial
158
+ ? `wre-rail-tab outline-none ${focusRingClass}`
159
+ : PILL_TRIGGER_CLASS
160
+ }
161
+ >
162
+ {editorial ? "Workflow" : "Workflow "}
163
+ {workflowCount !== undefined && workflowCount > 0 ? (
164
+ <span
165
+ className={
166
+ editorial
167
+ ? "wre-rail-tab__count"
168
+ : "ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary"
169
+ }
170
+ >
171
+ {workflowCount}
172
+ </span>
173
+ ) : null}
174
+ </Tabs.Trigger>
68
175
  <Tabs.Trigger
69
176
  value="comments"
70
- className={`flex-1 rounded-lg px-3 py-1.5 text-xs font-medium text-tertiary transition-colors data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-[0_1px_0_var(--color-shadow)] outline-none ${focusRingClass}`}
177
+ className={
178
+ editorial
179
+ ? `wre-rail-tab outline-none ${focusRingClass}`
180
+ : PILL_TRIGGER_CLASS
181
+ }
71
182
  >
72
- Comments{" "}
183
+ {editorial ? "Comments" : "Comments "}
73
184
  {props.comments.totalCount > 0 ? (
74
- <span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary">
185
+ <span
186
+ className={
187
+ editorial
188
+ ? "wre-rail-tab__count"
189
+ : "ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary"
190
+ }
191
+ >
75
192
  {props.comments.totalCount}
76
193
  </span>
77
194
  ) : null}
78
195
  </Tabs.Trigger>
79
196
  <Tabs.Trigger
80
197
  value="changes"
81
- className={`flex-1 rounded-lg px-3 py-1.5 text-xs font-medium text-tertiary transition-colors data-[state=active]:bg-surface data-[state=active]:text-primary data-[state=active]:shadow-[0_1px_0_var(--color-shadow)] outline-none ${focusRingClass}`}
198
+ className={
199
+ editorial
200
+ ? `wre-rail-tab outline-none ${focusRingClass}`
201
+ : PILL_TRIGGER_CLASS
202
+ }
82
203
  >
83
- Changes{" "}
204
+ {editorial ? "Changes" : "Changes "}
84
205
  {props.trackedChanges.totalCount > 0 ? (
85
- <span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary">
206
+ <span
207
+ className={
208
+ editorial
209
+ ? "wre-rail-tab__count"
210
+ : "ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-subtle px-1.5 py-px text-[10px] font-medium text-tertiary"
211
+ }
212
+ >
86
213
  {props.trackedChanges.totalCount}
87
214
  </span>
88
215
  ) : null}
@@ -91,6 +218,16 @@ export function TwReviewRail(props: TwReviewRailProps) {
91
218
 
92
219
  <ScrollArea.Root className="flex-1 min-h-0">
93
220
  <ScrollArea.Viewport className="h-full w-full">
221
+ <Tabs.Content value="workflow" className="p-3 outline-none">
222
+ {props.workflowTab ?? (
223
+ <TwWorkflowTab
224
+ segments={workflowSegments}
225
+ activeScopeId={props.activeScopeId ?? null}
226
+ onOpenScope={props.onOpenScope}
227
+ />
228
+ )}
229
+ </Tabs.Content>
230
+
94
231
  <Tabs.Content value="comments" className="p-3 outline-none">
95
232
  <TwCommentSidebar
96
233
  currentUserId={props.currentUserId}
@@ -125,6 +262,24 @@ export function TwReviewRail(props: TwReviewRailProps) {
125
262
  </ScrollArea.Scrollbar>
126
263
  </ScrollArea.Root>
127
264
  </Tabs.Root>
265
+
266
+ {props.railFooter ? <TwReviewRailFooter {...props.railFooter} /> : null}
128
267
  </aside>
129
268
  );
130
269
  }
270
+
271
+ function resolveHeaderTitle(props: TwReviewRailProps): string {
272
+ if (props.workflowScopesTitle) {
273
+ return props.workflowScopesTitle;
274
+ }
275
+ if (props.activeTab === "workflow") {
276
+ return "Workflow Scopes";
277
+ }
278
+ if (props.activeTab === "comments") {
279
+ return "Comments";
280
+ }
281
+ if (props.activeTab === "changes") {
282
+ return "Tracked Changes";
283
+ }
284
+ return "Review";
285
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Workflow tab content for the review rail.
3
+ *
4
+ * Renders scope cards sourced from `ref.layout.getAllScopeRailSegments()`.
5
+ * Cards mirror the right-sidebar "Document Intelligence / Workflow Scopes"
6
+ * panel in image copy.png with state chips + labels. Clicking a card
7
+ * forwards to `onOpenScope(segment)` so consumers can scroll to the scope
8
+ * and focus the rail label.
9
+ */
10
+
11
+ import React from "react";
12
+ import type { ScopeRailSegment, ScopeRailPosture } from "../../runtime/layout";
13
+
14
+ export interface TwWorkflowTabProps {
15
+ segments: readonly ScopeRailSegment[];
16
+ activeScopeId?: string | null;
17
+ onOpenScope?: (segment: ScopeRailSegment) => void;
18
+ }
19
+
20
+ const POSTURE_META: Record<
21
+ ScopeRailPosture,
22
+ { chip: string; kind: "accent" | "warning" | "insert" | "secondary" | "danger" }
23
+ > = {
24
+ edit: { chip: "EDIT", kind: "accent" },
25
+ suggest: { chip: "SUGGEST", kind: "warning" },
26
+ comment: { chip: "COMMENT", kind: "insert" },
27
+ view: { chip: "IN REVIEW", kind: "secondary" },
28
+ candidate: { chip: "SCHEDULED", kind: "warning" },
29
+ "preserve-only": { chip: "BLOCKED REGION", kind: "danger" },
30
+ "blocked-import": { chip: "BLOCKED REGION", kind: "danger" },
31
+ };
32
+
33
+ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
34
+ segments,
35
+ activeScopeId,
36
+ onOpenScope,
37
+ }) => {
38
+ // Dedupe by scopeId so a scope spanning multiple pages shows once.
39
+ const byScopeId = new Map<string, ScopeRailSegment>();
40
+ for (const segment of segments) {
41
+ if (!byScopeId.has(segment.scopeId)) {
42
+ byScopeId.set(segment.scopeId, segment);
43
+ }
44
+ }
45
+ const uniqueSegments = Array.from(byScopeId.values());
46
+
47
+ if (uniqueSegments.length === 0) {
48
+ return (
49
+ <div
50
+ className="wre-workflow-tab-empty rounded-md border border-dashed border-border/60 bg-canvas/50 p-4 text-[11px] text-tertiary"
51
+ data-testid="workflow-tab-empty"
52
+ >
53
+ <div className="font-semibold uppercase tracking-[0.1em] text-secondary">
54
+ Document Intelligence
55
+ </div>
56
+ <div className="mt-1 text-[11px] text-tertiary">
57
+ No workflow scopes on this document yet.
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <div className="wre-workflow-tab flex flex-col gap-2" data-testid="workflow-tab">
65
+ <div className="mb-1">
66
+ <div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-accent">
67
+ Document Intelligence
68
+ </div>
69
+ <div className="text-[15px] font-semibold text-primary">Workflow Scopes</div>
70
+ </div>
71
+ {uniqueSegments.map((segment) => {
72
+ const meta = POSTURE_META[segment.posture];
73
+ const isActive = activeScopeId === segment.scopeId || segment.isActiveWorkItem;
74
+ return (
75
+ <button
76
+ key={segment.scopeId}
77
+ type="button"
78
+ className={`wre-workflow-card flex flex-col gap-1 rounded-md border border-border/50 bg-canvas p-3 text-left transition-shadow hover:shadow-md ${
79
+ isActive ? "ring-1 ring-accent/60" : ""
80
+ }`}
81
+ onClick={onOpenScope ? () => onOpenScope(segment) : undefined}
82
+ data-scope-id={segment.scopeId}
83
+ data-posture={segment.posture}
84
+ >
85
+ <div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.12em]">
86
+ <span
87
+ className={`wre-workflow-card-chip wre-workflow-card-chip-${meta.kind} inline-flex items-center gap-1 rounded-full px-2 py-0.5`}
88
+ data-kind={meta.kind}
89
+ >
90
+ {meta.chip}
91
+ </span>
92
+ <span className="text-tertiary">Page {segment.pageIndex + 1}</span>
93
+ </div>
94
+ <div className="text-[13px] font-semibold text-primary">
95
+ {segment.label || segment.scopeId}
96
+ </div>
97
+ <div className="text-[11px] text-tertiary">
98
+ Section {segment.sectionIndex + 1}
99
+ {segment.isActiveWorkItem ? " • Active work item" : ""}
100
+ </div>
101
+ </button>
102
+ );
103
+ })}
104
+ </div>
105
+ );
106
+ };
107
+
108
+ export default TwWorkflowTab;