@beyondwork/docx-react-component 1.0.53 → 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 +1 -1
- package/src/api/public-types.ts +35 -7
- package/src/io/docx-session.ts +30 -6
- 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 +23 -9
- 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/render/render-frame-diff.ts +38 -2
- package/src/ui/WordReviewEditor.tsx +6 -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/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- 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
|
@@ -16,128 +16,216 @@ export interface TwRevisionSidebarProps {
|
|
|
16
16
|
onRejectAllChanges?: () => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type TypeFilter = "all" | "insertion" | "deletion" | "formatting";
|
|
20
|
+
|
|
21
|
+
function labelFor(type: TypeFilter): string {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "all": return "All";
|
|
24
|
+
case "insertion": return "Insertions";
|
|
25
|
+
case "deletion": return "Deletions";
|
|
26
|
+
case "formatting": return "Formatting";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
const focusRingClass =
|
|
20
31
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
21
32
|
|
|
22
33
|
export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
23
34
|
const { trackedChanges, markupDisplay, activeRevisionId } = props;
|
|
24
35
|
const visibleRevisions = selectVisibleRevisions(trackedChanges.revisions, markupDisplay);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
36
|
+
|
|
37
|
+
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
|
38
|
+
const [authorFilter, setAuthorFilter] = React.useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
// Derive distinct authors from all visible revisions
|
|
41
|
+
const authors = React.useMemo(() => {
|
|
42
|
+
const seen = new Set<string>();
|
|
43
|
+
for (const r of visibleRevisions) {
|
|
44
|
+
if (r.authorId) seen.add(r.authorId);
|
|
45
|
+
}
|
|
46
|
+
return Array.from(seen).sort();
|
|
47
|
+
}, [visibleRevisions]);
|
|
48
|
+
|
|
49
|
+
// Filtered list based on type + author chips
|
|
50
|
+
const filteredRevisions = React.useMemo(() => {
|
|
51
|
+
return visibleRevisions.filter((r) => {
|
|
52
|
+
if (typeFilter !== "all") {
|
|
53
|
+
// Map filter chip value to revision kinds
|
|
54
|
+
if (typeFilter === "insertion" && r.kind !== "insertion") return false;
|
|
55
|
+
if (typeFilter === "deletion" && r.kind !== "deletion") return false;
|
|
56
|
+
if (typeFilter === "formatting" && r.kind !== "formatting") return false;
|
|
57
|
+
}
|
|
58
|
+
if (authorFilter !== null && r.authorId !== authorFilter) return false;
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}, [visibleRevisions, typeFilter, authorFilter]);
|
|
62
|
+
|
|
63
|
+
const handleAcceptAll = React.useCallback(() => {
|
|
64
|
+
if (props.onAcceptAllChanges && typeFilter === "all" && authorFilter === null) {
|
|
65
|
+
// Use dedicated bulk handler when no filter is active
|
|
66
|
+
props.onAcceptAllChanges();
|
|
67
|
+
} else {
|
|
68
|
+
filteredRevisions.forEach((r) => props.onAcceptRevision?.(r.revisionId));
|
|
69
|
+
}
|
|
70
|
+
}, [filteredRevisions, typeFilter, authorFilter, props.onAcceptAllChanges, props.onAcceptRevision]);
|
|
71
|
+
|
|
72
|
+
const handleRejectAll = React.useCallback(() => {
|
|
73
|
+
if (props.onRejectAllChanges && typeFilter === "all" && authorFilter === null) {
|
|
74
|
+
// Use dedicated bulk handler when no filter is active
|
|
75
|
+
props.onRejectAllChanges();
|
|
76
|
+
} else {
|
|
77
|
+
filteredRevisions.forEach((r) => props.onRejectRevision?.(r.revisionId));
|
|
78
|
+
}
|
|
79
|
+
}, [filteredRevisions, typeFilter, authorFilter, props.onRejectAllChanges, props.onRejectRevision]);
|
|
28
80
|
|
|
29
81
|
return (
|
|
30
|
-
<div className="outline-none">
|
|
31
|
-
|
|
82
|
+
<div className="flex flex-col outline-none">
|
|
83
|
+
{/* Stats header */}
|
|
84
|
+
<p className="px-3 pt-3 pb-1 text-[11px] text-[var(--color-text-tertiary)]">
|
|
32
85
|
{trackedChanges.pendingChangeIds.length} active · {trackedChanges.acceptedChangeIds.length} accepted · {trackedChanges.preserveOnlyChangeIds.length} preserve-only
|
|
33
86
|
</p>
|
|
34
87
|
|
|
35
|
-
{/*
|
|
36
|
-
<div className="
|
|
88
|
+
{/* Filter chip row */}
|
|
89
|
+
<div className="flex flex-wrap items-center gap-1 px-3 py-2 border-b border-[var(--color-border-subtle)]/60">
|
|
90
|
+
{(["all", "insertion", "deletion", "formatting"] as const).map((type) => (
|
|
91
|
+
<button
|
|
92
|
+
key={type}
|
|
93
|
+
type="button"
|
|
94
|
+
aria-pressed={typeFilter === type}
|
|
95
|
+
onClick={() => setTypeFilter(type)}
|
|
96
|
+
className={`inline-flex h-6 items-center rounded-[var(--radius-sm)] px-2 text-[11px] font-medium transition-colors
|
|
97
|
+
aria-pressed:bg-[var(--color-accent-soft)] aria-pressed:text-[var(--color-accent-primary)] aria-pressed:ring-1 aria-pressed:ring-[var(--color-accent-primary)]/20
|
|
98
|
+
text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)]`}
|
|
99
|
+
>
|
|
100
|
+
{labelFor(type)}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
103
|
+
{authors.length > 1 && (
|
|
104
|
+
<select
|
|
105
|
+
value={authorFilter ?? ""}
|
|
106
|
+
onChange={(e) => setAuthorFilter(e.target.value || null)}
|
|
107
|
+
className="h-6 rounded-[var(--radius-sm)] border border-[var(--color-border-subtle)] bg-[var(--color-bg-canvas)] px-2 text-[11px] text-[var(--color-text-primary)]"
|
|
108
|
+
>
|
|
109
|
+
<option value="">All authors</option>
|
|
110
|
+
{authors.map((a) => (<option key={a} value={a}>{a}</option>))}
|
|
111
|
+
</select>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Revision list — scrollable */}
|
|
116
|
+
<div className="flex-1 overflow-y-auto px-3 py-2">
|
|
117
|
+
{filteredRevisions.length > 0 ? (
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
{filteredRevisions.map((rev) => {
|
|
120
|
+
const isActive = activeRevisionId === rev.revisionId;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
key={rev.revisionId}
|
|
125
|
+
role="button"
|
|
126
|
+
tabIndex={0}
|
|
127
|
+
className={`flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border/40 ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
128
|
+
onClick={() => props.onOpenRevision?.(rev)}
|
|
129
|
+
onKeyDown={(event) => {
|
|
130
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
props.onOpenRevision?.(rev);
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
137
|
+
rev.kind === "insertion" ? "bg-insert"
|
|
138
|
+
: rev.kind === "deletion" ? "bg-danger"
|
|
139
|
+
: "bg-tertiary"
|
|
140
|
+
}`} />
|
|
141
|
+
<div className="p-2 flex-1 min-w-0">
|
|
142
|
+
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
143
|
+
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
144
|
+
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
145
|
+
</div>
|
|
146
|
+
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
147
|
+
{rev.excerpt ? (
|
|
148
|
+
<p className={`text-[11px] ${
|
|
149
|
+
rev.kind === "insertion" ? "text-insert"
|
|
150
|
+
: rev.kind === "deletion" ? "text-danger line-through"
|
|
151
|
+
: "text-secondary"
|
|
152
|
+
}`}>
|
|
153
|
+
{rev.excerpt}
|
|
154
|
+
</p>
|
|
155
|
+
) : (
|
|
156
|
+
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
157
|
+
)}
|
|
158
|
+
{rev.detail ? (
|
|
159
|
+
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
160
|
+
) : null}
|
|
161
|
+
<div className="mt-2 flex gap-1.5">
|
|
162
|
+
{rev.actionability === "actionable" ? (
|
|
163
|
+
<>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
167
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
168
|
+
onClick={(e) => {
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
props.onAcceptRevision?.(rev.revisionId);
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<Check className="h-3 w-3" /> Accept
|
|
174
|
+
</button>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
disabled={!rev.canReject || rev.status === "rejected"}
|
|
178
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
179
|
+
onClick={(e) => {
|
|
180
|
+
e.stopPropagation();
|
|
181
|
+
props.onRejectRevision?.(rev.revisionId);
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<X className="h-3 w-3" /> Reject
|
|
185
|
+
</button>
|
|
186
|
+
</>
|
|
187
|
+
) : (
|
|
188
|
+
<span className="px-1.5 py-0.5 text-[10px] text-tertiary">Preserve-only</span>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
195
|
+
</div>
|
|
196
|
+
) : (
|
|
197
|
+
<p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border/40">
|
|
198
|
+
{trackedChanges.totalCount > 0
|
|
199
|
+
? (visibleRevisions.length > 0
|
|
200
|
+
? "No revisions match the current filter."
|
|
201
|
+
: "Switch to Full markup to see all tracked changes.")
|
|
202
|
+
: "Tracked change cards will appear here when present."}
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Sticky footer — bulk actions */}
|
|
208
|
+
<div className="sticky bottom-0 border-t border-[var(--color-border-subtle)]/60 bg-[var(--color-bg-sidebar)] px-3 py-2 flex items-center gap-2">
|
|
37
209
|
<button
|
|
38
210
|
type="button"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
211
|
+
onClick={handleAcceptAll}
|
|
212
|
+
disabled={filteredRevisions.length === 0}
|
|
213
|
+
className="inline-flex h-7 items-center rounded-[var(--radius-sm)] bg-[var(--color-accent-primary)] px-3 text-[11px] font-medium text-[var(--color-text-on-accent)] hover:bg-[var(--color-accent-primary-hover)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
42
214
|
>
|
|
43
|
-
Accept all
|
|
215
|
+
Accept all
|
|
44
216
|
</button>
|
|
45
217
|
<button
|
|
46
218
|
type="button"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
219
|
+
onClick={handleRejectAll}
|
|
220
|
+
disabled={filteredRevisions.length === 0}
|
|
221
|
+
className="inline-flex h-7 items-center rounded-[var(--radius-sm)] border border-[var(--color-semantic-error)]/35 px-3 text-[11px] font-medium text-[var(--color-semantic-error)] hover:bg-[var(--color-semantic-error-soft)] disabled:opacity-40 disabled:cursor-not-allowed"
|
|
50
222
|
>
|
|
51
223
|
Reject all
|
|
52
224
|
</button>
|
|
225
|
+
<span className="ml-auto text-[11px] text-[var(--color-text-tertiary)]">
|
|
226
|
+
{filteredRevisions.length} of {visibleRevisions.length}
|
|
227
|
+
</span>
|
|
53
228
|
</div>
|
|
54
|
-
|
|
55
|
-
{visibleRevisions.length > 0 ? (
|
|
56
|
-
<div className="space-y-2">
|
|
57
|
-
{visibleRevisions.map((rev) => {
|
|
58
|
-
const isActive = activeRevisionId === rev.revisionId;
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div
|
|
62
|
-
key={rev.revisionId}
|
|
63
|
-
role="button"
|
|
64
|
-
tabIndex={0}
|
|
65
|
-
className={`flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border/40 ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
66
|
-
onClick={() => props.onOpenRevision?.(rev)}
|
|
67
|
-
onKeyDown={(event) => {
|
|
68
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
69
|
-
event.preventDefault();
|
|
70
|
-
props.onOpenRevision?.(rev);
|
|
71
|
-
}
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
75
|
-
rev.kind === "insertion" ? "bg-insert"
|
|
76
|
-
: rev.kind === "deletion" ? "bg-danger"
|
|
77
|
-
: "bg-tertiary"
|
|
78
|
-
}`} />
|
|
79
|
-
<div className="p-2 flex-1 min-w-0">
|
|
80
|
-
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
81
|
-
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
82
|
-
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
83
|
-
</div>
|
|
84
|
-
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
85
|
-
{rev.excerpt ? (
|
|
86
|
-
<p className={`text-[11px] ${
|
|
87
|
-
rev.kind === "insertion" ? "text-insert"
|
|
88
|
-
: rev.kind === "deletion" ? "text-danger line-through"
|
|
89
|
-
: "text-secondary"
|
|
90
|
-
}`}>
|
|
91
|
-
{rev.excerpt}
|
|
92
|
-
</p>
|
|
93
|
-
) : (
|
|
94
|
-
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
95
|
-
)}
|
|
96
|
-
{rev.detail ? (
|
|
97
|
-
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
98
|
-
) : null}
|
|
99
|
-
<div className="mt-2 flex gap-1.5">
|
|
100
|
-
{rev.actionability === "actionable" ? (
|
|
101
|
-
<>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
105
|
-
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
106
|
-
onClick={(e) => {
|
|
107
|
-
e.stopPropagation();
|
|
108
|
-
props.onAcceptRevision?.(rev.revisionId);
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
<Check className="h-3 w-3" /> Accept
|
|
112
|
-
</button>
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
disabled={!rev.canReject || rev.status === "rejected"}
|
|
116
|
-
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
117
|
-
onClick={(e) => {
|
|
118
|
-
e.stopPropagation();
|
|
119
|
-
props.onRejectRevision?.(rev.revisionId);
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
122
|
-
<X className="h-3 w-3" /> Reject
|
|
123
|
-
</button>
|
|
124
|
-
</>
|
|
125
|
-
) : (
|
|
126
|
-
<span className="px-1.5 py-0.5 text-[10px] text-tertiary">Preserve-only</span>
|
|
127
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
})}
|
|
133
|
-
</div>
|
|
134
|
-
) : (
|
|
135
|
-
<p className="rounded-lg bg-surface px-3 py-4 text-xs text-tertiary ring-1 ring-border/40">
|
|
136
|
-
{trackedChanges.totalCount > 0
|
|
137
|
-
? "Switch to Full markup to see all tracked changes."
|
|
138
|
-
: "Tracked change cards will appear here when present."}
|
|
139
|
-
</p>
|
|
140
|
-
)}
|
|
141
229
|
</div>
|
|
142
230
|
);
|
|
143
231
|
}
|
|
@@ -15,6 +15,12 @@ export interface TwWorkflowTabProps {
|
|
|
15
15
|
segments: readonly ScopeRailSegment[];
|
|
16
16
|
activeScopeId?: string | null;
|
|
17
17
|
onOpenScope?: (segment: ScopeRailSegment) => void;
|
|
18
|
+
/**
|
|
19
|
+
* U6 — bidirectional rail↔scope focus sync. Called when the user
|
|
20
|
+
* clicks a workflow card so the scope card layer can activate the
|
|
21
|
+
* matching overlay card. If omitted, focus sync is not wired.
|
|
22
|
+
*/
|
|
23
|
+
onActiveScopeChange?: (scopeId: string) => void;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
const POSTURE_META: Record<
|
|
@@ -34,6 +40,7 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
34
40
|
segments,
|
|
35
41
|
activeScopeId,
|
|
36
42
|
onOpenScope,
|
|
43
|
+
onActiveScopeChange,
|
|
37
44
|
}) => {
|
|
38
45
|
// Dedupe by scopeId so a scope spanning multiple pages shows once.
|
|
39
46
|
const byScopeId = new Map<string, ScopeRailSegment>();
|
|
@@ -78,7 +85,10 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
78
85
|
className={`wre-workflow-card flex flex-col gap-1 rounded-md border border-border/50 bg-canvas p-3 text-left transition-shadow hover:shadow-md ${
|
|
79
86
|
isActive ? "ring-1 ring-accent/60" : ""
|
|
80
87
|
}`}
|
|
81
|
-
onClick={
|
|
88
|
+
onClick={() => {
|
|
89
|
+
onOpenScope?.(segment);
|
|
90
|
+
onActiveScopeChange?.(segment.scopeId);
|
|
91
|
+
}}
|
|
82
92
|
data-scope-id={segment.scopeId}
|
|
83
93
|
data-posture={segment.posture}
|
|
84
94
|
>
|
|
@@ -33,8 +33,34 @@ export interface TwStatusBarProps {
|
|
|
33
33
|
* canvas backend is live vs. the empirical fallback. Absent = skip.
|
|
34
34
|
*/
|
|
35
35
|
measurementFidelity?: PublicMeasurementFidelity;
|
|
36
|
+
/**
|
|
37
|
+
* Lane 6b §6b.S4: opt-in diagnostics flag. When `true`, the status
|
|
38
|
+
* bar reveals the measurement-fidelity badge on the right zone.
|
|
39
|
+
* Hosts must pair this with `debugMode` in production — the fidelity
|
|
40
|
+
* badge is a maintainer affordance, not a user-facing surface.
|
|
41
|
+
*/
|
|
42
|
+
debugMode?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Lane 6b §6b.S4: explicit opt-in to render the fidelity badge when
|
|
45
|
+
* `debugMode` is on. Defaults to `false` so even a debug harness
|
|
46
|
+
* keeps the badge hidden unless a human asks for it.
|
|
47
|
+
*/
|
|
48
|
+
showFidelityBadge?: boolean;
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
/**
|
|
52
|
+
* TwStatusBar — bottom chrome strip, 28-px tall (designsystem §6.16).
|
|
53
|
+
*
|
|
54
|
+
* Three-zone CSS grid (Lane 6b §6b.S4):
|
|
55
|
+
* LEFT — save state dot + export state dot + comment / change counts
|
|
56
|
+
* CENTER — Page N of M indicator
|
|
57
|
+
* RIGHT — measurement fidelity badge (opt-in) + session-active label
|
|
58
|
+
*
|
|
59
|
+
* Status dots bind `--color-status-*` tokens (Lane 6a §3 semantic family)
|
|
60
|
+
* so the review / draft / paused / blocked colour ladder stays consistent
|
|
61
|
+
* with the workflow card grammar. The fidelity badge is diagnostic-only:
|
|
62
|
+
* hidden unless `debugMode && showFidelityBadge` are both `true`.
|
|
63
|
+
*/
|
|
38
64
|
export function TwStatusBar(props: TwStatusBarProps) {
|
|
39
65
|
const saveState = props.isExportBlocked
|
|
40
66
|
? "Read-only"
|
|
@@ -46,58 +72,100 @@ export function TwStatusBar(props: TwStatusBarProps) {
|
|
|
46
72
|
: props.preserveOnlyCount > 0
|
|
47
73
|
? "Warnings"
|
|
48
74
|
: "Ready";
|
|
75
|
+
|
|
76
|
+
const saveDotColor = props.isExportBlocked
|
|
77
|
+
? "bg-[var(--color-status-blocked)]"
|
|
78
|
+
: props.isDirty
|
|
79
|
+
? "bg-[var(--color-status-in-progress)]"
|
|
80
|
+
: "bg-[var(--color-status-ready)]";
|
|
81
|
+
const exportDotColor = props.isExportBlocked
|
|
82
|
+
? "bg-[var(--color-status-blocked)]"
|
|
83
|
+
: props.preserveOnlyCount > 0
|
|
84
|
+
? "bg-[var(--color-semantic-warning)]"
|
|
85
|
+
: "bg-[var(--color-status-ready)]";
|
|
86
|
+
|
|
87
|
+
const showFidelity =
|
|
88
|
+
props.measurementFidelity != null &&
|
|
89
|
+
props.debugMode === true &&
|
|
90
|
+
props.showFidelityBadge === true;
|
|
91
|
+
|
|
49
92
|
return (
|
|
50
93
|
<footer
|
|
51
94
|
data-testid="status-bar"
|
|
52
|
-
|
|
95
|
+
style={{
|
|
96
|
+
// Lane 6b §6b.U5 — density opt-in. Scales the 28 px base by the
|
|
97
|
+
// root `--space-density-multiplier` (compact 0.85 / standard 1 /
|
|
98
|
+
// comfortable 1.15). Hosts toggle via the `useDensity` hook
|
|
99
|
+
// from Lane 6a.
|
|
100
|
+
height:
|
|
101
|
+
"calc(28px * var(--space-density-multiplier, 1))",
|
|
102
|
+
}}
|
|
103
|
+
className={[
|
|
104
|
+
"grid shrink-0 grid-cols-[1fr_auto_1fr] items-center gap-3",
|
|
105
|
+
"border-t border-[var(--color-border-subtle)]",
|
|
106
|
+
"bg-[var(--color-bg-chrome)]/72",
|
|
107
|
+
"px-3 text-[11px] text-[var(--color-text-tertiary)]",
|
|
108
|
+
].join(" ")}
|
|
53
109
|
>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<span
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: "bg-accent"
|
|
74
|
-
}`}
|
|
75
|
-
/>
|
|
76
|
-
Export {exportState.toLowerCase()}
|
|
77
|
-
</span>
|
|
78
|
-
<span>
|
|
79
|
-
{props.commentCount} comment{props.commentCount !== 1 ? "s" : ""} ·{" "}
|
|
80
|
-
{props.changeCount} change{props.changeCount !== 1 ? "s" : ""}
|
|
81
|
-
</span>
|
|
82
|
-
{props.displayPageNumber != null && props.pageCount != null ? (
|
|
83
|
-
<span data-testid="status-bar-page-count">
|
|
84
|
-
Page {props.displayPageNumber} of {props.pageCount}
|
|
110
|
+
{/* LEFT zone — save + export + counts */}
|
|
111
|
+
<div
|
|
112
|
+
className="flex items-center gap-3"
|
|
113
|
+
data-region="left"
|
|
114
|
+
data-testid="status-bar__region-left"
|
|
115
|
+
>
|
|
116
|
+
<span className="flex items-center gap-1.5">
|
|
117
|
+
<span
|
|
118
|
+
className={`inline-block h-1.5 w-1.5 rounded-[var(--radius-pill)] ${saveDotColor}`}
|
|
119
|
+
data-testid="status-bar__save-dot"
|
|
120
|
+
/>
|
|
121
|
+
{saveState}
|
|
122
|
+
</span>
|
|
123
|
+
<span className="flex items-center gap-1.5">
|
|
124
|
+
<span
|
|
125
|
+
className={`inline-block h-1.5 w-1.5 rounded-[var(--radius-pill)] ${exportDotColor}`}
|
|
126
|
+
data-testid="status-bar__export-dot"
|
|
127
|
+
/>
|
|
128
|
+
Export {exportState.toLowerCase()}
|
|
85
129
|
</span>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
<span>
|
|
131
|
+
{props.commentCount} comment{props.commentCount !== 1 ? "s" : ""} ·{" "}
|
|
132
|
+
{props.changeCount} change{props.changeCount !== 1 ? "s" : ""}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* CENTER zone — page indicator */}
|
|
137
|
+
<div
|
|
138
|
+
className="flex items-center justify-center"
|
|
139
|
+
data-region="center"
|
|
140
|
+
data-testid="status-bar__region-center"
|
|
141
|
+
>
|
|
142
|
+
{props.displayPageNumber != null && props.pageCount != null ? (
|
|
143
|
+
<span data-testid="status-bar-page-count">
|
|
144
|
+
Page {props.displayPageNumber} of {props.pageCount}
|
|
145
|
+
</span>
|
|
146
|
+
) : null}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* RIGHT zone — fidelity badge (opt-in) + session */}
|
|
150
|
+
<div
|
|
151
|
+
className="flex items-center justify-end gap-2"
|
|
152
|
+
data-region="right"
|
|
153
|
+
data-testid="status-bar__region-right"
|
|
154
|
+
>
|
|
155
|
+
{showFidelity ? (
|
|
156
|
+
<span
|
|
157
|
+
data-testid="status-bar-measurement-fidelity"
|
|
158
|
+
data-fidelity={props.measurementFidelity}
|
|
159
|
+
className="uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]/70"
|
|
160
|
+
title={`Measurement fidelity: ${props.measurementFidelity}`}
|
|
161
|
+
>
|
|
162
|
+
{formatFidelityBadge(props.measurementFidelity!)}
|
|
163
|
+
</span>
|
|
164
|
+
) : null}
|
|
165
|
+
<span className="truncate text-[10px] uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]/80">
|
|
166
|
+
Session active
|
|
96
167
|
</span>
|
|
97
|
-
|
|
98
|
-
<span className="truncate text-[10px] uppercase tracking-[0.12em] text-tertiary/80">
|
|
99
|
-
Session active
|
|
100
|
-
</span>
|
|
168
|
+
</div>
|
|
101
169
|
</footer>
|
|
102
170
|
);
|
|
103
171
|
}
|