@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.
- package/dist/charts/empty-chart-state.d.ts +11 -0
- package/dist/charts/empty-chart-state.js +70 -0
- package/dist/charts/empty-chart-state.js.map +1 -0
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +2 -1
- package/dist/charts/pipeline-overview.js +32 -1
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/days-open-cell.d.ts +16 -0
- package/dist/components/days-open-cell.js +73 -0
- package/dist/components/days-open-cell.js.map +1 -0
- package/dist/components/detail-drawer.d.ts +16 -0
- package/dist/components/detail-drawer.js +45 -0
- package/dist/components/detail-drawer.js.map +1 -0
- package/dist/components/insights-filter-bar.d.ts +2 -1
- package/dist/components/insights-filter-bar.js +13 -5
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +14 -0
- package/dist/components/linked-entity-cell.js +96 -0
- package/dist/components/linked-entity-cell.js.map +1 -0
- package/dist/components/metric-card.d.ts +14 -1
- package/dist/components/metric-card.js +97 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/pill.d.ts +26 -0
- package/dist/components/pill.js +77 -0
- package/dist/components/pill.js.map +1 -0
- package/dist/components/quick-segment.d.ts +13 -0
- package/dist/components/quick-segment.js +96 -0
- package/dist/components/quick-segment.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/timeline-activity.d.ts +1 -16
- package/dist/components/timeline-activity.js +1 -69
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +1 -11
- package/dist/prototype/prototype-inbox-view.js +33 -101
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/__tests__/insights-charts.test.tsx +62 -0
- package/src/charts/empty-chart-state.tsx +44 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/pipeline-overview.tsx +41 -1
- package/src/components/__tests__/insights-primitives.test.tsx +135 -0
- package/src/components/days-open-cell.tsx +50 -0
- package/src/components/detail-drawer.tsx +60 -0
- package/src/components/insights-filter-bar.tsx +13 -4
- package/src/components/linked-entity-cell.tsx +74 -0
- package/src/components/metric-card.tsx +98 -0
- package/src/components/pill.tsx +67 -0
- package/src/components/quick-segment.tsx +68 -0
- package/src/components/timeline-activity.tsx +1 -112
- package/src/index.ts +5 -0
- package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
- package/src/prototype/prototype-inbox-view.tsx +30 -131
- package/src/components/__tests__/timeline-activity.test.tsx +0 -137
- 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
|
|
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=
|
|
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=
|
|
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">·</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">·</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=
|
|
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
|
|
93
|
-
const timelineButton = container.querySelector(
|
|
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
|
|