@handled-ai/design-system 0.18.3 → 0.18.5

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 (61) hide show
  1. package/dist/charts/empty-chart-state.d.ts +11 -0
  2. package/dist/charts/empty-chart-state.js +70 -0
  3. package/dist/charts/empty-chart-state.js.map +1 -0
  4. package/dist/charts/index.d.ts +1 -0
  5. package/dist/charts/index.js +1 -0
  6. package/dist/charts/index.js.map +1 -1
  7. package/dist/charts/pipeline-overview.d.ts +2 -1
  8. package/dist/charts/pipeline-overview.js +32 -1
  9. package/dist/charts/pipeline-overview.js.map +1 -1
  10. package/dist/components/badge.d.ts +1 -1
  11. package/dist/components/button.d.ts +1 -1
  12. package/dist/components/days-open-cell.d.ts +16 -0
  13. package/dist/components/days-open-cell.js +73 -0
  14. package/dist/components/days-open-cell.js.map +1 -0
  15. package/dist/components/detail-drawer.d.ts +16 -0
  16. package/dist/components/detail-drawer.js +45 -0
  17. package/dist/components/detail-drawer.js.map +1 -0
  18. package/dist/components/insights-filter-bar.d.ts +2 -1
  19. package/dist/components/insights-filter-bar.js +13 -5
  20. package/dist/components/insights-filter-bar.js.map +1 -1
  21. package/dist/components/linked-entity-cell.d.ts +14 -0
  22. package/dist/components/linked-entity-cell.js +96 -0
  23. package/dist/components/linked-entity-cell.js.map +1 -0
  24. package/dist/components/metric-card.d.ts +14 -1
  25. package/dist/components/metric-card.js +97 -0
  26. package/dist/components/metric-card.js.map +1 -1
  27. package/dist/components/pill.d.ts +26 -0
  28. package/dist/components/pill.js +77 -0
  29. package/dist/components/pill.js.map +1 -0
  30. package/dist/components/quick-segment.d.ts +13 -0
  31. package/dist/components/quick-segment.js +96 -0
  32. package/dist/components/quick-segment.js.map +1 -0
  33. package/dist/components/tabs.d.ts +1 -1
  34. package/dist/components/timeline-activity.d.ts +1 -16
  35. package/dist/components/timeline-activity.js +1 -69
  36. package/dist/components/timeline-activity.js.map +1 -1
  37. package/dist/index.d.ts +8 -2
  38. package/dist/index.js +5 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/prototype/prototype-inbox-view.d.ts +1 -11
  41. package/dist/prototype/prototype-inbox-view.js +33 -101
  42. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/charts/__tests__/insights-charts.test.tsx +62 -0
  45. package/src/charts/empty-chart-state.tsx +44 -0
  46. package/src/charts/index.ts +1 -0
  47. package/src/charts/pipeline-overview.tsx +41 -1
  48. package/src/components/__tests__/insights-primitives.test.tsx +135 -0
  49. package/src/components/days-open-cell.tsx +50 -0
  50. package/src/components/detail-drawer.tsx +60 -0
  51. package/src/components/insights-filter-bar.tsx +13 -4
  52. package/src/components/linked-entity-cell.tsx +74 -0
  53. package/src/components/metric-card.tsx +98 -0
  54. package/src/components/pill.tsx +67 -0
  55. package/src/components/quick-segment.tsx +68 -0
  56. package/src/components/timeline-activity.tsx +1 -112
  57. package/src/index.ts +5 -0
  58. package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
  59. package/src/prototype/prototype-inbox-view.tsx +30 -131
  60. package/src/components/__tests__/timeline-activity.test.tsx +0 -137
  61. package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +0 -322
@@ -0,0 +1,50 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { StatusPill, type PillStatus } from "./pill"
7
+
8
+ export interface DaysOpenCellProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ days: number | null | undefined
10
+ warningAt?: number
11
+ criticalAt?: number
12
+ emptyLabel?: string
13
+ suffix?: string
14
+ }
15
+
16
+ function getDaysOpenIntent(days: number, warningAt: number, criticalAt: number): PillStatus {
17
+ if (days >= criticalAt) return "error"
18
+ if (days >= warningAt) return "warning"
19
+ return "success"
20
+ }
21
+
22
+ export function DaysOpenCell({
23
+ days,
24
+ warningAt = 7,
25
+ criticalAt = 30,
26
+ emptyLabel = "—",
27
+ suffix = "d open",
28
+ className,
29
+ ...props
30
+ }: DaysOpenCellProps) {
31
+ if (days === null || days === undefined) {
32
+ return (
33
+ <div data-slot="days-open-cell" className={cn("text-sm text-muted-foreground", className)} {...props}>
34
+ {emptyLabel}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ const intent = getDaysOpenIntent(days, warningAt, criticalAt)
40
+
41
+ return (
42
+ <div data-slot="days-open-cell" className={cn("inline-flex items-center", className)} {...props}>
43
+ <StatusPill data-testid="days-open-pill" status={`${days} ${suffix}`} intent={intent}>
44
+ {days} {suffix}
45
+ </StatusPill>
46
+ </div>
47
+ )
48
+ }
49
+
50
+ export { getDaysOpenIntent }
@@ -0,0 +1,60 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetDescription,
10
+ SheetFooter,
11
+ SheetHeader,
12
+ SheetTitle,
13
+ } from "./sheet"
14
+
15
+ export interface DetailDrawerProps {
16
+ open: boolean
17
+ onOpenChange: (open: boolean) => void
18
+ title: React.ReactNode
19
+ description?: React.ReactNode
20
+ children: React.ReactNode
21
+ footer?: React.ReactNode
22
+ side?: "right" | "left"
23
+ className?: string
24
+ contentClassName?: string
25
+ }
26
+
27
+ export function DetailDrawer({
28
+ open,
29
+ onOpenChange,
30
+ title,
31
+ description,
32
+ children,
33
+ footer,
34
+ side = "right",
35
+ className,
36
+ contentClassName,
37
+ }: DetailDrawerProps) {
38
+ return (
39
+ <Sheet open={open} onOpenChange={onOpenChange}>
40
+ <SheetContent
41
+ data-slot="detail-drawer"
42
+ side={side}
43
+ className={cn("flex w-full flex-col gap-0 p-0 sm:max-w-xl", className)}
44
+ >
45
+ <SheetHeader data-slot="detail-drawer-header" className="border-b border-border p-5">
46
+ <SheetTitle>{title}</SheetTitle>
47
+ {description ? <SheetDescription>{description}</SheetDescription> : null}
48
+ </SheetHeader>
49
+ <div data-slot="detail-drawer-content" className={cn("flex-1 overflow-y-auto p-5", contentClassName)}>
50
+ {children}
51
+ </div>
52
+ {footer ? (
53
+ <SheetFooter data-slot="detail-drawer-footer" className="border-t border-border p-5">
54
+ {footer}
55
+ </SheetFooter>
56
+ ) : null}
57
+ </SheetContent>
58
+ </Sheet>
59
+ )
60
+ }
@@ -23,6 +23,7 @@ export interface FilterDefinition {
23
23
 
24
24
  export interface InsightsFilterBarProps {
25
25
  filters: FilterDefinition[]
26
+ variant?: "default" | "compact"
26
27
  values: Record<string, string>
27
28
  onChange: (filterId: string, value: string) => void
28
29
  onClearAll?: () => void
@@ -45,6 +46,7 @@ function InsightsFilterBar({
45
46
  onChange,
46
47
  onClearAll,
47
48
  className,
49
+ variant = "default",
48
50
  }: InsightsFilterBarProps) {
49
51
  const showClearAll = onClearAll && hasNonDefaultValue(filters, values)
50
52
 
@@ -52,11 +54,12 @@ function InsightsFilterBar({
52
54
  <div
53
55
  data-slot="insights-filter-bar"
54
56
  className={cn(
55
- "flex flex-wrap items-center gap-3 rounded-md border border-border bg-card p-4 shadow-sm",
57
+ "flex flex-wrap items-center rounded-md border border-border bg-card shadow-sm",
58
+ variant === "compact" ? "gap-2 p-2" : "gap-3 p-4",
56
59
  className
57
60
  )}
58
61
  >
59
- <div className="flex items-center gap-2">
62
+ <div className={cn("flex items-center gap-2", variant === "compact" && "sr-only")}>
60
63
  <FilterIcon className="h-4 w-4 text-muted-foreground" />
61
64
  <span className="text-sm font-medium text-muted-foreground">
62
65
  Filters:
@@ -80,7 +83,10 @@ function InsightsFilterBar({
80
83
  <Button
81
84
  variant="outline"
82
85
  size="sm"
83
- className="h-8 gap-1.5 text-xs font-normal shadow-none"
86
+ className={cn(
87
+ "gap-1.5 text-xs font-normal shadow-none",
88
+ variant === "compact" ? "h-7 px-2" : "h-8"
89
+ )}
84
90
  >
85
91
  {IconComp ? (
86
92
  <IconComp className="h-3.5 w-3.5 text-muted-foreground" />
@@ -118,7 +124,10 @@ function InsightsFilterBar({
118
124
  <Button
119
125
  variant="ghost"
120
126
  size="sm"
121
- className="h-8 text-xs text-destructive hover:text-destructive"
127
+ className={cn(
128
+ "text-xs text-destructive hover:text-destructive",
129
+ variant === "compact" ? "h-7 px-2" : "h-8"
130
+ )}
122
131
  onClick={onClearAll}
123
132
  >
124
133
  Clear All
@@ -0,0 +1,74 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ExternalLink } from "lucide-react"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ export interface LinkedEntityCellProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ name: React.ReactNode
10
+ href?: string
11
+ subtitle?: React.ReactNode
12
+ meta?: React.ReactNode
13
+ icon?: React.ReactNode
14
+ external?: boolean
15
+ onNavigate?: () => void
16
+ }
17
+
18
+ export function LinkedEntityCell({
19
+ name,
20
+ href,
21
+ subtitle,
22
+ meta,
23
+ icon,
24
+ external = false,
25
+ onNavigate,
26
+ className,
27
+ ...props
28
+ }: LinkedEntityCellProps) {
29
+ const content = (
30
+ <>
31
+ <span className="truncate">{name}</span>
32
+ {external ? <ExternalLink className="h-3 w-3 shrink-0 opacity-60" aria-hidden="true" /> : null}
33
+ </>
34
+ )
35
+
36
+ return (
37
+ <div
38
+ data-slot="linked-entity-cell"
39
+ className={cn("flex min-w-0 items-center gap-2", className)}
40
+ {...props}
41
+ >
42
+ {icon ? (
43
+ <span data-slot="linked-entity-cell-icon" className="shrink-0 text-muted-foreground">
44
+ {icon}
45
+ </span>
46
+ ) : null}
47
+ <div className="min-w-0 flex-1">
48
+ {href ? (
49
+ <a
50
+ data-slot="linked-entity-cell-link"
51
+ href={href}
52
+ target={external ? "_blank" : undefined}
53
+ rel={external ? "noreferrer" : undefined}
54
+ onClick={onNavigate}
55
+ className="inline-flex max-w-full items-center gap-1 truncate font-medium text-foreground underline-offset-4 hover:text-primary hover:underline"
56
+ >
57
+ {content}
58
+ </a>
59
+ ) : (
60
+ <span data-slot="linked-entity-cell-name" className="block truncate font-medium text-foreground">
61
+ {name}
62
+ </span>
63
+ )}
64
+ {subtitle || meta ? (
65
+ <div data-slot="linked-entity-cell-meta" className="mt-0.5 truncate text-xs text-muted-foreground">
66
+ {subtitle}
67
+ {subtitle && meta ? <span className="px-1">·</span> : null}
68
+ {meta}
69
+ </div>
70
+ ) : null}
71
+ </div>
72
+ </div>
73
+ )
74
+ }
@@ -1,5 +1,6 @@
1
1
  import * as React from "react"
2
2
  import { ArrowUp, ArrowDown, Info, ExternalLink } from "lucide-react"
3
+ import type { LucideIcon } from "lucide-react"
3
4
  import { cn } from "../lib/utils"
4
5
 
5
6
  export interface MetricDataPoint {
@@ -24,6 +25,103 @@ export interface MetricCardProps {
24
25
  showInfo?: boolean
25
26
  }
26
27
 
28
+ export interface KpiStripItem {
29
+ id?: string
30
+ label: React.ReactNode
31
+ value: React.ReactNode
32
+ unit?: React.ReactNode
33
+ subtitle?: React.ReactNode
34
+ change?: MetricCardProps["change"]
35
+ }
36
+
37
+ export interface KpiStripProps extends React.HTMLAttributes<HTMLDivElement> {
38
+ items: KpiStripItem[]
39
+ columns?: 2 | 3 | 4
40
+ }
41
+
42
+ function getChangePresentation(change: MetricCardProps["change"]): {
43
+ icon: LucideIcon | null
44
+ className: string
45
+ } | null {
46
+ if (!change) return null
47
+ if (change.direction === "neutral") {
48
+ return { icon: null, className: "text-muted-foreground" }
49
+ }
50
+
51
+ const isGoodDirection = change.isGood !== undefined
52
+ ? change.isGood
53
+ : change.direction === "up"
54
+
55
+ return {
56
+ icon: change.direction === "down" ? ArrowDown : ArrowUp,
57
+ className: isGoodDirection ? "text-emerald-600" : "text-red-600",
58
+ }
59
+ }
60
+
61
+ export function KpiStrip({ items, columns = 4, className, ...props }: KpiStripProps) {
62
+ return (
63
+ <div
64
+ data-slot="kpi-strip"
65
+ className={cn(
66
+ "grid gap-3 rounded-xl border border-border bg-card p-3 shadow-sm",
67
+ columns === 2 && "sm:grid-cols-2",
68
+ columns === 3 && "sm:grid-cols-3",
69
+ columns === 4 && "sm:grid-cols-2 lg:grid-cols-4",
70
+ className
71
+ )}
72
+ {...props}
73
+ >
74
+ {items.map((item, index) => {
75
+ const changePresentation = getChangePresentation(item.change)
76
+ const ChangeIcon = changePresentation?.icon
77
+
78
+ return (
79
+ <div
80
+ key={item.id ?? index}
81
+ data-slot="kpi-strip-item"
82
+ className="min-w-0 rounded-lg bg-muted/40 px-3 py-2"
83
+ >
84
+ <div data-slot="kpi-strip-label" className="truncate text-xs font-medium text-muted-foreground">
85
+ {item.label}
86
+ </div>
87
+ <div className="mt-1 flex items-baseline gap-1">
88
+ <span data-slot="kpi-strip-value" className="truncate text-2xl font-bold tracking-tight text-foreground">
89
+ {item.value}
90
+ </span>
91
+ {item.unit ? (
92
+ <span data-slot="kpi-strip-unit" className="text-sm font-semibold text-muted-foreground">
93
+ {item.unit}
94
+ </span>
95
+ ) : null}
96
+ </div>
97
+ {item.subtitle || item.change ? (
98
+ <div className="mt-1 flex items-center gap-2 text-xs">
99
+ {item.change ? (
100
+ <span
101
+ data-slot="kpi-strip-change"
102
+ className={cn(
103
+ "inline-flex items-center gap-0.5 font-semibold",
104
+ changePresentation?.className
105
+ )}
106
+ >
107
+ {ChangeIcon ? <ChangeIcon className="h-3 w-3 stroke-[3]" /> : null}
108
+ {item.change.value}
109
+ </span>
110
+ ) : null}
111
+ {item.subtitle ? (
112
+ <span data-slot="kpi-strip-subtitle" className="truncate text-muted-foreground">
113
+ {item.subtitle}
114
+ </span>
115
+ ) : null}
116
+ </div>
117
+ ) : null}
118
+ </div>
119
+ )
120
+ })}
121
+ </div>
122
+ )
123
+ }
124
+
27
125
  export function MetricCard({
28
126
  title,
29
127
  value,
@@ -0,0 +1,67 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../lib/utils"
5
+
6
+ /**
7
+ * Insights-friendly pill convenience wrappers.
8
+ *
9
+ * Pill and StatusPill are small, rounded wrappers for dense Insights surfaces
10
+ * such as tables, KPI strips, and filter summaries. They intentionally wrap the
11
+ * existing Badge/StatusBadge visual language and are not a replacement for all
12
+ * Badge usage across the design system.
13
+ */
14
+ export type PillStatus = "success" | "warning" | "error" | "neutral" | "info"
15
+
16
+ const pillVariants = cva(
17
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors [&>svg]:size-3",
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: "border-transparent bg-primary text-primary-foreground",
22
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
23
+ destructive: "border-transparent bg-destructive text-white dark:bg-destructive/60",
24
+ outline: "border-border bg-background text-foreground",
25
+ ghost: "border-transparent bg-transparent text-muted-foreground",
26
+ success: "border-transparent bg-green-100 text-green-950 dark:bg-green-950 dark:text-green-100",
27
+ warning: "border-transparent bg-yellow-100 text-yellow-950 dark:bg-yellow-950 dark:text-yellow-100",
28
+ error: "border-transparent bg-red-100 text-red-950 dark:bg-red-950 dark:text-red-100",
29
+ neutral: "border-transparent bg-muted text-foreground",
30
+ info: "border-transparent bg-blue-100 text-blue-950 dark:bg-blue-950 dark:text-blue-100",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "neutral",
35
+ },
36
+ }
37
+ )
38
+
39
+ export interface PillProps
40
+ extends React.ComponentProps<"span">,
41
+ VariantProps<typeof pillVariants> {}
42
+
43
+ export function Pill({ className, variant = "neutral", ...props }: PillProps) {
44
+ return (
45
+ <span
46
+ data-slot="pill"
47
+ data-variant={variant}
48
+ className={cn(pillVariants({ variant }), className)}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ export interface StatusPillProps extends Omit<PillProps, "variant"> {
55
+ status: React.ReactNode
56
+ intent?: PillStatus
57
+ }
58
+
59
+ export function StatusPill({ status, intent = "neutral", children, ...props }: StatusPillProps) {
60
+ return (
61
+ <Pill data-slot="status-pill" variant={intent} {...props}>
62
+ {children ?? status}
63
+ </Pill>
64
+ )
65
+ }
66
+
67
+ export { pillVariants }
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+
7
+ export interface QuickSegmentProps
8
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "value"> {
9
+ label: React.ReactNode
10
+ value: string
11
+ selected?: boolean
12
+ count?: number | string
13
+ description?: React.ReactNode
14
+ onSelect?: (value: string) => void
15
+ }
16
+
17
+ export function QuickSegment({
18
+ label,
19
+ value,
20
+ selected = false,
21
+ count,
22
+ description,
23
+ onSelect,
24
+ className,
25
+ type = "button",
26
+ ...props
27
+ }: QuickSegmentProps) {
28
+ return (
29
+ <button
30
+ data-slot="quick-segment"
31
+ data-selected={selected ? "true" : "false"}
32
+ type={type}
33
+ aria-pressed={selected}
34
+ className={cn(
35
+ "inline-flex min-h-8 items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors",
36
+ selected
37
+ ? "border-primary bg-primary text-primary-foreground shadow-sm"
38
+ : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground",
39
+ className
40
+ )}
41
+ onClick={(event) => {
42
+ props.onClick?.(event)
43
+ if (!event.defaultPrevented) onSelect?.(value)
44
+ }}
45
+ {...props}
46
+ >
47
+ <span data-slot="quick-segment-label">{label}</span>
48
+ {count !== undefined ? (
49
+ <span
50
+ data-slot="quick-segment-count"
51
+ className={cn(
52
+ "rounded-full px-1.5 py-0.5 text-[11px] leading-none",
53
+ selected
54
+ ? "bg-primary-foreground/20 text-primary-foreground"
55
+ : "bg-muted text-muted-foreground"
56
+ )}
57
+ >
58
+ {count}
59
+ </span>
60
+ ) : null}
61
+ {description ? (
62
+ <span data-slot="quick-segment-description" className="sr-only">
63
+ {description}
64
+ </span>
65
+ ) : null}
66
+ </button>
67
+ )
68
+ }
@@ -4,24 +4,6 @@ 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
-
25
7
  export interface TimelineEvent {
26
8
  id: string
27
9
  icon: React.ReactNode
@@ -46,57 +28,8 @@ export interface TimelineEvent {
46
28
  defaultExpanded?: boolean
47
29
  isInteractive?: boolean
48
30
  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
- },
95
31
  }
96
32
 
97
- const NEUTRAL_DOT_CLASSES = "border-border/60 bg-background"
98
- const NEUTRAL_ICON_CLASSES = "text-muted-foreground"
99
-
100
33
  export interface TimelineActivityProps {
101
34
  events: TimelineEvent[]
102
35
  className?: string
@@ -116,54 +49,12 @@ export function TimelineActivity({ events, className }: TimelineActivityProps) {
116
49
  )
117
50
  }
118
51
 
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
-
157
52
  function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean }) {
158
53
  const [expanded, setExpanded] = React.useState(event.defaultExpanded ?? false)
159
54
  const [showAllRecipients, setShowAllRecipients] = React.useState(false)
160
55
  const hasContent = !!event.content
161
56
  const hasEmail = !!event.email
162
57
 
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
-
167
58
  return (
168
59
  <div className="group relative flex gap-3.5">
169
60
  {!isLast && (
@@ -171,7 +62,7 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
171
62
  )}
172
63
 
173
64
  <div className="relative z-10 mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-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">
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">
175
66
  {event.icon}
176
67
  </div>
177
68
  </div>
@@ -186,8 +77,6 @@ function TimelineItem({ event, isLast }: { event: TimelineEvent; isLast: boolean
186
77
  </span>
187
78
  </div>
188
79
 
189
- {event.actor && <ActorByline actor={event.actor} time={event.time} />}
190
-
191
80
  {(hasContent || hasEmail) && (
192
81
  <div className="mt-2">
193
82
  {event.isInteractive ? (
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export * from "./components/data-table-filter"
33
33
  export * from "./components/data-table-quick-views"
34
34
  export * from "./components/data-table-toolbar"
35
35
  export * from "./components/detail-view"
36
+ export * from "./components/detail-drawer"
36
37
  export * from "./components/dialog"
37
38
  export * from "./components/dropdown-menu"
38
39
  export * from "./components/empty-state"
@@ -47,6 +48,8 @@ export * from "./components/inbox-toolbar"
47
48
  export * from "./components/inline-banner"
48
49
  export * from "./components/input"
49
50
  export * from "./components/insights-filter-bar"
51
+ export * from "./components/days-open-cell"
52
+ export * from "./components/linked-entity-cell"
50
53
  export * from "./components/item-list"
51
54
  export * from "./components/item-list-display"
52
55
  export * from "./components/item-list-filter"
@@ -56,9 +59,11 @@ export * from "./components/label"
56
59
  export * from "./components/message"
57
60
  export * from "./components/metric-card"
58
61
  export * from "./components/performance-metrics-table"
62
+ export * from "./components/pill"
59
63
  export * from "./components/preview-list"
60
64
  export * from "./components/progress"
61
65
  export * from "./components/quick-action-chat-area"
66
+ export * from "./components/quick-segment"
62
67
  export {
63
68
  QuickActionModal,
64
69
  type QuickActionPriority,
@@ -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 collapse button to expand
93
- const timelineButton = container.querySelector('[data-testid="timeline-collapse-btn"]') as HTMLElement;
92
+ // Click the timeline header button to expand
93
+ const timelineButton = container.querySelector("button.group\\/timeline") as HTMLElement;
94
94
  expect(timelineButton).not.toBeNull();
95
95
  fireEvent.click(timelineButton);
96
96