@beyondwork/docx-react-component 1.0.53 → 1.0.55
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 +125 -7
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +27 -3
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/parse-field-switches.ts +134 -0
- package/src/io/ooxml/parse-fields.ts +28 -2
- package/src/model/canonical-document.ts +13 -2
- package/src/runtime/chart/chart-model-store.ts +88 -0
- package/src/runtime/chart/chart-snapshot.ts +239 -0
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +1 -2
- package/src/runtime/document-runtime.ts +115 -13
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +58 -1
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/page-number-format.ts +207 -0
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/surface-projection.ts +32 -3
- package/src/ui/WordReviewEditor.tsx +57 -3
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +90 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +4 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +14 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/editor-theme.css +249 -22
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -99,7 +99,9 @@ export interface TwReviewRailProps {
|
|
|
99
99
|
const focusRingClass =
|
|
100
100
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
101
101
|
|
|
102
|
-
const PILL_TRIGGER_CLASS = `flex-1 rounded-
|
|
102
|
+
const PILL_TRIGGER_CLASS = `flex-1 rounded-[var(--radius-md)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-tertiary)] transition-colors data-[state=active]:bg-[var(--color-bg-selected)] data-[state=active]:text-[var(--color-text-primary)] outline-none ${focusRingClass}`;
|
|
103
|
+
|
|
104
|
+
const UNDERLINE_TRIGGER_CLASS = `flex items-center gap-1.5 px-3 py-2 text-xs font-medium text-[var(--color-text-tertiary)] border-b-2 border-transparent transition-colors data-[state=active]:border-[var(--color-accent-primary)] data-[state=active]:text-[var(--color-text-primary)] outline-none ${focusRingClass}`;
|
|
103
105
|
|
|
104
106
|
export function TwReviewRail(props: TwReviewRailProps) {
|
|
105
107
|
const variant = props.variant ?? "docked";
|
|
@@ -110,18 +112,19 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
110
112
|
const workflowSegments = props.scopeRailSegments ?? [];
|
|
111
113
|
const workflowCount =
|
|
112
114
|
props.workflowCount ?? (props.workflowTab ? undefined : workflowSegments.length);
|
|
113
|
-
const widthClass = editorial ? "w-[360px]" : "w-[
|
|
115
|
+
const widthClass = editorial ? "w-[360px]" : "w-[320px]";
|
|
114
116
|
const drawerWidthClass = editorial
|
|
115
117
|
? "w-[min(360px,calc(100vw-1rem))]"
|
|
116
118
|
: "w-[min(336px,calc(100vw-1rem))]";
|
|
117
119
|
|
|
118
120
|
return (
|
|
119
121
|
<aside
|
|
122
|
+
role="complementary"
|
|
120
123
|
aria-label="Review rail"
|
|
121
124
|
data-wre-drawer={variant === "drawer" ? "true" : "false"}
|
|
122
125
|
data-editorial-header={editorial ? "true" : "false"}
|
|
123
126
|
className={[
|
|
124
|
-
"flex flex-col border-l border-border/60 bg-[var(--color-sidebar
|
|
127
|
+
"flex flex-col border-l border-[var(--color-border-subtle)]/60 bg-[var(--color-bg-sidebar)]",
|
|
125
128
|
variant === "drawer"
|
|
126
129
|
? `h-full ${drawerWidthClass} max-w-full shrink-0 shadow-[var(--shadow-float)]`
|
|
127
130
|
: `${widthClass} shrink-0`,
|
|
@@ -133,11 +136,19 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
133
136
|
className="flex flex-1 flex-col min-h-0"
|
|
134
137
|
>
|
|
135
138
|
{editorial ? (
|
|
136
|
-
<header
|
|
137
|
-
|
|
139
|
+
<header
|
|
140
|
+
className="shrink-0 px-4 pt-[18px] pb-3"
|
|
141
|
+
style={{
|
|
142
|
+
paddingLeft: "calc(16px * var(--space-density-multiplier))",
|
|
143
|
+
paddingRight: "calc(16px * var(--space-density-multiplier))",
|
|
144
|
+
paddingTop: "calc(18px * var(--space-density-multiplier))",
|
|
145
|
+
paddingBottom: "calc(12px * var(--space-density-multiplier))",
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-[var(--color-text-tertiary)]">
|
|
138
149
|
{intelligenceEyebrow}
|
|
139
150
|
</p>
|
|
140
|
-
<p className="mt-1 text-[16px] font-semibold text-primary">
|
|
151
|
+
<p className="mt-1 text-[16px] font-semibold text-[var(--color-text-primary)]">
|
|
141
152
|
{headerTitle}
|
|
142
153
|
</p>
|
|
143
154
|
</header>
|
|
@@ -149,67 +160,50 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
149
160
|
? "flex shrink-0 items-center gap-2 border-b border-border/60 px-2"
|
|
150
161
|
: "flex shrink-0 border-b border-border/60 px-3 py-2"
|
|
151
162
|
}
|
|
163
|
+
style={
|
|
164
|
+
editorial
|
|
165
|
+
? {
|
|
166
|
+
paddingLeft: "calc(8px * var(--space-density-multiplier))",
|
|
167
|
+
paddingRight: "calc(8px * var(--space-density-multiplier))",
|
|
168
|
+
}
|
|
169
|
+
: {
|
|
170
|
+
paddingLeft: "calc(12px * var(--space-density-multiplier))",
|
|
171
|
+
paddingRight: "calc(12px * var(--space-density-multiplier))",
|
|
172
|
+
paddingTop: "calc(8px * var(--space-density-multiplier))",
|
|
173
|
+
paddingBottom: "calc(8px * var(--space-density-multiplier))",
|
|
174
|
+
}
|
|
175
|
+
}
|
|
152
176
|
aria-label="Review rail sections"
|
|
153
177
|
>
|
|
154
178
|
<Tabs.Trigger
|
|
155
179
|
value="workflow"
|
|
156
|
-
className={
|
|
157
|
-
editorial
|
|
158
|
-
? `wre-rail-tab outline-none ${focusRingClass}`
|
|
159
|
-
: PILL_TRIGGER_CLASS
|
|
160
|
-
}
|
|
180
|
+
className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
|
|
161
181
|
>
|
|
162
182
|
{editorial ? "Workflow" : "Workflow "}
|
|
163
183
|
{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
|
-
>
|
|
184
|
+
<span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-[var(--color-bg-muted)] px-1.5 py-px text-[10px] font-medium text-[var(--color-text-tertiary)]">
|
|
171
185
|
{workflowCount}
|
|
172
186
|
</span>
|
|
173
187
|
) : null}
|
|
174
188
|
</Tabs.Trigger>
|
|
175
189
|
<Tabs.Trigger
|
|
176
190
|
value="comments"
|
|
177
|
-
className={
|
|
178
|
-
editorial
|
|
179
|
-
? `wre-rail-tab outline-none ${focusRingClass}`
|
|
180
|
-
: PILL_TRIGGER_CLASS
|
|
181
|
-
}
|
|
191
|
+
className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
|
|
182
192
|
>
|
|
183
193
|
{editorial ? "Comments" : "Comments "}
|
|
184
194
|
{props.comments.totalCount > 0 ? (
|
|
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
|
-
>
|
|
195
|
+
<span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-[var(--color-bg-muted)] px-1.5 py-px text-[10px] font-medium text-[var(--color-text-tertiary)]">
|
|
192
196
|
{props.comments.totalCount}
|
|
193
197
|
</span>
|
|
194
198
|
) : null}
|
|
195
199
|
</Tabs.Trigger>
|
|
196
200
|
<Tabs.Trigger
|
|
197
201
|
value="changes"
|
|
198
|
-
className={
|
|
199
|
-
editorial
|
|
200
|
-
? `wre-rail-tab outline-none ${focusRingClass}`
|
|
201
|
-
: PILL_TRIGGER_CLASS
|
|
202
|
-
}
|
|
202
|
+
className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
|
|
203
203
|
>
|
|
204
204
|
{editorial ? "Changes" : "Changes "}
|
|
205
205
|
{props.trackedChanges.totalCount > 0 ? (
|
|
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
|
-
>
|
|
206
|
+
<span className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full bg-[var(--color-bg-muted)] px-1.5 py-px text-[10px] font-medium text-[var(--color-text-tertiary)]">
|
|
213
207
|
{props.trackedChanges.totalCount}
|
|
214
208
|
</span>
|
|
215
209
|
) : null}
|
|
@@ -16,128 +16,216 @@ export interface TwRevisionSidebarProps {
|
|
|
16
16
|
onRejectAllChanges?: () => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type TypeFilter = "all" | "insertion" | "deletion" | "formatting";
|
|
20
|
+
|
|
21
|
+
function labelFor(type: TypeFilter): string {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "all": return "All";
|
|
24
|
+
case "insertion": return "Insertions";
|
|
25
|
+
case "deletion": return "Deletions";
|
|
26
|
+
case "formatting": return "Formatting";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
const focusRingClass =
|
|
20
31
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
21
32
|
|
|
22
33
|
export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
23
34
|
const { trackedChanges, markupDisplay, activeRevisionId } = props;
|
|
24
35
|
const visibleRevisions = selectVisibleRevisions(trackedChanges.revisions, markupDisplay);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
36
|
+
|
|
37
|
+
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
|
38
|
+
const [authorFilter, setAuthorFilter] = React.useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
// Derive distinct authors from all visible revisions
|
|
41
|
+
const authors = React.useMemo(() => {
|
|
42
|
+
const seen = new Set<string>();
|
|
43
|
+
for (const r of visibleRevisions) {
|
|
44
|
+
if (r.authorId) seen.add(r.authorId);
|
|
45
|
+
}
|
|
46
|
+
return Array.from(seen).sort();
|
|
47
|
+
}, [visibleRevisions]);
|
|
48
|
+
|
|
49
|
+
// Filtered list based on type + author chips
|
|
50
|
+
const filteredRevisions = React.useMemo(() => {
|
|
51
|
+
return visibleRevisions.filter((r) => {
|
|
52
|
+
if (typeFilter !== "all") {
|
|
53
|
+
// Map filter chip value to revision kinds
|
|
54
|
+
if (typeFilter === "insertion" && r.kind !== "insertion") return false;
|
|
55
|
+
if (typeFilter === "deletion" && r.kind !== "deletion") return false;
|
|
56
|
+
if (typeFilter === "formatting" && r.kind !== "formatting") return false;
|
|
57
|
+
}
|
|
58
|
+
if (authorFilter !== null && r.authorId !== authorFilter) return false;
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}, [visibleRevisions, typeFilter, authorFilter]);
|
|
62
|
+
|
|
63
|
+
const handleAcceptAll = React.useCallback(() => {
|
|
64
|
+
if (props.onAcceptAllChanges && typeFilter === "all" && authorFilter === null) {
|
|
65
|
+
// Use dedicated bulk handler when no filter is active
|
|
66
|
+
props.onAcceptAllChanges();
|
|
67
|
+
} else {
|
|
68
|
+
filteredRevisions.forEach((r) => props.onAcceptRevision?.(r.revisionId));
|
|
69
|
+
}
|
|
70
|
+
}, [filteredRevisions, typeFilter, authorFilter, props.onAcceptAllChanges, props.onAcceptRevision]);
|
|
71
|
+
|
|
72
|
+
const handleRejectAll = React.useCallback(() => {
|
|
73
|
+
if (props.onRejectAllChanges && typeFilter === "all" && authorFilter === null) {
|
|
74
|
+
// Use dedicated bulk handler when no filter is active
|
|
75
|
+
props.onRejectAllChanges();
|
|
76
|
+
} else {
|
|
77
|
+
filteredRevisions.forEach((r) => props.onRejectRevision?.(r.revisionId));
|
|
78
|
+
}
|
|
79
|
+
}, [filteredRevisions, typeFilter, authorFilter, props.onRejectAllChanges, props.onRejectRevision]);
|
|
28
80
|
|
|
29
81
|
return (
|
|
30
|
-
<div className="outline-none">
|
|
31
|
-
|
|
82
|
+
<div className="flex flex-col outline-none">
|
|
83
|
+
{/* Stats header */}
|
|
84
|
+
<p className="px-3 pt-3 pb-1 text-[11px] text-[var(--color-text-tertiary)]">
|
|
32
85
|
{trackedChanges.pendingChangeIds.length} active · {trackedChanges.acceptedChangeIds.length} accepted · {trackedChanges.preserveOnlyChangeIds.length} preserve-only
|
|
33
86
|
</p>
|
|
34
87
|
|
|
35
|
-
{/*
|
|
36
|
-
<div className="
|
|
88
|
+
{/* Filter chip row */}
|
|
89
|
+
<div className="flex flex-wrap items-center gap-1 px-3 py-2 border-b border-[var(--color-border-subtle)]/60">
|
|
90
|
+
{(["all", "insertion", "deletion", "formatting"] as const).map((type) => (
|
|
91
|
+
<button
|
|
92
|
+
key={type}
|
|
93
|
+
type="button"
|
|
94
|
+
aria-pressed={typeFilter === type}
|
|
95
|
+
onClick={() => setTypeFilter(type)}
|
|
96
|
+
className={`inline-flex h-6 items-center rounded-[var(--radius-sm)] px-2 text-[11px] font-medium transition-colors
|
|
97
|
+
aria-pressed:bg-[var(--color-accent-soft)] aria-pressed:text-[var(--color-accent-primary)] aria-pressed:ring-1 aria-pressed:ring-[var(--color-accent-primary)]/20
|
|
98
|
+
text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]`}
|
|
99
|
+
>
|
|
100
|
+
{labelFor(type)}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
103
|
+
{authors.length > 1 && (
|
|
104
|
+
<select
|
|
105
|
+
value={authorFilter ?? ""}
|
|
106
|
+
onChange={(e) => setAuthorFilter(e.target.value || null)}
|
|
107
|
+
className="h-6 rounded-[var(--radius-sm)] border border-[var(--color-border-subtle)] bg-[var(--color-bg-canvas)] px-2 text-[11px] text-[var(--color-text-primary)]"
|
|
108
|
+
>
|
|
109
|
+
<option value="">All authors</option>
|
|
110
|
+
{authors.map((a) => (<option key={a} value={a}>{a}</option>))}
|
|
111
|
+
</select>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Revision list — scrollable */}
|
|
116
|
+
<div className="flex-1 overflow-y-auto px-3 py-2">
|
|
117
|
+
{filteredRevisions.length > 0 ? (
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
{filteredRevisions.map((rev) => {
|
|
120
|
+
const isActive = activeRevisionId === rev.revisionId;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
key={rev.revisionId}
|
|
125
|
+
role="button"
|
|
126
|
+
tabIndex={0}
|
|
127
|
+
className={`flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border/40 ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
128
|
+
onClick={() => props.onOpenRevision?.(rev)}
|
|
129
|
+
onKeyDown={(event) => {
|
|
130
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
props.onOpenRevision?.(rev);
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
137
|
+
rev.kind === "insertion" ? "bg-insert"
|
|
138
|
+
: rev.kind === "deletion" ? "bg-danger"
|
|
139
|
+
: "bg-tertiary"
|
|
140
|
+
}`} />
|
|
141
|
+
<div className="p-2 flex-1 min-w-0">
|
|
142
|
+
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
143
|
+
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
144
|
+
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
145
|
+
</div>
|
|
146
|
+
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
147
|
+
{rev.excerpt ? (
|
|
148
|
+
<p className={`text-[11px] ${
|
|
149
|
+
rev.kind === "insertion" ? "text-insert"
|
|
150
|
+
: rev.kind === "deletion" ? "text-danger line-through"
|
|
151
|
+
: "text-secondary"
|
|
152
|
+
}`}>
|
|
153
|
+
{rev.excerpt}
|
|
154
|
+
</p>
|
|
155
|
+
) : (
|
|
156
|
+
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
157
|
+
)}
|
|
158
|
+
{rev.detail ? (
|
|
159
|
+
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
160
|
+
) : null}
|
|
161
|
+
<div className="mt-2 flex gap-1.5">
|
|
162
|
+
{rev.actionability === "actionable" ? (
|
|
163
|
+
<>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
167
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
168
|
+
onClick={(e) => {
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
props.onAcceptRevision?.(rev.revisionId);
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<Check className="h-3 w-3" /> Accept
|
|
174
|
+
</button>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
disabled={!rev.canReject || rev.status === "rejected"}
|
|
178
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
179
|
+
onClick={(e) => {
|
|
180
|
+
e.stopPropagation();
|
|
181
|
+
props.onRejectRevision?.(rev.revisionId);
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<X className="h-3 w-3" /> Reject
|
|
185
|
+
</button>
|
|
186
|
+
</>
|
|
187
|
+
) : (
|
|
188
|
+
<span className="px-1.5 py-0.5 text-[10px] text-tertiary">Preserve-only</span>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
195
|
+
</div>
|
|
196
|
+
) : (
|
|
197
|
+
<p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border/40">
|
|
198
|
+
{trackedChanges.totalCount > 0
|
|
199
|
+
? (visibleRevisions.length > 0
|
|
200
|
+
? "No revisions match the current filter."
|
|
201
|
+
: "Switch to Full markup to see all tracked changes.")
|
|
202
|
+
: "Tracked change cards will appear here when present."}
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Sticky footer — bulk actions */}
|
|
208
|
+
<div className="sticky bottom-0 border-t border-[var(--color-border-subtle)]/60 bg-[var(--color-bg-sidebar)] px-3 py-2 flex items-center gap-2">
|
|
37
209
|
<button
|
|
38
210
|
type="button"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
211
|
+
onClick={handleAcceptAll}
|
|
212
|
+
disabled={filteredRevisions.length === 0}
|
|
213
|
+
className="inline-flex h-7 items-center rounded-[var(--radius-sm)] bg-[var(--color-accent-primary)] px-3 text-[11px] font-medium text-[var(--color-text-on-accent)] hover:bg-[var(--color-accent-primary-hover)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
42
214
|
>
|
|
43
|
-
Accept all
|
|
215
|
+
Accept all
|
|
44
216
|
</button>
|
|
45
217
|
<button
|
|
46
218
|
type="button"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
219
|
+
onClick={handleRejectAll}
|
|
220
|
+
disabled={filteredRevisions.length === 0}
|
|
221
|
+
className="inline-flex h-7 items-center rounded-[var(--radius-sm)] border border-[var(--color-semantic-error)]/35 px-3 text-[11px] font-medium text-[var(--color-semantic-error)] hover:bg-[var(--color-semantic-error-soft)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
50
222
|
>
|
|
51
223
|
Reject all
|
|
52
224
|
</button>
|
|
225
|
+
<span className="ml-auto text-[11px] text-[var(--color-text-tertiary)]">
|
|
226
|
+
{filteredRevisions.length} of {visibleRevisions.length}
|
|
227
|
+
</span>
|
|
53
228
|
</div>
|
|
54
|
-
|
|
55
|
-
{visibleRevisions.length > 0 ? (
|
|
56
|
-
<div className="space-y-2">
|
|
57
|
-
{visibleRevisions.map((rev) => {
|
|
58
|
-
const isActive = activeRevisionId === rev.revisionId;
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div
|
|
62
|
-
key={rev.revisionId}
|
|
63
|
-
role="button"
|
|
64
|
-
tabIndex={0}
|
|
65
|
-
className={`flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border/40 ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
66
|
-
onClick={() => props.onOpenRevision?.(rev)}
|
|
67
|
-
onKeyDown={(event) => {
|
|
68
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
69
|
-
event.preventDefault();
|
|
70
|
-
props.onOpenRevision?.(rev);
|
|
71
|
-
}
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
75
|
-
rev.kind === "insertion" ? "bg-insert"
|
|
76
|
-
: rev.kind === "deletion" ? "bg-danger"
|
|
77
|
-
: "bg-tertiary"
|
|
78
|
-
}`} />
|
|
79
|
-
<div className="p-2 flex-1 min-w-0">
|
|
80
|
-
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
81
|
-
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
82
|
-
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
83
|
-
</div>
|
|
84
|
-
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
85
|
-
{rev.excerpt ? (
|
|
86
|
-
<p className={`text-[11px] ${
|
|
87
|
-
rev.kind === "insertion" ? "text-insert"
|
|
88
|
-
: rev.kind === "deletion" ? "text-danger line-through"
|
|
89
|
-
: "text-secondary"
|
|
90
|
-
}`}>
|
|
91
|
-
{rev.excerpt}
|
|
92
|
-
</p>
|
|
93
|
-
) : (
|
|
94
|
-
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
95
|
-
)}
|
|
96
|
-
{rev.detail ? (
|
|
97
|
-
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
98
|
-
) : null}
|
|
99
|
-
<div className="mt-2 flex gap-1.5">
|
|
100
|
-
{rev.actionability === "actionable" ? (
|
|
101
|
-
<>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
105
|
-
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
106
|
-
onClick={(e) => {
|
|
107
|
-
e.stopPropagation();
|
|
108
|
-
props.onAcceptRevision?.(rev.revisionId);
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
<Check className="h-3 w-3" /> Accept
|
|
112
|
-
</button>
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
disabled={!rev.canReject || rev.status === "rejected"}
|
|
116
|
-
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
117
|
-
onClick={(e) => {
|
|
118
|
-
e.stopPropagation();
|
|
119
|
-
props.onRejectRevision?.(rev.revisionId);
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
122
|
-
<X className="h-3 w-3" /> Reject
|
|
123
|
-
</button>
|
|
124
|
-
</>
|
|
125
|
-
) : (
|
|
126
|
-
<span className="px-1.5 py-0.5 text-[10px] text-tertiary">Preserve-only</span>
|
|
127
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
})}
|
|
133
|
-
</div>
|
|
134
|
-
) : (
|
|
135
|
-
<p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border/40">
|
|
136
|
-
{trackedChanges.totalCount > 0
|
|
137
|
-
? "Switch to Full markup to see all tracked changes."
|
|
138
|
-
: "Tracked change cards will appear here when present."}
|
|
139
|
-
</p>
|
|
140
|
-
)}
|
|
141
229
|
</div>
|
|
142
230
|
);
|
|
143
231
|
}
|
|
@@ -15,6 +15,12 @@ export interface TwWorkflowTabProps {
|
|
|
15
15
|
segments: readonly ScopeRailSegment[];
|
|
16
16
|
activeScopeId?: string | null;
|
|
17
17
|
onOpenScope?: (segment: ScopeRailSegment) => void;
|
|
18
|
+
/**
|
|
19
|
+
* U6 — bidirectional rail↔scope focus sync. Called when the user
|
|
20
|
+
* clicks a workflow card so the scope card layer can activate the
|
|
21
|
+
* matching overlay card. If omitted, focus sync is not wired.
|
|
22
|
+
*/
|
|
23
|
+
onActiveScopeChange?: (scopeId: string) => void;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
const POSTURE_META: Record<
|
|
@@ -34,6 +40,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
34
40
|
segments,
|
|
35
41
|
activeScopeId,
|
|
36
42
|
onOpenScope,
|
|
43
|
+
onActiveScopeChange,
|
|
37
44
|
}) => {
|
|
38
45
|
// Dedupe by scopeId so a scope spanning multiple pages shows once.
|
|
39
46
|
const byScopeId = new Map<string, ScopeRailSegment>();
|
|
@@ -78,7 +85,10 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
78
85
|
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
86
|
isActive ? "ring-1 ring-accent/60" : ""
|
|
80
87
|
}`}
|
|
81
|
-
onClick={
|
|
88
|
+
onClick={() => {
|
|
89
|
+
onOpenScope?.(segment);
|
|
90
|
+
onActiveScopeChange?.(segment.scopeId);
|
|
91
|
+
}}
|
|
82
92
|
data-scope-id={segment.scopeId}
|
|
83
93
|
data-posture={segment.posture}
|
|
84
94
|
>
|