@handled-ai/design-system 0.17.1 → 0.18.1
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/empty-chart-state.d.ts +11 -0
- package/dist/charts/empty-chart-state.js +70 -0
- package/dist/charts/empty-chart-state.js.map +1 -0
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +2 -1
- package/dist/charts/pipeline-overview.js +29 -1
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/actor-byline.d.ts +3 -0
- package/dist/components/actor-byline.js +5 -0
- package/dist/components/actor-byline.js.map +1 -0
- package/dist/components/days-open-cell.d.ts +16 -0
- package/dist/components/days-open-cell.js +73 -0
- package/dist/components/days-open-cell.js.map +1 -0
- package/dist/components/detail-drawer.d.ts +16 -0
- package/dist/components/detail-drawer.js +45 -0
- package/dist/components/detail-drawer.js.map +1 -0
- package/dist/components/feedback-primitives.d.ts +66 -0
- package/dist/components/feedback-primitives.js +295 -0
- package/dist/components/feedback-primitives.js.map +1 -0
- package/dist/components/insights-filter-bar.d.ts +2 -1
- package/dist/components/insights-filter-bar.js +13 -5
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +14 -0
- package/dist/components/linked-entity-cell.js +96 -0
- package/dist/components/linked-entity-cell.js.map +1 -0
- package/dist/components/metric-card.d.ts +14 -1
- package/dist/components/metric-card.js +86 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/performance-metrics-table.d.ts +2 -1
- package/dist/components/performance-metrics-table.js +78 -46
- package/dist/components/performance-metrics-table.js.map +1 -1
- package/dist/components/pill.d.ts +26 -0
- package/dist/components/pill.js +77 -0
- package/dist/components/pill.js.map +1 -0
- package/dist/components/quick-segment.d.ts +13 -0
- package/dist/components/quick-segment.js +96 -0
- package/dist/components/quick-segment.js.map +1 -0
- package/dist/components/score-why-chips.d.ts +8 -17
- package/dist/components/score-why-chips.js +266 -180
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +17 -0
- package/dist/components/signal-priority-popover.js +247 -0
- package/dist/components/signal-priority-popover.js.map +1 -0
- package/dist/components/user-display.d.ts +22 -0
- package/dist/components/user-display.js +138 -0
- package/dist/components/user-display.js.map +1 -0
- package/dist/components/user-pill.d.ts +3 -0
- package/dist/components/user-pill.js +5 -0
- package/dist/components/user-pill.js.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/user-display.d.ts +31 -0
- package/dist/lib/user-display.js +57 -0
- package/dist/lib/user-display.js.map +1 -0
- package/dist/prototype/index.d.ts +2 -1
- package/dist/prototype/prototype-accounts-view.d.ts +2 -1
- package/dist/prototype/prototype-admin-view.d.ts +2 -1
- package/dist/prototype/prototype-config.d.ts +15 -332
- package/dist/prototype/prototype-inbox-view.d.ts +2 -1
- package/dist/prototype/prototype-inbox-view.js +11 -12
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +2 -1
- package/dist/prototype/prototype-shell.d.ts +2 -1
- package/dist/signal-priority-popover-DQ_VuHac.d.ts +390 -0
- package/package.json +1 -1
- package/src/charts/__tests__/insights-charts.test.tsx +62 -0
- package/src/charts/empty-chart-state.tsx +44 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/pipeline-overview.tsx +38 -1
- package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +99 -188
- package/src/components/__tests__/feedback-primitives.test.tsx +509 -0
- package/src/components/__tests__/insights-primitives.test.tsx +117 -0
- package/src/components/__tests__/performance-metrics-table.test.tsx +54 -0
- package/src/components/__tests__/score-why-chips.test.tsx +540 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +312 -0
- package/src/components/__tests__/user-display.test.tsx +75 -0
- package/src/components/actor-byline.tsx +1 -0
- package/src/components/days-open-cell.tsx +50 -0
- package/src/components/detail-drawer.tsx +60 -0
- package/src/components/feedback-primitives.tsx +424 -0
- package/src/components/insights-filter-bar.tsx +13 -4
- package/src/components/linked-entity-cell.tsx +74 -0
- package/src/components/metric-card.tsx +82 -0
- package/src/components/performance-metrics-table.tsx +99 -63
- package/src/components/pill.tsx +67 -0
- package/src/components/quick-segment.tsx +68 -0
- package/src/components/score-why-chips.tsx +413 -203
- package/src/components/signal-priority-popover.tsx +359 -0
- package/src/components/user-display.tsx +96 -0
- package/src/components/user-pill.tsx +1 -0
- package/src/index.ts +11 -0
- package/src/lib/__tests__/user-display.test.ts +85 -0
- package/src/lib/user-display.ts +88 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +33 -29
- package/src/prototype/__tests__/detail-view-title-slots.test.tsx +65 -0
- package/src/prototype/prototype-config.ts +28 -4
- package/src/prototype/prototype-inbox-view.tsx +8 -10
- package/src/prototype/__tests__/detail-view-title-subtext.test.tsx +0 -72
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ThumbsUp, ThumbsDown } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Structured feedback data shape.
|
|
13
|
+
*
|
|
14
|
+
* Preserves the tree structure for DB persistence:
|
|
15
|
+
* reasonTop -> tier-1 chip label (maps to case_feedback.reason_top)
|
|
16
|
+
* reasonSub -> tier-2 sub-chip label (maps to case_feedback.reason_sub)
|
|
17
|
+
* pills -> any additional selected chips (maps to case_feedback.pills)
|
|
18
|
+
* detail -> free-text input (maps to case_feedback.free_text)
|
|
19
|
+
*/
|
|
20
|
+
export interface FeedbackSubmitData {
|
|
21
|
+
sentiment: "positive" | "negative"
|
|
22
|
+
reasonTop?: string
|
|
23
|
+
reasonSub?: string
|
|
24
|
+
pills: string[]
|
|
25
|
+
detail: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Defines a tier-1 chip that may have tier-2 sub-chips.
|
|
30
|
+
*/
|
|
31
|
+
export interface FeedbackChipTree {
|
|
32
|
+
label: string
|
|
33
|
+
subPrompt?: string
|
|
34
|
+
subChips?: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// FeedbackChipGroup
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export interface FeedbackChipGroupProps {
|
|
42
|
+
chips: string[]
|
|
43
|
+
selected: string[]
|
|
44
|
+
onToggle: (chip: string) => void
|
|
45
|
+
flavor: "positive" | "negative"
|
|
46
|
+
className?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CHIP_SELECTED_CLASSES: Record<"positive" | "negative", string> = {
|
|
50
|
+
negative: "bg-red-50 text-red-700 border-red-200",
|
|
51
|
+
positive: "bg-muted text-foreground border-border",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CHIP_IDLE_CLASS =
|
|
55
|
+
"bg-background text-muted-foreground border-border hover:bg-muted/50"
|
|
56
|
+
|
|
57
|
+
export function FeedbackChipGroup({
|
|
58
|
+
chips,
|
|
59
|
+
selected,
|
|
60
|
+
onToggle,
|
|
61
|
+
flavor,
|
|
62
|
+
className,
|
|
63
|
+
}: FeedbackChipGroupProps) {
|
|
64
|
+
return (
|
|
65
|
+
<div className={cn("flex flex-wrap gap-1.5", className)}>
|
|
66
|
+
{chips.map((chip) => {
|
|
67
|
+
const isSelected = selected.includes(chip)
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
key={chip}
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={() => onToggle(chip)}
|
|
73
|
+
className={cn(
|
|
74
|
+
"rounded-md px-2.5 py-1 text-[11px] font-medium border transition-colors",
|
|
75
|
+
isSelected ? CHIP_SELECTED_CLASSES[flavor] : CHIP_IDLE_CLASS,
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{chip}
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
})}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// FeedbackInput
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
export interface FeedbackInputProps {
|
|
91
|
+
placeholder?: string
|
|
92
|
+
value: string
|
|
93
|
+
onChange: (value: string) => void
|
|
94
|
+
onSubmit?: () => void
|
|
95
|
+
className?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function FeedbackInput({
|
|
99
|
+
placeholder,
|
|
100
|
+
value,
|
|
101
|
+
onChange,
|
|
102
|
+
onSubmit,
|
|
103
|
+
className,
|
|
104
|
+
}: FeedbackInputProps) {
|
|
105
|
+
return (
|
|
106
|
+
<input
|
|
107
|
+
type="text"
|
|
108
|
+
value={value}
|
|
109
|
+
onChange={(e) => onChange(e.target.value)}
|
|
110
|
+
onKeyDown={(e) => {
|
|
111
|
+
if (e.key === "Enter" && onSubmit) {
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
onSubmit()
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
placeholder={placeholder}
|
|
117
|
+
className={cn(
|
|
118
|
+
"w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring",
|
|
119
|
+
className,
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// FeedbackActions
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export interface FeedbackActionsProps {
|
|
130
|
+
onSubmit: () => void
|
|
131
|
+
onCancel: () => void
|
|
132
|
+
submitDisabled?: boolean
|
|
133
|
+
submitLabel?: string
|
|
134
|
+
cancelLabel?: string
|
|
135
|
+
hint?: string
|
|
136
|
+
className?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function FeedbackActions({
|
|
140
|
+
onSubmit,
|
|
141
|
+
onCancel,
|
|
142
|
+
submitDisabled = false,
|
|
143
|
+
submitLabel = "Submit",
|
|
144
|
+
cancelLabel = "Cancel",
|
|
145
|
+
hint,
|
|
146
|
+
className,
|
|
147
|
+
}: FeedbackActionsProps) {
|
|
148
|
+
return (
|
|
149
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={onSubmit}
|
|
153
|
+
disabled={submitDisabled}
|
|
154
|
+
className="bg-foreground text-background rounded-md px-3 py-1.5 text-xs font-semibold disabled:opacity-50"
|
|
155
|
+
>
|
|
156
|
+
{submitLabel}
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={onCancel}
|
|
161
|
+
className="border border-border rounded-md px-3 py-1.5 text-xs font-medium"
|
|
162
|
+
>
|
|
163
|
+
{cancelLabel}
|
|
164
|
+
</button>
|
|
165
|
+
{hint && (
|
|
166
|
+
<span className="ml-auto text-[11px] text-muted-foreground">
|
|
167
|
+
{hint}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// FeedbackFooter
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
export interface FeedbackFooterProps {
|
|
179
|
+
feedback: "positive" | "negative" | null
|
|
180
|
+
onFeedbackChange: (value: "positive" | "negative" | null) => void
|
|
181
|
+
onSubmit: (data: FeedbackSubmitData) => void
|
|
182
|
+
metaText?: string
|
|
183
|
+
positivePrompt?: string
|
|
184
|
+
negativePrompt?: string
|
|
185
|
+
negativeChips?: FeedbackChipTree[]
|
|
186
|
+
positiveChips?: string[]
|
|
187
|
+
className?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const SENTIMENT_BUTTON_ACTIVE: Record<"positive" | "negative", string> = {
|
|
191
|
+
negative: "text-red-600 bg-red-50 border-red-200",
|
|
192
|
+
positive: "text-foreground bg-muted border-border",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const SENTIMENT_BUTTON_IDLE =
|
|
196
|
+
"text-muted-foreground hover:text-foreground"
|
|
197
|
+
|
|
198
|
+
export function FeedbackFooter({
|
|
199
|
+
feedback,
|
|
200
|
+
onFeedbackChange,
|
|
201
|
+
onSubmit,
|
|
202
|
+
metaText,
|
|
203
|
+
positivePrompt = "Thanks! Anything to keep about this score?",
|
|
204
|
+
negativePrompt = "What's the issue?",
|
|
205
|
+
negativeChips = [],
|
|
206
|
+
positiveChips = [],
|
|
207
|
+
className,
|
|
208
|
+
}: FeedbackFooterProps) {
|
|
209
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
210
|
+
const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)
|
|
211
|
+
const [selectedTier2, setSelectedTier2] = React.useState<string | null>(null)
|
|
212
|
+
const [additionalPills, setAdditionalPills] = React.useState<string[]>([])
|
|
213
|
+
const [detailText, setDetailText] = React.useState("")
|
|
214
|
+
const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(
|
|
215
|
+
null,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
// Reset state when feedback collapses
|
|
219
|
+
const resetState = React.useCallback(() => {
|
|
220
|
+
setExpanded(false)
|
|
221
|
+
setSelectedTier1(null)
|
|
222
|
+
setSelectedTier2(null)
|
|
223
|
+
setAdditionalPills([])
|
|
224
|
+
setDetailText("")
|
|
225
|
+
setActiveTreeIndex(null)
|
|
226
|
+
}, [])
|
|
227
|
+
|
|
228
|
+
const handleSentimentClick = React.useCallback(
|
|
229
|
+
(sentiment: "positive" | "negative") => {
|
|
230
|
+
onFeedbackChange(sentiment)
|
|
231
|
+
// Reset chip state when switching sentiment, then expand
|
|
232
|
+
resetState()
|
|
233
|
+
setExpanded(true)
|
|
234
|
+
},
|
|
235
|
+
[onFeedbackChange, resetState],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const handleTier1Toggle = React.useCallback(
|
|
239
|
+
(chipLabel: string) => {
|
|
240
|
+
if (selectedTier1 === chipLabel) {
|
|
241
|
+
// Deselect the tier-1 chip
|
|
242
|
+
setSelectedTier1(null)
|
|
243
|
+
setSelectedTier2(null)
|
|
244
|
+
setActiveTreeIndex(null)
|
|
245
|
+
} else if (selectedTier1 === null) {
|
|
246
|
+
// First selection becomes the primary reasonTop
|
|
247
|
+
setSelectedTier1(chipLabel)
|
|
248
|
+
setSelectedTier2(null)
|
|
249
|
+
// Find the chip's tree index to show sub-chips
|
|
250
|
+
const idx = negativeChips.findIndex((c) => c.label === chipLabel)
|
|
251
|
+
if (idx !== -1 && negativeChips[idx].subChips) {
|
|
252
|
+
setActiveTreeIndex(idx)
|
|
253
|
+
} else {
|
|
254
|
+
setActiveTreeIndex(null)
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Additional selections become pills
|
|
258
|
+
setAdditionalPills((prev) =>
|
|
259
|
+
prev.includes(chipLabel)
|
|
260
|
+
? prev.filter((p) => p !== chipLabel)
|
|
261
|
+
: [...prev, chipLabel],
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
[selectedTier1, negativeChips],
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const handleTier2Toggle = React.useCallback((subChip: string) => {
|
|
269
|
+
setSelectedTier2((prev) => (prev === subChip ? null : subChip))
|
|
270
|
+
}, [])
|
|
271
|
+
|
|
272
|
+
const handlePositiveChipToggle = React.useCallback(
|
|
273
|
+
(chip: string) => {
|
|
274
|
+
if (selectedTier1 === chip) {
|
|
275
|
+
setSelectedTier1(null)
|
|
276
|
+
} else if (selectedTier1 === null) {
|
|
277
|
+
setSelectedTier1(chip)
|
|
278
|
+
} else {
|
|
279
|
+
setAdditionalPills((prev) =>
|
|
280
|
+
prev.includes(chip)
|
|
281
|
+
? prev.filter((p) => p !== chip)
|
|
282
|
+
: [...prev, chip],
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
[selectedTier1],
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const handleSubmit = React.useCallback(() => {
|
|
290
|
+
if (!feedback) return
|
|
291
|
+
onSubmit({
|
|
292
|
+
sentiment: feedback,
|
|
293
|
+
reasonTop: selectedTier1 ?? undefined,
|
|
294
|
+
reasonSub: selectedTier2 ?? undefined,
|
|
295
|
+
pills: additionalPills,
|
|
296
|
+
detail: detailText,
|
|
297
|
+
})
|
|
298
|
+
resetState()
|
|
299
|
+
}, [
|
|
300
|
+
feedback,
|
|
301
|
+
selectedTier1,
|
|
302
|
+
selectedTier2,
|
|
303
|
+
additionalPills,
|
|
304
|
+
detailText,
|
|
305
|
+
onSubmit,
|
|
306
|
+
resetState,
|
|
307
|
+
])
|
|
308
|
+
|
|
309
|
+
const handleCancel = React.useCallback(() => {
|
|
310
|
+
resetState()
|
|
311
|
+
onFeedbackChange(null)
|
|
312
|
+
}, [resetState, onFeedbackChange])
|
|
313
|
+
|
|
314
|
+
// Determine which chips are selected (combining tier1 + additionalPills)
|
|
315
|
+
const allSelectedChips = React.useMemo(() => {
|
|
316
|
+
const result: string[] = []
|
|
317
|
+
if (selectedTier1) result.push(selectedTier1)
|
|
318
|
+
result.push(...additionalPills)
|
|
319
|
+
return result
|
|
320
|
+
}, [selectedTier1, additionalPills])
|
|
321
|
+
|
|
322
|
+
// Active tier-1 chip tree (for showing sub-chips)
|
|
323
|
+
const activeTree =
|
|
324
|
+
activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div className={cn("space-y-3", className)}>
|
|
328
|
+
{/* Sentiment buttons + meta text bar */}
|
|
329
|
+
<div className="flex items-center justify-between">
|
|
330
|
+
<div className="flex items-center gap-3">
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
onClick={() => handleSentimentClick("positive")}
|
|
334
|
+
className={cn(
|
|
335
|
+
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
336
|
+
feedback === "positive"
|
|
337
|
+
? SENTIMENT_BUTTON_ACTIVE.positive
|
|
338
|
+
: SENTIMENT_BUTTON_IDLE,
|
|
339
|
+
)}
|
|
340
|
+
>
|
|
341
|
+
<ThumbsUp className="h-[11px] w-[11px]" />
|
|
342
|
+
Helpful
|
|
343
|
+
</button>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
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,
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
<ThumbsDown className="h-[11px] w-[11px]" />
|
|
355
|
+
Not helpful
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
{metaText && (
|
|
359
|
+
<span className="text-[11px] text-muted-foreground">{metaText}</span>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
{/* Expanded feedback area */}
|
|
364
|
+
{expanded && feedback && (
|
|
365
|
+
<div className="space-y-3">
|
|
366
|
+
{/* Prompt text */}
|
|
367
|
+
<p className="text-xs text-muted-foreground">
|
|
368
|
+
{feedback === "negative" ? negativePrompt : positivePrompt}
|
|
369
|
+
</p>
|
|
370
|
+
|
|
371
|
+
{/* Chip area */}
|
|
372
|
+
{feedback === "negative" && negativeChips.length > 0 && (
|
|
373
|
+
<div className="space-y-2">
|
|
374
|
+
{/* Tier-1 chips */}
|
|
375
|
+
<FeedbackChipGroup
|
|
376
|
+
chips={negativeChips.map((c) => c.label)}
|
|
377
|
+
selected={allSelectedChips}
|
|
378
|
+
onToggle={handleTier1Toggle}
|
|
379
|
+
flavor="negative"
|
|
380
|
+
/>
|
|
381
|
+
|
|
382
|
+
{/* Tier-2 sub-chips (shown when a tier-1 with sub-chips is active) */}
|
|
383
|
+
{activeTree && activeTree.subChips && (
|
|
384
|
+
<div className="pl-3 space-y-1.5">
|
|
385
|
+
{activeTree.subPrompt && (
|
|
386
|
+
<p className="text-[11px] text-muted-foreground">
|
|
387
|
+
{activeTree.subPrompt}
|
|
388
|
+
</p>
|
|
389
|
+
)}
|
|
390
|
+
<FeedbackChipGroup
|
|
391
|
+
chips={activeTree.subChips}
|
|
392
|
+
selected={selectedTier2 ? [selectedTier2] : []}
|
|
393
|
+
onToggle={handleTier2Toggle}
|
|
394
|
+
flavor="negative"
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{feedback === "positive" && positiveChips.length > 0 && (
|
|
402
|
+
<FeedbackChipGroup
|
|
403
|
+
chips={positiveChips}
|
|
404
|
+
selected={allSelectedChips}
|
|
405
|
+
onToggle={handlePositiveChipToggle}
|
|
406
|
+
flavor="positive"
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{/* Detail text input */}
|
|
411
|
+
<FeedbackInput
|
|
412
|
+
placeholder="Add optional detail…"
|
|
413
|
+
value={detailText}
|
|
414
|
+
onChange={setDetailText}
|
|
415
|
+
onSubmit={handleSubmit}
|
|
416
|
+
/>
|
|
417
|
+
|
|
418
|
+
{/* Action buttons */}
|
|
419
|
+
<FeedbackActions onSubmit={handleSubmit} onCancel={handleCancel} />
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
@@ -23,6 +23,7 @@ export interface FilterDefinition {
|
|
|
23
23
|
|
|
24
24
|
export interface InsightsFilterBarProps {
|
|
25
25
|
filters: FilterDefinition[]
|
|
26
|
+
variant?: "default" | "compact"
|
|
26
27
|
values: Record<string, string>
|
|
27
28
|
onChange: (filterId: string, value: string) => void
|
|
28
29
|
onClearAll?: () => void
|
|
@@ -45,6 +46,7 @@ function InsightsFilterBar({
|
|
|
45
46
|
onChange,
|
|
46
47
|
onClearAll,
|
|
47
48
|
className,
|
|
49
|
+
variant = "default",
|
|
48
50
|
}: InsightsFilterBarProps) {
|
|
49
51
|
const showClearAll = onClearAll && hasNonDefaultValue(filters, values)
|
|
50
52
|
|
|
@@ -52,11 +54,12 @@ function InsightsFilterBar({
|
|
|
52
54
|
<div
|
|
53
55
|
data-slot="insights-filter-bar"
|
|
54
56
|
className={cn(
|
|
55
|
-
"flex flex-wrap items-center
|
|
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",
|
|
56
59
|
className
|
|
57
60
|
)}
|
|
58
61
|
>
|
|
59
|
-
<div className="flex items-center gap-2">
|
|
62
|
+
<div className={cn("flex items-center gap-2", variant === "compact" && "sr-only")}>
|
|
60
63
|
<FilterIcon className="h-4 w-4 text-muted-foreground" />
|
|
61
64
|
<span className="text-sm font-medium text-muted-foreground">
|
|
62
65
|
Filters:
|
|
@@ -80,7 +83,10 @@ function InsightsFilterBar({
|
|
|
80
83
|
<Button
|
|
81
84
|
variant="outline"
|
|
82
85
|
size="sm"
|
|
83
|
-
className=
|
|
86
|
+
className={cn(
|
|
87
|
+
"gap-1.5 text-xs font-normal shadow-none",
|
|
88
|
+
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
89
|
+
)}
|
|
84
90
|
>
|
|
85
91
|
{IconComp ? (
|
|
86
92
|
<IconComp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
@@ -118,7 +124,10 @@ function InsightsFilterBar({
|
|
|
118
124
|
<Button
|
|
119
125
|
variant="ghost"
|
|
120
126
|
size="sm"
|
|
121
|
-
className=
|
|
127
|
+
className={cn(
|
|
128
|
+
"text-xs text-destructive hover:text-destructive",
|
|
129
|
+
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
130
|
+
)}
|
|
122
131
|
onClick={onClearAll}
|
|
123
132
|
>
|
|
124
133
|
Clear All
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ExternalLink } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
export interface LinkedEntityCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
name: React.ReactNode
|
|
10
|
+
href?: string
|
|
11
|
+
subtitle?: React.ReactNode
|
|
12
|
+
meta?: React.ReactNode
|
|
13
|
+
icon?: React.ReactNode
|
|
14
|
+
external?: boolean
|
|
15
|
+
onNavigate?: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function LinkedEntityCell({
|
|
19
|
+
name,
|
|
20
|
+
href,
|
|
21
|
+
subtitle,
|
|
22
|
+
meta,
|
|
23
|
+
icon,
|
|
24
|
+
external = false,
|
|
25
|
+
onNavigate,
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: LinkedEntityCellProps) {
|
|
29
|
+
const content = (
|
|
30
|
+
<>
|
|
31
|
+
<span className="truncate">{name}</span>
|
|
32
|
+
{external ? <ExternalLink className="h-3 w-3 shrink-0 opacity-60" aria-hidden="true" /> : null}
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="linked-entity-cell"
|
|
39
|
+
className={cn("flex min-w-0 items-center gap-2", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{icon ? (
|
|
43
|
+
<span data-slot="linked-entity-cell-icon" className="shrink-0 text-muted-foreground">
|
|
44
|
+
{icon}
|
|
45
|
+
</span>
|
|
46
|
+
) : null}
|
|
47
|
+
<div className="min-w-0 flex-1">
|
|
48
|
+
{href ? (
|
|
49
|
+
<a
|
|
50
|
+
data-slot="linked-entity-cell-link"
|
|
51
|
+
href={href}
|
|
52
|
+
target={external ? "_blank" : undefined}
|
|
53
|
+
rel={external ? "noreferrer" : undefined}
|
|
54
|
+
onClick={onNavigate}
|
|
55
|
+
className="inline-flex max-w-full items-center gap-1 truncate font-medium text-foreground underline-offset-4 hover:text-primary hover:underline"
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</a>
|
|
59
|
+
) : (
|
|
60
|
+
<span data-slot="linked-entity-cell-name" className="block truncate font-medium text-foreground">
|
|
61
|
+
{name}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
{subtitle || meta ? (
|
|
65
|
+
<div data-slot="linked-entity-cell-meta" className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
66
|
+
{subtitle}
|
|
67
|
+
{subtitle && meta ? <span className="px-1">·</span> : null}
|
|
68
|
+
{meta}
|
|
69
|
+
</div>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -24,6 +24,88 @@ 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
|
+
|
|
27
109
|
export function MetricCard({
|
|
28
110
|
title,
|
|
29
111
|
value,
|