@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
|
@@ -27,6 +27,11 @@ export interface TwPageFooterBandProps {
|
|
|
27
27
|
widthPx: number;
|
|
28
28
|
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
29
29
|
isActiveSlot: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Lane 6d.U1 — section label for the active-band ribbon (e.g. "Footer — Section 1").
|
|
32
|
+
* Only rendered when `isActiveSlot` is true.
|
|
33
|
+
*/
|
|
34
|
+
sectionLabel?: string;
|
|
30
35
|
onClick: () => void;
|
|
31
36
|
"data-testid"?: string;
|
|
32
37
|
}
|
|
@@ -39,13 +44,16 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
39
44
|
leftPx,
|
|
40
45
|
widthPx,
|
|
41
46
|
isActiveSlot,
|
|
47
|
+
sectionLabel,
|
|
42
48
|
onClick,
|
|
43
49
|
"data-testid": testId,
|
|
44
50
|
}) => {
|
|
45
51
|
return (
|
|
46
52
|
<div
|
|
53
|
+
className="wre-page-band"
|
|
47
54
|
data-page-band="footer"
|
|
48
55
|
data-page-index={pageIndex}
|
|
56
|
+
data-active={isActiveSlot ? "true" : undefined}
|
|
49
57
|
data-testid={testId}
|
|
50
58
|
onClick={onClick}
|
|
51
59
|
style={{
|
|
@@ -57,6 +65,11 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
57
65
|
cursor: "pointer",
|
|
58
66
|
}}
|
|
59
67
|
>
|
|
68
|
+
{isActiveSlot && sectionLabel ? (
|
|
69
|
+
<span className="wre-page-band__label" data-kind="page-band-label">
|
|
70
|
+
{sectionLabel}
|
|
71
|
+
</span>
|
|
72
|
+
) : null}
|
|
60
73
|
{isActiveSlot ? (
|
|
61
74
|
<div
|
|
62
75
|
data-pm-portal-slot
|
|
@@ -28,6 +28,11 @@ export interface TwPageHeaderBandProps {
|
|
|
28
28
|
widthPx: number;
|
|
29
29
|
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
30
30
|
isActiveSlot: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Lane 6d.U1 — section label for the active-band ribbon (e.g. "Header — Section 1").
|
|
33
|
+
* Only rendered when `isActiveSlot` is true.
|
|
34
|
+
*/
|
|
35
|
+
sectionLabel?: string;
|
|
31
36
|
onClick: () => void;
|
|
32
37
|
"data-testid"?: string;
|
|
33
38
|
}
|
|
@@ -40,13 +45,16 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
40
45
|
leftPx,
|
|
41
46
|
widthPx,
|
|
42
47
|
isActiveSlot,
|
|
48
|
+
sectionLabel,
|
|
43
49
|
onClick,
|
|
44
50
|
"data-testid": testId,
|
|
45
51
|
}) => {
|
|
46
52
|
return (
|
|
47
53
|
<div
|
|
54
|
+
className="wre-page-band"
|
|
48
55
|
data-page-band="header"
|
|
49
56
|
data-page-index={pageIndex}
|
|
57
|
+
data-active={isActiveSlot ? "true" : undefined}
|
|
50
58
|
data-testid={testId}
|
|
51
59
|
onClick={onClick}
|
|
52
60
|
style={{
|
|
@@ -58,6 +66,11 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
58
66
|
cursor: "pointer",
|
|
59
67
|
}}
|
|
60
68
|
>
|
|
69
|
+
{isActiveSlot && sectionLabel ? (
|
|
70
|
+
<span className="wre-page-band__label" data-kind="page-band-label">
|
|
71
|
+
{sectionLabel}
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
61
74
|
{isActiveSlot ? (
|
|
62
75
|
<div
|
|
63
76
|
data-pm-portal-slot
|
|
@@ -361,6 +361,14 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
361
361
|
ref={overlayRootRef}
|
|
362
362
|
data-page-stack-chrome-layer=""
|
|
363
363
|
data-testid={testId ?? "page-stack-chrome-layer"}
|
|
364
|
+
// L6d.U1 — expose the active-story kind so the CSS can dim the
|
|
365
|
+
// body when an H/F story is being edited and un-dim again when
|
|
366
|
+
// the active story is the main body.
|
|
367
|
+
data-story-active={
|
|
368
|
+
activeStory.kind === "header" || activeStory.kind === "footer"
|
|
369
|
+
? activeStory.kind
|
|
370
|
+
: undefined
|
|
371
|
+
}
|
|
364
372
|
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
|
|
365
373
|
>
|
|
366
374
|
{rects.map((rect, pageIndex) => {
|
|
@@ -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
|
|