@handled-ai/design-system 0.17.0 → 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 (51) hide show
  1. package/dist/components/badge.d.ts +1 -1
  2. package/dist/components/button.d.ts +1 -1
  3. package/dist/components/feedback-primitives.d.ts +66 -0
  4. package/dist/components/feedback-primitives.js +295 -0
  5. package/dist/components/feedback-primitives.js.map +1 -0
  6. package/dist/components/score-why-chips.d.ts +8 -17
  7. package/dist/components/score-why-chips.js +266 -180
  8. package/dist/components/score-why-chips.js.map +1 -1
  9. package/dist/components/signal-priority-popover.d.ts +17 -0
  10. package/dist/components/signal-priority-popover.js +247 -0
  11. package/dist/components/signal-priority-popover.js.map +1 -0
  12. package/dist/components/tabs.d.ts +1 -1
  13. package/dist/components/user-display.d.ts +22 -0
  14. package/dist/components/user-display.js +138 -0
  15. package/dist/components/user-display.js.map +1 -0
  16. package/dist/components/user-pill.d.ts +3 -0
  17. package/dist/components/user-pill.js +5 -0
  18. package/dist/components/user-pill.js.map +1 -0
  19. package/dist/index.d.ts +6 -3
  20. package/dist/index.js +12 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/user-display.d.ts +31 -0
  23. package/dist/lib/user-display.js +57 -0
  24. package/dist/lib/user-display.js.map +1 -0
  25. package/dist/prototype/index.d.ts +2 -1
  26. package/dist/prototype/prototype-accounts-view.d.ts +2 -1
  27. package/dist/prototype/prototype-admin-view.d.ts +2 -1
  28. package/dist/prototype/prototype-config.d.ts +15 -328
  29. package/dist/prototype/prototype-inbox-view.d.ts +8 -3
  30. package/dist/prototype/prototype-inbox-view.js +24 -13
  31. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  32. package/dist/prototype/prototype-insights-view.d.ts +2 -1
  33. package/dist/prototype/prototype-shell.d.ts +2 -1
  34. package/dist/signal-priority-popover-DQ_VuHac.d.ts +390 -0
  35. package/package.json +1 -1
  36. package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +99 -188
  37. package/src/components/__tests__/feedback-primitives.test.tsx +509 -0
  38. package/src/components/__tests__/score-why-chips.test.tsx +540 -0
  39. package/src/components/__tests__/signal-priority-popover.test.tsx +312 -0
  40. package/src/components/feedback-primitives.tsx +424 -0
  41. package/src/components/score-why-chips.tsx +413 -203
  42. package/src/components/signal-priority-popover.tsx +359 -0
  43. package/src/components/user-display.tsx +96 -0
  44. package/src/components/user-pill.tsx +1 -0
  45. package/src/index.ts +6 -0
  46. package/src/lib/__tests__/user-display.test.ts +43 -0
  47. package/src/lib/user-display.ts +88 -0
  48. package/src/prototype/__tests__/detail-view-score-why.test.tsx +33 -29
  49. package/src/prototype/__tests__/detail-view-title-slots.test.tsx +65 -0
  50. package/src/prototype/prototype-config.ts +28 -0
  51. package/src/prototype/prototype-inbox-view.tsx +25 -11
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Tests for feedback primitives.
3
+ *
4
+ * Covers:
5
+ * - FeedbackFooter renders thumbs buttons
6
+ * - Clicking "Not helpful" expands the negative feedback area
7
+ * - Clicking "Helpful" expands the positive feedback area
8
+ * - Chip selection toggles (tier-1)
9
+ * - Tier-2 expansion when a tier-1 chip with sub-chips is selected
10
+ * - Submit callback fires with correct structured FeedbackSubmitData shape
11
+ * - Cancel resets state and calls onFeedbackChange(null)
12
+ * - FeedbackChipGroup renders chips with correct colors per flavor
13
+ * - FeedbackInput renders and accepts text
14
+ * - FeedbackActions renders submit/cancel buttons
15
+ */
16
+
17
+ import { describe, it, expect, vi } from "vitest"
18
+ import React from "react"
19
+ import { render, screen, fireEvent, act } from "@testing-library/react"
20
+ import {
21
+ FeedbackFooter,
22
+ FeedbackChipGroup,
23
+ FeedbackInput,
24
+ FeedbackActions,
25
+ } from "../feedback-primitives"
26
+ import type {
27
+ FeedbackChipTree,
28
+ FeedbackSubmitData,
29
+ } from "../feedback-primitives"
30
+
31
+ // ─── Mock data ───────────────────────────────────────────────────────────────
32
+
33
+ const mockNegativeChips: FeedbackChipTree[] = [
34
+ {
35
+ label: "Inaccurate data",
36
+ subPrompt: "Which field?",
37
+ subChips: ["Balance figures", "Counterparty", "Timestamp", "Other"],
38
+ },
39
+ { label: "Bad timing" },
40
+ { label: "Not relevant" },
41
+ { label: "Already handled" },
42
+ ]
43
+
44
+ const mockPositiveChips = ["Right timing", "Accurate data", "Actionable"]
45
+
46
+ // ─── FeedbackFooter ──────────────────────────────────────────────────────────
47
+
48
+ describe("FeedbackFooter", () => {
49
+ it("renders Helpful and Not helpful buttons", () => {
50
+ render(
51
+ <FeedbackFooter
52
+ feedback={null}
53
+ onFeedbackChange={vi.fn()}
54
+ onSubmit={vi.fn()}
55
+ />,
56
+ )
57
+ expect(screen.getByText("Helpful")).toBeDefined()
58
+ expect(screen.getByText("Not helpful")).toBeDefined()
59
+ })
60
+
61
+ it("renders meta text when provided", () => {
62
+ render(
63
+ <FeedbackFooter
64
+ feedback={null}
65
+ onFeedbackChange={vi.fn()}
66
+ onSubmit={vi.fn()}
67
+ metaText="Updated 4m ago"
68
+ />,
69
+ )
70
+ expect(screen.getByText("Updated 4m ago")).toBeDefined()
71
+ })
72
+
73
+ it("expands negative feedback area when 'Not helpful' is clicked", async () => {
74
+ const onFeedbackChange = vi.fn()
75
+ render(
76
+ <FeedbackFooter
77
+ feedback={null}
78
+ onFeedbackChange={onFeedbackChange}
79
+ onSubmit={vi.fn()}
80
+ negativeChips={mockNegativeChips}
81
+ negativePrompt="What's the issue?"
82
+ />,
83
+ )
84
+
85
+ // Click "Not helpful"
86
+ await act(async () => {
87
+ fireEvent.click(screen.getByText("Not helpful"))
88
+ })
89
+
90
+ expect(onFeedbackChange).toHaveBeenCalledWith("negative")
91
+ })
92
+
93
+ it("shows negative prompt and chips when feedback is negative", () => {
94
+ render(
95
+ <FeedbackFooter
96
+ feedback="negative"
97
+ onFeedbackChange={vi.fn()}
98
+ onSubmit={vi.fn()}
99
+ negativeChips={mockNegativeChips}
100
+ negativePrompt="What's the issue?"
101
+ />,
102
+ )
103
+
104
+ // Click to expand
105
+ fireEvent.click(screen.getByText("Not helpful"))
106
+
107
+ expect(screen.getByText("What's the issue?")).toBeDefined()
108
+ expect(screen.getByText("Inaccurate data")).toBeDefined()
109
+ expect(screen.getByText("Bad timing")).toBeDefined()
110
+ expect(screen.getByText("Not relevant")).toBeDefined()
111
+ expect(screen.getByText("Already handled")).toBeDefined()
112
+ })
113
+
114
+ it("expands positive feedback area when 'Helpful' is clicked", () => {
115
+ const onFeedbackChange = vi.fn()
116
+ render(
117
+ <FeedbackFooter
118
+ feedback={null}
119
+ onFeedbackChange={onFeedbackChange}
120
+ onSubmit={vi.fn()}
121
+ positiveChips={mockPositiveChips}
122
+ positivePrompt="Thanks! What was good?"
123
+ />,
124
+ )
125
+
126
+ fireEvent.click(screen.getByText("Helpful"))
127
+
128
+ expect(onFeedbackChange).toHaveBeenCalledWith("positive")
129
+ })
130
+
131
+ it("shows positive prompt and chips when feedback is positive and expanded", () => {
132
+ render(
133
+ <FeedbackFooter
134
+ feedback="positive"
135
+ onFeedbackChange={vi.fn()}
136
+ onSubmit={vi.fn()}
137
+ positiveChips={mockPositiveChips}
138
+ positivePrompt="Thanks! What was good?"
139
+ />,
140
+ )
141
+
142
+ // Click Helpful to expand
143
+ fireEvent.click(screen.getByText("Helpful"))
144
+
145
+ expect(screen.getByText("Thanks! What was good?")).toBeDefined()
146
+ expect(screen.getByText("Right timing")).toBeDefined()
147
+ expect(screen.getByText("Accurate data")).toBeDefined()
148
+ expect(screen.getByText("Actionable")).toBeDefined()
149
+ })
150
+
151
+ it("tier-1 chip selection toggles and tier-2 expansion works", () => {
152
+ render(
153
+ <FeedbackFooter
154
+ feedback="negative"
155
+ onFeedbackChange={vi.fn()}
156
+ onSubmit={vi.fn()}
157
+ negativeChips={mockNegativeChips}
158
+ negativePrompt="What's the issue?"
159
+ />,
160
+ )
161
+
162
+ // Expand
163
+ fireEvent.click(screen.getByText("Not helpful"))
164
+
165
+ // Select "Inaccurate data" (has sub-chips)
166
+ fireEvent.click(screen.getByText("Inaccurate data"))
167
+
168
+ // Should show the sub-prompt and sub-chips
169
+ expect(screen.getByText("Which field?")).toBeDefined()
170
+ expect(screen.getByText("Balance figures")).toBeDefined()
171
+ expect(screen.getByText("Counterparty")).toBeDefined()
172
+ expect(screen.getByText("Timestamp")).toBeDefined()
173
+
174
+ // Select a tier-2 sub-chip
175
+ fireEvent.click(screen.getByText("Balance figures"))
176
+
177
+ // Now deselect the tier-1 chip to hide sub-chips
178
+ fireEvent.click(screen.getByText("Inaccurate data"))
179
+
180
+ // Sub-chips should be hidden
181
+ expect(screen.queryByText("Which field?")).toBeNull()
182
+ expect(screen.queryByText("Balance figures")).toBeNull()
183
+ })
184
+
185
+ it("submit callback fires with correct FeedbackSubmitData shape", () => {
186
+ const onSubmit = vi.fn()
187
+ render(
188
+ <FeedbackFooter
189
+ feedback="negative"
190
+ onFeedbackChange={vi.fn()}
191
+ onSubmit={onSubmit}
192
+ negativeChips={mockNegativeChips}
193
+ negativePrompt="What's the issue?"
194
+ />,
195
+ )
196
+
197
+ // Expand
198
+ fireEvent.click(screen.getByText("Not helpful"))
199
+
200
+ // Select tier-1
201
+ fireEvent.click(screen.getByText("Inaccurate data"))
202
+
203
+ // Select tier-2
204
+ fireEvent.click(screen.getByText("Counterparty"))
205
+
206
+ // Select an additional pill (second tier-1 chip)
207
+ fireEvent.click(screen.getByText("Bad timing"))
208
+
209
+ // Type detail text
210
+ const input = screen.getByPlaceholderText("Add optional detail…")
211
+ fireEvent.change(input, { target: { value: "Data was stale" } })
212
+
213
+ // Submit
214
+ fireEvent.click(screen.getByText("Submit"))
215
+
216
+ expect(onSubmit).toHaveBeenCalledTimes(1)
217
+ const data: FeedbackSubmitData = onSubmit.mock.calls[0][0]
218
+ expect(data.sentiment).toBe("negative")
219
+ expect(data.reasonTop).toBe("Inaccurate data")
220
+ expect(data.reasonSub).toBe("Counterparty")
221
+ expect(data.pills).toEqual(["Bad timing"])
222
+ expect(data.detail).toBe("Data was stale")
223
+ })
224
+
225
+ it("submit with positive feedback sends correct shape", () => {
226
+ const onSubmit = vi.fn()
227
+ render(
228
+ <FeedbackFooter
229
+ feedback="positive"
230
+ onFeedbackChange={vi.fn()}
231
+ onSubmit={onSubmit}
232
+ positiveChips={mockPositiveChips}
233
+ positivePrompt="Thanks!"
234
+ />,
235
+ )
236
+
237
+ // Expand
238
+ fireEvent.click(screen.getByText("Helpful"))
239
+
240
+ // Select a chip
241
+ fireEvent.click(screen.getByText("Right timing"))
242
+
243
+ // Submit
244
+ fireEvent.click(screen.getByText("Submit"))
245
+
246
+ expect(onSubmit).toHaveBeenCalledTimes(1)
247
+ const data: FeedbackSubmitData = onSubmit.mock.calls[0][0]
248
+ expect(data.sentiment).toBe("positive")
249
+ expect(data.reasonTop).toBe("Right timing")
250
+ expect(data.reasonSub).toBeUndefined()
251
+ expect(data.pills).toEqual([])
252
+ expect(data.detail).toBe("")
253
+ })
254
+
255
+ it("cancel resets state and calls onFeedbackChange(null)", () => {
256
+ const onFeedbackChange = vi.fn()
257
+ render(
258
+ <FeedbackFooter
259
+ feedback="negative"
260
+ onFeedbackChange={onFeedbackChange}
261
+ onSubmit={vi.fn()}
262
+ negativeChips={mockNegativeChips}
263
+ negativePrompt="What's the issue?"
264
+ />,
265
+ )
266
+
267
+ // Expand
268
+ fireEvent.click(screen.getByText("Not helpful"))
269
+
270
+ // Select a chip
271
+ fireEvent.click(screen.getByText("Inaccurate data"))
272
+
273
+ // Cancel
274
+ fireEvent.click(screen.getByText("Cancel"))
275
+
276
+ // Should call onFeedbackChange(null)
277
+ expect(onFeedbackChange).toHaveBeenCalledWith(null)
278
+
279
+ // After cancel, the expanded area should be gone
280
+ expect(screen.queryByText("What's the issue?")).toBeNull()
281
+ })
282
+
283
+ it("submit with no chips selected still sends correct shape with no reasonTop", () => {
284
+ const onSubmit = vi.fn()
285
+ render(
286
+ <FeedbackFooter
287
+ feedback="negative"
288
+ onFeedbackChange={vi.fn()}
289
+ onSubmit={onSubmit}
290
+ negativeChips={mockNegativeChips}
291
+ />,
292
+ )
293
+
294
+ // Expand
295
+ fireEvent.click(screen.getByText("Not helpful"))
296
+
297
+ // Submit without selecting any chips
298
+ fireEvent.click(screen.getByText("Submit"))
299
+
300
+ expect(onSubmit).toHaveBeenCalledTimes(1)
301
+ const data: FeedbackSubmitData = onSubmit.mock.calls[0][0]
302
+ expect(data.sentiment).toBe("negative")
303
+ expect(data.reasonTop).toBeUndefined()
304
+ expect(data.reasonSub).toBeUndefined()
305
+ expect(data.pills).toEqual([])
306
+ expect(data.detail).toBe("")
307
+ })
308
+
309
+ it("enter key in detail input triggers submit", () => {
310
+ const onSubmit = vi.fn()
311
+ render(
312
+ <FeedbackFooter
313
+ feedback="negative"
314
+ onFeedbackChange={vi.fn()}
315
+ onSubmit={onSubmit}
316
+ negativeChips={mockNegativeChips}
317
+ />,
318
+ )
319
+
320
+ // Expand
321
+ fireEvent.click(screen.getByText("Not helpful"))
322
+
323
+ // Type and press enter
324
+ const input = screen.getByPlaceholderText("Add optional detail…")
325
+ fireEvent.change(input, { target: { value: "test" } })
326
+ fireEvent.keyDown(input, { key: "Enter" })
327
+
328
+ expect(onSubmit).toHaveBeenCalledTimes(1)
329
+ })
330
+ })
331
+
332
+ // ─── FeedbackChipGroup ──────────────────────────────────────────────────────
333
+
334
+ describe("FeedbackChipGroup", () => {
335
+ it("renders all chips", () => {
336
+ render(
337
+ <FeedbackChipGroup
338
+ chips={["Alpha", "Beta", "Gamma"]}
339
+ selected={[]}
340
+ onToggle={vi.fn()}
341
+ flavor="negative"
342
+ />,
343
+ )
344
+ expect(screen.getByText("Alpha")).toBeDefined()
345
+ expect(screen.getByText("Beta")).toBeDefined()
346
+ expect(screen.getByText("Gamma")).toBeDefined()
347
+ })
348
+
349
+ it("applies negative selected classes to selected chips", () => {
350
+ render(
351
+ <FeedbackChipGroup
352
+ chips={["Alpha", "Beta"]}
353
+ selected={["Alpha"]}
354
+ onToggle={vi.fn()}
355
+ flavor="negative"
356
+ />,
357
+ )
358
+ const alphaButton = screen.getByText("Alpha")
359
+ // Selected negative chip should have red styling
360
+ expect(alphaButton.className).toContain("bg-red-50")
361
+ expect(alphaButton.className).toContain("text-red-700")
362
+ expect(alphaButton.className).toContain("border-red-200")
363
+
364
+ // Unselected chip should have idle styling
365
+ const betaButton = screen.getByText("Beta")
366
+ expect(betaButton.className).toContain("bg-background")
367
+ expect(betaButton.className).toContain("text-muted-foreground")
368
+ })
369
+
370
+ it("applies positive selected classes to selected chips", () => {
371
+ render(
372
+ <FeedbackChipGroup
373
+ chips={["Alpha", "Beta"]}
374
+ selected={["Alpha"]}
375
+ onToggle={vi.fn()}
376
+ flavor="positive"
377
+ />,
378
+ )
379
+ const alphaButton = screen.getByText("Alpha")
380
+ // Selected positive chip should have muted styling
381
+ expect(alphaButton.className).toContain("bg-muted")
382
+ expect(alphaButton.className).toContain("text-foreground")
383
+ expect(alphaButton.className).toContain("border-border")
384
+ })
385
+
386
+ it("calls onToggle when a chip is clicked", () => {
387
+ const onToggle = vi.fn()
388
+ render(
389
+ <FeedbackChipGroup
390
+ chips={["Alpha", "Beta"]}
391
+ selected={[]}
392
+ onToggle={onToggle}
393
+ flavor="negative"
394
+ />,
395
+ )
396
+ fireEvent.click(screen.getByText("Alpha"))
397
+ expect(onToggle).toHaveBeenCalledWith("Alpha")
398
+ })
399
+ })
400
+
401
+ // ─── FeedbackInput ──────────────────────────────────────────────────────────
402
+
403
+ describe("FeedbackInput", () => {
404
+ it("renders with placeholder text", () => {
405
+ render(
406
+ <FeedbackInput
407
+ placeholder="Type here…"
408
+ value=""
409
+ onChange={vi.fn()}
410
+ />,
411
+ )
412
+ expect(screen.getByPlaceholderText("Type here…")).toBeDefined()
413
+ })
414
+
415
+ it("calls onChange when text is entered", () => {
416
+ const onChange = vi.fn()
417
+ render(
418
+ <FeedbackInput
419
+ placeholder="Type here…"
420
+ value=""
421
+ onChange={onChange}
422
+ />,
423
+ )
424
+ const input = screen.getByPlaceholderText("Type here…")
425
+ fireEvent.change(input, { target: { value: "hello" } })
426
+ expect(onChange).toHaveBeenCalledWith("hello")
427
+ })
428
+
429
+ it("calls onSubmit when Enter is pressed", () => {
430
+ const onSubmit = vi.fn()
431
+ render(
432
+ <FeedbackInput
433
+ placeholder="Type here…"
434
+ value="test"
435
+ onChange={vi.fn()}
436
+ onSubmit={onSubmit}
437
+ />,
438
+ )
439
+ const input = screen.getByPlaceholderText("Type here…")
440
+ fireEvent.keyDown(input, { key: "Enter" })
441
+ expect(onSubmit).toHaveBeenCalledTimes(1)
442
+ })
443
+ })
444
+
445
+ // ─── FeedbackActions ─────────────────────────────────────────────────────────
446
+
447
+ describe("FeedbackActions", () => {
448
+ it("renders submit and cancel buttons with default labels", () => {
449
+ render(
450
+ <FeedbackActions onSubmit={vi.fn()} onCancel={vi.fn()} />,
451
+ )
452
+ expect(screen.getByText("Submit")).toBeDefined()
453
+ expect(screen.getByText("Cancel")).toBeDefined()
454
+ })
455
+
456
+ it("renders custom labels", () => {
457
+ render(
458
+ <FeedbackActions
459
+ onSubmit={vi.fn()}
460
+ onCancel={vi.fn()}
461
+ submitLabel="Send"
462
+ cancelLabel="Discard"
463
+ />,
464
+ )
465
+ expect(screen.getByText("Send")).toBeDefined()
466
+ expect(screen.getByText("Discard")).toBeDefined()
467
+ })
468
+
469
+ it("renders hint text", () => {
470
+ render(
471
+ <FeedbackActions
472
+ onSubmit={vi.fn()}
473
+ onCancel={vi.fn()}
474
+ hint="Routes to model training queue"
475
+ />,
476
+ )
477
+ expect(screen.getByText("Routes to model training queue")).toBeDefined()
478
+ })
479
+
480
+ it("calls onSubmit when submit button is clicked", () => {
481
+ const onSubmit = vi.fn()
482
+ render(
483
+ <FeedbackActions onSubmit={onSubmit} onCancel={vi.fn()} />,
484
+ )
485
+ fireEvent.click(screen.getByText("Submit"))
486
+ expect(onSubmit).toHaveBeenCalledTimes(1)
487
+ })
488
+
489
+ it("calls onCancel when cancel button is clicked", () => {
490
+ const onCancel = vi.fn()
491
+ render(
492
+ <FeedbackActions onSubmit={vi.fn()} onCancel={onCancel} />,
493
+ )
494
+ fireEvent.click(screen.getByText("Cancel"))
495
+ expect(onCancel).toHaveBeenCalledTimes(1)
496
+ })
497
+
498
+ it("disables submit button when submitDisabled is true", () => {
499
+ render(
500
+ <FeedbackActions
501
+ onSubmit={vi.fn()}
502
+ onCancel={vi.fn()}
503
+ submitDisabled={true}
504
+ />,
505
+ )
506
+ const submitButton = screen.getByText("Submit")
507
+ expect(submitButton.hasAttribute("disabled")).toBe(true)
508
+ })
509
+ })