@handled-ai/design-system 0.20.1 → 0.20.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.
@@ -21,6 +21,8 @@ const EMAIL_HTML_CLASS = cn(
21
21
  "[&_.gmail_quote]:border-l-2 [&_.gmail_quote]:border-border [&_.gmail_quote]:pl-3 [&_.gmail_quote]:text-muted-foreground [&_.gmail_quote]:text-[13px]"
22
22
  )
23
23
 
24
+ export type TimelineActivityVariant = "default" | "case-panel"
25
+
24
26
  export type TimelineEventTone =
25
27
  | "red"
26
28
  | "amber"
@@ -121,19 +123,79 @@ export const TONE_CLASSES: Record<
121
123
  const NEUTRAL_DOT_CLASSES = "border-border/60 bg-background"
122
124
  const NEUTRAL_ICON_CLASSES = "text-muted-foreground"
123
125
 
126
+ type TimelineVariantClasses = {
127
+ outerRowGap: string
128
+ connector: string
129
+ dotWrapperSize: string
130
+ dot: string
131
+ contentPadding: string
132
+ titleRowSpacing: string
133
+ title: string
134
+ time: string
135
+ cardContainer: string
136
+ cardHeader: string
137
+ cardBody: string
138
+ cardFooter: string
139
+ collapsedPreview: string
140
+ actionLinkRow: string
141
+ actionLink: string
142
+ nonInteractiveContent: string
143
+ }
144
+
145
+ const TIMELINE_VARIANT_CLASSES: Record<TimelineActivityVariant, TimelineVariantClasses> = {
146
+ default: {
147
+ outerRowGap: "group relative flex gap-3.5",
148
+ connector: "absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60",
149
+ dotWrapperSize: "relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background",
150
+ dot: "flex h-4.5 w-4.5 items-center justify-center rounded-full border ring-4 ring-background",
151
+ contentPadding: "flex-1 pb-5 pt-0.5",
152
+ titleRowSpacing: "flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between",
153
+ title: "pr-4 text-[13px] leading-relaxed text-foreground",
154
+ time: "mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70",
155
+ cardContainer: "overflow-hidden rounded-md border border-border/80 bg-muted/20",
156
+ cardHeader: "px-3 pt-2.5",
157
+ cardBody: "px-3 py-2.5 text-sm",
158
+ cardFooter: "px-3 pb-2.5",
159
+ collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2.5 text-sm text-muted-foreground",
160
+ actionLinkRow: "flex items-center gap-3 px-3 pb-2.5",
161
+ actionLink: "inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground",
162
+ nonInteractiveContent: "pr-2 text-sm leading-relaxed text-muted-foreground",
163
+ },
164
+ "case-panel": {
165
+ outerRowGap: "group relative flex gap-3",
166
+ connector: "absolute left-[7px] top-[18px] bottom-[-4px] w-px bg-border/60",
167
+ dotWrapperSize: "relative z-10 mt-1 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background",
168
+ dot: "flex h-3.5 w-3.5 items-center justify-center rounded-full border ring-[3px] ring-background",
169
+ contentPadding: "flex-1 pb-4 pt-0.5",
170
+ titleRowSpacing: "flex min-w-0 flex-col gap-0.5 sm:flex-row sm:items-start sm:justify-between",
171
+ title: "pr-3 text-[13px] leading-snug text-foreground",
172
+ time: "mt-0.5 shrink-0 whitespace-nowrap text-[11px] leading-snug text-muted-foreground/70",
173
+ cardContainer: "overflow-hidden rounded-lg border border-border/70 bg-card shadow-sm",
174
+ cardHeader: "border-b border-border/60 bg-background px-3 py-2",
175
+ cardBody: "px-3 py-2.5 text-sm",
176
+ cardFooter: "border-t border-border/60 bg-background/50 px-3 py-1.5",
177
+ collapsedPreview: "flex items-center justify-between gap-2 px-3 py-2 text-sm text-muted-foreground",
178
+ actionLinkRow: "flex items-center justify-end gap-2 px-3 py-1.5",
179
+ actionLink: "inline-flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/70 transition-colors hover:text-foreground",
180
+ nonInteractiveContent: "pr-2 text-[13px] leading-snug text-muted-foreground",
181
+ },
182
+ }
183
+
124
184
  export interface TimelineActivityProps {
125
185
  events: TimelineEvent[]
126
186
  className?: string
187
+ variant?: TimelineActivityVariant
127
188
  }
128
189
 
129
- export function TimelineActivity({ events, className }: TimelineActivityProps) {
190
+ export function TimelineActivity({ events, className, variant = "default" }: TimelineActivityProps) {
130
191
  return (
131
- <div className={cn("space-y-0", className)}>
192
+ <div className={cn("space-y-0", className)} data-variant={variant}>
132
193
  {events.map((event, index) => (
133
194
  <TimelineItem
134
195
  key={event.id}
135
196
  event={event}
136
197
  isLast={index === events.length - 1}
198
+ variant={variant}
137
199
  />
138
200
  ))}
139
201
  </div>
@@ -178,34 +240,43 @@ function ActorByline({ actor, time }: { actor: TimelineEventActor; time: string
178
240
  )
179
241
  }
180
242
 
181
- function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean }) {
243
+ function TimelineItem({
244
+ event,
245
+ isLast,
246
+ variant,
247
+ }: {
248
+ event: TimelineEvent
249
+ isLast: boolean
250
+ variant: TimelineActivityVariant
251
+ }) {
182
252
  const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)
183
253
  const [showAllRecipients, setShowAllRecipients] = React.useState(false)
184
254
  const hasContent = !!event.content
185
255
  const hasEmail = !!event.email
256
+ const classes = TIMELINE_VARIANT_CLASSES[variant]
186
257
 
187
258
  const toneStyle = event.tone ? TONE_CLASSES[event.tone] : null
188
259
  const dotClasses = toneStyle ? toneStyle.dot : NEUTRAL_DOT_CLASSES
189
260
  const iconClasses = toneStyle ? toneStyle.icon : NEUTRAL_ICON_CLASSES
190
261
 
191
262
  return (
192
- <div className="group relative flex gap-3.5">
263
+ <div className={classes.outerRowGap}>
193
264
  {!isLast && (
194
- <div className="absolute left-[9px] top-5 bottom-[-6px] w-px bg-border/60" />
265
+ <div className={classes.connector} />
195
266
  )}
196
267
 
197
- <div className="relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-background">
198
- <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">
268
+ <div className={classes.dotWrapperSize}>
269
+ <div className={cn(classes.dot, dotClasses, iconClasses)} data-testid="timeline-dot">
199
270
  {event.icon}
200
271
  </div>
201
272
  </div>
202
273
 
203
- <div className="flex-1 pb-5 pt-0.5">
204
- <div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
205
- <div className="pr-4 text-[13px] leading-relaxed text-foreground">
274
+ <div className={classes.contentPadding}>
275
+ <div className={classes.titleRowSpacing}>
276
+ <div className={classes.title}>
206
277
  {event.title}
207
278
  </div>
208
- <span className="mt-0.5 shrink-0 whitespace-nowrap text-[11px] text-muted-foreground/70">
279
+ <span className={classes.time}>
209
280
  {event.time}
210
281
  </span>
211
282
  </div>
@@ -216,158 +287,26 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
216
287
  <div className="mt-2">
217
288
  {event.isInteractive ? (
218
289
  hasEmail ? (
219
- <div className="overflow-hidden rounded-md border border-border/80 bg-muted/20">
220
- <div
221
- className={cn(
222
- "px-3 py-2.5 text-sm",
223
- !expanded && "cursor-pointer hover:bg-muted/30 transition-colors"
224
- )}
225
- onClick={() => !expanded && setExpanded(true)}
226
- >
227
- {expanded && event.email ? (
228
- <div className="space-y-3">
229
- <div>
230
- <div className="flex items-center justify-between gap-4">
231
- <div className="flex min-w-0 items-baseline gap-1.5">
232
- <span className="font-semibold text-foreground text-[13px] whitespace-nowrap">{event.email.from}</span>
233
- {event.email.fromEmail && (
234
- <span className="text-muted-foreground/60 text-xs truncate">{event.email.fromEmail}</span>
235
- )}
236
- </div>
237
- {event.email.date && (
238
- <span className="shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap">{event.email.date}</span>
239
- )}
240
- </div>
241
- <div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
242
- <span className="truncate">
243
- To {event.email.to}
244
- {!showAllRecipients && (event.email.cc || event.email.bcc) ? (
245
- <>, ...</>
246
- ) : null}
247
- {showAllRecipients && event.email.cc ? (
248
- <>, {event.email.cc}</>
249
- ) : null}
250
- {showAllRecipients && event.email.bcc ? (
251
- <> <span className="text-muted-foreground/40">bcc</span> {event.email.bcc}</>
252
- ) : null}
253
- </span>
254
- {(event.email.cc || event.email.bcc) && (
255
- <button
256
- type="button"
257
- onClick={(e) => {
258
- e.stopPropagation()
259
- setShowAllRecipients((prev) => !prev)
260
- }}
261
- className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
262
- >
263
- <ChevronDown className={cn("h-3 w-3 transition-transform", showAllRecipients && "rotate-180")} />
264
- </button>
265
- )}
266
- </div>
267
- </div>
268
-
269
- {event.email.bodyHtml ? (
270
- // Gmail reading-pane typography; quoted history
271
- // (blockquote.gmail_quote) is de-emphasized with a left rule.
272
- <div
273
- data-slot="timeline-email-html"
274
- className={EMAIL_HTML_CLASS}
275
- dangerouslySetInnerHTML={{ __html: sanitizeHtml(event.email.bodyHtml) }}
276
- />
277
- ) : (
278
- <div className="whitespace-pre-line text-sm leading-relaxed text-foreground/90">
279
- {event.email.body}
280
- </div>
281
- )}
282
-
283
- <button
284
- onClick={(e) => {
285
- e.stopPropagation()
286
- setExpanded(false)
287
- }}
288
- className="mt-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
289
- >
290
- Show less <ChevronUp className="h-3 w-3" />
291
- </button>
292
- </div>
293
- ) : (
294
- <div className="flex items-center justify-between gap-2 text-muted-foreground">
295
- <span className="line-clamp-1 pr-3 text-[13px]">
296
- <span className="text-muted-foreground">{event.email?.from}</span>
297
- <span className="mx-1.5 text-muted-foreground/40">&middot;</span>
298
- {event.email?.subject ? (
299
- <>
300
- <span className="text-muted-foreground">{event.email.subject}</span>
301
- <span className="mx-1.5 text-muted-foreground/40">&middot;</span>
302
- </>
303
- ) : null}
304
- <span className="text-muted-foreground">{event.preview}</span>
305
- </span>
306
- <button className="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground">
307
- Expand <ChevronDown className="h-3 w-3" />
308
- </button>
309
- </div>
310
- )}
311
- </div>
312
- </div>
290
+ <EmailCard
291
+ event={event}
292
+ expanded={expanded}
293
+ setExpanded={setExpanded}
294
+ showAllRecipients={showAllRecipients}
295
+ setShowAllRecipients={setShowAllRecipients}
296
+ variant={variant}
297
+ classes={classes}
298
+ />
313
299
  ) : (
314
- <div className="overflow-hidden rounded-md border border-border/80 bg-muted/20">
315
- <div
316
- className={cn(
317
- "px-3 py-2.5 text-sm",
318
- !expanded && "cursor-pointer hover:bg-muted/30 transition-colors"
319
- )}
320
- onClick={() => !expanded && setExpanded(true)}
321
- >
322
- {expanded ? (
323
- <div className="space-y-2">
324
- {event.content}
325
- <div className="mt-2 flex items-center gap-3">
326
- {event.source ? (
327
- event.onSourceClick ? (
328
- <button
329
- type="button"
330
- onClick={(e) => { e.stopPropagation(); event.onSourceClick?.(); }}
331
- className="mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
332
- >
333
- Open in {event.source.label}
334
- <ExternalLink className="h-3 w-3" />
335
- </button>
336
- ) : (
337
- <a
338
- href={event.source.url}
339
- target="_blank"
340
- rel="noreferrer noopener"
341
- className="mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
342
- >
343
- Open in {event.source.label}
344
- <ExternalLink className="h-3 w-3" />
345
- </a>
346
- )
347
- ) : null}
348
- <button
349
- onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
350
- className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
351
- >
352
- Show less <ChevronUp className="h-3 w-3" />
353
- </button>
354
- </div>
355
- </div>
356
- ) : (
357
- <div className="flex items-center justify-between gap-2 text-muted-foreground">
358
- <span className="line-clamp-1 pr-3">
359
- {event.preview ?? event.content}
360
- </span>
361
- <button className="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground">
362
- Expand <ChevronDown className="h-3 w-3" />
363
- </button>
364
- </div>
365
- )}
366
- </div>
367
- </div>
300
+ <ContentCard
301
+ event={event}
302
+ expanded={expanded}
303
+ setExpanded={setExpanded}
304
+ variant={variant}
305
+ classes={classes}
306
+ />
368
307
  )
369
308
  ) : (
370
- <div className="pr-2 text-sm leading-relaxed text-muted-foreground">
309
+ <div className={classes.nonInteractiveContent}>
371
310
  {event.content}
372
311
  </div>
373
312
  )}
@@ -377,3 +316,356 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
377
316
  </div>
378
317
  )
379
318
  }
319
+
320
+ type TimelineEmail = NonNullable<TimelineEvent["email"]>
321
+ type TimelineSource = NonNullable<TimelineEvent["source"]>
322
+
323
+ function EmailMetadata({
324
+ email,
325
+ showAllRecipients,
326
+ setShowAllRecipients,
327
+ }: {
328
+ email: TimelineEmail
329
+ showAllRecipients: boolean
330
+ setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>
331
+ }) {
332
+ return (
333
+ <>
334
+ <div className="flex items-center justify-between gap-4">
335
+ <div className="flex min-w-0 items-baseline gap-1.5">
336
+ <span className="font-semibold text-foreground text-[13px] whitespace-nowrap">{email.from}</span>
337
+ {email.fromEmail && (
338
+ <span className="text-muted-foreground/60 text-xs truncate">{email.fromEmail}</span>
339
+ )}
340
+ </div>
341
+ {email.date && (
342
+ <span className="shrink-0 text-xs text-muted-foreground/50 whitespace-nowrap">{email.date}</span>
343
+ )}
344
+ </div>
345
+ <div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
346
+ <span className="truncate">
347
+ To {email.to}
348
+ {!showAllRecipients && (email.cc || email.bcc) ? (
349
+ <>, ...</>
350
+ ) : null}
351
+ {showAllRecipients && email.cc ? (
352
+ <>, {email.cc}</>
353
+ ) : null}
354
+ {showAllRecipients && email.bcc ? (
355
+ <> <span className="text-muted-foreground/40">bcc</span> {email.bcc}</>
356
+ ) : null}
357
+ </span>
358
+ {(email.cc || email.bcc) && (
359
+ <button
360
+ type="button"
361
+ onClick={(e) => {
362
+ e.stopPropagation()
363
+ setShowAllRecipients((prev) => !prev)
364
+ }}
365
+ className="shrink-0 text-muted-foreground/40 hover:text-muted-foreground transition-colors"
366
+ >
367
+ <ChevronDown className={cn("h-3 w-3 transition-transform", showAllRecipients && "rotate-180")} />
368
+ </button>
369
+ )}
370
+ </div>
371
+ </>
372
+ )
373
+ }
374
+
375
+ function EmailBody({ email }: { email: TimelineEmail }) {
376
+ if (email.bodyHtml) {
377
+ return (
378
+ // Gmail reading-pane typography; quoted history
379
+ // (blockquote.gmail_quote) is de-emphasized with a left rule.
380
+ <div
381
+ data-slot="timeline-email-html"
382
+ className={EMAIL_HTML_CLASS}
383
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(email.bodyHtml) }}
384
+ />
385
+ )
386
+ }
387
+
388
+ return (
389
+ <div className="whitespace-pre-line text-sm leading-relaxed text-foreground/90">
390
+ {email.body}
391
+ </div>
392
+ )
393
+ }
394
+
395
+ function CollapsedEmailPreview({
396
+ email,
397
+ preview,
398
+ className,
399
+ actionClassName,
400
+ onClick,
401
+ }: {
402
+ email?: TimelineEmail
403
+ preview?: React.ReactNode
404
+ className: string
405
+ actionClassName: string
406
+ onClick?: () => void
407
+ }) {
408
+ return (
409
+ <div className={className} onClick={onClick}>
410
+ <span className="line-clamp-1 pr-3 text-[13px]">
411
+ <span className="text-muted-foreground">{email?.from}</span>
412
+ <span className="mx-1.5 text-muted-foreground/40">&middot;</span>
413
+ {email?.subject ? (
414
+ <>
415
+ <span className="text-muted-foreground">{email.subject}</span>
416
+ <span className="mx-1.5 text-muted-foreground/40">&middot;</span>
417
+ </>
418
+ ) : null}
419
+ <span className="text-muted-foreground">{preview}</span>
420
+ </span>
421
+ <button type="button" className={actionClassName}>
422
+ Expand <ChevronDown className="h-3 w-3" />
423
+ </button>
424
+ </div>
425
+ )
426
+ }
427
+
428
+ function ShowLessButton({
429
+ className,
430
+ onClick,
431
+ type,
432
+ }: {
433
+ className: string
434
+ onClick: React.MouseEventHandler<HTMLButtonElement>
435
+ type?: "button"
436
+ }) {
437
+ return (
438
+ <button type={type} onClick={onClick} className={className}>
439
+ Show less <ChevronUp className="h-3 w-3" />
440
+ </button>
441
+ )
442
+ }
443
+
444
+ function SourceAction({
445
+ source,
446
+ onSourceClick,
447
+ className,
448
+ }: {
449
+ source: TimelineSource
450
+ onSourceClick?: () => void
451
+ className: string
452
+ }) {
453
+ if (onSourceClick) {
454
+ return (
455
+ <button
456
+ type="button"
457
+ onClick={(e) => { e.stopPropagation(); onSourceClick(); }}
458
+ className={className}
459
+ >
460
+ Open in {source.label}
461
+ <ExternalLink className="h-3 w-3" />
462
+ </button>
463
+ )
464
+ }
465
+
466
+ return (
467
+ <a
468
+ href={source.url}
469
+ target="_blank"
470
+ rel="noreferrer noopener"
471
+ className={className}
472
+ >
473
+ Open in {source.label}
474
+ <ExternalLink className="h-3 w-3" />
475
+ </a>
476
+ )
477
+ }
478
+
479
+ function EmailCard({
480
+ event,
481
+ expanded,
482
+ setExpanded,
483
+ showAllRecipients,
484
+ setShowAllRecipients,
485
+ variant,
486
+ classes,
487
+ }: {
488
+ event: TimelineEvent
489
+ expanded: boolean
490
+ setExpanded: React.Dispatch<React.SetStateAction<boolean>>
491
+ showAllRecipients: boolean
492
+ setShowAllRecipients: React.Dispatch<React.SetStateAction<boolean>>
493
+ variant: TimelineActivityVariant
494
+ classes: TimelineVariantClasses
495
+ }) {
496
+ if (variant === "default") {
497
+ return (
498
+ <div className={classes.cardContainer} data-variant={variant}>
499
+ <div
500
+ className={cn(
501
+ "px-3 py-2.5 text-sm",
502
+ !expanded && "cursor-pointer hover:bg-muted/30 transition-colors"
503
+ )}
504
+ onClick={() => !expanded && setExpanded(true)}
505
+ >
506
+ {expanded && event.email ? (
507
+ <div className="space-y-3">
508
+ <div>
509
+ <EmailMetadata
510
+ email={event.email}
511
+ showAllRecipients={showAllRecipients}
512
+ setShowAllRecipients={setShowAllRecipients}
513
+ />
514
+ </div>
515
+
516
+ <EmailBody email={event.email} />
517
+
518
+ <ShowLessButton
519
+ onClick={(e) => {
520
+ e.stopPropagation()
521
+ setExpanded(false)
522
+ }}
523
+ className="mt-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
524
+ />
525
+ </div>
526
+ ) : (
527
+ <CollapsedEmailPreview
528
+ email={event.email}
529
+ preview={event.preview}
530
+ className="flex items-center justify-between gap-2 text-muted-foreground"
531
+ actionClassName="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground"
532
+ />
533
+ )}
534
+ </div>
535
+ </div>
536
+ )
537
+ }
538
+
539
+ return (
540
+ <div className={classes.cardContainer} data-variant={variant}>
541
+ {expanded && event.email ? (
542
+ <>
543
+ <div className={classes.cardHeader} data-slot="timeline-card-header">
544
+ <EmailMetadata
545
+ email={event.email}
546
+ showAllRecipients={showAllRecipients}
547
+ setShowAllRecipients={setShowAllRecipients}
548
+ />
549
+ </div>
550
+
551
+ <div className={classes.cardBody} data-slot="timeline-card-body">
552
+ <EmailBody email={event.email} />
553
+ </div>
554
+
555
+ <div className={cn(classes.cardFooter, classes.actionLinkRow)} data-slot="timeline-card-footer">
556
+ <ShowLessButton
557
+ type="button"
558
+ onClick={(e) => {
559
+ e.stopPropagation()
560
+ setExpanded(false)
561
+ }}
562
+ className={classes.actionLink}
563
+ />
564
+ </div>
565
+ </>
566
+ ) : (
567
+ <CollapsedEmailPreview
568
+ email={event.email}
569
+ preview={event.preview}
570
+ className={cn(classes.collapsedPreview, "cursor-pointer hover:bg-muted/30 transition-colors")}
571
+ actionClassName={cn(classes.actionLink, "shrink-0")}
572
+ onClick={() => setExpanded(true)}
573
+ />
574
+ )}
575
+ </div>
576
+ )
577
+ }
578
+
579
+ function ContentCard({
580
+ event,
581
+ expanded,
582
+ setExpanded,
583
+ variant,
584
+ classes,
585
+ }: {
586
+ event: TimelineEvent
587
+ expanded: boolean
588
+ setExpanded: React.Dispatch<React.SetStateAction<boolean>>
589
+ variant: TimelineActivityVariant
590
+ classes: TimelineVariantClasses
591
+ }) {
592
+ if (variant === "default") {
593
+ return (
594
+ <div className={classes.cardContainer} data-variant={variant}>
595
+ <div
596
+ className={cn(
597
+ "px-3 py-2.5 text-sm",
598
+ !expanded && "cursor-pointer hover:bg-muted/30 transition-colors"
599
+ )}
600
+ onClick={() => !expanded && setExpanded(true)}
601
+ >
602
+ {expanded ? (
603
+ <div className="space-y-2">
604
+ {event.content}
605
+ <div className="mt-2 flex items-center gap-3">
606
+ {event.source ? (
607
+ <SourceAction
608
+ source={event.source}
609
+ onSourceClick={event.onSourceClick}
610
+ className="mr-auto inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
611
+ />
612
+ ) : null}
613
+ <ShowLessButton
614
+ onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
615
+ className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
616
+ />
617
+ </div>
618
+ </div>
619
+ ) : (
620
+ <div className="flex items-center justify-between gap-2 text-muted-foreground">
621
+ <span className="line-clamp-1 pr-3">
622
+ {event.preview ?? event.content}
623
+ </span>
624
+ <button className="flex shrink-0 items-center gap-1 text-[11px] font-semibold uppercase tracking-wider transition-colors hover:text-foreground">
625
+ Expand <ChevronDown className="h-3 w-3" />
626
+ </button>
627
+ </div>
628
+ )}
629
+ </div>
630
+ </div>
631
+ )
632
+ }
633
+
634
+ return (
635
+ <div className={classes.cardContainer} data-variant={variant}>
636
+ {expanded ? (
637
+ <>
638
+ <div className={classes.cardBody} data-slot="timeline-card-body">
639
+ {event.content}
640
+ </div>
641
+ <div className={cn(classes.cardFooter, classes.actionLinkRow, event.source ? "justify-between" : "justify-end")} data-slot="timeline-card-footer">
642
+ {event.source ? (
643
+ <SourceAction
644
+ source={event.source}
645
+ onSourceClick={event.onSourceClick}
646
+ className={classes.actionLink}
647
+ />
648
+ ) : null}
649
+ <ShowLessButton
650
+ type="button"
651
+ onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
652
+ className={classes.actionLink}
653
+ />
654
+ </div>
655
+ </>
656
+ ) : (
657
+ <div
658
+ className={cn(classes.collapsedPreview, "cursor-pointer hover:bg-muted/30 transition-colors")}
659
+ onClick={() => setExpanded(true)}
660
+ >
661
+ <span className="line-clamp-1 pr-3">
662
+ {event.preview ?? event.content}
663
+ </span>
664
+ <button type="button" className={cn(classes.actionLink, "shrink-0")}>
665
+ Expand <ChevronDown className="h-3 w-3" />
666
+ </button>
667
+ </div>
668
+ )}
669
+ </div>
670
+ )
671
+ }