@handled-ai/design-system 0.18.1 → 0.18.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.
- package/dist/charts/index.d.ts +0 -1
- package/dist/charts/index.js +0 -1
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +1 -2
- package/dist/charts/pipeline-overview.js +1 -29
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/insights-filter-bar.d.ts +1 -2
- package/dist/components/insights-filter-bar.js +5 -13
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/metric-card.d.ts +1 -14
- package/dist/components/metric-card.js +0 -86
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/timeline-activity.d.ts +16 -1
- package/dist/components/timeline-activity.js +69 -1
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +2 -8
- package/dist/index.js +0 -5
- package/dist/index.js.map +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +11 -1
- package/dist/prototype/prototype-inbox-view.js +101 -33
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/index.ts +0 -1
- package/src/charts/pipeline-overview.tsx +1 -38
- package/src/components/__tests__/timeline-activity.test.tsx +137 -0
- package/src/components/insights-filter-bar.tsx +4 -13
- package/src/components/metric-card.tsx +0 -82
- package/src/components/timeline-activity.tsx +112 -1
- package/src/index.ts +0 -5
- package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +322 -0
- package/src/prototype/prototype-inbox-view.tsx +131 -30
- package/dist/charts/empty-chart-state.d.ts +0 -11
- package/dist/charts/empty-chart-state.js +0 -70
- package/dist/charts/empty-chart-state.js.map +0 -1
- package/dist/components/days-open-cell.d.ts +0 -16
- package/dist/components/days-open-cell.js +0 -73
- package/dist/components/days-open-cell.js.map +0 -1
- package/dist/components/detail-drawer.d.ts +0 -16
- package/dist/components/detail-drawer.js +0 -45
- package/dist/components/detail-drawer.js.map +0 -1
- package/dist/components/linked-entity-cell.d.ts +0 -14
- package/dist/components/linked-entity-cell.js +0 -96
- package/dist/components/linked-entity-cell.js.map +0 -1
- package/dist/components/pill.d.ts +0 -26
- package/dist/components/pill.js +0 -77
- package/dist/components/pill.js.map +0 -1
- package/dist/components/quick-segment.d.ts +0 -13
- package/dist/components/quick-segment.js +0 -96
- package/dist/components/quick-segment.js.map +0 -1
- package/src/charts/__tests__/insights-charts.test.tsx +0 -62
- package/src/charts/empty-chart-state.tsx +0 -44
- package/src/components/__tests__/insights-primitives.test.tsx +0 -117
- package/src/components/days-open-cell.tsx +0 -50
- package/src/components/detail-drawer.tsx +0 -60
- package/src/components/linked-entity-cell.tsx +0 -74
- package/src/components/pill.tsx +0 -67
- package/src/components/quick-segment.tsx +0 -68
package/dist/components/pill.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __defProps = Object.defineProperties;
|
|
3
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
-
var __spreadValues = (a, b) => {
|
|
9
|
-
for (var prop in b || (b = {}))
|
|
10
|
-
if (__hasOwnProp.call(b, prop))
|
|
11
|
-
__defNormalProp(a, prop, b[prop]);
|
|
12
|
-
if (__getOwnPropSymbols)
|
|
13
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
-
if (__propIsEnum.call(b, prop))
|
|
15
|
-
__defNormalProp(a, prop, b[prop]);
|
|
16
|
-
}
|
|
17
|
-
return a;
|
|
18
|
-
};
|
|
19
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
-
var __objRest = (source, exclude) => {
|
|
21
|
-
var target = {};
|
|
22
|
-
for (var prop in source)
|
|
23
|
-
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
24
|
-
target[prop] = source[prop];
|
|
25
|
-
if (source != null && __getOwnPropSymbols)
|
|
26
|
-
for (var prop of __getOwnPropSymbols(source)) {
|
|
27
|
-
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
28
|
-
target[prop] = source[prop];
|
|
29
|
-
}
|
|
30
|
-
return target;
|
|
31
|
-
};
|
|
32
|
-
import { jsx } from "react/jsx-runtime";
|
|
33
|
-
import { cva } from "class-variance-authority";
|
|
34
|
-
import { cn } from "../lib/utils.js";
|
|
35
|
-
const pillVariants = cva(
|
|
36
|
-
"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",
|
|
37
|
-
{
|
|
38
|
-
variants: {
|
|
39
|
-
variant: {
|
|
40
|
-
default: "border-transparent bg-primary text-primary-foreground",
|
|
41
|
-
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
42
|
-
destructive: "border-transparent bg-destructive text-white dark:bg-destructive/60",
|
|
43
|
-
outline: "border-border bg-background text-foreground",
|
|
44
|
-
ghost: "border-transparent bg-transparent text-muted-foreground",
|
|
45
|
-
success: "border-transparent bg-green-100 text-green-950 dark:bg-green-950 dark:text-green-100",
|
|
46
|
-
warning: "border-transparent bg-yellow-100 text-yellow-950 dark:bg-yellow-950 dark:text-yellow-100",
|
|
47
|
-
error: "border-transparent bg-red-100 text-red-950 dark:bg-red-950 dark:text-red-100",
|
|
48
|
-
neutral: "border-transparent bg-muted text-foreground",
|
|
49
|
-
info: "border-transparent bg-blue-100 text-blue-950 dark:bg-blue-950 dark:text-blue-100"
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
defaultVariants: {
|
|
53
|
-
variant: "neutral"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
function Pill(_a) {
|
|
58
|
-
var _b = _a, { className, variant = "neutral" } = _b, props = __objRest(_b, ["className", "variant"]);
|
|
59
|
-
return /* @__PURE__ */ jsx(
|
|
60
|
-
"span",
|
|
61
|
-
__spreadValues({
|
|
62
|
-
"data-slot": "pill",
|
|
63
|
-
"data-variant": variant,
|
|
64
|
-
className: cn(pillVariants({ variant }), className)
|
|
65
|
-
}, props)
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
function StatusPill(_c) {
|
|
69
|
-
var _d = _c, { status, intent = "neutral", children } = _d, props = __objRest(_d, ["status", "intent", "children"]);
|
|
70
|
-
return /* @__PURE__ */ jsx(Pill, __spreadProps(__spreadValues({ "data-slot": "status-pill", variant: intent }, props), { children: children != null ? children : status }));
|
|
71
|
-
}
|
|
72
|
-
export {
|
|
73
|
-
Pill,
|
|
74
|
-
StatusPill,
|
|
75
|
-
pillVariants
|
|
76
|
-
};
|
|
77
|
-
//# sourceMappingURL=pill.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/pill.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../lib/utils\"\n\n/**\n * Insights-friendly pill convenience wrappers.\n *\n * Pill and StatusPill are small, rounded wrappers for dense Insights surfaces\n * such as tables, KPI strips, and filter summaries. They intentionally wrap the\n * existing Badge/StatusBadge visual language and are not a replacement for all\n * Badge usage across the design system.\n */\nexport type PillStatus = \"success\" | \"warning\" | \"error\" | \"neutral\" | \"info\"\n\nconst pillVariants = cva(\n \"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\",\n {\n variants: {\n variant: {\n default: \"border-transparent bg-primary text-primary-foreground\",\n secondary: \"border-transparent bg-secondary text-secondary-foreground\",\n destructive: \"border-transparent bg-destructive text-white dark:bg-destructive/60\",\n outline: \"border-border bg-background text-foreground\",\n ghost: \"border-transparent bg-transparent text-muted-foreground\",\n success: \"border-transparent bg-green-100 text-green-950 dark:bg-green-950 dark:text-green-100\",\n warning: \"border-transparent bg-yellow-100 text-yellow-950 dark:bg-yellow-950 dark:text-yellow-100\",\n error: \"border-transparent bg-red-100 text-red-950 dark:bg-red-950 dark:text-red-100\",\n neutral: \"border-transparent bg-muted text-foreground\",\n info: \"border-transparent bg-blue-100 text-blue-950 dark:bg-blue-950 dark:text-blue-100\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n },\n }\n)\n\nexport interface PillProps\n extends React.ComponentProps<\"span\">,\n VariantProps<typeof pillVariants> {}\n\nexport function Pill({ className, variant = \"neutral\", ...props }: PillProps) {\n return (\n <span\n data-slot=\"pill\"\n data-variant={variant}\n className={cn(pillVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport interface StatusPillProps extends Omit<PillProps, \"variant\"> {\n status: React.ReactNode\n intent?: PillStatus\n}\n\nexport function StatusPill({ status, intent = \"neutral\", children, ...props }: StatusPillProps) {\n return (\n <Pill data-slot=\"status-pill\" variant={intent} {...props}>\n {children ?? status}\n </Pill>\n )\n}\n\nexport { pillVariants }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CI;AA3CJ,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAYnB,MAAM,eAAe;AAAA,EACnB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,WAAW;AAAA,QACX,aAAa;AAAA,QACb,SAAS;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,IACX;AAAA,EACF;AACF;AAMO,SAAS,KAAK,IAAyD;AAAzD,eAAE,aAAW,UAAU,UA1C5C,IA0CqB,IAAqC,kBAArC,IAAqC,CAAnC,aAAW;AAChC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc;AAAA,MACd,WAAW,GAAG,aAAa,EAAE,QAAQ,CAAC,GAAG,SAAS;AAAA,OAC9C;AAAA,EACN;AAEJ;AAOO,SAAS,WAAW,IAAqE;AAArE,eAAE,UAAQ,SAAS,WAAW,SA1DzD,IA0D2B,IAA2C,kBAA3C,IAA2C,CAAzC,UAAQ,UAAoB;AACvD,SACE,oBAAC,qCAAK,aAAU,eAAc,SAAS,UAAY,QAAlD,EACE,wCAAY,SACf;AAEJ;","names":[]}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
interface QuickSegmentProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "value"> {
|
|
4
|
-
label: React.ReactNode;
|
|
5
|
-
value: string;
|
|
6
|
-
selected?: boolean;
|
|
7
|
-
count?: number | string;
|
|
8
|
-
description?: React.ReactNode;
|
|
9
|
-
onSelect?: (value: string) => void;
|
|
10
|
-
}
|
|
11
|
-
declare function QuickSegment({ label, value, selected, count, description, onSelect, className, type, ...props }: QuickSegmentProps): React.JSX.Element;
|
|
12
|
-
|
|
13
|
-
export { QuickSegment, type QuickSegmentProps };
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
"use client";
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __defProps = Object.defineProperties;
|
|
6
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
-
var __spreadValues = (a, b) => {
|
|
12
|
-
for (var prop in b || (b = {}))
|
|
13
|
-
if (__hasOwnProp.call(b, prop))
|
|
14
|
-
__defNormalProp(a, prop, b[prop]);
|
|
15
|
-
if (__getOwnPropSymbols)
|
|
16
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
-
if (__propIsEnum.call(b, prop))
|
|
18
|
-
__defNormalProp(a, prop, b[prop]);
|
|
19
|
-
}
|
|
20
|
-
return a;
|
|
21
|
-
};
|
|
22
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
23
|
-
var __objRest = (source, exclude) => {
|
|
24
|
-
var target = {};
|
|
25
|
-
for (var prop in source)
|
|
26
|
-
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
27
|
-
target[prop] = source[prop];
|
|
28
|
-
if (source != null && __getOwnPropSymbols)
|
|
29
|
-
for (var prop of __getOwnPropSymbols(source)) {
|
|
30
|
-
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
31
|
-
target[prop] = source[prop];
|
|
32
|
-
}
|
|
33
|
-
return target;
|
|
34
|
-
};
|
|
35
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
36
|
-
import { cn } from "../lib/utils.js";
|
|
37
|
-
function QuickSegment(_a) {
|
|
38
|
-
var _b = _a, {
|
|
39
|
-
label,
|
|
40
|
-
value,
|
|
41
|
-
selected = false,
|
|
42
|
-
count,
|
|
43
|
-
description,
|
|
44
|
-
onSelect,
|
|
45
|
-
className,
|
|
46
|
-
type = "button"
|
|
47
|
-
} = _b, props = __objRest(_b, [
|
|
48
|
-
"label",
|
|
49
|
-
"value",
|
|
50
|
-
"selected",
|
|
51
|
-
"count",
|
|
52
|
-
"description",
|
|
53
|
-
"onSelect",
|
|
54
|
-
"className",
|
|
55
|
-
"type"
|
|
56
|
-
]);
|
|
57
|
-
return /* @__PURE__ */ jsxs(
|
|
58
|
-
"button",
|
|
59
|
-
__spreadProps(__spreadValues({
|
|
60
|
-
"data-slot": "quick-segment",
|
|
61
|
-
"data-selected": selected ? "true" : "false",
|
|
62
|
-
type,
|
|
63
|
-
"aria-pressed": selected,
|
|
64
|
-
className: cn(
|
|
65
|
-
"inline-flex min-h-8 items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors",
|
|
66
|
-
selected ? "border-primary bg-primary text-primary-foreground shadow-sm" : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
67
|
-
className
|
|
68
|
-
),
|
|
69
|
-
onClick: (event) => {
|
|
70
|
-
var _a2;
|
|
71
|
-
(_a2 = props.onClick) == null ? void 0 : _a2.call(props, event);
|
|
72
|
-
if (!event.defaultPrevented) onSelect == null ? void 0 : onSelect(value);
|
|
73
|
-
}
|
|
74
|
-
}, props), {
|
|
75
|
-
children: [
|
|
76
|
-
/* @__PURE__ */ jsx("span", { "data-slot": "quick-segment-label", children: label }),
|
|
77
|
-
count !== void 0 ? /* @__PURE__ */ jsx(
|
|
78
|
-
"span",
|
|
79
|
-
{
|
|
80
|
-
"data-slot": "quick-segment-count",
|
|
81
|
-
className: cn(
|
|
82
|
-
"rounded-full px-1.5 py-0.5 text-[11px] leading-none",
|
|
83
|
-
selected ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted text-muted-foreground"
|
|
84
|
-
),
|
|
85
|
-
children: count
|
|
86
|
-
}
|
|
87
|
-
) : null,
|
|
88
|
-
description ? /* @__PURE__ */ jsx("span", { "data-slot": "quick-segment-description", className: "sr-only", children: description }) : null
|
|
89
|
-
]
|
|
90
|
-
})
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
export {
|
|
94
|
-
QuickSegment
|
|
95
|
-
};
|
|
96
|
-
//# sourceMappingURL=quick-segment.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/quick-segment.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../lib/utils\"\n\nexport interface QuickSegmentProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onSelect\" | \"value\"> {\n label: React.ReactNode\n value: string\n selected?: boolean\n count?: number | string\n description?: React.ReactNode\n onSelect?: (value: string) => void\n}\n\nexport function QuickSegment({\n label,\n value,\n selected = false,\n count,\n description,\n onSelect,\n className,\n type = \"button\",\n ...props\n}: QuickSegmentProps) {\n return (\n <button\n data-slot=\"quick-segment\"\n data-selected={selected ? \"true\" : \"false\"}\n type={type}\n aria-pressed={selected}\n className={cn(\n \"inline-flex min-h-8 items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors\",\n selected\n ? \"border-primary bg-primary text-primary-foreground shadow-sm\"\n : \"border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground\",\n className\n )}\n onClick={(event) => {\n props.onClick?.(event)\n if (!event.defaultPrevented) onSelect?.(value)\n }}\n {...props}\n >\n <span data-slot=\"quick-segment-label\">{label}</span>\n {count !== undefined ? (\n <span\n data-slot=\"quick-segment-count\"\n className={cn(\n \"rounded-full px-1.5 py-0.5 text-[11px] leading-none\",\n selected\n ? \"bg-primary-foreground/20 text-primary-foreground\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n {count}\n </span>\n ) : null}\n {description ? (\n <span data-slot=\"quick-segment-description\" className=\"sr-only\">\n {description}\n </span>\n ) : null}\n </button>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BI,SAkBE,KAlBF;AAxBJ,SAAS,UAAU;AAYZ,SAAS,aAAa,IAUP;AAVO,eAC3B;AAAA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EAxBT,IAgB6B,IASxB,kBATwB,IASxB;AAAA,IARH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAGA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,iBAAe,WAAW,SAAS;AAAA,MACnC;AAAA,MACA,gBAAc;AAAA,MACd,WAAW;AAAA,QACT;AAAA,QACA,WACI,gEACA;AAAA,QACJ;AAAA,MACF;AAAA,MACA,SAAS,CAAC,UAAU;AAxC1B,YAAAA;AAyCQ,SAAAA,MAAA,MAAM,YAAN,gBAAAA,IAAA,YAAgB;AAChB,YAAI,CAAC,MAAM,iBAAkB,sCAAW;AAAA,MAC1C;AAAA,OACI,QAhBL;AAAA,MAkBC;AAAA,4BAAC,UAAK,aAAU,uBAAuB,iBAAM;AAAA,QAC5C,UAAU,SACT;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA,cACT;AAAA,cACA,WACI,qDACA;AAAA,YACN;AAAA,YAEC;AAAA;AAAA,QACH,IACE;AAAA,QACH,cACC,oBAAC,UAAK,aAAU,6BAA4B,WAAU,WACnD,uBACH,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":["_a"]}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import { describe, expect, it, vi } from "vitest"
|
|
3
|
-
import { render, screen, fireEvent } from "@testing-library/react"
|
|
4
|
-
|
|
5
|
-
import { EmptyChartState } from "../empty-chart-state"
|
|
6
|
-
import { PipelineOverview, type PipelineStage, type PipelineStageMetrics } from "../pipeline-overview"
|
|
7
|
-
|
|
8
|
-
vi.mock("@nivo/sankey", () => ({
|
|
9
|
-
ResponsiveSankey: ({ data }: { data: { nodes: unknown[]; links: unknown[] } }) => (
|
|
10
|
-
<div data-testid="mock-sankey">{data.nodes.length}:{data.links.length}</div>
|
|
11
|
-
),
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
const stages: PipelineStage[] = [
|
|
15
|
-
{ id: "received", label: "Received", count: 100, trend: "+5%", nextConversion: "80%" },
|
|
16
|
-
{ id: "contacted", label: "Contacted", count: 80, trend: "+3%", nextConversion: null },
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
const metrics: Record<string, PipelineStageMetrics> = {
|
|
20
|
-
received: { medianTime: "1d", avgTime: "2d", dropOffs: [] },
|
|
21
|
-
contacted: { medianTime: "2d", avgTime: "3d", dropOffs: [] },
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function renderPipeline(variant?: "sankey" | "compact", onFilterChange = vi.fn()) {
|
|
25
|
-
return render(
|
|
26
|
-
<PipelineOverview
|
|
27
|
-
variant={variant}
|
|
28
|
-
stages={stages}
|
|
29
|
-
stageMetrics={metrics}
|
|
30
|
-
stageTimings={[{ median: "1d", avg: "2d" }, null]}
|
|
31
|
-
filterOptions={["Facility", "Source"]}
|
|
32
|
-
onFilterChange={onFilterChange}
|
|
33
|
-
/>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("Insights chart primitives", () => {
|
|
38
|
-
it("EmptyChartState renders defaults and action", () => {
|
|
39
|
-
const { container } = render(<EmptyChartState action={<button type="button">Reset</button>} />)
|
|
40
|
-
|
|
41
|
-
expect(container.querySelector('[data-slot="empty-chart-state"]')).not.toBeNull()
|
|
42
|
-
expect(screen.getByText("No chart data")).not.toBeNull()
|
|
43
|
-
expect(screen.getByRole("button", { name: "Reset" })).not.toBeNull()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it("PipelineOverview default variant preserves Sankey rendering and callbacks", () => {
|
|
47
|
-
const onFilterChange = vi.fn()
|
|
48
|
-
renderPipeline(undefined, onFilterChange)
|
|
49
|
-
|
|
50
|
-
expect(screen.getByTestId("mock-sankey")).not.toBeNull()
|
|
51
|
-
fireEvent.click(screen.getByRole("button", { name: "Source" }))
|
|
52
|
-
expect(onFilterChange).toHaveBeenCalledWith("Source")
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it("PipelineOverview compact variant renders compact bars instead of Sankey", () => {
|
|
56
|
-
const { container } = renderPipeline("compact")
|
|
57
|
-
|
|
58
|
-
expect(screen.queryByTestId("mock-sankey")).toBeNull()
|
|
59
|
-
expect(container.querySelector('[data-slot="pipeline-overview-compact"]')).not.toBeNull()
|
|
60
|
-
expect(container.querySelectorAll('[data-slot="pipeline-overview-compact-bar"]')).toHaveLength(2)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { BarChart3 } from "lucide-react"
|
|
3
|
-
|
|
4
|
-
import { cn } from "../lib/utils"
|
|
5
|
-
|
|
6
|
-
export interface EmptyChartStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
|
-
title?: React.ReactNode
|
|
8
|
-
description?: React.ReactNode
|
|
9
|
-
icon?: React.ReactNode
|
|
10
|
-
action?: React.ReactNode
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function EmptyChartState({
|
|
14
|
-
title = "No chart data",
|
|
15
|
-
description = "Try adjusting filters or selecting a different time range.",
|
|
16
|
-
icon,
|
|
17
|
-
action,
|
|
18
|
-
className,
|
|
19
|
-
...props
|
|
20
|
-
}: EmptyChartStateProps) {
|
|
21
|
-
return (
|
|
22
|
-
<div
|
|
23
|
-
data-slot="empty-chart-state"
|
|
24
|
-
className={cn(
|
|
25
|
-
"flex min-h-[240px] flex-col items-center justify-center rounded-xl border border-dashed border-border bg-muted/30 p-8 text-center",
|
|
26
|
-
className
|
|
27
|
-
)}
|
|
28
|
-
{...props}
|
|
29
|
-
>
|
|
30
|
-
<div data-slot="empty-chart-state-icon" className="mb-3 text-muted-foreground [&>svg]:h-10 [&>svg]:w-10">
|
|
31
|
-
{icon ?? <BarChart3 aria-hidden="true" />}
|
|
32
|
-
</div>
|
|
33
|
-
<div data-slot="empty-chart-state-title" className="text-sm font-semibold text-foreground">
|
|
34
|
-
{title}
|
|
35
|
-
</div>
|
|
36
|
-
{description ? (
|
|
37
|
-
<div data-slot="empty-chart-state-description" className="mt-1 max-w-sm text-sm text-muted-foreground">
|
|
38
|
-
{description}
|
|
39
|
-
</div>
|
|
40
|
-
) : null}
|
|
41
|
-
{action ? <div data-slot="empty-chart-state-action" className="mt-4">{action}</div> : null}
|
|
42
|
-
</div>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import { describe, expect, it, vi } from "vitest"
|
|
3
|
-
import { render, screen, fireEvent } from "@testing-library/react"
|
|
4
|
-
|
|
5
|
-
import { DaysOpenCell, getDaysOpenIntent } from "../days-open-cell"
|
|
6
|
-
import { DetailDrawer } from "../detail-drawer"
|
|
7
|
-
import { InsightsFilterBar } from "../insights-filter-bar"
|
|
8
|
-
import { LinkedEntityCell } from "../linked-entity-cell"
|
|
9
|
-
import { KpiStrip } from "../metric-card"
|
|
10
|
-
import { Pill, StatusPill } from "../pill"
|
|
11
|
-
import { QuickSegment } from "../quick-segment"
|
|
12
|
-
|
|
13
|
-
describe("Insights primitives", () => {
|
|
14
|
-
it("renders compact InsightsFilterBar without changing default API", () => {
|
|
15
|
-
const { container } = render(
|
|
16
|
-
<InsightsFilterBar
|
|
17
|
-
variant="compact"
|
|
18
|
-
filters={[{ id: "status", label: "Status", options: ["All", "Open"], defaultValue: "All" }]}
|
|
19
|
-
values={{ status: "Open" }}
|
|
20
|
-
onChange={() => {}}
|
|
21
|
-
onClearAll={() => {}}
|
|
22
|
-
/>
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const bar = container.querySelector('[data-slot="insights-filter-bar"]')!
|
|
26
|
-
expect(bar.className).toContain("p-2")
|
|
27
|
-
expect(bar.className).toContain("gap-2")
|
|
28
|
-
expect(screen.getByRole("button", { name: /Status: Open/i }).className).toContain("h-7")
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it("renders KpiStrip items and changes", () => {
|
|
32
|
-
const { container } = render(
|
|
33
|
-
<KpiStrip
|
|
34
|
-
items={[
|
|
35
|
-
{ label: "New", value: 42, unit: "leads", change: { value: "8%", direction: "up" } },
|
|
36
|
-
{ label: "Aging", value: 12, subtitle: "over SLA", change: { value: "3%", direction: "down", isGood: true } },
|
|
37
|
-
]}
|
|
38
|
-
/>
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
expect(container.querySelectorAll('[data-slot="kpi-strip-item"]')).toHaveLength(2)
|
|
42
|
-
expect(screen.getByText("New")).not.toBeNull()
|
|
43
|
-
expect(screen.getByText("42")).not.toBeNull()
|
|
44
|
-
expect(screen.getByText("8%")).not.toBeNull()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it("QuickSegment exposes selected state and invokes onSelect", () => {
|
|
48
|
-
const onSelect = vi.fn()
|
|
49
|
-
render(<QuickSegment label="At risk" value="risk" count={5} selected onSelect={onSelect} />)
|
|
50
|
-
|
|
51
|
-
const button = screen.getByRole("button", { name: /At risk/i })
|
|
52
|
-
expect(button.getAttribute("aria-pressed")).toBe("true")
|
|
53
|
-
expect(screen.getByText("5")).not.toBeNull()
|
|
54
|
-
fireEvent.click(button)
|
|
55
|
-
expect(onSelect).toHaveBeenCalledWith("risk")
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it("LinkedEntityCell renders entity links and metadata", () => {
|
|
59
|
-
const onNavigate = vi.fn()
|
|
60
|
-
render(
|
|
61
|
-
<LinkedEntityCell
|
|
62
|
-
name="Acme Health"
|
|
63
|
-
href="/accounts/acme"
|
|
64
|
-
subtitle="Account"
|
|
65
|
-
meta="Tier 1"
|
|
66
|
-
onNavigate={onNavigate}
|
|
67
|
-
/>
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
const link = screen.getByRole("link", { name: "Acme Health" })
|
|
71
|
-
expect(link.getAttribute("href")).toBe("/accounts/acme")
|
|
72
|
-
expect(screen.getByText(/Tier 1/)).not.toBeNull()
|
|
73
|
-
fireEvent.click(link)
|
|
74
|
-
expect(onNavigate).toHaveBeenCalled()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it("DaysOpenCell maps thresholds to status intents", () => {
|
|
78
|
-
expect(getDaysOpenIntent(2, 7, 30)).toBe("success")
|
|
79
|
-
expect(getDaysOpenIntent(10, 7, 30)).toBe("warning")
|
|
80
|
-
expect(getDaysOpenIntent(45, 7, 30)).toBe("error")
|
|
81
|
-
|
|
82
|
-
render(<DaysOpenCell days={45} />)
|
|
83
|
-
expect(screen.getByTestId("days-open-pill").getAttribute("data-variant")).toBe("error")
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it("Pill and StatusPill render wrapper variants", () => {
|
|
87
|
-
const { container } = render(
|
|
88
|
-
<div>
|
|
89
|
-
<Pill variant="info">Info</Pill>
|
|
90
|
-
<StatusPill status="Blocked" intent="error" />
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
expect(container.querySelector('[data-slot="pill"]')?.getAttribute("data-variant")).toBe("info")
|
|
95
|
-
expect(container.querySelector('[data-slot="status-pill"]')?.getAttribute("data-variant")).toBe("error")
|
|
96
|
-
expect(screen.getByText("Blocked")).not.toBeNull()
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it("DetailDrawer renders title, content, and footer when open", () => {
|
|
100
|
-
render(
|
|
101
|
-
<DetailDrawer
|
|
102
|
-
open
|
|
103
|
-
onOpenChange={() => {}}
|
|
104
|
-
title="Referral details"
|
|
105
|
-
description="A drawer for insights"
|
|
106
|
-
footer={<button type="button">Done</button>}
|
|
107
|
-
>
|
|
108
|
-
<div>Drawer body</div>
|
|
109
|
-
</DetailDrawer>
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
expect(screen.getByText("Referral details")).not.toBeNull()
|
|
113
|
-
expect(screen.getByText("A drawer for insights")).not.toBeNull()
|
|
114
|
-
expect(screen.getByText("Drawer body")).not.toBeNull()
|
|
115
|
-
expect(screen.getByRole("button", { name: "Done" })).not.toBeNull()
|
|
116
|
-
})
|
|
117
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
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 }
|
|
@@ -1,60 +0,0 @@
|
|
|
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("w-full 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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|