@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,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
|
-
|
|
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
|
-
?
|
|
59
|
-
:
|
|
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
|
-
|
|
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={
|
|
177
|
+
className={
|
|
178
|
+
editorial
|
|
179
|
+
? `wre-rail-tab outline-none ${focusRingClass}`
|
|
180
|
+
: PILL_TRIGGER_CLASS
|
|
181
|
+
}
|
|
71
182
|
>
|
|
72
|
-
|
|
183
|
+
{editorial ? "Comments" : "Comments "}
|
|
73
184
|
{props.comments.totalCount > 0 ? (
|
|
74
|
-
<span
|
|
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={
|
|
198
|
+
className={
|
|
199
|
+
editorial
|
|
200
|
+
? `wre-rail-tab outline-none ${focusRingClass}`
|
|
201
|
+
: PILL_TRIGGER_CLASS
|
|
202
|
+
}
|
|
82
203
|
>
|
|
83
|
-
|
|
204
|
+
{editorial ? "Changes" : "Changes "}
|
|
84
205
|
{props.trackedChanges.totalCount > 0 ? (
|
|
85
|
-
<span
|
|
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;
|