@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
@@ -17,14 +17,10 @@ import {
17
17
  ChevronDown,
18
18
  ChevronUp,
19
19
  Info,
20
- ThumbsUp,
21
- ThumbsDown,
22
- Check,
23
- Pencil,
24
20
  } from "lucide-react"
25
21
  import { cn } from "../lib/utils"
26
22
  import { FeedbackFooter } from "./feedback-primitives"
27
- import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "./feedback-primitives"
23
+ import type { FeedbackChipTree, FeedbackSubmitData } from "./feedback-primitives"
28
24
  import type { SignalScoreUrgencyLabel } from "../prototype/prototype-config"
29
25
  import { getSignalScoreUrgencyLabel, scoreRangeForUrgency, SIGNAL_TONE_CLASSES } from "./score-why-chips"
30
26
 
@@ -62,12 +58,6 @@ export interface SignalPriorityPopoverProps {
62
58
  feedbackChips?: FeedbackChipTree[]
63
59
  onFeedbackSubmit?: (data: FeedbackSubmitData) => void
64
60
  className?: string
65
- /** Persisted factor-level feedback (keyed by factor key). */
66
- initialFactorFeedback?: Record<string, { type: "up" | "down"; detail: string; ownershipLabel?: string }>
67
- /** Callback when user submits factor-level feedback. */
68
- onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
69
- /** Persisted priority-level feedback for the footer. */
70
- initialPriorityFeedback?: PersistedFeedbackData | null
71
61
  }
72
62
 
73
63
  // ---------------------------------------------------------------------------
@@ -162,13 +152,7 @@ function DirectionIcon({ direction }: { direction: PriorityFactor["direction"] }
162
152
  // PriorityFactorRow
163
153
  // ---------------------------------------------------------------------------
164
154
 
165
- interface PriorityFactorRowProps {
166
- factor: PriorityFactor
167
- initialFeedback?: { type: "up" | "down"; detail: string; ownershipLabel?: string }
168
- onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
169
- }
170
-
171
- function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }: PriorityFactorRowProps) {
155
+ function PriorityFactorRow({ factor }: { factor: PriorityFactor }) {
172
156
  const IconComponent = FACTOR_ICONS[factor.icon] ?? Activity
173
157
  const toneClasses = TONE_ICON_CLASSES[factor.tone]
174
158
  const directionClasses = DIRECTION_CLASSES[factor.direction]
@@ -179,50 +163,6 @@ function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }: Priori
179
163
  ? "Lowers"
180
164
  : "Neutral"
181
165
 
182
- const [thumbState, setThumbState] = React.useState<"up" | "down" | null>(
183
- initialFeedback?.type ?? null,
184
- )
185
- const [showInput, setShowInput] = React.useState(false)
186
- const [detailText, setDetailText] = React.useState(initialFeedback?.detail ?? "")
187
- const [saved, setSaved] = React.useState(!!initialFeedback)
188
- const [savedDetail, setSavedDetail] = React.useState(initialFeedback?.detail ?? "")
189
- const ownershipLabel = initialFeedback?.ownershipLabel ?? "Your feedback"
190
-
191
- // Sync with initialFeedback prop changes
192
- React.useEffect(() => {
193
- if (initialFeedback) {
194
- setThumbState(initialFeedback.type)
195
- setSaved(true)
196
- setSavedDetail(initialFeedback.detail)
197
- }
198
- }, [initialFeedback])
199
-
200
- const handleThumbClick = React.useCallback(
201
- (type: "up" | "down") => {
202
- if (thumbState === type) {
203
- // Toggle off
204
- setThumbState(null)
205
- setShowInput(false)
206
- setSaved(false)
207
- onFactorFeedback?.(factor.key, null)
208
- } else {
209
- setThumbState(type)
210
- setShowInput(true)
211
- setSaved(false)
212
- }
213
- },
214
- [thumbState, factor.key, onFactorFeedback],
215
- )
216
-
217
- const handleSubmitDetail = React.useCallback(() => {
218
- if (!thumbState) return
219
- const text = detailText.trim()
220
- onFactorFeedback?.(factor.key, thumbState, text)
221
- setSaved(true)
222
- setSavedDetail(text)
223
- setShowInput(false)
224
- }, [thumbState, detailText, factor.key, onFactorFeedback])
225
-
226
166
  return (
227
167
  <div
228
168
  className="grid grid-cols-[20px_1fr_auto] gap-x-3 gap-y-1 px-4 py-3"
@@ -284,124 +224,6 @@ function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }: Priori
284
224
 
285
225
  {/* empty grid cell under score column */}
286
226
  <div />
287
-
288
- {/* Factor-level feedback row (spans icon + content columns) */}
289
- {onFactorFeedback && (
290
- <>
291
- <div />
292
- <div className="col-span-2 mt-1">
293
- {saved && !showInput ? (
294
- /* Persisted / saved indicator */
295
- <button
296
- type="button"
297
- onClick={() => {
298
- setDetailText(savedDetail)
299
- setShowInput(true)
300
- setSaved(false)
301
- }}
302
- className="group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
303
- data-testid={`factor-feedback-persisted-${factor.key}`}
304
- >
305
- <span className="font-medium">{ownershipLabel}:</span>
306
- {thumbState === "up" ? (
307
- <ThumbsUp className="h-[10px] w-[10px]" />
308
- ) : (
309
- <ThumbsDown className="h-[10px] w-[10px]" />
310
- )}
311
- {savedDetail && (
312
- <span className="max-w-[180px] truncate text-muted-foreground/70">
313
- {savedDetail}
314
- </span>
315
- )}
316
- <Pencil className="h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity" />
317
- </button>
318
- ) : (
319
- <div className="flex items-center gap-1.5">
320
- {/* Inline thumb buttons */}
321
- <button
322
- type="button"
323
- onClick={() => handleThumbClick("up")}
324
- className={cn(
325
- "p-1 rounded transition-colors",
326
- thumbState === "up"
327
- ? "text-foreground bg-muted"
328
- : "text-muted-foreground/40 hover:text-foreground hover:bg-muted/50",
329
- )}
330
- title="This factor is accurate"
331
- data-testid={`factor-thumb-up-${factor.key}`}
332
- >
333
- <ThumbsUp className="h-[10px] w-[10px]" />
334
- </button>
335
- <button
336
- type="button"
337
- onClick={() => handleThumbClick("down")}
338
- className={cn(
339
- "p-1 rounded transition-colors",
340
- thumbState === "down"
341
- ? "text-red-600 bg-red-50"
342
- : "text-muted-foreground/40 hover:text-red-600 hover:bg-red-50/50",
343
- )}
344
- title="Report issue with this factor"
345
- data-testid={`factor-thumb-down-${factor.key}`}
346
- >
347
- <ThumbsDown className="h-[10px] w-[10px]" />
348
- </button>
349
-
350
- {/* Transient "Saved" pill */}
351
- {saved && (
352
- <span
353
- className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600"
354
- role="status"
355
- data-testid={`factor-saved-${factor.key}`}
356
- >
357
- <Check className="h-[10px] w-[10px]" />
358
- Saved
359
- </span>
360
- )}
361
- </div>
362
- )}
363
-
364
- {/* Inline detail input */}
365
- {showInput && thumbState && (
366
- <div className="mt-1.5">
367
- <input
368
- type="text"
369
- value={detailText}
370
- onChange={(e) => setDetailText(e.target.value)}
371
- onKeyDown={(e) => {
372
- if (e.key === "Enter") handleSubmitDetail()
373
- if (e.key === "Escape") setShowInput(false)
374
- }}
375
- placeholder={
376
- thumbState === "up"
377
- ? "What\u2019s accurate? (optional)"
378
- : "What\u2019s wrong? (optional)"
379
- }
380
- className="w-full h-6 rounded border border-border bg-background px-2 text-[11px] text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring"
381
- data-testid={`factor-detail-input-${factor.key}`}
382
- />
383
- <div className="mt-1 flex items-center gap-1.5">
384
- <button
385
- type="button"
386
- onClick={handleSubmitDetail}
387
- className="bg-foreground text-background rounded px-2 py-0.5 text-[10px] font-semibold"
388
- data-testid={`factor-submit-${factor.key}`}
389
- >
390
- Submit
391
- </button>
392
- <button
393
- type="button"
394
- onClick={() => setShowInput(false)}
395
- className="border border-border rounded px-2 py-0.5 text-[10px] font-medium"
396
- >
397
- Cancel
398
- </button>
399
- </div>
400
- </div>
401
- )}
402
- </div>
403
- </>
404
- )}
405
227
  </div>
406
228
  )
407
229
  }
@@ -419,9 +241,6 @@ export function SignalPriorityPopover({
419
241
  feedbackChips,
420
242
  onFeedbackSubmit,
421
243
  className,
422
- initialFactorFeedback,
423
- onFactorFeedback,
424
- initialPriorityFeedback,
425
244
  }: SignalPriorityPopoverProps) {
426
245
  const urgencyLabel = getSignalScoreUrgencyLabel(score, providedLabel)
427
246
  const scoreRange = scoreRangeForUrgency(urgencyLabel)
@@ -433,9 +252,6 @@ export function SignalPriorityPopover({
433
252
  const triggerHover = URGENCY_TRIGGER_HOVER[urgencyLabel]
434
253
  const triggerOpen = URGENCY_TRIGGER_OPEN[urgencyLabel]
435
254
 
436
- // Derive a stable feedbackKey for the footer from score + urgencyLabel
437
- const footerFeedbackKey = `priority-${score}-${urgencyLabel}`
438
-
439
255
  return (
440
256
  <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
441
257
  <PopoverPrimitive.Trigger asChild>
@@ -516,12 +332,7 @@ export function SignalPriorityPopover({
516
332
  {/* Factor rows */}
517
333
  <div className="divide-y divide-border/40">
518
334
  {factors.map((factor) => (
519
- <PriorityFactorRow
520
- key={factor.key}
521
- factor={factor}
522
- initialFeedback={initialFactorFeedback?.[factor.key]}
523
- onFactorFeedback={onFactorFeedback}
524
- />
335
+ <PriorityFactorRow key={factor.key} factor={factor} />
525
336
  ))}
526
337
  </div>
527
338
  </>
@@ -538,8 +349,6 @@ export function SignalPriorityPopover({
538
349
  negativeChips={feedbackChips ?? DEFAULT_PRIORITY_FEEDBACK_CHIPS}
539
350
  positivePrompt="Thanks. Anything to keep about this score?"
540
351
  className="px-4 py-3"
541
- initialFeedback={initialPriorityFeedback}
542
- feedbackKey={footerFeedbackKey}
543
352
  />
544
353
  </div>
545
354
  )}
@@ -4,6 +4,24 @@ import * as React from "react"
4
4
  import { cn } from "../lib/utils"
5
5
  import { ChevronDown, ChevronUp, ExternalLink } from "lucide-react"
6
6
 
7
+ export type TimelineEventTone =
8
+ | "red"
9
+ | "amber"
10
+ | "emerald"
11
+ | "violet"
12
+ | "blue"
13
+ | "slate"
14
+ | "salesforce"
15
+ | "gmail"
16
+
17
+ export interface TimelineEventActor {
18
+ kind: "user" | "integration" | "system"
19
+ name?: string
20
+ initials?: string
21
+ avatarUrl?: string
22
+ verb?: string
23
+ }
24
+
7
25
  export interface TimelineEvent {
8
26
  id: string
9
27
  icon: React.ReactNode
@@ -28,8 +46,57 @@ export interface TimelineEvent {
28
46
  defaultExpanded?: boolean
29
47
  isInteractive?: boolean
30
48
  onSourceClick?: () => void
49
+ tone?: TimelineEventTone
50
+ actor?: TimelineEventActor
51
+ isSystemNoise?: boolean
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Tone class map — every class is a complete static string literal so
56
+ // Tailwind's JIT scanner can detect them. NO interpolation.
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export const TONE_CLASSES: Record<
60
+ TimelineEventTone,
61
+ { dot: string; icon: string }
62
+ > = {
63
+ red: {
64
+ dot: "bg-red-50 border-red-200 dark:bg-red-950/30 dark:border-red-900/40",
65
+ icon: "text-red-600 dark:text-red-300",
66
+ },
67
+ amber: {
68
+ dot: "bg-amber-50 border-amber-200 dark:bg-amber-950/30 dark:border-amber-900/40",
69
+ icon: "text-amber-600 dark:text-amber-300",
70
+ },
71
+ emerald: {
72
+ dot: "bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900/40",
73
+ icon: "text-emerald-600 dark:text-emerald-300",
74
+ },
75
+ violet: {
76
+ dot: "bg-violet-50 border-violet-200 dark:bg-violet-950/30 dark:border-violet-900/40",
77
+ icon: "text-violet-600 dark:text-violet-300",
78
+ },
79
+ blue: {
80
+ dot: "bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-900/40",
81
+ icon: "text-blue-600 dark:text-blue-300",
82
+ },
83
+ slate: {
84
+ dot: "bg-slate-100 border-slate-200 dark:bg-slate-800/50 dark:border-slate-700",
85
+ icon: "text-slate-500 dark:text-slate-300",
86
+ },
87
+ salesforce: {
88
+ dot: "bg-white border-[#00A1E0]/25 dark:bg-background dark:border-[#00A1E0]/25",
89
+ icon: "text-[#00A1E0]",
90
+ },
91
+ gmail: {
92
+ dot: "bg-white border-red-200 dark:bg-background dark:border-red-900/40",
93
+ icon: "text-red-500 dark:text-red-300",
94
+ },
31
95
  }
32
96
 
97
+ const NEUTRAL_DOT_CLASSES = "border-border/60 bg-background"
98
+ const NEUTRAL_ICON_CLASSES = "text-muted-foreground"
99
+
33
100
  export interface TimelineActivityProps {
34
101
  events: TimelineEvent[]
35
102
  className?: string
@@ -49,12 +116,54 @@ export function TimelineActivity({ events, className }: TimelineActivityProps) {
49
116
  )
50
117
  }
51
118
 
119
+ function ActorByline({ actor, time }: { actor: TimelineEventActor; time: string }) {
120
+ if (actor.kind === "system") return null
121
+
122
+ if (actor.kind === "integration") {
123
+ return (
124
+ <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground" data-testid="actor-byline">
125
+ <span>Integration</span>
126
+ <span className="text-muted-foreground/40">&middot;</span>
127
+ <span>{time}</span>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ // actor.kind === "user"
133
+ const verb = actor.verb ?? "performed this action"
134
+ const displayInitials = actor.initials ?? (actor.name ? actor.name.charAt(0).toUpperCase() : "?")
135
+
136
+ return (
137
+ <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground" data-testid="actor-byline">
138
+ {actor.avatarUrl ? (
139
+ <img
140
+ src={actor.avatarUrl}
141
+ alt={actor.name ?? "User"}
142
+ className="h-4 w-4 rounded-full object-cover"
143
+ />
144
+ ) : (
145
+ <span className="flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/10 text-[8px] font-semibold text-muted-foreground">
146
+ {displayInitials}
147
+ </span>
148
+ )}
149
+ <span className="text-foreground font-medium">{actor.name}</span>
150
+ <span>{verb}</span>
151
+ <span className="text-muted-foreground/40">&middot;</span>
152
+ <span>{time}</span>
153
+ </div>
154
+ )
155
+ }
156
+
52
157
  function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean }) {
53
158
  const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)
54
159
  const [showAllRecipients, setShowAllRecipients] = React.useState(false)
55
160
  const hasContent = !!event.content
56
161
  const hasEmail = !!event.email
57
162
 
163
+ const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null
164
+ const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES
165
+ const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES
166
+
58
167
  return (
59
168
  <div className="group relative flex gap-3.5">
60
169
  {!isLast && (
@@ -62,7 +171,7 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
62
171
  )}
63
172
 
64
173
  <div className="relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background">
65
- <div className="flex h-4.5 w-4.5 items-center justify-center rounded-full border border-border/60 bg-background text-muted-foreground ring-4 ring-background">
174
+ <div className={cn("flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background", dotClasses, iconClasses)} data-testid="timeline-dot">
66
175
  {event.icon}
67
176
  </div>
68
177
  </div>
@@ -77,6 +186,8 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
77
186
  </span>
78
187
  </div>
79
188
 
189
+ {event.actor && <ActorByline actor={event.actor} time={event.time} />}
190
+
80
191
  {(hasContent || hasEmail) && (
81
192
  <div className="mt-2">
82
193
  {event.isInteractive ? (
package/src/index.ts CHANGED
@@ -38,7 +38,7 @@ export * from "./components/dropdown-menu"
38
38
  export * from "./components/empty-state"
39
39
  export * from "./components/entity-panel"
40
40
  export { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions } from "./components/feedback-primitives"
41
- export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData } from "./components/feedback-primitives"
41
+ export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData } from "./components/feedback-primitives"
42
42
  export { SignalPriorityPopover } from "./components/signal-priority-popover"
43
43
  export type { SignalPriorityPopoverProps, PriorityFactor } from "./components/signal-priority-popover"
44
44
  export * from "./components/filter-chip"
@@ -89,8 +89,8 @@ describe("DetailView attentionCount", () => {
89
89
  expect(pill).not.toBeNull();
90
90
  expect(pill!.textContent).toContain("5");
91
91
 
92
- // Click the timeline header button to expand
93
- const timelineButton = container.querySelector("button.group\\/timeline") as HTMLElement;
92
+ // Click the timeline collapse button to expand
93
+ const timelineButton = container.querySelector('[data-testid="timeline-collapse-btn"]') as HTMLElement;
94
94
  expect(timelineButton).not.toBeNull();
95
95
  fireEvent.click(timelineButton);
96
96