@handled-ai/design-system 0.20.12 → 0.20.13

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.
@@ -55,34 +55,29 @@ function CommentComposer({
55
55
  data-slot="comment-composer"
56
56
  data-open={open ? "true" : undefined}
57
57
  className={cn(
58
- "flex items-start gap-4 rounded-xl transition-colors",
58
+ "border-border bg-background flex items-start gap-2 rounded-lg border px-2 py-1.5 transition-colors",
59
+ open && "ring-ring/30 ring-2",
59
60
  className
60
61
  )}
61
62
  >
62
- <Avatar size="sm" className="mt-1">
63
+ <Avatar size="sm" className="mt-px">
63
64
  {author?.avatarUrl ? <AvatarImage src={author.avatarUrl} alt={author.name ?? "You"} /> : null}
64
- <AvatarFallback className="bg-slate-700 text-[10px] font-semibold uppercase text-white dark:bg-slate-200 dark:text-slate-900">
65
+ <AvatarFallback className="bg-muted text-muted-foreground text-[10px] font-medium uppercase">
65
66
  {getInitials({ name: author?.name, email: author?.email })}
66
67
  </AvatarFallback>
67
68
  </Avatar>
68
69
 
69
- <div
70
- data-slot="comment-composer-shell"
71
- className={cn(
72
- "min-w-0 flex-1 rounded-xl border border-border bg-background transition-[box-shadow,border-color]",
73
- open ? "overflow-hidden shadow-sm" : "shadow-none"
74
- )}
75
- >
70
+ <div className="min-w-0 flex-1">
76
71
  <Textarea
77
72
  data-slot="comment-composer-input"
78
73
  value={text}
79
74
  onChange={(e) => setText(e.target.value)}
80
75
  onFocus={() => setFocused(true)}
81
76
  placeholder={placeholder}
82
- rows={open ? 4 : 1}
77
+ rows={open ? 3 : 1}
83
78
  className={cn(
84
- "resize-none rounded-none border-0 bg-transparent px-5 py-4 text-[15px] leading-6 shadow-none outline-none placeholder:text-muted-foreground/60 focus-visible:ring-0 focus-visible:ring-offset-0",
85
- open ? "min-h-32" : "min-h-14"
79
+ "resize-none border-0 bg-transparent px-1 py-0.5 text-sm leading-snug shadow-none focus-visible:ring-0",
80
+ !open && "min-h-0"
86
81
  )}
87
82
  onKeyDown={(e) => {
88
83
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
@@ -93,16 +88,15 @@ function CommentComposer({
93
88
  />
94
89
 
95
90
  {open ? (
96
- <div className="flex items-center justify-between gap-3 border-t border-border bg-muted/10 px-5 py-4">
97
- <span className="inline-flex items-center gap-2 text-sm text-muted-foreground">
98
- <Lock size={16} strokeWidth={1.75} /> {hint}
91
+ <div className="mt-0.5 flex items-center justify-between gap-2">
92
+ <span className="text-muted-foreground inline-flex items-center gap-1 text-[11px]">
93
+ <Lock size={12} /> {hint}
99
94
  </span>
100
- <span className="flex items-center gap-3">
95
+ <span className="flex items-center gap-2">
101
96
  <Button
102
97
  type="button"
103
98
  variant="ghost"
104
99
  size="sm"
105
- className="px-2 text-sm font-medium text-muted-foreground hover:bg-transparent hover:text-foreground"
106
100
  onClick={() => {
107
101
  setText("")
108
102
  setFocused(false)
@@ -110,15 +104,9 @@ function CommentComposer({
110
104
  >
111
105
  Cancel
112
106
  </Button>
113
- <Button
114
- type="button"
115
- size="sm"
116
- disabled={!canPost}
117
- onClick={post}
118
- className="rounded-lg bg-foreground px-4 text-sm font-semibold text-background shadow-none hover:bg-foreground/90"
119
- >
107
+ <Button type="button" size="sm" disabled={!canPost} onClick={post}>
120
108
  Comment
121
- <kbd className="ml-1 rounded px-1 text-[10px] text-background/70">⌘↵</kbd>
109
+ <kbd className="bg-primary-foreground/15 ml-1 rounded px-1 text-[10px]">⌘↵</kbd>
122
110
  </Button>
123
111
  </span>
124
112
  </div>
@@ -298,19 +298,34 @@ function PersonAvatar({ person, size = "sm" }: { person: ConvParticipant; size?:
298
298
  }
299
299
 
300
300
  const STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {
301
- responded: { label: "New reply", cls: "bg-status-active-bg text-status-active-fg border-status-active-border" },
301
+ responded: { label: "NEW REPLY", cls: "bg-status-warning-bg text-status-warning-fg border-status-warning-border" },
302
302
  draft: { label: "Draft", cls: "bg-background text-foreground/80 border-border" },
303
- awaiting: { label: "Awaiting", cls: "bg-status-pending-bg text-status-pending-fg border-status-pending-border" },
303
+ awaiting: { label: "SENT", cls: "bg-status-info-bg text-status-info-fg border-status-info-border" },
304
304
  viewing: { label: "Viewing", cls: "bg-muted text-muted-foreground border-border" },
305
305
  }
306
306
 
307
307
  const STATUS_DOT: Record<ConvStatus, string> = {
308
- responded: "bg-status-active-fg",
308
+ responded: "bg-status-warning-fg",
309
309
  draft: "bg-status-pending-fg",
310
- awaiting: "bg-status-pending-fg",
310
+ awaiting: "bg-status-info-fg",
311
311
  viewing: "bg-muted-foreground/50",
312
312
  }
313
313
 
314
+ const THREAD_ROW_ACCENT: Record<ConvStatus, string> = {
315
+ responded: "border-l-4 border-l-status-warning-border bg-status-warning-bg/25",
316
+ awaiting: "border-l-4 border-l-status-info-border bg-status-info-bg/25",
317
+ draft: "border-l-4 border-l-status-pending-border bg-status-pending-bg/20",
318
+ viewing: "",
319
+ }
320
+
321
+ const RECEIPT_CHIP: Record<NonNullable<ConvMessage["receipt"]>["kind"], string> = {
322
+ new: "border-status-warning-border bg-status-warning-bg text-status-warning-fg",
323
+ read: "border-status-info-border bg-status-info-bg text-status-info-fg",
324
+ opened: "border-status-info-border bg-status-info-bg text-status-info-fg",
325
+ sent: "border-status-info-border bg-status-info-bg text-status-info-fg",
326
+ draft: "border-status-pending-border bg-status-pending-bg text-status-pending-fg",
327
+ }
328
+
314
329
  function effectiveStatus(t: ConversationThread): ConvStatus {
315
330
  return t.canReply === false ? "viewing" : t.status
316
331
  }
@@ -432,10 +447,10 @@ function MessageView({
432
447
  </span>
433
448
  <span className="flex shrink-0 items-center gap-2">
434
449
  {message.receipt ? (
435
- <span className="text-muted-foreground inline-flex items-center gap-1 text-[11px]">
450
+ <span className={cn("inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4", RECEIPT_CHIP[message.receipt.kind])}>
436
451
  {message.receipt.kind === "new" ? (
437
452
  <CornerUpLeft size={11} />
438
- ) : message.receipt.kind === "read" ? (
453
+ ) : message.receipt.kind === "read" || message.receipt.kind === "sent" ? (
439
454
  <CheckCheck size={11} />
440
455
  ) : message.receipt.kind === "draft" ? (
441
456
  <FilePenLine size={11} />
@@ -811,10 +826,10 @@ function ThreadBody({
811
826
  return (
812
827
  <div data-slot="conv-thread-body" className="space-y-2">
813
828
  {canReply && thread.paused ? (
814
- <div className="border-status-pending-border bg-status-pending-bg text-status-pending-fg flex items-start gap-2 rounded-md border p-2.5 text-[12px]">
829
+ <div className="border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]">
815
830
  <Pause size={13} className="mt-0.5 shrink-0" />
816
831
  <span>
817
- <b>Follow-up actions stopped.</b> Your {thread.paused.playbook} next steps won’t send
832
+ <b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send
818
833
  automatically while this conversation is live. Continue it in {tenantName ?? "the app"} or Gmail.
819
834
  </span>
820
835
  </div>
@@ -930,7 +945,12 @@ function ThreadRow({
930
945
  const pill = STATUS_PILL[status]
931
946
 
932
947
  return (
933
- <div data-slot="conv-thread" data-open={open ? "true" : undefined} className="border-border border-b last:border-b-0">
948
+ <div
949
+ data-slot="conv-thread"
950
+ data-status={status}
951
+ data-open={open ? "true" : undefined}
952
+ className={cn("border-border border-b last:border-b-0", THREAD_ROW_ACCENT[status])}
953
+ >
934
954
  <button
935
955
  type="button"
936
956
  onClick={onToggleOpen}
@@ -991,11 +1011,13 @@ function ConversationPanel({
991
1011
  const draft = threads.filter((t) => effectiveStatus(t) === "draft").length
992
1012
  const awaiting = threads.filter((t) => effectiveStatus(t) === "awaiting").length
993
1013
  const anyPaused = threads.some((t) => t.paused)
1014
+ const hubGmailThread = threads.find((t) => canOpenInGmail(t, onOpenInGmail))
1015
+ const firstAwaiting = threads.find((t) => effectiveStatus(t) === "awaiting")
994
1016
 
995
1017
  const [hubOpen, setHubOpen] = React.useState(true)
996
1018
  const [openId, setOpenId] = React.useState<string | null>(() => {
997
1019
  if (defaultOpenThreadId) return defaultOpenThreadId
998
- const firstActionable = threads.find((t) => ["responded", "draft"].includes(t.status) && t.canReply !== false)
1020
+ const firstActionable = threads.find((t) => ["responded", "draft", "awaiting"].includes(t.status) && t.canReply !== false)
999
1021
  return firstActionable ? firstActionable.threadId : null
1000
1022
  })
1001
1023
 
@@ -1004,11 +1026,11 @@ function ConversationPanel({
1004
1026
  // Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.
1005
1027
  const badge =
1006
1028
  responded > 0
1007
- ? { label: "Email response detected", dot: "bg-status-active-fg", ring: "bg-status-active-fg/30" }
1029
+ ? { label: "Email response detected", dot: "bg-status-warning-fg", ring: "bg-status-warning-fg/30" }
1008
1030
  : draft > 0
1009
1031
  ? { label: "Draft ready", dot: "bg-status-pending-fg", ring: "bg-status-pending-fg/30" }
1010
1032
  : awaiting > 0
1011
- ? { label: "Awaiting response", dot: "bg-status-pending-fg", ring: "bg-status-pending-fg/30" }
1033
+ ? { label: "Email sent · awaiting reply", dot: "bg-status-info-fg", ring: "bg-status-info-fg/30" }
1012
1034
  : { label: "Conversations", dot: "bg-muted-foreground/50", ring: "bg-muted-foreground/20" }
1013
1035
 
1014
1036
  const headTitle =
@@ -1017,30 +1039,56 @@ function ConversationPanel({
1017
1039
  : draft > 0
1018
1040
  ? `Draft ready on ${draft} ${draft === 1 ? "thread" : "threads"}`
1019
1041
  : awaiting > 0
1020
- ? `Awaiting response on ${awaiting} ${awaiting === 1 ? "thread" : "threads"}`
1042
+ ? awaiting === 1 && firstAwaiting
1043
+ ? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`
1044
+ : `Awaiting responses on ${awaiting} threads`
1021
1045
  : `${threads.length} email ${threads.length === 1 ? "thread" : "threads"}`
1022
1046
 
1047
+ const panelState = responded > 0 ? "responded" : draft > 0 ? "draft" : awaiting > 0 ? "awaiting" : "viewing"
1048
+
1023
1049
  return (
1024
1050
  <section
1025
1051
  data-slot="conversation-panel"
1026
1052
  data-responded={responded > 0 ? "true" : undefined}
1027
- className={cn("border-border bg-background overflow-hidden rounded-xl border", className)}
1053
+ data-state={panelState}
1054
+ className={cn(
1055
+ "bg-background overflow-hidden rounded-xl border",
1056
+ panelState === "responded"
1057
+ ? "border-status-warning-border"
1058
+ : panelState === "awaiting"
1059
+ ? "border-status-info-border"
1060
+ : "border-border",
1061
+ className,
1062
+ )}
1028
1063
  >
1029
- <button
1030
- type="button"
1031
- onClick={() => setHubOpen((v) => !v)}
1032
- aria-expanded={hubOpen}
1033
- className="flex w-full items-center gap-3 px-3 py-2.5 text-left"
1064
+ <div
1065
+ data-slot="conversation-panel-header"
1066
+ className={cn(
1067
+ "flex w-full items-center gap-2 px-3 py-2.5",
1068
+ panelState === "responded"
1069
+ ? "bg-status-warning-bg/45"
1070
+ : panelState === "awaiting"
1071
+ ? "bg-status-info-bg/55"
1072
+ : "bg-background",
1073
+ )}
1034
1074
  >
1075
+ <button
1076
+ type="button"
1077
+ onClick={() => setHubOpen((v) => !v)}
1078
+ aria-expanded={hubOpen}
1079
+ className="flex min-w-0 flex-1 items-center gap-3 text-left"
1080
+ >
1035
1081
  <span
1036
1082
  data-slot="conversation-badge"
1037
1083
  className={cn(
1038
1084
  "inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold",
1039
1085
  responded > 0
1040
- ? "bg-status-active-bg text-status-active-fg"
1041
- : draft > 0 || awaiting > 0
1042
- ? "bg-status-pending-bg text-status-pending-fg"
1043
- : "bg-muted text-muted-foreground"
1086
+ ? "bg-status-warning-bg text-status-warning-fg"
1087
+ : awaiting > 0
1088
+ ? "bg-status-info-bg text-status-info-fg"
1089
+ : draft > 0
1090
+ ? "bg-status-pending-bg text-status-pending-fg"
1091
+ : "bg-muted text-muted-foreground"
1044
1092
  )}
1045
1093
  >
1046
1094
  <span className="relative inline-flex size-2">
@@ -1056,12 +1104,18 @@ function ConversationPanel({
1056
1104
  {anyPaused ? <> · <b>playbook stopped</b></> : null}
1057
1105
  </span>
1058
1106
  </span>
1059
- {hubOpen ? (
1060
- <ChevronUp size={16} className="text-muted-foreground shrink-0" />
1061
- ) : (
1062
- <ChevronDown size={16} className="text-muted-foreground shrink-0" />
1063
- )}
1064
- </button>
1107
+ {hubOpen ? (
1108
+ <ChevronUp size={16} className="text-muted-foreground shrink-0" />
1109
+ ) : (
1110
+ <ChevronDown size={16} className="text-muted-foreground shrink-0" />
1111
+ )}
1112
+ </button>
1113
+ {hubGmailThread ? (
1114
+ <div className="shrink-0" onClick={(event) => event.stopPropagation()}>
1115
+ <OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />
1116
+ </div>
1117
+ ) : null}
1118
+ </div>
1065
1119
 
1066
1120
  {hubOpen ? (
1067
1121
  <div className="border-border border-t">
@@ -101,7 +101,7 @@ describe("DetailView case-panel-v2 section layout", () => {
101
101
  screen.getByText("Cash movement"),
102
102
  screen.getByText("Approve action"),
103
103
  screen.getByText("After-score marker"),
104
- screen.getByText(/activity timeline/i),
104
+ screen.getByText("Activity timeline"),
105
105
  screen.getByText("Legacy detail extra marker"),
106
106
  )
107
107
  })
@@ -125,7 +125,7 @@ describe("DetailView case-panel-v2 section layout", () => {
125
125
  screen.getByText("Opportunity marker"),
126
126
  screen.getByText("Primary action marker"),
127
127
  screen.getByText("Comment area marker"),
128
- screen.getByText(/activity timeline/i),
128
+ screen.getByText("Activity timeline"),
129
129
  )
130
130
  })
131
131
 
@@ -148,7 +148,7 @@ describe("DetailView case-panel-v2 section layout", () => {
148
148
 
149
149
  expectInDocumentOrder(
150
150
  screen.getByText("Comment composer marker"),
151
- screen.getByText(/activity timeline/i),
151
+ screen.getByText("Activity timeline"),
152
152
  )
153
153
  })
154
154
 
@@ -209,22 +209,20 @@ describe("DetailView timeline system-events toggle", () => {
209
209
  const badge = container.querySelector('[data-testid="hidden-count-badge"]')
210
210
  expect(badge).not.toBeNull()
211
211
  expect(badge?.textContent).toBe("2")
212
- expect(badge).toHaveClass("min-w-[22px]")
212
+ expect(badge).toHaveClass("min-w-[18px]")
213
213
  })
214
214
 
215
- it("calls localStorage.setItem when toggle changes and shows the active pill style", () => {
215
+ it("calls localStorage.setItem when toggle changes and shows a stronger pressed style", () => {
216
216
  const { container } = render(<DetailView {...baseProps()} />)
217
217
  expandTimeline(container)
218
218
  const toggle = container.querySelector(
219
219
  '[data-testid="system-events-toggle"]',
220
220
  ) as HTMLElement
221
221
  expect(toggle).toHaveAttribute("aria-pressed", "false")
222
- expect(toggle).toHaveAttribute("title", "Score changes are hidden.")
223
222
  fireEvent.click(toggle)
224
223
  expect(toggle).toHaveAttribute("aria-pressed", "true")
225
- expect(toggle).toHaveClass("border-foreground")
226
- expect(toggle).toHaveClass("bg-foreground")
227
- expect(toggle).toHaveAttribute("title", "Showing 2 score changes.")
224
+ expect(toggle.className).toContain("border-primary/40")
225
+ expect(toggle.className).toContain("bg-primary/10")
228
226
  expect(localStorageMock.setItem).toHaveBeenCalledWith(
229
227
  "test-show-score-changes",
230
228
  "true",
@@ -314,19 +312,18 @@ describe("DetailView timeline system-events toggle", () => {
314
312
  expect(toggle).toBeNull()
315
313
  })
316
314
 
317
- it("does not render a footer hint and uses the hidden hint as toggle help", () => {
315
+ it("shows footer hint below the case-panel timeline when timeline is expanded and system events are hidden", () => {
318
316
  const { container } = render(<DetailView {...baseProps()} />)
319
317
  expandTimeline(container)
320
318
  const timeline = container.querySelector('[data-variant="case-panel"]')
321
319
  const hint = container.querySelector('[data-testid="timeline-footer-hint"]')
322
- const toggle = container.querySelector('[data-testid="system-events-toggle"]')
323
320
  expect(timeline).not.toBeNull()
324
- expect(hint).toBeNull()
325
- expect(toggle).toHaveAttribute("title", "Score changes are hidden.")
326
- expect(toggle).toHaveAttribute("aria-label", "Score changes are hidden.")
321
+ expect(hint).not.toBeNull()
322
+ expect(hint?.textContent).toBe("Score changes are hidden.")
323
+ expect(timeline?.compareDocumentPosition(hint as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
327
324
  })
328
325
 
329
- it("uses visible footer hint text as toggle help with count when system events are shown", () => {
326
+ it("shows visible footer hint with count when system events are shown", () => {
330
327
  const { container } = render(<DetailView {...baseProps()} />)
331
328
  expandTimeline(container)
332
329
  // Toggle on
@@ -335,8 +332,8 @@ describe("DetailView timeline system-events toggle", () => {
335
332
  ) as HTMLElement
336
333
  fireEvent.click(toggle)
337
334
  const hint = container.querySelector('[data-testid="timeline-footer-hint"]')
338
- expect(hint).toBeNull()
339
- expect(toggle).toHaveAttribute("title", "Showing 2 score changes.")
335
+ expect(hint).not.toBeNull()
336
+ expect(hint?.textContent).toBe("Showing 2 score changes.")
340
337
  })
341
338
 
342
339
  // --- Toggle always renders when system-noise events exist (review fix #1) ---
@@ -417,10 +414,10 @@ describe("DetailView timeline system-events toggle", () => {
417
414
  const toggle = container.querySelector('[data-testid="system-events-toggle"]')
418
415
  expect(toggle).not.toBeNull()
419
416
  expect(toggle?.textContent).toContain("Legacy label")
420
- // Deprecated hint props are accepted and exposed as toggle help.
417
+ // Footer hint should work too
421
418
  expandTimeline(container)
422
419
  const hint = container.querySelector('[data-testid="timeline-footer-hint"]')
423
- expect(hint).toBeNull()
424
- expect(toggle).toHaveAttribute("title", "Legacy hidden hint.")
420
+ expect(hint).not.toBeNull()
421
+ expect(hint?.textContent).toBe("Legacy hidden hint.")
425
422
  })
426
423
  })
@@ -222,7 +222,6 @@ function TimelineSection({
222
222
  attentionCount,
223
223
  sysEvtConfig,
224
224
  lastActivityTime,
225
- isCasePanel = false,
226
225
  }: {
227
226
  timelineEvents: TimelineEvent[]
228
227
  showTimeline: boolean
@@ -232,7 +231,6 @@ function TimelineSection({
232
231
  attentionCount?: number
233
232
  sysEvtConfig?: TimelineSystemEventsConfig
234
233
  lastActivityTime?: string
235
- isCasePanel?: boolean
236
234
  }) {
237
235
  // Single-pass partition: compute visibleEvents and hiddenCount together
238
236
  const visibleEvents: TimelineEvent[] = []
@@ -247,9 +245,6 @@ function TimelineSection({
247
245
  // config was provided — so consumers that emit `isSystemNoise: true` always
248
246
  // give users a way to reveal those events.
249
247
  const toggleLabel = sysEvtConfig?.toggleLabel ?? "System events"
250
- const toggleHelp = showSystemEvents
251
- ? sysEvtConfig?.visibleHint?.replace("{count}", String(hiddenCount)) ?? "Hide system events"
252
- : sysEvtConfig?.hiddenHint ?? "Show system events"
253
248
 
254
249
  // Derive "Last activity" from the first *visible* event so the collapsed
255
250
  // header never points at a hidden score-update. The caller-supplied
@@ -267,106 +262,84 @@ function TimelineSection({
267
262
  const eventCountLabel = `${visibleCount} ${visibleCount === 1 ? "event" : "events"}`
268
263
 
269
264
  return (
270
- <div
271
- className={cn(
272
- isCasePanel ? "mt-8 border-t border-border pt-8 pb-8" : "mb-8"
273
- )}
274
- >
265
+ <div className="mb-8">
275
266
  {/* Header — outer non-interactive container */}
276
267
  <div
277
- className={cn(
278
- "flex w-full items-center justify-between",
279
- isCasePanel
280
- ? "gap-4 border-b border-border pb-5"
281
- : "group/timeline gap-2 rounded-md py-2 transition-colors hover:bg-muted/40 -mx-2 px-2"
282
- )}
268
+ className="group/timeline flex w-full items-center justify-between gap-2 py-2 rounded-md transition-colors hover:bg-muted/40 -mx-2 px-2"
283
269
  data-testid="timeline-header"
284
270
  >
285
271
  {/* Left: collapse/expand button */}
286
272
  <button
287
273
  type="button"
288
274
  onClick={() => setShowTimeline((prev) => !prev)}
289
- className="flex min-w-0 cursor-pointer items-center gap-2 border-0 bg-transparent p-0 text-left"
275
+ className="flex items-center gap-2 cursor-pointer bg-transparent border-0 p-0"
290
276
  data-testid="timeline-collapse-btn"
291
277
  >
292
- <h3 className="text-xs font-bold uppercase tracking-[0.16em] text-muted-foreground transition-colors group-hover/timeline:text-foreground">ACTIVITY TIMELINE</h3>
278
+ <h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider group-hover/timeline:text-foreground transition-colors">Activity timeline</h3>
293
279
  {!showTimeline && attentionCount != null && attentionCount > 0 && (
294
280
  <span className="inline-flex items-center gap-1 rounded-full bg-destructive/10 px-1.5 py-0.5 text-[10px] font-semibold text-destructive border border-destructive/20">
295
281
  {attentionCount} new
296
282
  </span>
297
283
  )}
298
- {!isCasePanel && !showTimeline && firstVisibleTime && (
284
+ {!showTimeline && firstVisibleTime && (
299
285
  <span className="text-[11px] text-muted-foreground/60" data-testid="last-activity-hint">
300
286
  &middot; Last activity {firstVisibleTime}
301
287
  </span>
302
288
  )}
289
+ <div className="flex items-center gap-1.5">
290
+ <span className="text-[11px] font-medium text-muted-foreground" data-testid="event-count">{eventCountLabel}</span>
291
+ <ChevronDown className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
292
+ </div>
303
293
  </button>
304
294
 
305
- {/* Right: system-events toggle, event count, and collapse affordance */}
306
- <div className="flex shrink-0 items-center gap-4">
307
- {hasSystemNoise && (
308
- <button
309
- type="button"
310
- onClick={() => setShowSystemEvents((prev) => !prev)}
311
- className={cn(
312
- "inline-flex shrink-0 cursor-pointer items-center gap-3 rounded-full border px-3.5 py-2 text-sm font-semibold transition-colors",
313
- showSystemEvents
314
- ? "border-foreground bg-foreground text-background shadow-sm hover:bg-foreground/90"
315
- : "border-border bg-background text-muted-foreground shadow-sm hover:bg-muted/40 hover:text-foreground"
316
- )}
317
- aria-pressed={showSystemEvents}
318
- aria-label={toggleHelp}
319
- title={toggleHelp}
320
- data-testid="system-events-toggle"
321
- >
322
- <span
323
- className={cn(
324
- "relative inline-flex h-4 w-8 shrink-0 items-center rounded-full p-0.5 transition-colors",
325
- showSystemEvents ? "bg-teal-600" : "bg-muted-foreground/30"
326
- )}
327
- aria-hidden="true"
328
- data-testid="system-events-indicator"
329
- >
330
- <span
331
- className={cn(
332
- "block h-3 w-3 rounded-full bg-white shadow-sm transition-transform",
333
- showSystemEvents ? "translate-x-4" : "translate-x-0"
334
- )}
335
- />
336
- </span>
337
- <span>{toggleLabel}</span>
338
- {!showSystemEvents ? (
339
- <span
340
- className="inline-flex min-w-[22px] items-center justify-center rounded-full bg-muted px-1.5 text-xs font-bold tabular-nums text-muted-foreground"
341
- data-testid="hidden-count-badge"
342
- >
343
- {hiddenCount}
344
- </span>
345
- ) : null}
346
- </button>
347
- )}
348
-
295
+ {/* Right: system-events toggle always rendered when noise events exist */}
296
+ {hasSystemNoise && (
349
297
  <button
350
298
  type="button"
351
- onClick={() => setShowTimeline((prev) => !prev)}
299
+ onClick={() => setShowSystemEvents((prev) => !prev)}
352
300
  className={cn(
353
- "inline-flex shrink-0 cursor-pointer items-center border-0 bg-transparent p-0 text-muted-foreground transition-colors hover:text-foreground",
354
- isCasePanel ? "gap-3 text-sm" : "gap-1.5 text-[11px]"
301
+ "flex shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors hover:text-foreground",
302
+ showSystemEvents
303
+ ? "border-primary/40 bg-primary/10 text-primary shadow-sm hover:bg-primary/15"
304
+ : "border-border bg-background text-muted-foreground hover:bg-muted/40"
355
305
  )}
356
- aria-label={showTimeline ? "Collapse activity timeline" : "Expand activity timeline"}
306
+ aria-pressed={showSystemEvents}
307
+ data-testid="system-events-toggle"
357
308
  >
358
- <span className="font-medium" data-testid="event-count">{eventCountLabel}</span>
359
- <ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${showTimeline ? "rotate-180" : ""}`} />
309
+ {toggleLabel}
310
+ <span
311
+ className={cn(
312
+ "inline-flex min-w-[18px] items-center justify-center rounded-full px-1.5 text-[10px] font-semibold tabular-nums",
313
+ showSystemEvents
314
+ ? "bg-primary/15 text-primary ring-1 ring-primary/30"
315
+ : "bg-muted text-muted-foreground ring-1 ring-border/70"
316
+ )}
317
+ data-testid="hidden-count-badge"
318
+ >
319
+ {hiddenCount}
320
+ </span>
360
321
  </button>
361
- </div>
322
+ )}
362
323
  </div>
363
324
 
364
325
  {/* Timeline body */}
365
326
  {showTimeline && visibleEvents.length > 0 && (
366
- <div className="mt-6">
327
+ <div className="mt-3">
367
328
  <TimelineActivity events={visibleEvents} variant="case-panel" />
368
329
  </div>
369
330
  )}
331
+
332
+ {/* Footer hint */}
333
+ {showTimeline && !showSystemEvents && sysEvtConfig?.hiddenHint && hasSystemNoise && (
334
+ <p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
335
+ {sysEvtConfig.hiddenHint}
336
+ </p>
337
+ )}
338
+ {showTimeline && showSystemEvents && sysEvtConfig?.visibleHint && hasSystemNoise && (
339
+ <p className="mt-2 text-[11px] text-muted-foreground/60 border-t border-dashed border-border pt-2" data-testid="timeline-footer-hint">
340
+ {sysEvtConfig.visibleHint.replace("{count}", String(hiddenCount))}
341
+ </p>
342
+ )}
370
343
  </div>
371
344
  )
372
345
  }
@@ -610,7 +583,6 @@ export function DetailView({
610
583
  attentionCount={attentionCount}
611
584
  sysEvtConfig={sysEvtConfig}
612
585
  lastActivityTime={lastActivityTime}
613
- isCasePanel={isCasePanelV2}
614
586
  />
615
587
  ) : null
616
588