@handled-ai/design-system 0.18.2 → 0.18.3

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 (37) hide show
  1. package/dist/charts/chart.d.ts +1 -1
  2. package/dist/components/feedback-primitives.d.ts +2 -21
  3. package/dist/components/feedback-primitives.js +6 -90
  4. package/dist/components/feedback-primitives.js.map +1 -1
  5. package/dist/components/score-why-chips.d.ts +1 -1
  6. package/dist/components/score-why-chips.js +5 -26
  7. package/dist/components/score-why-chips.js.map +1 -1
  8. package/dist/components/signal-priority-popover.d.ts +1 -1
  9. package/dist/components/signal-priority-popover.js +7 -172
  10. package/dist/components/signal-priority-popover.js.map +1 -1
  11. package/dist/components/timeline-activity.d.ts +16 -1
  12. package/dist/components/timeline-activity.js +69 -1
  13. package/dist/components/timeline-activity.js.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js.map +1 -1
  16. package/dist/prototype/index.d.ts +1 -1
  17. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  18. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  19. package/dist/prototype/prototype-config.d.ts +1 -1
  20. package/dist/prototype/prototype-inbox-view.d.ts +12 -2
  21. package/dist/prototype/prototype-inbox-view.js +102 -37
  22. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  23. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  24. package/dist/prototype/prototype-shell.d.ts +1 -1
  25. package/dist/{signal-priority-popover-DWaAMhPI.d.ts → signal-priority-popover-DQ_VuHac.d.ts} +2 -26
  26. package/package.json +1 -3
  27. package/src/components/__tests__/timeline-activity.test.tsx +137 -0
  28. package/src/components/feedback-primitives.tsx +26 -148
  29. package/src/components/score-why-chips.tsx +2 -28
  30. package/src/components/signal-priority-popover.tsx +3 -194
  31. package/src/components/timeline-activity.tsx +112 -1
  32. package/src/index.ts +1 -1
  33. package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
  34. package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +322 -0
  35. package/src/prototype/prototype-config.ts +1 -11
  36. package/src/prototype/prototype-inbox-view.tsx +131 -33
  37. package/src/components/__tests__/wit-636-feedback-states.test.tsx +0 -546
@@ -1,546 +0,0 @@
1
- /**
2
- * Tests for WIT-636: Feedback confirmation and persisted-state support.
3
- *
4
- * Covers:
5
- * - SignalPriorityPopover factor feedback with no note shows `Saved`
6
- * - SignalPriorityPopover factor feedback with existing current-user note shows `Your feedback:` after initial render and after prop updates
7
- * - SignalPriorityPopover factor feedback with teammate fallback shows `Team feedback:`
8
- * - Priority popover footer initial feedback hydrates and submit confirmation appears
9
- * - Bucket footer submit shows saved/submitted state
10
- * - Bucket initial feedback shows `Your feedback:` or `Team feedback:`
11
- * - Footer syncs when initialFeedback arrives after mount, switches when feedbackKey changes, does not overwrite active edits for same key
12
- * - Balance strip renders for structured WHY rows
13
- */
14
-
15
- import { describe, it, expect, vi } from "vitest"
16
- import React from "react"
17
- import { render, screen, fireEvent, act } from "@testing-library/react"
18
- import { FeedbackFooter } from "../feedback-primitives"
19
- import type { PersistedFeedbackData, FeedbackSubmitData } from "../feedback-primitives"
20
- import { SignalPriorityPopover } from "../signal-priority-popover"
21
- import type { PriorityFactor } from "../signal-priority-popover"
22
- import { ScoreWhyChips } from "../score-why-chips"
23
- import type {
24
- QueueItem,
25
- SignalScoreData,
26
- SignalScoreExplanationBucket,
27
- SignalScoreExplanationSignal,
28
- } from "../../prototype/prototype-config"
29
-
30
- // ─── Shared mock data ────────────────────────────────────────────────────────
31
-
32
- const mockFactors: PriorityFactor[] = [
33
- {
34
- key: "timing-asymmetry",
35
- label: "Timing asymmetry",
36
- icon: "radar",
37
- tone: "alert",
38
- direction: "raises",
39
- score: 92,
40
- rationale: "Test transaction 12h ago → no follow-on liquidation yet.",
41
- },
42
- {
43
- key: "funds-at-stake",
44
- label: "Funds at stake",
45
- icon: "wallet",
46
- tone: "alert",
47
- direction: "raises",
48
- score: 88,
49
- rationale: "$3.4M moved in 8h · current treasury balance $0.00",
50
- },
51
- ]
52
-
53
- const mockItem: QueueItem = {
54
- id: "case-1",
55
- title: "Test Case",
56
- details: "Some details",
57
- statusColor: "red",
58
- time: "2h ago",
59
- company: "TestCorp",
60
- tag1: "urgent",
61
- }
62
-
63
- function makeSignal(overrides: Partial<SignalScoreExplanationSignal> = {}): SignalScoreExplanationSignal {
64
- return {
65
- id: "sig-1",
66
- label: "Treasury Liquidation",
67
- primaryValue: "-$1,724,310.11",
68
- qualifier: "100% of balance",
69
- counterparty: "-> JPMorgan Chase --6042",
70
- time: "7h ago",
71
- ...overrides,
72
- }
73
- }
74
-
75
- function makeBucket(overrides: Partial<SignalScoreExplanationBucket> = {}): SignalScoreExplanationBucket {
76
- return {
77
- key: "treasury_liquidation",
78
- label: "Treasury Liquidation",
79
- kind: "signal",
80
- signalCount: 2,
81
- icon: "trending-down",
82
- tone: "alert",
83
- signals: [
84
- makeSignal({ id: "sig-1" }),
85
- makeSignal({ id: "sig-2", primaryValue: "-$500,000.00", qualifier: "45% of balance" }),
86
- ],
87
- ...overrides,
88
- }
89
- }
90
-
91
- function makeSignalData(overrides: Partial<SignalScoreData> = {}): SignalScoreData {
92
- return {
93
- score: 85,
94
- factors: [],
95
- whyNow: "Large treasury movement",
96
- evidence: [],
97
- confidence: 90,
98
- explanationBuckets: [makeBucket()],
99
- ...overrides,
100
- }
101
- }
102
-
103
- // ─── SignalPriorityPopover factor feedback ──────────────────────────────────
104
-
105
- describe("SignalPriorityPopover factor feedback", () => {
106
- it("factor feedback with no note shows Saved (persisted state)", () => {
107
- const onFactorFeedback = vi.fn()
108
- render(
109
- <SignalPriorityPopover
110
- score={79}
111
- urgencyLabel="High"
112
- factors={mockFactors}
113
- onFeedbackSubmit={vi.fn()}
114
- onFactorFeedback={onFactorFeedback}
115
- />,
116
- )
117
-
118
- // Open popover
119
- fireEvent.click(screen.getByTestId("priority-popover-trigger"))
120
-
121
- // Click thumb-up on first factor
122
- fireEvent.click(screen.getByTestId("factor-thumb-up-timing-asymmetry"))
123
-
124
- // Submit without typing a note
125
- fireEvent.click(screen.getByTestId("factor-submit-timing-asymmetry"))
126
-
127
- // After submit, factor transitions to persisted state showing "Your feedback:"
128
- const indicator = screen.getByTestId("factor-feedback-persisted-timing-asymmetry")
129
- expect(indicator).toBeTruthy()
130
- expect(indicator.textContent).toContain("Your feedback:")
131
-
132
- // Callback should have been called
133
- expect(onFactorFeedback).toHaveBeenCalledWith("timing-asymmetry", "up", "")
134
- })
135
-
136
- it("factor feedback with existing current-user note shows 'Your feedback:' after initial render", () => {
137
- const initialFactorFeedback = {
138
- "timing-asymmetry": { type: "down" as const, detail: "Score is too high", ownershipLabel: "Your feedback" },
139
- }
140
- render(
141
- <SignalPriorityPopover
142
- score={79}
143
- urgencyLabel="High"
144
- factors={mockFactors}
145
- onFeedbackSubmit={vi.fn()}
146
- onFactorFeedback={vi.fn()}
147
- initialFactorFeedback={initialFactorFeedback}
148
- />,
149
- )
150
-
151
- // Open popover
152
- fireEvent.click(screen.getByTestId("priority-popover-trigger"))
153
-
154
- // Should show persisted indicator with "Your feedback:"
155
- const indicator = screen.getByTestId("factor-feedback-persisted-timing-asymmetry")
156
- expect(indicator).toBeTruthy()
157
- expect(indicator.textContent).toContain("Your feedback:")
158
- expect(indicator.textContent).toContain("Score is too high")
159
- })
160
-
161
- it("factor feedback with existing current-user note shows 'Your feedback:' after prop updates", () => {
162
- const onFactorFeedback = vi.fn()
163
-
164
- const { rerender } = render(
165
- <SignalPriorityPopover
166
- score={79}
167
- urgencyLabel="High"
168
- factors={mockFactors}
169
- onFeedbackSubmit={vi.fn()}
170
- onFactorFeedback={onFactorFeedback}
171
- />,
172
- )
173
-
174
- // Open popover
175
- fireEvent.click(screen.getByTestId("priority-popover-trigger"))
176
-
177
- // Initially no persisted feedback
178
- expect(screen.queryByTestId("factor-feedback-persisted-timing-asymmetry")).toBeNull()
179
-
180
- // Rerender with initialFactorFeedback
181
- rerender(
182
- <SignalPriorityPopover
183
- score={79}
184
- urgencyLabel="High"
185
- factors={mockFactors}
186
- onFeedbackSubmit={vi.fn()}
187
- onFactorFeedback={onFactorFeedback}
188
- initialFactorFeedback={{
189
- "timing-asymmetry": { type: "up", detail: "Looks correct", ownershipLabel: "Your feedback" },
190
- }}
191
- />,
192
- )
193
-
194
- // Now should show persisted indicator
195
- const indicator = screen.getByTestId("factor-feedback-persisted-timing-asymmetry")
196
- expect(indicator).toBeTruthy()
197
- expect(indicator.textContent).toContain("Your feedback:")
198
- expect(indicator.textContent).toContain("Looks correct")
199
- })
200
-
201
- it("factor feedback with teammate fallback shows 'Team feedback:'", () => {
202
- const initialFactorFeedback = {
203
- "funds-at-stake": { type: "up" as const, detail: "Accurate numbers", ownershipLabel: "Team feedback" },
204
- }
205
- render(
206
- <SignalPriorityPopover
207
- score={79}
208
- urgencyLabel="High"
209
- factors={mockFactors}
210
- onFeedbackSubmit={vi.fn()}
211
- onFactorFeedback={vi.fn()}
212
- initialFactorFeedback={initialFactorFeedback}
213
- />,
214
- )
215
-
216
- // Open popover
217
- fireEvent.click(screen.getByTestId("priority-popover-trigger"))
218
-
219
- // Should show "Team feedback:" for the funds-at-stake factor
220
- const indicator = screen.getByTestId("factor-feedback-persisted-funds-at-stake")
221
- expect(indicator).toBeTruthy()
222
- expect(indicator.textContent).toContain("Team feedback:")
223
- expect(indicator.textContent).toContain("Accurate numbers")
224
- })
225
- })
226
-
227
- // ─── Priority popover footer initial feedback ────────────────────────────────
228
-
229
- describe("Priority popover footer initial feedback", () => {
230
- it("hydrates from initialPriorityFeedback and submit confirmation appears", () => {
231
- const initialPriorityFeedback: PersistedFeedbackData = {
232
- sentiment: "negative",
233
- reasonTop: "Wrong factor weighting",
234
- detail: "Last-contact weight is too high",
235
- ownershipLabel: "Your feedback",
236
- }
237
- render(
238
- <SignalPriorityPopover
239
- score={79}
240
- urgencyLabel="High"
241
- factors={mockFactors}
242
- onFeedbackSubmit={vi.fn()}
243
- initialPriorityFeedback={initialPriorityFeedback}
244
- />,
245
- )
246
-
247
- // Open popover
248
- fireEvent.click(screen.getByTestId("priority-popover-trigger"))
249
-
250
- // Should show persisted indicator in the footer
251
- const indicator = screen.getByTestId("persisted-feedback-indicator")
252
- expect(indicator).toBeTruthy()
253
- expect(indicator.textContent).toContain("Your feedback:")
254
- expect(indicator.textContent).toContain("Last-contact weight is too high")
255
- })
256
- })
257
-
258
- // ─── Bucket footer submit shows saved/submitted state ─────────────────────
259
-
260
- describe("Bucket feedback footer", () => {
261
- it("submit shows saved/submitted state", () => {
262
- const onBucketFeedback = vi.fn()
263
- render(
264
- <ScoreWhyChips
265
- item={mockItem}
266
- signalData={makeSignalData({ onBucketFeedback })}
267
- />,
268
- )
269
-
270
- // Expand bucket
271
- fireEvent.click(screen.getByText("Treasury Liquidation"))
272
-
273
- // Click "Not helpful"
274
- fireEvent.click(screen.getByText("Not helpful"))
275
-
276
- // Submit
277
- fireEvent.click(screen.getByText("Submit"))
278
-
279
- // Should show "Saved" pill
280
- const savedPill = screen.getByTestId("feedback-submitted-pill")
281
- expect(savedPill).toBeTruthy()
282
- expect(savedPill.textContent).toContain("Saved")
283
- })
284
-
285
- it("bucket initial feedback shows 'Your feedback:'", () => {
286
- const onBucketFeedback = vi.fn()
287
- const initialBucketFeedback: Record<string, PersistedFeedbackData> = {
288
- treasury_liquidation: {
289
- sentiment: "positive",
290
- detail: "Counterparty match was right",
291
- ownershipLabel: "Your feedback",
292
- },
293
- }
294
- render(
295
- <ScoreWhyChips
296
- item={mockItem}
297
- signalData={makeSignalData({ onBucketFeedback, initialBucketFeedback })}
298
- />,
299
- )
300
-
301
- // Expand bucket
302
- fireEvent.click(screen.getByText("Treasury Liquidation"))
303
-
304
- // Should show persisted indicator
305
- const indicator = screen.getByTestId("persisted-feedback-indicator")
306
- expect(indicator).toBeTruthy()
307
- expect(indicator.textContent).toContain("Your feedback:")
308
- expect(indicator.textContent).toContain("Counterparty match was right")
309
- })
310
-
311
- it("bucket initial feedback shows 'Team feedback:'", () => {
312
- const onBucketFeedback = vi.fn()
313
- const initialBucketFeedback: Record<string, PersistedFeedbackData> = {
314
- treasury_liquidation: {
315
- sentiment: "negative",
316
- detail: "Timing was off",
317
- ownershipLabel: "Team feedback",
318
- },
319
- }
320
- render(
321
- <ScoreWhyChips
322
- item={mockItem}
323
- signalData={makeSignalData({ onBucketFeedback, initialBucketFeedback })}
324
- />,
325
- )
326
-
327
- // Expand bucket
328
- fireEvent.click(screen.getByText("Treasury Liquidation"))
329
-
330
- // Should show persisted indicator with team label
331
- const indicator = screen.getByTestId("persisted-feedback-indicator")
332
- expect(indicator).toBeTruthy()
333
- expect(indicator.textContent).toContain("Team feedback:")
334
- expect(indicator.textContent).toContain("Timing was off")
335
- })
336
- })
337
-
338
- // ─── FeedbackFooter sync behavior ────────────────────────────────────────────
339
-
340
- describe("FeedbackFooter sync behavior", () => {
341
- it("syncs when initialFeedback arrives after mount", () => {
342
- const onFeedbackChange = vi.fn()
343
- const { rerender } = render(
344
- <FeedbackFooter
345
- feedback={null}
346
- onFeedbackChange={onFeedbackChange}
347
- onSubmit={vi.fn()}
348
- feedbackKey="bucket-1"
349
- />,
350
- )
351
-
352
- // Initially no persisted indicator
353
- expect(screen.queryByTestId("persisted-feedback-indicator")).toBeNull()
354
- expect(screen.getByText("Helpful")).toBeTruthy()
355
-
356
- // Simulate async data arriving
357
- const incoming: PersistedFeedbackData = {
358
- sentiment: "positive",
359
- detail: "Looks good",
360
- ownershipLabel: "Your feedback",
361
- }
362
-
363
- rerender(
364
- <FeedbackFooter
365
- feedback="positive"
366
- onFeedbackChange={onFeedbackChange}
367
- onSubmit={vi.fn()}
368
- feedbackKey="bucket-1"
369
- initialFeedback={incoming}
370
- />,
371
- )
372
-
373
- // Now should show persisted indicator
374
- const indicator = screen.getByTestId("persisted-feedback-indicator")
375
- expect(indicator).toBeTruthy()
376
- expect(indicator.textContent).toContain("Your feedback:")
377
- expect(indicator.textContent).toContain("Looks good")
378
- })
379
-
380
- it("switches when feedbackKey changes", () => {
381
- const onFeedbackChange = vi.fn()
382
- const fb1: PersistedFeedbackData = {
383
- sentiment: "positive",
384
- detail: "Good",
385
- ownershipLabel: "Your feedback",
386
- }
387
- const fb2: PersistedFeedbackData = {
388
- sentiment: "negative",
389
- detail: "Bad",
390
- ownershipLabel: "Team feedback",
391
- }
392
-
393
- const { rerender } = render(
394
- <FeedbackFooter
395
- feedback="positive"
396
- onFeedbackChange={onFeedbackChange}
397
- onSubmit={vi.fn()}
398
- feedbackKey="bucket-1"
399
- initialFeedback={fb1}
400
- />,
401
- )
402
-
403
- // Should show fb1 content
404
- expect(screen.getByTestId("persisted-feedback-indicator").textContent).toContain("Good")
405
-
406
- // Switch to different key with different feedback
407
- rerender(
408
- <FeedbackFooter
409
- feedback="negative"
410
- onFeedbackChange={onFeedbackChange}
411
- onSubmit={vi.fn()}
412
- feedbackKey="bucket-2"
413
- initialFeedback={fb2}
414
- />,
415
- )
416
-
417
- // Should now show fb2 content
418
- const indicator = screen.getByTestId("persisted-feedback-indicator")
419
- expect(indicator.textContent).toContain("Team feedback:")
420
- expect(indicator.textContent).toContain("Bad")
421
- })
422
-
423
- it("does not overwrite active edits for same key", () => {
424
- const onFeedbackChange = vi.fn()
425
- const onSubmit = vi.fn()
426
- const fb: PersistedFeedbackData = {
427
- sentiment: "positive",
428
- detail: "Initial note",
429
- ownershipLabel: "Your feedback",
430
- }
431
-
432
- const { rerender } = render(
433
- <FeedbackFooter
434
- feedback="positive"
435
- onFeedbackChange={onFeedbackChange}
436
- onSubmit={onSubmit}
437
- feedbackKey="bucket-1"
438
- initialFeedback={fb}
439
- />,
440
- )
441
-
442
- // Click the persisted indicator to open editor
443
- fireEvent.click(screen.getByTestId("persisted-feedback-indicator"))
444
-
445
- // Now should be in editing mode with the expansion open
446
- expect(screen.getByPlaceholderText("Add optional detail…")).toBeTruthy()
447
-
448
- // Type new detail
449
- const input = screen.getByPlaceholderText("Add optional detail…")
450
- fireEvent.change(input, { target: { value: "My new note" } })
451
-
452
- // Rerender with updated initialFeedback (simulating prop change) - same key
453
- const fbUpdated: PersistedFeedbackData = {
454
- sentiment: "negative",
455
- detail: "Server-side update",
456
- ownershipLabel: "Team feedback",
457
- }
458
- rerender(
459
- <FeedbackFooter
460
- feedback="positive"
461
- onFeedbackChange={onFeedbackChange}
462
- onSubmit={onSubmit}
463
- feedbackKey="bucket-1"
464
- initialFeedback={fbUpdated}
465
- />,
466
- )
467
-
468
- // The editor should still be open with the user's text - not the server update
469
- const editInput = screen.getByPlaceholderText("Add optional detail…")
470
- expect(editInput).toBeTruthy()
471
- expect((editInput as HTMLInputElement).value).toBe("My new note")
472
- })
473
- })
474
-
475
- // ─── Balance strip rendering ──────────────────────────────────────────────────
476
-
477
- describe("Balance context strip", () => {
478
- it("renders for structured WHY rows with currentBalance", () => {
479
- const signalWithBalance = makeSignal({
480
- id: "sig-balance",
481
- currentBalance: "$3.0M",
482
- balanceContext: "down from $23M",
483
- })
484
- render(
485
- <ScoreWhyChips
486
- item={mockItem}
487
- signalData={makeSignalData({
488
- explanationBuckets: [
489
- makeBucket({ signals: [signalWithBalance], signalCount: 1 }),
490
- ],
491
- })}
492
- />,
493
- )
494
-
495
- // Expand bucket
496
- fireEvent.click(screen.getByText("Treasury Liquidation"))
497
-
498
- // Should render balance strip
499
- const strip = screen.getByTestId("balance-context-strip")
500
- expect(strip).toBeTruthy()
501
- expect(strip.textContent).toContain("Current balance")
502
- expect(strip.textContent).toContain("$3.0M")
503
- expect(strip.textContent).toContain("down from $23M")
504
- })
505
-
506
- it("renders only balanceContext when currentBalance is absent", () => {
507
- const signalWithContext = makeSignal({
508
- id: "sig-ctx",
509
- balanceContext: "significant velocity",
510
- })
511
- render(
512
- <ScoreWhyChips
513
- item={mockItem}
514
- signalData={makeSignalData({
515
- explanationBuckets: [
516
- makeBucket({ signals: [signalWithContext], signalCount: 1 }),
517
- ],
518
- })}
519
- />,
520
- )
521
-
522
- // Expand bucket
523
- fireEvent.click(screen.getByText("Treasury Liquidation"))
524
-
525
- // Should render balance strip with only context
526
- const strip = screen.getByTestId("balance-context-strip")
527
- expect(strip).toBeTruthy()
528
- expect(strip.textContent).toContain("significant velocity")
529
- expect(strip.textContent).not.toContain("Current balance")
530
- })
531
-
532
- it("does not render balance strip when neither currentBalance nor balanceContext is present", () => {
533
- render(
534
- <ScoreWhyChips
535
- item={mockItem}
536
- signalData={makeSignalData()}
537
- />,
538
- )
539
-
540
- // Expand bucket
541
- fireEvent.click(screen.getByText("Treasury Liquidation"))
542
-
543
- // Should NOT render balance strip
544
- expect(screen.queryByTestId("balance-context-strip")).toBeNull()
545
- })
546
- })