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