@handled-ai/design-system 0.18.1 → 0.18.2
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/dist/charts/chart.d.ts +1 -1
- package/dist/charts/index.d.ts +0 -1
- package/dist/charts/index.js +0 -1
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +1 -2
- package/dist/charts/pipeline-overview.js +1 -29
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/feedback-primitives.d.ts +21 -2
- package/dist/components/feedback-primitives.js +90 -6
- package/dist/components/feedback-primitives.js.map +1 -1
- package/dist/components/insights-filter-bar.d.ts +1 -2
- package/dist/components/insights-filter-bar.js +5 -13
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/metric-card.d.ts +1 -14
- package/dist/components/metric-card.js +0 -86
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/score-why-chips.js +26 -5
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +172 -7
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/index.d.ts +3 -9
- package/dist/index.js +0 -5
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +4 -1
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DQ_VuHac.d.ts → signal-priority-popover-DWaAMhPI.d.ts} +26 -2
- package/package.json +3 -1
- package/src/charts/index.ts +0 -1
- package/src/charts/pipeline-overview.tsx +1 -38
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +546 -0
- package/src/components/feedback-primitives.tsx +148 -26
- package/src/components/insights-filter-bar.tsx +4 -13
- package/src/components/metric-card.tsx +0 -82
- package/src/components/score-why-chips.tsx +28 -2
- package/src/components/signal-priority-popover.tsx +194 -3
- package/src/index.ts +1 -6
- package/src/prototype/prototype-config.ts +11 -1
- package/src/prototype/prototype-inbox-view.tsx +3 -0
- package/dist/charts/empty-chart-state.d.ts +0 -11
- package/dist/charts/empty-chart-state.js +0 -70
- package/dist/charts/empty-chart-state.js.map +0 -1
- package/dist/components/days-open-cell.d.ts +0 -16
- package/dist/components/days-open-cell.js +0 -73
- package/dist/components/days-open-cell.js.map +0 -1
- package/dist/components/detail-drawer.d.ts +0 -16
- package/dist/components/detail-drawer.js +0 -45
- package/dist/components/detail-drawer.js.map +0 -1
- package/dist/components/linked-entity-cell.d.ts +0 -14
- package/dist/components/linked-entity-cell.js +0 -96
- package/dist/components/linked-entity-cell.js.map +0 -1
- package/dist/components/pill.d.ts +0 -26
- package/dist/components/pill.js +0 -77
- package/dist/components/pill.js.map +0 -1
- package/dist/components/quick-segment.d.ts +0 -13
- package/dist/components/quick-segment.js +0 -96
- package/dist/components/quick-segment.js.map +0 -1
- package/src/charts/__tests__/insights-charts.test.tsx +0 -62
- package/src/charts/empty-chart-state.tsx +0 -44
- package/src/components/__tests__/insights-primitives.test.tsx +0 -117
- package/src/components/days-open-cell.tsx +0 -50
- package/src/components/detail-drawer.tsx +0 -60
- package/src/components/linked-entity-cell.tsx +0 -74
- package/src/components/pill.tsx +0 -67
- package/src/components/quick-segment.tsx +0 -68
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { ThumbsUp, ThumbsDown } from "lucide-react"
|
|
4
|
+
import { ThumbsUp, ThumbsDown, Check, Pencil } from "lucide-react"
|
|
5
5
|
import { cn } from "../lib/utils"
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -25,6 +25,19 @@ export interface FeedbackSubmitData {
|
|
|
25
25
|
detail: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Persisted feedback data from a previous submission, used to hydrate the
|
|
30
|
+
* footer into its "already submitted" visual state.
|
|
31
|
+
*/
|
|
32
|
+
export interface PersistedFeedbackData {
|
|
33
|
+
sentiment: "positive" | "negative"
|
|
34
|
+
reasonTop?: string
|
|
35
|
+
reasonSub?: string
|
|
36
|
+
pills?: string[]
|
|
37
|
+
detail?: string
|
|
38
|
+
ownershipLabel: "Your feedback" | "Team feedback"
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
/**
|
|
29
42
|
* Defines a tier-1 chip that may have tier-2 sub-chips.
|
|
30
43
|
*/
|
|
@@ -185,6 +198,13 @@ export interface FeedbackFooterProps {
|
|
|
185
198
|
negativeChips?: FeedbackChipTree[]
|
|
186
199
|
positiveChips?: string[]
|
|
187
200
|
className?: string
|
|
201
|
+
/** Pre-existing feedback to hydrate from (e.g. after page reload). */
|
|
202
|
+
initialFeedback?: PersistedFeedbackData | null
|
|
203
|
+
/** Label shown in the transient confirmation pill after submit. */
|
|
204
|
+
submittedLabel?: string
|
|
205
|
+
/** Stable key for syncing initialFeedback into local state. When this
|
|
206
|
+
* changes, the component resets to the new initialFeedback value. */
|
|
207
|
+
feedbackKey?: string
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
const SENTIMENT_BUTTON_ACTIVE: Record<"positive" | "negative", string> = {
|
|
@@ -205,6 +225,9 @@ export function FeedbackFooter({
|
|
|
205
225
|
negativeChips = [],
|
|
206
226
|
positiveChips = [],
|
|
207
227
|
className,
|
|
228
|
+
initialFeedback,
|
|
229
|
+
submittedLabel = "Saved",
|
|
230
|
+
feedbackKey,
|
|
208
231
|
}: FeedbackFooterProps) {
|
|
209
232
|
const [expanded, setExpanded] = React.useState(false)
|
|
210
233
|
const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)
|
|
@@ -214,6 +237,43 @@ export function FeedbackFooter({
|
|
|
214
237
|
const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(
|
|
215
238
|
null,
|
|
216
239
|
)
|
|
240
|
+
/** Transient "Saved" confirmation — shown after successful submit. */
|
|
241
|
+
const [submitted, setSubmitted] = React.useState(false)
|
|
242
|
+
/** Persisted feedback shown as a clickable indicator (survives reload). */
|
|
243
|
+
const [persisted, setPersisted] = React.useState<PersistedFeedbackData | null>(
|
|
244
|
+
initialFeedback ?? null,
|
|
245
|
+
)
|
|
246
|
+
/** Tracks whether the user is actively editing (to guard against prop overwrites). */
|
|
247
|
+
const [isEditing, setIsEditing] = React.useState(false)
|
|
248
|
+
/** Track the last synced feedbackKey to detect key changes. */
|
|
249
|
+
const lastKeyRef = React.useRef<string | undefined>(feedbackKey)
|
|
250
|
+
|
|
251
|
+
// Sync initialFeedback into local state via useEffect keyed on feedbackKey.
|
|
252
|
+
// When feedbackKey changes, reset to new target. Preserve active edits
|
|
253
|
+
// when feedbackKey stays the same.
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
const keyChanged = feedbackKey !== lastKeyRef.current
|
|
256
|
+
lastKeyRef.current = feedbackKey
|
|
257
|
+
|
|
258
|
+
if (keyChanged) {
|
|
259
|
+
// Key changed — full reset to new target
|
|
260
|
+
setPersisted(initialFeedback ?? null)
|
|
261
|
+
setSubmitted(false)
|
|
262
|
+
setExpanded(false)
|
|
263
|
+
setIsEditing(false)
|
|
264
|
+
if (initialFeedback) {
|
|
265
|
+
onFeedbackChange(initialFeedback.sentiment)
|
|
266
|
+
} else {
|
|
267
|
+
onFeedbackChange(null)
|
|
268
|
+
}
|
|
269
|
+
} else if (!isEditing) {
|
|
270
|
+
// Same key, not actively editing — safe to sync
|
|
271
|
+
setPersisted(initialFeedback ?? null)
|
|
272
|
+
if (initialFeedback) {
|
|
273
|
+
onFeedbackChange(initialFeedback.sentiment)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}, [initialFeedback, feedbackKey]) // eslint-disable-line react-hooks/exhaustive-deps -- reads isEditing as guard, not trigger
|
|
217
277
|
|
|
218
278
|
// Reset state when feedback collapses
|
|
219
279
|
const resetState = React.useCallback(() => {
|
|
@@ -223,6 +283,7 @@ export function FeedbackFooter({
|
|
|
223
283
|
setAdditionalPills([])
|
|
224
284
|
setDetailText("")
|
|
225
285
|
setActiveTreeIndex(null)
|
|
286
|
+
setIsEditing(false)
|
|
226
287
|
}, [])
|
|
227
288
|
|
|
228
289
|
const handleSentimentClick = React.useCallback(
|
|
@@ -231,10 +292,26 @@ export function FeedbackFooter({
|
|
|
231
292
|
// Reset chip state when switching sentiment, then expand
|
|
232
293
|
resetState()
|
|
233
294
|
setExpanded(true)
|
|
295
|
+
setSubmitted(false)
|
|
296
|
+
setPersisted(null)
|
|
297
|
+
setIsEditing(true)
|
|
234
298
|
},
|
|
235
299
|
[onFeedbackChange, resetState],
|
|
236
300
|
)
|
|
237
301
|
|
|
302
|
+
/** Open the persisted indicator for editing. */
|
|
303
|
+
const handlePersistedClick = React.useCallback(() => {
|
|
304
|
+
if (!persisted) return
|
|
305
|
+
onFeedbackChange(persisted.sentiment)
|
|
306
|
+
setSelectedTier1(persisted.reasonTop ?? null)
|
|
307
|
+
setSelectedTier2(persisted.reasonSub ?? null)
|
|
308
|
+
setAdditionalPills(persisted.pills ?? [])
|
|
309
|
+
setDetailText(persisted.detail ?? "")
|
|
310
|
+
setExpanded(true)
|
|
311
|
+
setSubmitted(false)
|
|
312
|
+
setIsEditing(true)
|
|
313
|
+
}, [persisted, onFeedbackChange])
|
|
314
|
+
|
|
238
315
|
const handleTier1Toggle = React.useCallback(
|
|
239
316
|
(chipLabel: string) => {
|
|
240
317
|
if (selectedTier1 === chipLabel) {
|
|
@@ -295,7 +372,16 @@ export function FeedbackFooter({
|
|
|
295
372
|
pills: additionalPills,
|
|
296
373
|
detail: detailText,
|
|
297
374
|
})
|
|
298
|
-
|
|
375
|
+
// Show transient "Saved" confirmation
|
|
376
|
+
setSubmitted(true)
|
|
377
|
+
// Collapse expansion but keep sentiment visible
|
|
378
|
+
setExpanded(false)
|
|
379
|
+
setSelectedTier1(null)
|
|
380
|
+
setSelectedTier2(null)
|
|
381
|
+
setAdditionalPills([])
|
|
382
|
+
setDetailText("")
|
|
383
|
+
setActiveTreeIndex(null)
|
|
384
|
+
setIsEditing(false)
|
|
299
385
|
}, [
|
|
300
386
|
feedback,
|
|
301
387
|
selectedTier1,
|
|
@@ -303,7 +389,6 @@ export function FeedbackFooter({
|
|
|
303
389
|
additionalPills,
|
|
304
390
|
detailText,
|
|
305
391
|
onSubmit,
|
|
306
|
-
resetState,
|
|
307
392
|
])
|
|
308
393
|
|
|
309
394
|
const handleCancel = React.useCallback(() => {
|
|
@@ -323,38 +408,75 @@ export function FeedbackFooter({
|
|
|
323
408
|
const activeTree =
|
|
324
409
|
activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null
|
|
325
410
|
|
|
411
|
+
// Determine if we should show the persisted indicator instead of bare buttons
|
|
412
|
+
const showPersistedIndicator = persisted && !expanded && !submitted
|
|
413
|
+
|
|
326
414
|
return (
|
|
327
415
|
<div className={cn("space-y-3", className)}>
|
|
328
416
|
{/* Sentiment buttons + meta text bar */}
|
|
329
417
|
<div className="flex items-center justify-between">
|
|
330
|
-
|
|
418
|
+
{showPersistedIndicator ? (
|
|
419
|
+
/* Persisted feedback indicator — clickable to reopen editor */
|
|
331
420
|
<button
|
|
332
421
|
type="button"
|
|
333
|
-
onClick={
|
|
334
|
-
className=
|
|
335
|
-
|
|
336
|
-
feedback === "positive"
|
|
337
|
-
? SENTIMENT_BUTTON_ACTIVE.positive
|
|
338
|
-
: SENTIMENT_BUTTON_IDLE,
|
|
339
|
-
)}
|
|
422
|
+
onClick={handlePersistedClick}
|
|
423
|
+
className="group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
|
424
|
+
data-testid="persisted-feedback-indicator"
|
|
340
425
|
>
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
onClick={() => handleSentimentClick("negative")}
|
|
347
|
-
className={cn(
|
|
348
|
-
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
349
|
-
feedback === "negative"
|
|
350
|
-
? SENTIMENT_BUTTON_ACTIVE.negative
|
|
351
|
-
: SENTIMENT_BUTTON_IDLE,
|
|
426
|
+
<span className="font-medium">{persisted.ownershipLabel}:</span>
|
|
427
|
+
{persisted.sentiment === "positive" ? (
|
|
428
|
+
<ThumbsUp className="h-[11px] w-[11px]" />
|
|
429
|
+
) : (
|
|
430
|
+
<ThumbsDown className="h-[11px] w-[11px]" />
|
|
352
431
|
)}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
432
|
+
{persisted.detail && (
|
|
433
|
+
<span className="max-w-[200px] truncate text-muted-foreground/70">
|
|
434
|
+
{persisted.detail}
|
|
435
|
+
</span>
|
|
436
|
+
)}
|
|
437
|
+
<Pencil className="h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
356
438
|
</button>
|
|
357
|
-
|
|
439
|
+
) : (
|
|
440
|
+
<div className="flex items-center gap-3">
|
|
441
|
+
<button
|
|
442
|
+
type="button"
|
|
443
|
+
onClick={() => handleSentimentClick("positive")}
|
|
444
|
+
className={cn(
|
|
445
|
+
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
446
|
+
feedback === "positive"
|
|
447
|
+
? SENTIMENT_BUTTON_ACTIVE.positive
|
|
448
|
+
: SENTIMENT_BUTTON_IDLE,
|
|
449
|
+
)}
|
|
450
|
+
>
|
|
451
|
+
<ThumbsUp className="h-[11px] w-[11px]" />
|
|
452
|
+
Helpful
|
|
453
|
+
</button>
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
onClick={() => handleSentimentClick("negative")}
|
|
457
|
+
className={cn(
|
|
458
|
+
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
459
|
+
feedback === "negative"
|
|
460
|
+
? SENTIMENT_BUTTON_ACTIVE.negative
|
|
461
|
+
: SENTIMENT_BUTTON_IDLE,
|
|
462
|
+
)}
|
|
463
|
+
>
|
|
464
|
+
<ThumbsDown className="h-[11px] w-[11px]" />
|
|
465
|
+
Not helpful
|
|
466
|
+
</button>
|
|
467
|
+
{/* Transient "Saved" confirmation pill */}
|
|
468
|
+
{submitted && feedback && (
|
|
469
|
+
<span
|
|
470
|
+
className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600"
|
|
471
|
+
role="status"
|
|
472
|
+
data-testid="feedback-submitted-pill"
|
|
473
|
+
>
|
|
474
|
+
<Check className="h-[11px] w-[11px]" />
|
|
475
|
+
{submittedLabel}
|
|
476
|
+
</span>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
358
480
|
{metaText && (
|
|
359
481
|
<span className="text-[11px] text-muted-foreground">{metaText}</span>
|
|
360
482
|
)}
|
|
@@ -23,7 +23,6 @@ export interface FilterDefinition {
|
|
|
23
23
|
|
|
24
24
|
export interface InsightsFilterBarProps {
|
|
25
25
|
filters: FilterDefinition[]
|
|
26
|
-
variant?: "default" | "compact"
|
|
27
26
|
values: Record<string, string>
|
|
28
27
|
onChange: (filterId: string, value: string) => void
|
|
29
28
|
onClearAll?: () => void
|
|
@@ -46,7 +45,6 @@ function InsightsFilterBar({
|
|
|
46
45
|
onChange,
|
|
47
46
|
onClearAll,
|
|
48
47
|
className,
|
|
49
|
-
variant = "default",
|
|
50
48
|
}: InsightsFilterBarProps) {
|
|
51
49
|
const showClearAll = onClearAll && hasNonDefaultValue(filters, values)
|
|
52
50
|
|
|
@@ -54,12 +52,11 @@ function InsightsFilterBar({
|
|
|
54
52
|
<div
|
|
55
53
|
data-slot="insights-filter-bar"
|
|
56
54
|
className={cn(
|
|
57
|
-
"flex flex-wrap items-center rounded-md border border-border bg-card shadow-sm",
|
|
58
|
-
variant === "compact" ? "gap-2 p-2" : "gap-3 p-4",
|
|
55
|
+
"flex flex-wrap items-center gap-3 rounded-md border border-border bg-card p-4 shadow-sm",
|
|
59
56
|
className
|
|
60
57
|
)}
|
|
61
58
|
>
|
|
62
|
-
<div className=
|
|
59
|
+
<div className="flex items-center gap-2">
|
|
63
60
|
<FilterIcon className="h-4 w-4 text-muted-foreground" />
|
|
64
61
|
<span className="text-sm font-medium text-muted-foreground">
|
|
65
62
|
Filters:
|
|
@@ -83,10 +80,7 @@ function InsightsFilterBar({
|
|
|
83
80
|
<Button
|
|
84
81
|
variant="outline"
|
|
85
82
|
size="sm"
|
|
86
|
-
className=
|
|
87
|
-
"gap-1.5 text-xs font-normal shadow-none",
|
|
88
|
-
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
89
|
-
)}
|
|
83
|
+
className="h-8 gap-1.5 text-xs font-normal shadow-none"
|
|
90
84
|
>
|
|
91
85
|
{IconComp ? (
|
|
92
86
|
<IconComp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
@@ -124,10 +118,7 @@ function InsightsFilterBar({
|
|
|
124
118
|
<Button
|
|
125
119
|
variant="ghost"
|
|
126
120
|
size="sm"
|
|
127
|
-
className=
|
|
128
|
-
"text-xs text-destructive hover:text-destructive",
|
|
129
|
-
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
130
|
-
)}
|
|
121
|
+
className="h-8 text-xs text-destructive hover:text-destructive"
|
|
131
122
|
onClick={onClearAll}
|
|
132
123
|
>
|
|
133
124
|
Clear All
|
|
@@ -24,88 +24,6 @@ export interface MetricCardProps {
|
|
|
24
24
|
showInfo?: boolean
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export interface KpiStripItem {
|
|
28
|
-
id?: string
|
|
29
|
-
label: React.ReactNode
|
|
30
|
-
value: React.ReactNode
|
|
31
|
-
unit?: React.ReactNode
|
|
32
|
-
subtitle?: React.ReactNode
|
|
33
|
-
change?: MetricCardProps["change"]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface KpiStripProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
37
|
-
items: KpiStripItem[]
|
|
38
|
-
columns?: 2 | 3 | 4
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function KpiStrip({ items, columns = 4, className, ...props }: KpiStripProps) {
|
|
42
|
-
return (
|
|
43
|
-
<div
|
|
44
|
-
data-slot="kpi-strip"
|
|
45
|
-
className={cn(
|
|
46
|
-
"grid gap-3 rounded-xl border border-border bg-card p-3 shadow-sm",
|
|
47
|
-
columns === 2 && "sm:grid-cols-2",
|
|
48
|
-
columns === 3 && "sm:grid-cols-3",
|
|
49
|
-
columns === 4 && "sm:grid-cols-2 lg:grid-cols-4",
|
|
50
|
-
className
|
|
51
|
-
)}
|
|
52
|
-
{...props}
|
|
53
|
-
>
|
|
54
|
-
{items.map((item, index) => {
|
|
55
|
-
const isGoodDirection = item.change
|
|
56
|
-
? item.change.isGood !== undefined
|
|
57
|
-
? item.change.isGood
|
|
58
|
-
: item.change.direction === "up"
|
|
59
|
-
: false
|
|
60
|
-
const ChangeIcon = item.change?.direction === "down" ? ArrowDown : ArrowUp
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div
|
|
64
|
-
key={item.id ?? index}
|
|
65
|
-
data-slot="kpi-strip-item"
|
|
66
|
-
className="min-w-0 rounded-lg bg-muted/40 px-3 py-2"
|
|
67
|
-
>
|
|
68
|
-
<div data-slot="kpi-strip-label" className="truncate text-xs font-medium text-muted-foreground">
|
|
69
|
-
{item.label}
|
|
70
|
-
</div>
|
|
71
|
-
<div className="mt-1 flex items-baseline gap-1">
|
|
72
|
-
<span data-slot="kpi-strip-value" className="truncate text-2xl font-bold tracking-tight text-foreground">
|
|
73
|
-
{item.value}
|
|
74
|
-
</span>
|
|
75
|
-
{item.unit ? (
|
|
76
|
-
<span data-slot="kpi-strip-unit" className="text-sm font-semibold text-muted-foreground">
|
|
77
|
-
{item.unit}
|
|
78
|
-
</span>
|
|
79
|
-
) : null}
|
|
80
|
-
</div>
|
|
81
|
-
{item.subtitle || item.change ? (
|
|
82
|
-
<div className="mt-1 flex items-center gap-2 text-xs">
|
|
83
|
-
{item.change ? (
|
|
84
|
-
<span
|
|
85
|
-
data-slot="kpi-strip-change"
|
|
86
|
-
className={cn(
|
|
87
|
-
"inline-flex items-center gap-0.5 font-semibold",
|
|
88
|
-
isGoodDirection ? "text-emerald-600" : "text-red-600"
|
|
89
|
-
)}
|
|
90
|
-
>
|
|
91
|
-
<ChangeIcon className="h-3 w-3 stroke-[3]" />
|
|
92
|
-
{item.change.value}
|
|
93
|
-
</span>
|
|
94
|
-
) : null}
|
|
95
|
-
{item.subtitle ? (
|
|
96
|
-
<span data-slot="kpi-strip-subtitle" className="truncate text-muted-foreground">
|
|
97
|
-
{item.subtitle}
|
|
98
|
-
</span>
|
|
99
|
-
) : null}
|
|
100
|
-
</div>
|
|
101
|
-
) : null}
|
|
102
|
-
</div>
|
|
103
|
-
)
|
|
104
|
-
})}
|
|
105
|
-
</div>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
27
|
export function MetricCard({
|
|
110
28
|
title,
|
|
111
29
|
value,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "lucide-react"
|
|
16
16
|
import type { LucideIcon } from "lucide-react"
|
|
17
17
|
import { FeedbackFooter } from "./feedback-primitives"
|
|
18
|
-
import type { FeedbackChipTree, FeedbackSubmitData } from "./feedback-primitives"
|
|
18
|
+
import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "./feedback-primitives"
|
|
19
19
|
import { cn } from "../lib/utils"
|
|
20
20
|
import type {
|
|
21
21
|
QueueItem,
|
|
@@ -266,6 +266,7 @@ function StructuredSignalRow({ item, bucketKey, signal, tone, onOpenSignalBucket
|
|
|
266
266
|
const IconComponent = resolveIcon(signal.signalTypeName)
|
|
267
267
|
const toneClass = tone ? (SIGNAL_TONE_CLASSES[tone] ?? DEFAULT_TONE_CLASS) : DEFAULT_TONE_CLASS
|
|
268
268
|
const isCombined = signal.signalTypeName === "combined_signal" && signal.components && signal.components.length > 0
|
|
269
|
+
const hasBalance = Boolean(signal.currentBalance || signal.balanceContext)
|
|
269
270
|
|
|
270
271
|
const rowContent = (
|
|
271
272
|
<>
|
|
@@ -304,6 +305,26 @@ function StructuredSignalRow({ item, bucketKey, signal, tone, onOpenSignalBucket
|
|
|
304
305
|
|
|
305
306
|
{/* Slot 5: Chevron */}
|
|
306
307
|
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5 group-hover:text-foreground/50" />
|
|
308
|
+
|
|
309
|
+
{/* Balance context strip — spans full row below grid columns */}
|
|
310
|
+
{hasBalance && (
|
|
311
|
+
<div
|
|
312
|
+
className="col-span-full mt-0.5 text-[10px] text-muted-foreground/70"
|
|
313
|
+
data-testid="balance-context-strip"
|
|
314
|
+
>
|
|
315
|
+
{signal.currentBalance && (
|
|
316
|
+
<span>
|
|
317
|
+
Current balance <span className="font-medium text-muted-foreground">{signal.currentBalance}</span>
|
|
318
|
+
</span>
|
|
319
|
+
)}
|
|
320
|
+
{signal.balanceContext && (
|
|
321
|
+
<span>
|
|
322
|
+
{signal.currentBalance ? " · " : ""}
|
|
323
|
+
{signal.balanceContext}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
307
328
|
</>
|
|
308
329
|
)
|
|
309
330
|
|
|
@@ -405,9 +426,11 @@ interface WhyCardProps {
|
|
|
405
426
|
panelId: string
|
|
406
427
|
onOpenSignalBucket?: ScoreWhyChipsProps["onOpenSignalBucket"]
|
|
407
428
|
onBucketFeedback?: (bucketKey: string, data: FeedbackSubmitData) => void
|
|
429
|
+
/** Persisted bucket-level feedback to hydrate from. */
|
|
430
|
+
initialBucketFeedback?: PersistedFeedbackData | null
|
|
408
431
|
}
|
|
409
432
|
|
|
410
|
-
function WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketFeedback }: WhyCardProps) {
|
|
433
|
+
function WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketFeedback, initialBucketFeedback }: WhyCardProps) {
|
|
411
434
|
const [showAll, setShowAll] = React.useState(false)
|
|
412
435
|
const [bucketFeedback, setBucketFeedback] = React.useState<"positive" | "negative" | null>(null)
|
|
413
436
|
const totalCount = bucket.signalCount ?? signals.length
|
|
@@ -488,6 +511,8 @@ function WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketF
|
|
|
488
511
|
negativeChips={BUCKET_NEGATIVE_CHIPS}
|
|
489
512
|
negativePrompt="Was this bucket useful?"
|
|
490
513
|
positivePrompt="Thanks! What was useful about this bucket?"
|
|
514
|
+
initialFeedback={initialBucketFeedback}
|
|
515
|
+
feedbackKey={bucket.key}
|
|
491
516
|
/>
|
|
492
517
|
</div>
|
|
493
518
|
)}
|
|
@@ -561,6 +586,7 @@ export function ScoreWhyChips({
|
|
|
561
586
|
panelId={selectedPanelId}
|
|
562
587
|
onOpenSignalBucket={onOpenSignalBucket}
|
|
563
588
|
onBucketFeedback={signalData.onBucketFeedback}
|
|
589
|
+
initialBucketFeedback={signalData.initialBucketFeedback?.[selectedBucket.key]}
|
|
564
590
|
/>
|
|
565
591
|
)}
|
|
566
592
|
</div>
|