@handled-ai/design-system 0.17.1 → 0.17.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.
Files changed (49) hide show
  1. package/dist/components/feedback-primitives.d.ts +66 -0
  2. package/dist/components/feedback-primitives.js +295 -0
  3. package/dist/components/feedback-primitives.js.map +1 -0
  4. package/dist/components/score-why-chips.d.ts +8 -17
  5. package/dist/components/score-why-chips.js +266 -180
  6. package/dist/components/score-why-chips.js.map +1 -1
  7. package/dist/components/signal-priority-popover.d.ts +17 -0
  8. package/dist/components/signal-priority-popover.js +247 -0
  9. package/dist/components/signal-priority-popover.js.map +1 -0
  10. package/dist/components/user-display.d.ts +22 -0
  11. package/dist/components/user-display.js +138 -0
  12. package/dist/components/user-display.js.map +1 -0
  13. package/dist/components/user-pill.d.ts +3 -0
  14. package/dist/components/user-pill.js +5 -0
  15. package/dist/components/user-pill.js.map +1 -0
  16. package/dist/index.d.ts +6 -3
  17. package/dist/index.js +12 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib/user-display.d.ts +31 -0
  20. package/dist/lib/user-display.js +57 -0
  21. package/dist/lib/user-display.js.map +1 -0
  22. package/dist/prototype/index.d.ts +2 -1
  23. package/dist/prototype/prototype-accounts-view.d.ts +2 -1
  24. package/dist/prototype/prototype-admin-view.d.ts +2 -1
  25. package/dist/prototype/prototype-config.d.ts +15 -332
  26. package/dist/prototype/prototype-inbox-view.d.ts +2 -1
  27. package/dist/prototype/prototype-inbox-view.js +11 -12
  28. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  29. package/dist/prototype/prototype-insights-view.d.ts +2 -1
  30. package/dist/prototype/prototype-shell.d.ts +2 -1
  31. package/dist/signal-priority-popover-DQ_VuHac.d.ts +390 -0
  32. package/package.json +1 -1
  33. package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +99 -188
  34. package/src/components/__tests__/feedback-primitives.test.tsx +509 -0
  35. package/src/components/__tests__/score-why-chips.test.tsx +540 -0
  36. package/src/components/__tests__/signal-priority-popover.test.tsx +312 -0
  37. package/src/components/feedback-primitives.tsx +424 -0
  38. package/src/components/score-why-chips.tsx +413 -203
  39. package/src/components/signal-priority-popover.tsx +359 -0
  40. package/src/components/user-display.tsx +96 -0
  41. package/src/components/user-pill.tsx +1 -0
  42. package/src/index.ts +6 -0
  43. package/src/lib/__tests__/user-display.test.ts +43 -0
  44. package/src/lib/user-display.ts +88 -0
  45. package/src/prototype/__tests__/detail-view-score-why.test.tsx +33 -29
  46. package/src/prototype/__tests__/detail-view-title-slots.test.tsx +65 -0
  47. package/src/prototype/prototype-config.ts +28 -4
  48. package/src/prototype/prototype-inbox-view.tsx +8 -10
  49. 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
+ }