@handled-ai/design-system 0.20.30 → 0.20.32

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 (33) hide show
  1. package/dist/components/account-contacts-popover.d.ts +1 -1
  2. package/dist/components/account-contacts-popover.js +25 -20
  3. package/dist/components/account-contacts-popover.js.map +1 -1
  4. package/dist/components/email-recipient-field.d.ts +9 -1
  5. package/dist/components/email-recipient-field.js +30 -4
  6. package/dist/components/email-recipient-field.js.map +1 -1
  7. package/dist/components/score-why-chips.d.ts +1 -1
  8. package/dist/components/signal-priority-popover.d.ts +1 -1
  9. package/dist/components/signal-priority-popover.js +14 -4
  10. package/dist/components/signal-priority-popover.js.map +1 -1
  11. package/dist/components/suggested-actions.d.ts +2 -1
  12. package/dist/components/suggested-actions.js.map +1 -1
  13. package/dist/components/timeline-activity.js +51 -41
  14. package/dist/components/timeline-activity.js.map +1 -1
  15. package/dist/index.d.ts +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 +1 -1
  21. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  22. package/dist/prototype/prototype-shell.d.ts +1 -1
  23. package/dist/{signal-priority-popover-DIUVhipw.d.ts → signal-priority-popover-BmG4WAxT.d.ts} +17 -1
  24. package/package.json +1 -1
  25. package/src/components/__tests__/account-contacts-popover.test.tsx +71 -0
  26. package/src/components/__tests__/email-recipient-field.test.tsx +123 -0
  27. package/src/components/__tests__/signal-priority-popover.test.tsx +72 -0
  28. package/src/components/__tests__/timeline-activity.test.tsx +16 -0
  29. package/src/components/account-contacts-popover.tsx +33 -19
  30. package/src/components/email-recipient-field.tsx +45 -0
  31. package/src/components/signal-priority-popover.tsx +50 -21
  32. package/src/components/suggested-actions.tsx +4 -3
  33. package/src/components/timeline-activity.tsx +7 -1
@@ -513,4 +513,127 @@ describe("EmailRecipientField", () => {
513
513
  // trigger another search.
514
514
  expect(secondOnSearch).not.toHaveBeenCalled()
515
515
  })
516
+
517
+ // --- Last activity (WIT-1007) ---
518
+
519
+ const activityContact: SuggestedContact = {
520
+ name: "Cara Customer",
521
+ role: "VP Ops",
522
+ email: "cara@example.com",
523
+ confirmed: true,
524
+ lastActivity: { date: "Jun 8, 2026", type: "email", timelineEventId: "evt-1" },
525
+ }
526
+
527
+ it("renders last activity as non-clickable text when no onOpenRecentActivity callback is provided", () => {
528
+ render(
529
+ <EmailRecipientField
530
+ label="To"
531
+ recipients={[]}
532
+ onRecipientsChange={vi.fn()}
533
+ showPicker
534
+ contacts={[activityContact]}
535
+ />,
536
+ )
537
+
538
+ fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
539
+ expect(screen.getByText("Jun 8, 2026")).toBeTruthy()
540
+ // No clickable activity button exists.
541
+ expect(screen.queryByRole("button", { name: /Last activity/i })).toBeNull()
542
+ })
543
+
544
+ it("renders last activity as non-clickable text when timelineEventId is missing even with a callback", () => {
545
+ const onOpenRecentActivity = vi.fn()
546
+ render(
547
+ <EmailRecipientField
548
+ label="To"
549
+ recipients={[]}
550
+ onRecipientsChange={vi.fn()}
551
+ showPicker
552
+ contacts={[
553
+ {
554
+ ...activityContact,
555
+ lastActivity: { date: "Jun 8, 2026", type: "email" },
556
+ },
557
+ ]}
558
+ onOpenRecentActivity={onOpenRecentActivity}
559
+ />,
560
+ )
561
+
562
+ fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
563
+ expect(screen.getByText("Jun 8, 2026")).toBeTruthy()
564
+ expect(screen.queryByRole("button", { name: /Last activity/i })).toBeNull()
565
+ })
566
+
567
+ it("renders last activity as a clickable button only when callback and timelineEventId both exist", () => {
568
+ const onOpenRecentActivity = vi.fn()
569
+ render(
570
+ <EmailRecipientField
571
+ label="To"
572
+ recipients={[]}
573
+ onRecipientsChange={vi.fn()}
574
+ showPicker
575
+ contacts={[activityContact]}
576
+ onOpenRecentActivity={onOpenRecentActivity}
577
+ />,
578
+ )
579
+
580
+ fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
581
+ const activityButton = screen.getByRole("button", { name: /Last activity/i })
582
+ expect(activityButton.tagName).toBe("BUTTON")
583
+ })
584
+
585
+ it("invokes onOpenRecentActivity with the contact and does NOT add a recipient on activity click", () => {
586
+ const onOpenRecentActivity = vi.fn()
587
+ const onChange = vi.fn()
588
+ render(
589
+ <EmailRecipientField
590
+ label="To"
591
+ recipients={[]}
592
+ onRecipientsChange={onChange}
593
+ showPicker
594
+ contacts={[activityContact]}
595
+ onOpenRecentActivity={onOpenRecentActivity}
596
+ />,
597
+ )
598
+
599
+ fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
600
+ fireEvent.click(screen.getByRole("button", { name: /Last activity/i }))
601
+
602
+ expect(onOpenRecentActivity).toHaveBeenCalledTimes(1)
603
+ expect(onOpenRecentActivity).toHaveBeenCalledWith(activityContact)
604
+ // Clicking the activity must not select/add the recipient.
605
+ expect(onChange).not.toHaveBeenCalled()
606
+ })
607
+
608
+ it("keeps the last-activity button clickable for already-added contacts without adding the recipient", () => {
609
+ const onOpenRecentActivity = vi.fn()
610
+ const onChange = vi.fn()
611
+ render(
612
+ <EmailRecipientField
613
+ label="To"
614
+ recipients={[]}
615
+ onRecipientsChange={onChange}
616
+ showPicker
617
+ contacts={[activityContact]}
618
+ addedEmails={new Set([activityContact.email!.toLowerCase()])}
619
+ onOpenRecentActivity={onOpenRecentActivity}
620
+ />,
621
+ )
622
+
623
+ fireEvent.click(screen.getByRole("button", { name: /Contacts/ }))
624
+
625
+ // The row is disabled (already added) but its activity button stays clickable.
626
+ const option = screen
627
+ .getAllByRole("option")
628
+ .find((o) => o.textContent?.includes("Cara Customer"))!
629
+ expect(option.className).toContain("pointer-events-none")
630
+ const activityButton = within(option).getByRole("button", { name: /Last activity/i })
631
+ expect(activityButton.className).toContain("pointer-events-auto")
632
+
633
+ fireEvent.click(activityButton)
634
+
635
+ expect(onOpenRecentActivity).toHaveBeenCalledTimes(1)
636
+ expect(onOpenRecentActivity).toHaveBeenCalledWith(activityContact)
637
+ expect(onChange).not.toHaveBeenCalled()
638
+ })
516
639
  })
@@ -378,4 +378,76 @@ describe("SignalPriorityPopover", () => {
378
378
  expect(trigger.className).toContain("text-amber-800")
379
379
  expect(trigger.textContent).toContain("Medium Priority")
380
380
  })
381
+
382
+ // ─── Custom trigger (Task 0) ───────────────────────────────────────────────
383
+
384
+ it("renders the default button trigger unchanged when no custom trigger is passed", () => {
385
+ render(<SignalPriorityPopover {...defaultProps} />)
386
+ const trigger = screen.getByTestId("priority-popover-trigger")
387
+ // Default trigger is a <button> reading "{urgencyLabel} Priority"
388
+ expect(trigger.tagName).toBe("BUTTON")
389
+ expect(trigger.textContent).toContain("High Priority")
390
+ })
391
+
392
+ it("renders a custom trigger inside the popover trigger, merging the data-testid, and opens the panel", () => {
393
+ render(
394
+ <SignalPriorityPopover
395
+ {...defaultProps}
396
+ renderTrigger={({ urgencyLabel, open }) => (
397
+ <button
398
+ type="button"
399
+ aria-label={`View ${urgencyLabel} urgency detail`}
400
+ data-open={open}
401
+ >
402
+ bar-glyph
403
+ </button>
404
+ )}
405
+ />,
406
+ )
407
+
408
+ // data-testid is merged onto the custom child element
409
+ const trigger = screen.getByTestId("priority-popover-trigger")
410
+ expect(trigger.tagName).toBe("BUTTON")
411
+ expect(trigger.textContent).toBe("bar-glyph")
412
+ expect(trigger.getAttribute("aria-label")).toBe("View High urgency detail")
413
+ // No default "High Priority" label rendered
414
+ expect(trigger.textContent).not.toContain("High Priority")
415
+
416
+ // Opens the panel
417
+ expect(screen.queryByTestId("priority-popover-content")).toBeNull()
418
+ fireEvent.click(trigger)
419
+ expect(screen.getByTestId("priority-popover-content")).toBeTruthy()
420
+ // open state is forwarded to the render function
421
+ expect(trigger.getAttribute("data-open")).toBe("true")
422
+ })
423
+
424
+ // ─── Controlled open state (Task 0) ─────────────────────────────────────────
425
+
426
+ it("respects the controlled open prop for visibility", () => {
427
+ const { rerender } = render(
428
+ <SignalPriorityPopover {...defaultProps} open={false} onOpenChange={vi.fn()} />,
429
+ )
430
+ expect(screen.queryByTestId("priority-popover-content")).toBeNull()
431
+
432
+ rerender(<SignalPriorityPopover {...defaultProps} open={true} onOpenChange={vi.fn()} />)
433
+ expect(screen.getByTestId("priority-popover-content")).toBeTruthy()
434
+ })
435
+
436
+ it("calls onOpenChange and does not self-open when controlled", () => {
437
+ const onOpenChange = vi.fn()
438
+ render(<SignalPriorityPopover {...defaultProps} open={false} onOpenChange={onOpenChange} />)
439
+
440
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
441
+
442
+ // Controlled: open stays driven by the prop (still false), but handler fires
443
+ expect(onOpenChange).toHaveBeenCalledWith(true)
444
+ expect(screen.queryByTestId("priority-popover-content")).toBeNull()
445
+ })
446
+
447
+ it("still works uncontrolled when open/onOpenChange are omitted", () => {
448
+ render(<SignalPriorityPopover {...defaultProps} />)
449
+ expect(screen.queryByTestId("priority-popover-content")).toBeNull()
450
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
451
+ expect(screen.getByTestId("priority-popover-content")).toBeTruthy()
452
+ })
381
453
  })
@@ -44,6 +44,22 @@ describe("TimelineActivity", () => {
44
44
  expect(container.firstElementChild).toHaveAttribute("data-variant", "default")
45
45
  })
46
46
 
47
+ it("renders each timeline item wrapper with scroll/focus anchors and test ids", () => {
48
+ const events = [minimal({ id: "evt-a" }), minimal({ id: "evt-b" })]
49
+ const { container } = render(<TimelineActivity events={events} />)
50
+
51
+ for (const id of ["evt-a", "evt-b"]) {
52
+ const wrapper = container.querySelector(`#timeline-event-${id}`)
53
+ expect(wrapper).not.toBeNull()
54
+ expect(wrapper).toHaveAttribute("data-testid", `timeline-event-${id}`)
55
+ expect(wrapper).toHaveAttribute("data-activity-id", id)
56
+ expect(wrapper).toHaveAttribute("tabindex", "-1")
57
+ }
58
+
59
+ // The test id is also reachable via screen for click-through targeting.
60
+ expect(screen.getByTestId("timeline-event-evt-a")).toBeTruthy()
61
+ })
62
+
47
63
  it("marks the root and interactive cards with the case-panel variant", () => {
48
64
  const event = minimal({
49
65
  isInteractive: true,
@@ -5,6 +5,7 @@ import {
5
5
  Clock,
6
6
  ExternalLink,
7
7
  } from "lucide-react"
8
+ import { cn } from "../lib/utils"
8
9
  import type { SuggestedContact, SuggestedActionsIconMap } from "./suggested-actions"
9
10
 
10
11
  // ---------------------------------------------------------------------------
@@ -37,7 +38,7 @@ export interface AccountContactsPopoverProps {
37
38
  /** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */
38
39
  onSelectSwitch?: (contact: SuggestedContact) => void
39
40
  onViewAll?: () => void
40
- onOpenRecentActivity?: () => void
41
+ onOpenRecentActivity?: (contact: SuggestedContact) => void
41
42
  trigger: React.ReactNode
42
43
  iconMap?: SuggestedActionsIconMap
43
44
  }
@@ -117,24 +118,37 @@ export function AccountContactsPopover({
117
118
  <div className="truncate text-xs text-muted-foreground leading-tight">
118
119
  {c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? ""}
119
120
  </div>
120
- {c.lastActivity && (
121
- <button
122
- type="button"
123
- onClick={(e) => {
124
- e.stopPropagation()
125
- onOpenRecentActivity?.()
126
- setOpen(false)
127
- }}
128
- className="mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
129
- >
130
- <Clock className="w-3 h-3 shrink-0" />
131
- <span className="shrink-0 font-medium">Last activity</span>
132
- <span className="shrink-0 text-muted-foreground/60">·</span>
133
- <span className="shrink-0">{c.lastActivity.date}</span>
134
- <span className="shrink-0 text-muted-foreground/60">·</span>
135
- <span className="truncate capitalize">{c.lastActivity.type}</span>
136
- </button>
137
- )}
121
+ {c.lastActivity && (() => {
122
+ const activityContent = (
123
+ <>
124
+ <Clock className="w-3 h-3 shrink-0" />
125
+ <span className="shrink-0 font-medium">Last activity</span>
126
+ <span className="shrink-0 text-muted-foreground/60">·</span>
127
+ <span className="shrink-0">{c.lastActivity.date}</span>
128
+ <span className="shrink-0 text-muted-foreground/60">·</span>
129
+ <span className="truncate capitalize">{c.lastActivity.type}</span>
130
+ </>
131
+ )
132
+ const chipBaseClass =
133
+ "mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground"
134
+ return onOpenRecentActivity && c.lastActivity.timelineEventId ? (
135
+ <button
136
+ type="button"
137
+ onClick={(e) => {
138
+ e.stopPropagation()
139
+ onOpenRecentActivity(c)
140
+ setOpen(false)
141
+ }}
142
+ className={cn(chipBaseClass, "hover:text-foreground hover:bg-muted/50 transition-colors")}
143
+ >
144
+ {activityContent}
145
+ </button>
146
+ ) : (
147
+ <div className={chipBaseClass}>
148
+ {activityContent}
149
+ </div>
150
+ )
151
+ })()}
138
152
  </div>
139
153
  <div className="ml-2 flex items-center gap-1.5 shrink-0">
140
154
  {resolvedDefaultSelectLabel && (
@@ -4,6 +4,7 @@ import * as React from "react"
4
4
  import { Popover as PopoverPrimitive } from "radix-ui"
5
5
  import {
6
6
  ChevronDown,
7
+ Clock,
7
8
  CornerDownLeft,
8
9
  Plus,
9
10
  Search,
@@ -63,6 +64,14 @@ export interface EmailRecipientFieldProps {
63
64
  onSearch?: (query: string) => void
64
65
  /** Shows a "Searching contacts..." indicator while async results load. */
65
66
  searchLoading?: boolean
67
+ /**
68
+ * Opens the recent activity for a contact in the Account Panel timeline.
69
+ * When provided AND the contact's `lastActivity.timelineEventId` exists, the
70
+ * last-activity line renders as a button that calls this callback (and stops
71
+ * propagation so it never adds/selects the recipient). Otherwise the line
72
+ * renders as non-clickable text.
73
+ */
74
+ onOpenRecentActivity?: (contact: SuggestedContact) => void
66
75
  }
67
76
 
68
77
  function RecipientChipPill({
@@ -146,6 +155,7 @@ function ContactPickerContents({
146
155
  onAddEmail,
147
156
  onSearch,
148
157
  searchLoading = false,
158
+ onOpenRecentActivity,
149
159
  }: {
150
160
  contacts: SuggestedContact[]
151
161
  addedEmails: Set<string>
@@ -153,6 +163,7 @@ function ContactPickerContents({
153
163
  onAddEmail: (email: string) => void
154
164
  onSearch?: (query: string) => void
155
165
  searchLoading?: boolean
166
+ onOpenRecentActivity?: (contact: SuggestedContact) => void
156
167
  }) {
157
168
  const [query, setQuery] = React.useState("")
158
169
 
@@ -279,6 +290,38 @@ function ContactPickerContents({
279
290
  {email}
280
291
  </div>
281
292
  ) : null}
293
+ {contact.lastActivity ? (() => {
294
+ const activityContent = (
295
+ <>
296
+ <Clock className="size-3 shrink-0" />
297
+ <span className="shrink-0">Last activity</span>
298
+ <span className="shrink-0 text-muted-foreground/60">·</span>
299
+ <span className="shrink-0">{contact.lastActivity.date}</span>
300
+ <span className="shrink-0 text-muted-foreground/60">·</span>
301
+ <span className="truncate capitalize">{contact.lastActivity.type}</span>
302
+ </>
303
+ )
304
+ return onOpenRecentActivity && contact.lastActivity.timelineEventId ? (
305
+ <button
306
+ type="button"
307
+ onClick={(event) => {
308
+ event.stopPropagation()
309
+ onOpenRecentActivity(contact)
310
+ }}
311
+ // `pointer-events-auto` keeps the activity button clickable even
312
+ // when the parent row is disabled (no email / already added) and
313
+ // gets `pointer-events-none` — the timeline activity is still
314
+ // openable for those contacts.
315
+ className="mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground hover:text-foreground transition-colors pointer-events-auto"
316
+ >
317
+ {activityContent}
318
+ </button>
319
+ ) : (
320
+ <div className="mt-1 flex max-w-full items-center gap-1 overflow-hidden text-[11px] text-muted-foreground">
321
+ {activityContent}
322
+ </div>
323
+ )
324
+ })() : null}
282
325
  </div>
283
326
  {alreadyAdded ? (
284
327
  <span className="shrink-0 text-[10.5px] font-medium text-muted-foreground">
@@ -318,6 +361,7 @@ export function EmailRecipientField({
318
361
  contactToRecipient,
319
362
  onSearch,
320
363
  searchLoading,
364
+ onOpenRecentActivity,
321
365
  }: EmailRecipientFieldProps) {
322
366
  const [value, setValue] = React.useState("")
323
367
  const [pickerOpen, setPickerOpen] = React.useState(false)
@@ -465,6 +509,7 @@ export function EmailRecipientField({
465
509
  }}
466
510
  onSearch={onSearch}
467
511
  searchLoading={searchLoading}
512
+ onOpenRecentActivity={onOpenRecentActivity}
468
513
  />
469
514
  </PopoverPrimitive.Content>
470
515
  </PopoverPrimitive.Portal>
@@ -74,6 +74,19 @@ export interface SignalPriorityPopoverProps {
74
74
  onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
75
75
  /** Persisted priority-level feedback for the footer. */
76
76
  initialPriorityFeedback?: PersistedFeedbackData | null
77
+ /**
78
+ * Optional custom trigger element. When provided, it is rendered inside
79
+ * `<PopoverPrimitive.Trigger asChild>` instead of the default button, so it
80
+ * must be a single `React.ReactElement` capable of receiving props/ref (not a
81
+ * generic `ReactNode`). Receives the resolved `urgencyLabel` and the current
82
+ * `open` state. Provide an accessible name on the element (e.g.
83
+ * `aria-label="View {urgencyLabel} urgency detail"`).
84
+ */
85
+ renderTrigger?: (args: { urgencyLabel: SignalScoreUrgencyLabel; open: boolean }) => React.ReactElement
86
+ /** Controlled open state. When defined, the popover is controlled; otherwise it falls back to internal state. */
87
+ open?: boolean
88
+ /** Called when the open state should change. Passed straight through to the Radix root. */
89
+ onOpenChange?: (open: boolean) => void
77
90
  }
78
91
 
79
92
  // ---------------------------------------------------------------------------
@@ -290,11 +303,24 @@ export function SignalPriorityPopover({
290
303
  initialPriorityFeedback,
291
304
  scoreDisplay = "number",
292
305
  formulaLabel = "Priority factors",
306
+ renderTrigger,
307
+ open: controlledOpen,
308
+ onOpenChange,
293
309
  }: SignalPriorityPopoverProps) {
294
310
  const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
295
311
  const scoreRange = getUrgencyRange(urgencyLabel)
296
312
 
297
- const [open, setOpen] = React.useState(false)
313
+ // Controlled/uncontrolled open state: use the controlled prop when defined,
314
+ // otherwise fall back to internal state.
315
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
316
+ const open = controlledOpen ?? uncontrolledOpen
317
+ const setOpen = React.useCallback(
318
+ (next: boolean) => {
319
+ if (controlledOpen === undefined) setUncontrolledOpen(next)
320
+ onOpenChange?.(next)
321
+ },
322
+ [controlledOpen, onOpenChange],
323
+ )
298
324
  const [feedback, setFeedback] = React.useState<"positive" | "negative" | null>(null)
299
325
 
300
326
  const triggerDefault = URGENCY_TRIGGER_DEFAULT[urgencyLabel]
@@ -306,26 +332,29 @@ export function SignalPriorityPopover({
306
332
 
307
333
  return (
308
334
  <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
309
- <PopoverPrimitive.Trigger asChild>
310
- <button
311
- type="button"
312
- className={cn(
313
- "inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
314
- triggerDefault,
315
- triggerHover,
316
- open && triggerOpen,
317
- open && "outline-2 outline-foreground outline-offset-2",
318
- className,
319
- )}
320
- data-testid="priority-popover-trigger"
321
- >
322
- {urgencyLabel} Priority
323
- {open ? (
324
- <ChevronUp className="h-3 w-3" />
325
- ) : (
326
- <ChevronDown className="h-3 w-3" />
327
- )}
328
- </button>
335
+ <PopoverPrimitive.Trigger asChild data-testid="priority-popover-trigger">
336
+ {renderTrigger ? (
337
+ renderTrigger({ urgencyLabel, open })
338
+ ) : (
339
+ <button
340
+ type="button"
341
+ className={cn(
342
+ "inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
343
+ triggerDefault,
344
+ triggerHover,
345
+ open && triggerOpen,
346
+ open && "outline-2 outline-foreground outline-offset-2",
347
+ className,
348
+ )}
349
+ >
350
+ {urgencyLabel} Priority
351
+ {open ? (
352
+ <ChevronUp className="h-3 w-3" />
353
+ ) : (
354
+ <ChevronDown className="h-3 w-3" />
355
+ )}
356
+ </button>
357
+ )}
329
358
  </PopoverPrimitive.Trigger>
330
359
 
331
360
  <PopoverPrimitive.Portal>
@@ -84,6 +84,7 @@ export interface SuggestedContact {
84
84
  lastActivity?: {
85
85
  date: string
86
86
  type: string
87
+ timelineEventId?: string
87
88
  }
88
89
  }
89
90
 
@@ -547,7 +548,7 @@ function EmailHeader({
547
548
  onBccAdd?: (contact: SuggestedContact) => void
548
549
  onBccRemove?: (index: number) => void
549
550
  onOpenAccountDetails?: () => void
550
- onOpenRecentActivity?: () => void
551
+ onOpenRecentActivity?: (contact: SuggestedContact) => void
551
552
  iconMap?: SuggestedActionsIconMap
552
553
  showSubject?: boolean
553
554
  accountDetailsLabel?: string
@@ -910,7 +911,7 @@ function SuggestedActionCard({
910
911
  signature?: string | React.ReactNode
911
912
  onDuplicate?: (id: number | string) => void
912
913
  onOpenAccountDetails?: () => void
913
- onOpenRecentActivity?: () => void
914
+ onOpenRecentActivity?: (contact: SuggestedContact) => void
914
915
  onMarkComplete?: (id: number | string) => void
915
916
  onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
916
917
  onFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
@@ -1571,7 +1572,7 @@ export interface SuggestedActionsProps {
1571
1572
  signature?: string | React.ReactNode
1572
1573
  onDuplicate?: (id: number | string) => void
1573
1574
  onOpenAccountDetails?: () => void
1574
- onOpenRecentActivity?: () => void
1575
+ onOpenRecentActivity?: (contact: SuggestedContact) => void
1575
1576
  onMarkComplete?: (id: number | string) => void
1576
1577
  onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
1577
1578
  iconMap?: SuggestedActionsIconMap
@@ -298,7 +298,13 @@ function TimelineItem({
298
298
  const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES
299
299
 
300
300
  return (
301
- <div className={classes.outerRowGap}>
301
+ <div
302
+ id={`timeline-event-${event.id}`}
303
+ data-testid={`timeline-event-${event.id}`}
304
+ data-activity-id={event.id}
305
+ tabIndex={-1}
306
+ className={classes.outerRowGap}
307
+ >
302
308
  {!isLast && (
303
309
  <div className={classes.connector} />
304
310
  )}