@beyondwork/docx-react-component 1.0.52 → 1.0.54
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 +31 -40
- package/src/api/public-types.ts +67 -7
- package/src/io/chart-preview-resolver.ts +41 -0
- package/src/io/docx-session.ts +217 -23
- 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 +88 -8
- package/src/runtime/document-runtime.ts +182 -9
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +97 -2
- 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 +70 -1
- package/src/runtime/prerender/cache-envelope.ts +30 -0
- package/src/runtime/prerender/customxml-cache.ts +17 -3
- package/src/runtime/prerender/prerender-document.ts +17 -1
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/render/render-kernel.ts +67 -19
- package/src/runtime/surface-projection.ts +28 -0
- package/src/runtime/table-schema.ts +27 -0
- package/src/runtime/table-style-resolver.ts +51 -0
- package/src/ui/WordReviewEditor.tsx +6 -3
- package/src/ui/editor-runtime-boundary.ts +39 -2
- 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/perf-probe.ts +1 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
- 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/chart-palette-adapter.ts +57 -0
- package/src/ui-tailwind/theme/editor-theme.css +275 -46
- package/src/ui-tailwind/theme/tokens.css +345 -0
- package/src/ui-tailwind/theme/tokens.ts +313 -0
- package/src/ui-tailwind/theme/use-density.ts +60 -0
- 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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Check, CornerDownRight, RotateCcw } from "lucide-react";
|
|
2
|
+
import { Check, ChevronRight, CornerDownRight, RotateCcw } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { TwEmptyState } from "../chrome/tw-empty-state";
|
|
3
5
|
|
|
4
6
|
import type { CommentSidebarSnapshot, CommentSidebarThreadSnapshot } from "../../api/public-types";
|
|
5
7
|
import type {
|
|
@@ -49,41 +51,81 @@ export function TwCommentSidebar(props: TwCommentSidebarProps) {
|
|
|
49
51
|
return map;
|
|
50
52
|
}, [commentPresentations]);
|
|
51
53
|
|
|
54
|
+
// Partition threads into open / detached / resolved for ordered rendering
|
|
55
|
+
const openThreads = useMemo(
|
|
56
|
+
() => comments.threads.filter((t) => t.status === "open"),
|
|
57
|
+
[comments.threads],
|
|
58
|
+
);
|
|
59
|
+
const detachedThreads = useMemo(
|
|
60
|
+
() => comments.threads.filter((t) => t.status === "detached"),
|
|
61
|
+
[comments.threads],
|
|
62
|
+
);
|
|
63
|
+
const resolvedThreads = useMemo(
|
|
64
|
+
() => comments.threads.filter((t) => t.status === "resolved"),
|
|
65
|
+
[comments.threads],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const openCount = openThreads.length;
|
|
69
|
+
const resolvedCount = resolvedThreads.length;
|
|
70
|
+
const detachedCount = detachedThreads.length;
|
|
71
|
+
|
|
72
|
+
function renderThreadCard(thread: CommentSidebarThreadSnapshot) {
|
|
73
|
+
return (
|
|
74
|
+
<CommentThreadCard
|
|
75
|
+
key={thread.commentId}
|
|
76
|
+
thread={thread}
|
|
77
|
+
isActive={activeCommentId === thread.commentId}
|
|
78
|
+
currentUserId={currentUserId}
|
|
79
|
+
presentation={presentationByCommentId.get(thread.commentId)}
|
|
80
|
+
resolveAttachmentHref={resolveAttachmentHref}
|
|
81
|
+
onOpenComment={props.onOpenComment}
|
|
82
|
+
onResolveComment={props.onResolveComment}
|
|
83
|
+
onReopenComment={props.onReopenComment}
|
|
84
|
+
onAddReply={props.onAddReply}
|
|
85
|
+
onEditBody={props.onEditBody}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
return (
|
|
53
91
|
<div className="outline-none">
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
{/* Counts header row */}
|
|
93
|
+
{(openCount + resolvedCount + detachedCount > 0) && (
|
|
94
|
+
<div className="mb-3 flex items-center gap-2 px-3 py-2 text-[11px] text-[var(--color-text-tertiary)]">
|
|
95
|
+
<span>{openCount} open</span>
|
|
96
|
+
<span aria-hidden="true">·</span>
|
|
97
|
+
<span>{resolvedCount} resolved</span>
|
|
98
|
+
{detachedCount > 0 && (
|
|
99
|
+
<>
|
|
100
|
+
<span aria-hidden="true">·</span>
|
|
101
|
+
<span className="text-[var(--color-semantic-warning)]">{detachedCount} detached</span>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
65
106
|
{comments.threads.length > 0 ? (
|
|
66
107
|
<div className="space-y-2">
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
{/* 1. Open threads first */}
|
|
109
|
+
{openThreads.map(renderThreadCard)}
|
|
110
|
+
|
|
111
|
+
{/* 2. Detached threads — visually distinct with warning left-rule */}
|
|
112
|
+
{detachedThreads.map(renderThreadCard)}
|
|
113
|
+
|
|
114
|
+
{/* 3. Resolved threads under a collapsed disclosure */}
|
|
115
|
+
{resolvedThreads.length > 0 && (
|
|
116
|
+
<details className="group mt-2">
|
|
117
|
+
<summary className="cursor-pointer list-none px-3 py-1.5 text-[11px] font-medium text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] flex items-center gap-1.5">
|
|
118
|
+
<ChevronRight className="h-3 w-3 transition-transform group-open:rotate-90" />
|
|
119
|
+
Resolved ({resolvedThreads.length})
|
|
120
|
+
</summary>
|
|
121
|
+
<div className="space-y-2 mt-1">
|
|
122
|
+
{resolvedThreads.map(renderThreadCard)}
|
|
123
|
+
</div>
|
|
124
|
+
</details>
|
|
125
|
+
)}
|
|
82
126
|
</div>
|
|
83
127
|
) : (
|
|
84
|
-
<
|
|
85
|
-
No comment threads yet. Select text and add one from the toolbar.
|
|
86
|
-
</div>
|
|
128
|
+
<TwEmptyState body="No comment threads yet. Select text and add one from the toolbar." />
|
|
87
129
|
)}
|
|
88
130
|
</div>
|
|
89
131
|
);
|
|
@@ -138,7 +180,9 @@ function CommentThreadCard(props: {
|
|
|
138
180
|
isActive
|
|
139
181
|
? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]"
|
|
140
182
|
: "hover:bg-surface",
|
|
141
|
-
thread.status === "detached"
|
|
183
|
+
thread.status === "detached"
|
|
184
|
+
? "border-l-[3px] border-[var(--color-semantic-warning)] opacity-70 pl-2.5"
|
|
185
|
+
: "",
|
|
142
186
|
].join(" ")}
|
|
143
187
|
onClick={() => props.onOpenComment?.(thread)}
|
|
144
188
|
onKeyDown={(event) => {
|
|
@@ -154,13 +198,20 @@ function CommentThreadCard(props: {
|
|
|
154
198
|
{thread.createdBy.charAt(0).toUpperCase()}
|
|
155
199
|
</span>
|
|
156
200
|
<span className="truncate text-[10px] font-medium text-primary">{thread.createdBy}</span>
|
|
201
|
+
{thread.status === "detached" && (
|
|
202
|
+
<span
|
|
203
|
+
data-comment-thread-detached-chip="true"
|
|
204
|
+
className="inline-flex items-center rounded-full bg-[var(--color-semantic-warning-soft)] text-[var(--color-semantic-warning)] text-[9px] font-semibold uppercase tracking-[0.08em] px-1.5 py-0.5 ml-1.5"
|
|
205
|
+
>
|
|
206
|
+
Detached
|
|
207
|
+
</span>
|
|
208
|
+
)}
|
|
157
209
|
<span data-comment-thread-created-at="true" className="text-[9px] text-tertiary">
|
|
158
210
|
{formatCommentDate(thread.createdAt)}
|
|
159
211
|
</span>
|
|
160
212
|
<span className="flex-1" />
|
|
161
213
|
{isDraftThread ? <StatusBadge label="draft" tone="draft" /> : null}
|
|
162
214
|
{thread.status === "resolved" ? <StatusBadge label="resolved" tone="resolved" /> : null}
|
|
163
|
-
{thread.status === "detached" ? <StatusBadge label="detached" tone="detached" /> : null}
|
|
164
215
|
</div>
|
|
165
216
|
|
|
166
217
|
{/* Excerpt — anchored text from document */}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
EditorWarning,
|
|
8
8
|
WorkflowBlockedCommandReason,
|
|
9
9
|
} from "../../api/public-types";
|
|
10
|
+
import { TwEmptyState } from "../chrome/tw-empty-state";
|
|
10
11
|
|
|
11
12
|
export interface TwHealthPanelProps {
|
|
12
13
|
compatibility: CompatibilityPanelSnapshot;
|
|
@@ -14,127 +15,186 @@ export interface TwHealthPanelProps {
|
|
|
14
15
|
blockedReasons?: WorkflowBlockedCommandReason[];
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
(e) => e.featureClass === "unsupported-fatal",
|
|
27
|
-
).length;
|
|
18
|
+
type SeverityKey = "error" | "warning" | "info";
|
|
19
|
+
|
|
20
|
+
interface IssueRow {
|
|
21
|
+
id: string;
|
|
22
|
+
message: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
badge?: string;
|
|
25
|
+
icon: React.ReactNode;
|
|
26
|
+
}
|
|
28
27
|
|
|
28
|
+
function renderIssueRow(item: IssueRow, severityKey: SeverityKey) {
|
|
29
29
|
return (
|
|
30
|
-
<div
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
<div
|
|
31
|
+
key={item.id}
|
|
32
|
+
className="flex rounded-[var(--radius-sm)] transition-colors hover:bg-[var(--color-bg-hover)]"
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
className="w-[3px] shrink-0 rounded-l-[var(--radius-sm)]"
|
|
36
|
+
style={{ backgroundColor: `var(--color-semantic-${severityKey})` }}
|
|
37
|
+
/>
|
|
38
|
+
<div className="flex items-start gap-2 p-2.5 flex-1">
|
|
39
|
+
{item.icon}
|
|
40
|
+
<div className="flex-1 min-w-0">
|
|
41
|
+
<div className="flex items-start justify-between gap-2">
|
|
42
|
+
<span className="text-sm font-medium text-[var(--color-text-primary)]">
|
|
43
|
+
{item.message}
|
|
44
|
+
</span>
|
|
45
|
+
{item.badge != null && (
|
|
46
|
+
<span
|
|
47
|
+
className="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium"
|
|
48
|
+
style={{
|
|
49
|
+
color: `var(--color-semantic-${severityKey})`,
|
|
50
|
+
backgroundColor: `var(--color-semantic-${severityKey}-soft)`,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{item.badge}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
{item.detail != null && (
|
|
58
|
+
<p className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{item.detail}</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
35
65
|
|
|
66
|
+
function renderGroup(
|
|
67
|
+
items: IssueRow[],
|
|
68
|
+
label: string,
|
|
69
|
+
severityKey: SeverityKey,
|
|
70
|
+
) {
|
|
71
|
+
if (items.length === 0) return null;
|
|
72
|
+
return (
|
|
73
|
+
<section key={label}>
|
|
74
|
+
<header className="flex items-center gap-2 mb-1.5">
|
|
75
|
+
<span
|
|
76
|
+
className="inline-flex items-center rounded-[var(--radius-sm)] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.08em]"
|
|
77
|
+
style={{
|
|
78
|
+
backgroundColor: `var(--color-semantic-${severityKey}-soft)`,
|
|
79
|
+
color: `var(--color-semantic-${severityKey})`,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{label}
|
|
83
|
+
</span>
|
|
84
|
+
<span className="text-[10px] text-[var(--color-text-tertiary)]">{items.length}</span>
|
|
85
|
+
</header>
|
|
36
86
|
<div className="space-y-1">
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}`} />
|
|
43
|
-
) : null}
|
|
44
|
-
<div className="flex items-start gap-2 p-2.5 flex-1">
|
|
45
|
-
<HealthIcon featureClass={entry.featureClass} />
|
|
46
|
-
<div className="flex-1 min-w-0">
|
|
47
|
-
<div className="flex items-start justify-between gap-2">
|
|
48
|
-
<span className="text-sm font-medium text-primary">{entry.message}</span>
|
|
49
|
-
<FeatureClassBadge featureClass={entry.featureClass} />
|
|
50
|
-
</div>
|
|
51
|
-
<p className="text-xs text-tertiary mt-0.5">{entry.featureKey}</p>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
))}
|
|
87
|
+
{items.map((item) => renderIssueRow(item, severityKey))}
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<div className={`w-0.5 shrink-0 rounded-l-lg ${
|
|
60
|
-
warning.severity === "warning" ? "bg-comment" : "bg-accent"
|
|
61
|
-
}`} />
|
|
62
|
-
<div className="flex items-start gap-2 p-2.5 flex-1">
|
|
63
|
-
{warning.severity === "warning" ? (
|
|
64
|
-
<AlertTriangle className="h-4 w-4 text-comment shrink-0 mt-0.5" />
|
|
65
|
-
) : (
|
|
66
|
-
<Info className="h-4 w-4 text-accent shrink-0 mt-0.5" />
|
|
67
|
-
)}
|
|
68
|
-
<div className="flex-1 min-w-0">
|
|
69
|
-
<div className="flex items-start justify-between gap-2">
|
|
70
|
-
<span className="text-sm font-medium text-primary">{warning.message}</span>
|
|
71
|
-
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${
|
|
72
|
-
warning.severity === "warning"
|
|
73
|
-
? "text-comment bg-warning-soft"
|
|
74
|
-
: "text-accent bg-accent-soft"
|
|
75
|
-
}`}>
|
|
76
|
-
{warning.code.replace(/_/g, " ")}
|
|
77
|
-
</span>
|
|
78
|
-
</div>
|
|
79
|
-
<p className="text-xs text-tertiary mt-0.5">{warning.source}</p>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
))}
|
|
93
|
+
export function TwHealthPanel(props: TwHealthPanelProps) {
|
|
94
|
+
const { compatibility, warnings, blockedReasons = [] } = props;
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</span>
|
|
101
|
-
</div>
|
|
102
|
-
{reason.scopeId ? (
|
|
103
|
-
<p className="text-xs text-tertiary mt-0.5">scope: {reason.scopeId}</p>
|
|
104
|
-
) : null}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
))}
|
|
109
|
-
</>
|
|
110
|
-
) : null}
|
|
96
|
+
// Blocked export group: unsupported-fatal entries
|
|
97
|
+
const blockedExportItems: IssueRow[] = compatibility.featureEntries
|
|
98
|
+
.filter((e) => e.featureClass === "unsupported-fatal")
|
|
99
|
+
.map((e) => ({
|
|
100
|
+
id: e.featureEntryId,
|
|
101
|
+
message: e.message,
|
|
102
|
+
detail: e.featureKey,
|
|
103
|
+
badge: "blocked",
|
|
104
|
+
icon: (
|
|
105
|
+
<ShieldAlert
|
|
106
|
+
className="h-4 w-4 shrink-0 mt-0.5"
|
|
107
|
+
style={{ color: "var(--color-semantic-error)" }}
|
|
108
|
+
/>
|
|
109
|
+
),
|
|
110
|
+
}));
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
// Warning group: preserve-only entries + workflow blocked reasons + "warning"-severity EditorWarnings
|
|
113
|
+
const warningItems: IssueRow[] = [
|
|
114
|
+
...compatibility.featureEntries
|
|
115
|
+
.filter((e) => e.featureClass === "preserve-only")
|
|
116
|
+
.map((e) => ({
|
|
117
|
+
id: e.featureEntryId,
|
|
118
|
+
message: e.message,
|
|
119
|
+
detail: e.featureKey,
|
|
120
|
+
badge: "preserve-only",
|
|
121
|
+
icon: (
|
|
122
|
+
<Shield
|
|
123
|
+
className="h-4 w-4 shrink-0 mt-0.5"
|
|
124
|
+
style={{ color: "var(--color-semantic-warning)" }}
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
})),
|
|
128
|
+
...blockedReasons.map((r, index) => ({
|
|
129
|
+
id: `blocked-reason-${index}`,
|
|
130
|
+
message: r.message,
|
|
131
|
+
detail: r.scopeId != null ? `scope: ${r.scopeId}` : undefined,
|
|
132
|
+
badge: r.code.replace(/_/g, " "),
|
|
133
|
+
icon: (
|
|
134
|
+
<ShieldAlert
|
|
135
|
+
className="h-4 w-4 shrink-0 mt-0.5"
|
|
136
|
+
style={{ color: "var(--color-semantic-warning)" }}
|
|
137
|
+
/>
|
|
138
|
+
),
|
|
139
|
+
})),
|
|
140
|
+
...warnings
|
|
141
|
+
.filter((w) => w.severity === "warning")
|
|
142
|
+
.map((w) => ({
|
|
143
|
+
id: w.warningId,
|
|
144
|
+
message: w.message,
|
|
145
|
+
detail: w.source,
|
|
146
|
+
badge: w.code.replace(/_/g, " "),
|
|
147
|
+
icon: (
|
|
148
|
+
<AlertTriangle
|
|
149
|
+
className="h-4 w-4 shrink-0 mt-0.5"
|
|
150
|
+
style={{ color: "var(--color-semantic-warning)" }}
|
|
151
|
+
/>
|
|
152
|
+
),
|
|
153
|
+
})),
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
// Info group: "info"-severity EditorWarnings
|
|
157
|
+
const infoItems: IssueRow[] = warnings
|
|
158
|
+
.filter((w) => w.severity === "info")
|
|
159
|
+
.map((w) => ({
|
|
160
|
+
id: w.warningId,
|
|
161
|
+
message: w.message,
|
|
162
|
+
detail: w.source,
|
|
163
|
+
badge: w.code.replace(/_/g, " "),
|
|
164
|
+
icon: (
|
|
165
|
+
<Info
|
|
166
|
+
className="h-4 w-4 shrink-0 mt-0.5"
|
|
167
|
+
style={{ color: "var(--color-semantic-info)" }}
|
|
168
|
+
/>
|
|
169
|
+
),
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const hasAnyIssue =
|
|
173
|
+
blockedExportItems.length > 0 || warningItems.length > 0 || infoItems.length > 0;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="outline-none">
|
|
177
|
+
{!hasAnyIssue ? (
|
|
178
|
+
<TwEmptyState body="All export checks passed." />
|
|
179
|
+
) : (
|
|
180
|
+
<div className="space-y-3">
|
|
181
|
+
{renderGroup(blockedExportItems, "Blocked export", "error")}
|
|
182
|
+
{renderGroup(warningItems, "Warning", "warning")}
|
|
183
|
+
{renderGroup(infoItems, "Info", "info")}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
118
186
|
</div>
|
|
119
187
|
);
|
|
120
188
|
}
|
|
121
189
|
|
|
122
|
-
function HealthIcon(props: { featureClass: CompatibilityFeatureEntry["featureClass"] }) {
|
|
123
|
-
switch (props.featureClass) {
|
|
124
|
-
case "supported-roundtrip":
|
|
125
|
-
return <ShieldCheck className="h-4 w-4 text-insert shrink-0 mt-0.5" />;
|
|
126
|
-
case "preserve-only":
|
|
127
|
-
return <Shield className="h-4 w-4 text-comment shrink-0 mt-0.5" />;
|
|
128
|
-
case "unsupported-fatal":
|
|
129
|
-
return <ShieldAlert className="h-4 w-4 text-danger shrink-0 mt-0.5" />;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
190
|
function FeatureClassBadge(props: { featureClass: CompatibilityFeatureEntry["featureClass"] }) {
|
|
134
191
|
const styles: Record<string, string> = {
|
|
135
|
-
"supported-roundtrip":
|
|
136
|
-
|
|
137
|
-
"
|
|
192
|
+
"supported-roundtrip":
|
|
193
|
+
"text-[var(--color-semantic-success)] bg-[var(--color-semantic-success-soft)]",
|
|
194
|
+
"preserve-only":
|
|
195
|
+
"text-[var(--color-semantic-warning)] bg-[var(--color-semantic-warning-soft)]",
|
|
196
|
+
"unsupported-fatal":
|
|
197
|
+
"text-[var(--color-semantic-error)] bg-[var(--color-semantic-error-soft)]",
|
|
138
198
|
};
|
|
139
199
|
const labels: Record<string, string> = {
|
|
140
200
|
"supported-roundtrip": "supported",
|
|
@@ -142,8 +202,13 @@ function FeatureClassBadge(props: { featureClass: CompatibilityFeatureEntry["fea
|
|
|
142
202
|
"unsupported-fatal": "blocked",
|
|
143
203
|
};
|
|
144
204
|
return (
|
|
145
|
-
<span
|
|
205
|
+
<span
|
|
206
|
+
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${styles[props.featureClass] ?? ""}`}
|
|
207
|
+
>
|
|
146
208
|
{labels[props.featureClass]}
|
|
147
209
|
</span>
|
|
148
210
|
);
|
|
149
211
|
}
|
|
212
|
+
|
|
213
|
+
// Keep FeatureClassBadge exported for potential external use
|
|
214
|
+
export { FeatureClassBadge };
|
|
@@ -51,6 +51,12 @@ export interface TwRailCardProps {
|
|
|
51
51
|
onClick?: () => void;
|
|
52
52
|
onSelect?: () => void;
|
|
53
53
|
isActive?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* U6 — bidirectional rail↔scope focus sync. When `true` the card
|
|
56
|
+
* receives a subtle focus ring (accent-primary/30) to signal that its
|
|
57
|
+
* matching scope card is currently open in the overlay.
|
|
58
|
+
*/
|
|
59
|
+
isFocused?: boolean;
|
|
54
60
|
dataTestId?: string;
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -69,6 +75,7 @@ export function TwRailCard(props: TwRailCardProps) {
|
|
|
69
75
|
onClick,
|
|
70
76
|
onSelect,
|
|
71
77
|
isActive,
|
|
78
|
+
isFocused,
|
|
72
79
|
dataTestId,
|
|
73
80
|
} = props;
|
|
74
81
|
|
|
@@ -80,9 +87,10 @@ export function TwRailCard(props: TwRailCardProps) {
|
|
|
80
87
|
: 0;
|
|
81
88
|
|
|
82
89
|
const commonProps: Record<string, unknown> = {
|
|
83
|
-
className:
|
|
90
|
+
className: `wre-rail-card block w-full text-left${isFocused ? " ring-1 ring-[var(--color-accent-primary)]/30" : ""}`,
|
|
84
91
|
"data-tone": tone,
|
|
85
92
|
"data-active": isActive ? "true" : "false",
|
|
93
|
+
"data-focused": isFocused ? "true" : undefined,
|
|
86
94
|
"data-testid": dataTestId,
|
|
87
95
|
};
|
|
88
96
|
|
|
@@ -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}
|