@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.
Files changed (103) hide show
  1. package/package.json +31 -40
  2. package/src/api/public-types.ts +67 -7
  3. package/src/io/chart-preview-resolver.ts +41 -0
  4. package/src/io/docx-session.ts +217 -23
  5. package/src/runtime/collab/checkpoint-store.ts +1 -1
  6. package/src/runtime/collab/event-types.ts +4 -0
  7. package/src/runtime/collab/runtime-collab-sync.ts +88 -8
  8. package/src/runtime/document-runtime.ts +182 -9
  9. package/src/runtime/layout/inert-layout-facet.ts +1 -0
  10. package/src/runtime/layout/layout-engine-version.ts +97 -2
  11. package/src/runtime/layout/layout-invalidation.ts +150 -30
  12. package/src/runtime/layout/page-graph.ts +19 -0
  13. package/src/runtime/layout/paginated-layout-engine.ts +128 -19
  14. package/src/runtime/layout/project-block-fragments.ts +27 -0
  15. package/src/runtime/layout/public-facet.ts +70 -1
  16. package/src/runtime/prerender/cache-envelope.ts +30 -0
  17. package/src/runtime/prerender/customxml-cache.ts +17 -3
  18. package/src/runtime/prerender/prerender-document.ts +17 -1
  19. package/src/runtime/render/render-frame-diff.ts +38 -2
  20. package/src/runtime/render/render-kernel.ts +67 -19
  21. package/src/runtime/surface-projection.ts +28 -0
  22. package/src/runtime/table-schema.ts +27 -0
  23. package/src/runtime/table-style-resolver.ts +51 -0
  24. package/src/ui/WordReviewEditor.tsx +6 -3
  25. package/src/ui/editor-runtime-boundary.ts +39 -2
  26. package/src/ui/headless/comment-decoration-model.ts +60 -5
  27. package/src/ui/headless/revision-decoration-model.ts +94 -6
  28. package/src/ui/shared/revision-filters.ts +16 -6
  29. package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
  30. package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
  31. package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
  32. package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
  33. package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
  34. package/src/ui-tailwind/chart/render/area.tsx +277 -0
  35. package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
  36. package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
  37. package/src/ui-tailwind/chart/render/combo.tsx +85 -0
  38. package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
  39. package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
  40. package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
  41. package/src/ui-tailwind/chart/render/line.tsx +363 -0
  42. package/src/ui-tailwind/chart/render/number-format.ts +120 -16
  43. package/src/ui-tailwind/chart/render/pie.tsx +275 -0
  44. package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
  45. package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
  46. package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
  47. package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
  48. package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
  49. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
  50. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
  51. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
  52. package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
  53. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
  54. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
  55. package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
  56. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
  57. package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
  58. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
  59. package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
  60. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
  61. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
  62. package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
  63. package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
  64. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
  65. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
  66. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
  67. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
  68. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
  69. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
  70. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
  71. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
  72. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
  73. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
  74. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
  75. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  76. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
  77. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
  78. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
  79. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
  80. package/src/ui-tailwind/index.ts +11 -0
  81. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
  82. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
  83. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
  84. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
  85. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
  86. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
  87. package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
  88. package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
  89. package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
  90. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
  91. package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
  92. package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
  93. package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
  94. package/src/ui-tailwind/theme/editor-theme.css +275 -46
  95. package/src/ui-tailwind/theme/tokens.css +345 -0
  96. package/src/ui-tailwind/theme/tokens.ts +313 -0
  97. package/src/ui-tailwind/theme/use-density.ts +60 -0
  98. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
  99. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
  100. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
  101. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
  102. package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
  103. 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
- <div className="mb-3 flex items-center gap-2 text-[11px] text-tertiary">
55
- <span>{comments.openCommentIds.length} open</span>
56
- <span className="text-border">·</span>
57
- <span>{comments.resolvedCommentIds.length} resolved</span>
58
- {comments.detachedCommentIds.length > 0 && (
59
- <>
60
- <span className="text-border">·</span>
61
- <span>{comments.detachedCommentIds.length} detached</span>
62
- </>
63
- )}
64
- </div>
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
- {comments.threads.map((thread) => (
68
- <CommentThreadCard
69
- key={thread.commentId}
70
- thread={thread}
71
- isActive={activeCommentId === thread.commentId}
72
- currentUserId={currentUserId}
73
- presentation={presentationByCommentId.get(thread.commentId)}
74
- resolveAttachmentHref={resolveAttachmentHref}
75
- onOpenComment={props.onOpenComment}
76
- onResolveComment={props.onResolveComment}
77
- onReopenComment={props.onReopenComment}
78
- onAddReply={props.onAddReply}
79
- onEditBody={props.onEditBody}
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
- <div className="rounded-lg bg-surface px-3 py-3 text-[11px] leading-5 text-tertiary ring-1 ring-border/40">
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" ? "opacity-70" : "",
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
- export function TwHealthPanel(props: TwHealthPanelProps) {
18
- const { compatibility, warnings, blockedReasons = [] } = props;
19
- const supportedCount = compatibility.featureEntries.filter(
20
- (e) => e.featureClass === "supported-roundtrip",
21
- ).length;
22
- const preserveOnlyCount = compatibility.featureEntries.filter(
23
- (e) => e.featureClass === "preserve-only",
24
- ).length;
25
- const blockedCount = compatibility.featureEntries.filter(
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 className="outline-none">
31
- <p className="text-xs text-tertiary mb-3">
32
- {supportedCount} supported · {preserveOnlyCount} preserve-only · {blockedCount} blocked
33
- {warnings.length > 0 ? ` · ${warnings.length} warning${warnings.length !== 1 ? "s" : ""}` : ""}
34
- </p>
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
- {compatibility.featureEntries.map((entry) => (
38
- <div key={entry.featureEntryId} className="flex rounded-lg transition-colors hover:bg-surface">
39
- {entry.featureClass !== "supported-roundtrip" ? (
40
- <div className={`w-0.5 shrink-0 rounded-l-lg ${
41
- entry.featureClass === "unsupported-fatal" ? "bg-danger" : "bg-comment"
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
- {warnings.map((warning) => (
58
- <div key={warning.warningId} className="flex rounded-lg transition-colors hover:bg-surface">
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
- {blockedReasons.length > 0 ? (
86
- <>
87
- <div className="border-t border-border mt-2 pt-2">
88
- <p className="text-xs font-medium text-tertiary mb-1">Workflow blocked reasons</p>
89
- </div>
90
- {blockedReasons.map((reason, index) => (
91
- <div key={`blocked-${index}`} className="flex rounded-lg transition-colors hover:bg-surface">
92
- <div className="w-0.5 shrink-0 rounded-l-lg bg-amber-400" />
93
- <div className="flex items-start gap-2 p-2.5 flex-1">
94
- <ShieldAlert className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />
95
- <div className="flex-1 min-w-0">
96
- <div className="flex items-start justify-between gap-2">
97
- <span className="text-sm font-medium text-primary">{reason.message}</span>
98
- <span className="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium text-amber-700 bg-amber-100">
99
- {reason.code.replace(/_/g, " ")}
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
- {compatibility.featureEntries.length === 0 && warnings.length === 0 && blockedReasons.length === 0 ? (
113
- <p className="text-xs text-tertiary py-4">
114
- No compatibility entries or warnings to display.
115
- </p>
116
- ) : null}
117
- </div>
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": "text-insert bg-insert-soft",
136
- "preserve-only": "text-comment bg-warning-soft",
137
- "unsupported-fatal": "text-danger bg-delete-soft",
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 className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${styles[props.featureClass]}`}>
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: "wre-rail-card block w-full text-left",
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-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}`;
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-[336px]";
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-tint)]",
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 className="shrink-0 px-4 pt-[18px] pb-3">
137
- <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-tertiary">
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}