@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/charts/index.d.ts
CHANGED
|
@@ -7,6 +7,5 @@ export { VolumeAnalysisChart, VolumeAnalysisChartProps, VolumeDataKey } from './
|
|
|
7
7
|
export { MetricCardData, TopLineMetrics, TopLineMetricsProps } from './top-line-metrics.js';
|
|
8
8
|
export { PipelineFilterBreakdown, PipelineOverview, PipelineOverviewProps, PipelineStage, PipelineStageMetrics, PipelineStageTiming } from './pipeline-overview.js';
|
|
9
9
|
export { SankeyChart, SankeyData, SankeyDropOff, SankeyHoverCardData, SankeyLink, SankeyNode, SankeyStageMetrics } from './sankey-chart.js';
|
|
10
|
-
export { EmptyChartState, EmptyChartStateProps } from './empty-chart-state.js';
|
|
11
10
|
import 'react';
|
|
12
11
|
import 'recharts';
|
package/dist/charts/index.js
CHANGED
package/dist/charts/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/charts/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system/charts\n * Chart components requiring recharts and @nivo/sankey\n */\nexport * from \"./chart\"\nexport * from \"./chart-tooltip\"\nexport * from \"./bar-chart-component\"\nexport * from \"./donut-chart\"\nexport * from \"./trend-area-chart\"\nexport * from \"./volume-analysis-chart\"\nexport * from \"./top-line-metrics\"\nexport * from \"./pipeline-overview\"\nexport * from \"./sankey-chart\"\
|
|
1
|
+
{"version":3,"sources":["../../src/charts/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system/charts\n * Chart components requiring recharts and @nivo/sankey\n */\nexport * from \"./chart\"\nexport * from \"./chart-tooltip\"\nexport * from \"./bar-chart-component\"\nexport * from \"./donut-chart\"\nexport * from \"./trend-area-chart\"\nexport * from \"./volume-analysis-chart\"\nexport * from \"./top-line-metrics\"\nexport * from \"./pipeline-overview\"\nexport * from \"./sankey-chart\"\n"],"mappings":"AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -25,7 +25,6 @@ interface PipelineFilterBreakdown {
|
|
|
25
25
|
}
|
|
26
26
|
interface PipelineOverviewProps {
|
|
27
27
|
title?: string;
|
|
28
|
-
variant?: "sankey" | "compact";
|
|
29
28
|
stages: PipelineStage[];
|
|
30
29
|
stageMetrics: Record<string, PipelineStageMetrics>;
|
|
31
30
|
stageTimings: (PipelineStageTiming | null)[];
|
|
@@ -72,6 +71,6 @@ interface PipelineOverviewProps {
|
|
|
72
71
|
onViewInWorkQueue?: (stageId: string) => void;
|
|
73
72
|
className?: string;
|
|
74
73
|
}
|
|
75
|
-
declare function PipelineOverview({ title,
|
|
74
|
+
declare function PipelineOverview({ title, stages, stageMetrics, stageTimings, filterOptions, onFilterChange, filterBreakdowns, countingModes, countingModeTooltip, flowNodes, dropOffDistribution, flowLinks, totalReceived, nodeAmounts, unitLabel, terminalUnitLabel, terminalNodeIds, sankeyMargin, sankeyLabelPadding, alwaysShowConversionBadges, dropOffNodeColor, onViewInWorkQueue: _onViewInWorkQueue, className, }: PipelineOverviewProps): React.JSX.Element;
|
|
76
75
|
|
|
77
76
|
export { type PipelineFilterBreakdown, PipelineOverview, type PipelineOverviewProps, type PipelineStage, type PipelineStageMetrics, type PipelineStageTiming };
|
|
@@ -89,7 +89,6 @@ function StageHoverCard({
|
|
|
89
89
|
}
|
|
90
90
|
function PipelineOverview({
|
|
91
91
|
title = "Pipeline Overview",
|
|
92
|
-
variant = "sankey",
|
|
93
92
|
stages,
|
|
94
93
|
stageMetrics,
|
|
95
94
|
stageTimings,
|
|
@@ -303,36 +302,9 @@ function PipelineOverview({
|
|
|
303
302
|
)
|
|
304
303
|
] }) }, stage.id);
|
|
305
304
|
}) }),
|
|
306
|
-
|
|
307
|
-
var _a;
|
|
308
|
-
const maxCount = Math.max(...stages.map((item) => item.count), 1);
|
|
309
|
-
const width = `${Math.max(stage.count / maxCount * 100, 4)}%`;
|
|
310
|
-
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-background p-3", children: [
|
|
311
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [
|
|
312
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
313
|
-
/* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: stage.label }),
|
|
314
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: stage.trend })
|
|
315
|
-
] }),
|
|
316
|
-
/* @__PURE__ */ jsx("div", { className: "text-right text-lg font-bold text-foreground", children: stage.count.toLocaleString() })
|
|
317
|
-
] }),
|
|
318
|
-
/* @__PURE__ */ jsx("div", { className: "h-2 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
|
|
319
|
-
"div",
|
|
320
|
-
{
|
|
321
|
-
"data-slot": "pipeline-overview-compact-bar",
|
|
322
|
-
className: "h-full rounded-full bg-emerald-600",
|
|
323
|
-
style: { width }
|
|
324
|
-
}
|
|
325
|
-
) }),
|
|
326
|
-
index < stages.length - 1 && stage.nextConversion ? /* @__PURE__ */ jsxs("div", { className: "mt-2 text-xs text-muted-foreground", children: [
|
|
327
|
-
stage.nextConversion,
|
|
328
|
-
" to ",
|
|
329
|
-
(_a = stages[index + 1]) == null ? void 0 : _a.label
|
|
330
|
-
] }) : null
|
|
331
|
-
] }, stage.id);
|
|
332
|
-
}) }) : /* @__PURE__ */ jsx(
|
|
305
|
+
/* @__PURE__ */ jsx(
|
|
333
306
|
"div",
|
|
334
307
|
{
|
|
335
|
-
"data-slot": "pipeline-overview-sankey",
|
|
336
308
|
className: "relative mt-4 w-full",
|
|
337
309
|
style: { height: 400, minWidth: 0 },
|
|
338
310
|
children: /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/charts/pipeline-overview.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { TrendingUp, Info, ArrowRight } from \"lucide-react\"\nimport { ResponsiveSankey } from \"@nivo/sankey\"\n\nimport { cn } from \"../lib/utils\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"../components/tooltip\"\n\nexport interface PipelineStage {\n id: string\n label: string\n count: number\n trend: string\n nextConversion: string | null\n}\n\nexport interface PipelineStageMetrics {\n medianTime: string\n avgTime: string\n dropOffs: { reason: string; count: number; pct: string }[]\n}\n\nexport interface PipelineStageTiming {\n median: string\n avg: string\n}\n\nexport interface PipelineFilterBreakdown {\n [stageId: string]: Record<string, number>\n}\n\nconst SEGMENT_PALETTE = [\n \"#0F4C3A\",\n \"#15803d\",\n \"#0ea5e9\",\n \"#8b5cf6\",\n \"#f59e0b\",\n \"#ef4444\",\n]\n\nconst DROP_OFF_NODES = [\n { id: \"Lost/Other\", nodeColor: \"#CBD5E1\" },\n { id: \"Coverage\", nodeColor: \"#F59E0B\" },\n { id: \"Unqualified\", nodeColor: \"#F59E0B\" },\n { id: \"No Contact\", nodeColor: \"#F59E0B\" },\n { id: \"Intake Drop\", nodeColor: \"#F59E0B\" },\n { id: \"No Show/Cancel\", nodeColor: \"#F59E0B\" },\n]\n\nfunction StageHoverCard({\n title,\n count,\n metrics,\n}: {\n title: string\n count: number | string\n metrics?: PipelineStageMetrics\n}) {\n return (\n <div className=\"w-[260px] overflow-hidden rounded-lg border border-border bg-card font-sans text-left shadow-xl\">\n <div className=\"border-b border-border p-3\">\n <div className=\"mb-0.5 text-xs font-medium text-muted-foreground\">{title}</div>\n <div className=\"text-2xl font-bold text-foreground\">{count}</div>\n </div>\n\n {metrics && (\n <div className=\"grid grid-cols-2 gap-2 border-b border-border bg-muted/30 px-3 py-2\">\n <div>\n <div className=\"text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70\">\n Median\n </div>\n <div className=\"text-xs font-bold text-foreground\">\n {metrics.medianTime}\n </div>\n </div>\n <div className=\"border-l border-border pl-2\">\n <div className=\"text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70\">\n Average\n </div>\n <div className=\"text-xs font-bold text-foreground\">\n {metrics.avgTime}\n </div>\n </div>\n </div>\n )}\n\n {metrics?.dropOffs && metrics.dropOffs.length > 0 && (\n <div className=\"p-2\">\n <div className=\"mb-1.5 px-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground\">\n Drop-off Reasons\n </div>\n <div className=\"space-y-0.5\">\n {metrics.dropOffs.map((drop, i) => (\n <div\n key={i}\n className=\"group flex cursor-pointer items-center justify-between rounded p-1.5 text-xs transition-colors hover:bg-muted\"\n >\n <span className=\"truncate pr-2 text-muted-foreground group-hover:text-foreground\">\n {drop.reason}\n </span>\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-[10px] text-muted-foreground/70\">{drop.pct}</span>\n <span className=\"inline-flex h-4 min-w-[20px] items-center justify-center rounded border border-border bg-muted px-1 text-[9px] font-semibold text-muted-foreground group-hover:bg-muted group-hover:text-foreground\">\n {drop.count}\n </span>\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n\n <div className=\"flex cursor-pointer items-center justify-between border-t border-border bg-muted px-3 py-2 text-xs font-medium text-blue-600 transition-colors hover:bg-muted hover:text-blue-700\">\n View in Work Queue <ArrowRight className=\"h-3 w-3\" />\n </div>\n </div>\n )\n}\n\nexport interface PipelineOverviewProps {\n title?: string\n variant?: \"sankey\" | \"compact\"\n stages: PipelineStage[]\n stageMetrics: Record<string, PipelineStageMetrics>\n stageTimings: (PipelineStageTiming | null)[]\n filterOptions?: string[]\n onFilterChange?: (filterOption: string) => void\n filterBreakdowns?: Record<string, PipelineFilterBreakdown>\n countingModes?: string[]\n countingModeTooltip?: string\n /** Main pipeline flow nodes (after the first stage) */\n flowNodes?: { id: string; nodeColor: string }[]\n /** Drop-off distribution from initial stage: { \"Lost/Other\": 56, \"Coverage\": 40, ... } */\n dropOffDistribution?: Record<string, number>\n /** Flow links after the first stage (middle of pipeline onward) */\n flowLinks?: { source: string; target: string; value: number }[]\n totalReceived?: number\n /** Dollar amounts to display on terminal node labels: { \"Retained\": \"$18.2M\" } */\n nodeAmounts?: Record<string, string>\n /** Unit noun for standard node/link tooltips (e.g. \"signals\", \"accounts\"). */\n unitLabel?: string\n /** Unit noun for terminal/drop-off node tooltips (e.g. \"opportunities\"). Defaults to unitLabel. */\n terminalUnitLabel?: string\n /** Node IDs that should use terminalUnitLabel. Defaults to keys of dropOffDistribution. */\n terminalNodeIds?: string[]\n /** Sankey chart margins, merged with defaults { top: 20, right: 120, bottom: 20, left: 140 }. */\n sankeyMargin?: { top?: number; right?: number; bottom?: number; left?: number }\n /** Gap between Sankey node bar and its outside label. */\n sankeyLabelPadding?: number\n /** When true, conversion badges use `flex`; when false, `hidden xl:flex`. */\n alwaysShowConversionBadges?: boolean\n /** Color for dynamically generated drop-off nodes. */\n dropOffNodeColor?: string\n onViewInWorkQueue?: (stageId: string) => void\n className?: string\n}\n\nexport function PipelineOverview({\n title = \"Pipeline Overview\",\n variant = \"sankey\",\n stages,\n stageMetrics,\n stageTimings,\n filterOptions = [\"Facility\", \"Source\", \"Lead Source\", \"Payer\", \"Channel\"],\n onFilterChange,\n filterBreakdowns,\n countingModes = [\"Unique Patients\", \"All Referrals\"],\n countingModeTooltip = \"Patients may be referred through multiple channels. 'All Referrals' shows total volume; 'Unique Patients' deduplicates to show distinct patient counts.\",\n flowNodes = [\n { id: \"Contacted\", nodeColor: \"#2A8F7A\" },\n { id: \"Intake Sent\", nodeColor: \"#3DB4A0\" },\n { id: \"Intake Done\", nodeColor: \"#4CC9B0\" },\n { id: \"Scheduled\", nodeColor: \"#5FCFBC\" },\n { id: \"Completed\", nodeColor: \"#79E2C9\" },\n ],\n dropOffDistribution = {\n \"Lost/Other\": 56,\n Coverage: 40,\n Unqualified: 30,\n },\n flowLinks = [\n { source: \"Contacted\", target: \"Intake Sent\", value: 660 },\n { source: \"Contacted\", target: \"No Contact\", value: 60 },\n { source: \"Intake Sent\", target: \"Intake Done\", value: 612 },\n { source: \"Intake Sent\", target: \"Intake Drop\", value: 48 },\n { source: \"Intake Done\", target: \"Scheduled\", value: 612 },\n { source: \"Scheduled\", target: \"Completed\", value: 520 },\n { source: \"Scheduled\", target: \"No Show/Cancel\", value: 92 },\n ],\n totalReceived = 847,\n nodeAmounts,\n unitLabel,\n terminalUnitLabel,\n terminalNodeIds,\n sankeyMargin,\n sankeyLabelPadding,\n alwaysShowConversionBadges = false,\n dropOffNodeColor,\n onViewInWorkQueue: _onViewInWorkQueue,\n className,\n}: PipelineOverviewProps) {\n const [selectedFilter, setSelectedFilter] = React.useState(filterOptions[0])\n const [countingMode, setCountingMode] = React.useState(countingModes[0])\n\n const effectiveUnitLabel = unitLabel ?? \"items\"\n const effectiveTerminalUnitLabel = terminalUnitLabel ?? effectiveUnitLabel\n const effectiveTerminalNodeIds = React.useMemo(\n () => new Set(terminalNodeIds ?? Object.keys(dropOffDistribution)),\n [terminalNodeIds, dropOffDistribution],\n )\n const effectiveMargin = React.useMemo(\n () => ({ top: 20, right: 120, bottom: 20, left: 140, ...sankeyMargin }),\n [sankeyMargin],\n )\n const effectiveLabelPadding = sankeyLabelPadding ?? 16\n\n const sankeyData = React.useMemo(() => {\n const breakdown =\n filterBreakdowns?.[selectedFilter]?.received ??\n ({ [stages[0]?.label ?? \"Received\"]: totalReceived } as Record<\n string,\n number\n >)\n\n const segments = Object.entries(breakdown).map(([name, value], index) => ({\n id: name,\n nodeColor: SEGMENT_PALETTE[index % SEGMENT_PALETTE.length],\n value,\n }))\n\n const dropOffKeys = Object.keys(dropOffDistribution)\n const dropOffNodes = dropOffKeys.length > 0\n ? dropOffKeys.map((reason) => ({\n id: reason,\n nodeColor: dropOffNodeColor ?? \"#F59E0B\",\n }))\n : DROP_OFF_NODES\n\n const nodes = [\n ...segments,\n ...flowNodes,\n ...dropOffNodes,\n ]\n\n const nodeIds = new Set(nodes.map((n) => n.id))\n for (const link of flowLinks) {\n for (const endpoint of [link.source, link.target]) {\n if (!nodeIds.has(endpoint)) {\n nodes.push({ id: endpoint, nodeColor: dropOffNodeColor ?? \"#F59E0B\" })\n nodeIds.add(endpoint)\n }\n }\n }\n\n const links: { source: string; target: string; value: number }[] = []\n\n const firstFlowNode = flowNodes[0]?.id ?? \"Contacted\"\n\n segments.forEach((segment) => {\n if (segment.value > 0) {\n links.push({\n source: segment.id,\n target: firstFlowNode,\n value: segment.value,\n })\n }\n })\n\n for (const [reason, count] of Object.entries(dropOffDistribution)) {\n if (count > 0) {\n links.push({ source: firstFlowNode, target: reason, value: count })\n }\n }\n\n flowLinks.forEach((link) => links.push({ ...link }))\n\n return { nodes, links }\n }, [\n selectedFilter,\n filterBreakdowns,\n stages,\n totalReceived,\n flowNodes,\n dropOffDistribution,\n dropOffNodeColor,\n flowLinks,\n ])\n\n return (\n <div\n className={cn(\n \"rounded-xl border border-border bg-card p-5 shadow-sm\",\n className,\n )}\n >\n {/* Header */}\n <div className=\"mb-6 flex flex-col gap-4\">\n <div className=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex items-center gap-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">{title}</h3>\n\n {/* Counting Mode Toggle */}\n {countingModes.length > 1 && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border bg-muted p-1\">\n {countingModes.map((mode) => (\n <button\n key={mode}\n onClick={() => setCountingMode(mode)}\n className={cn(\n \"rounded-md px-3 py-1 text-xs font-medium transition-all\",\n countingMode === mode\n ? \"border border-border bg-background text-foreground shadow-sm\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {mode}\n </button>\n ))}\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Info className=\"ml-1 h-3.5 w-3.5 cursor-help text-muted-foreground/70\" />\n </TooltipTrigger>\n <TooltipContent className=\"max-w-[250px] text-xs\">\n {countingModeTooltip}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n )}\n </div>\n\n {/* Filter Tabs */}\n {filterOptions.length > 1 && (\n <div className=\"flex items-center gap-1 self-start rounded-lg bg-muted p-1 md:self-auto\">\n {filterOptions.map((option) => (\n <button\n key={option}\n onClick={() => { setSelectedFilter(option); onFilterChange?.(option); }}\n className={cn(\n \"h-7 rounded-md border-none bg-transparent px-3 text-xs font-medium shadow-none transition-all hover:bg-background\",\n selectedFilter === option &&\n \"bg-background text-foreground shadow-sm\",\n )}\n >\n {option}\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n\n {/* Stage Metrics Row */}\n <div className=\"mb-2 grid gap-4\" style={{ gridTemplateColumns: `repeat(${stages.length}, minmax(0, 1fr))` }}>\n {stages.map((stage, index) => {\n const details = stageMetrics[stage.id]\n const timing = stageTimings[index] ?? null\n\n return (\n <TooltipProvider key={stage.id} delayDuration={100}>\n <Tooltip>\n <TooltipTrigger asChild>\n <div className=\"group relative flex cursor-pointer flex-col items-center rounded-lg p-2 text-center transition-colors hover:bg-muted\">\n <div\n className=\"mb-1 w-full truncate text-xs font-medium text-muted-foreground\"\n title={stage.label}\n >\n {stage.label}\n </div>\n <div className=\"mb-1 text-2xl font-bold text-foreground\">\n {stage.count.toLocaleString()}\n </div>\n <div className=\"mb-1 flex items-center justify-center gap-1 text-xs font-medium text-emerald-600\">\n {stage.trend} <TrendingUp className=\"h-3 w-3\" />\n </div>\n\n {/* Conversion badge + timing between stages */}\n {index < stages.length - 1 && stage.nextConversion && (\n <div className={cn(\n alwaysShowConversionBadges ? \"flex\" : \"hidden xl:flex\",\n \"absolute -right-2 top-1/2 z-10 -translate-y-1/2 translate-x-1/2 flex-col items-center\",\n )}>\n <span className=\"z-10 whitespace-nowrap rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm\">\n {stage.nextConversion}\n </span>\n {timing && (\n <div className=\"mt-1 flex flex-col items-center\">\n <div className=\"h-2 w-px bg-border\" />\n <div className=\"whitespace-nowrap rounded bg-background/80 px-1 py-0.5 text-[9px] font-medium leading-3 text-muted-foreground/70 backdrop-blur-[2px]\">\n <span className=\"mr-1 font-semibold text-muted-foreground\">\n Med: {timing.median}\n </span>\n <span className=\"text-muted-foreground/70\">\n Avg: {timing.avg}\n </span>\n </div>\n </div>\n )}\n </div>\n )}\n\n {/* Connector line down to Sankey */}\n <div className=\"mx-auto mb-1 mt-2 h-4 w-px bg-border\" />\n </div>\n </TooltipTrigger>\n <TooltipContent\n className=\"border-none bg-transparent p-0 shadow-none\"\n sideOffset={8}\n >\n <StageHoverCard\n title={stage.label}\n count={stage.count}\n metrics={details}\n />\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n )\n })}\n </div>\n\n {variant === \"compact\" ? (\n <div data-slot=\"pipeline-overview-compact\" className=\"mt-4 space-y-2\">\n {stages.map((stage, index) => {\n const maxCount = Math.max(...stages.map((item) => item.count), 1)\n const width = `${Math.max((stage.count / maxCount) * 100, 4)}%`\n\n return (\n <div key={stage.id} className=\"rounded-lg border border-border bg-background p-3\">\n <div className=\"mb-2 flex items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-medium text-foreground\">{stage.label}</div>\n <div className=\"text-xs text-muted-foreground\">{stage.trend}</div>\n </div>\n <div className=\"text-right text-lg font-bold text-foreground\">\n {stage.count.toLocaleString()}\n </div>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-muted\">\n <div\n data-slot=\"pipeline-overview-compact-bar\"\n className=\"h-full rounded-full bg-emerald-600\"\n style={{ width }}\n />\n </div>\n {index < stages.length - 1 && stage.nextConversion ? (\n <div className=\"mt-2 text-xs text-muted-foreground\">\n {stage.nextConversion} to {stages[index + 1]?.label}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n ) : (\n <div\n data-slot=\"pipeline-overview-sankey\"\n className=\"relative mt-4 w-full\"\n style={{ height: 400, minWidth: 0 }}\n >\n <ResponsiveSankey\n data={sankeyData}\n margin={effectiveMargin}\n align=\"justify\"\n colors={(node: { nodeColor?: string }) => node.nodeColor || \"#94a3b8\"}\n nodeOpacity={1}\n nodeHoverOthersOpacity={0.35}\n nodeThickness={18}\n nodeSpacing={16}\n nodeBorderWidth={0}\n nodeBorderRadius={3}\n linkOpacity={0.5}\n linkHoverOthersOpacity={0.1}\n linkContract={3}\n enableLinkGradient\n labelPosition=\"outside\"\n labelOrientation=\"horizontal\"\n labelPadding={effectiveLabelPadding}\n label={(node: { id: string }) => nodeAmounts?.[node.id] ? `${node.id} (${nodeAmounts[node.id]})` : node.id}\n labelTextColor={{ from: \"color\", modifiers: [[\"darker\", 1]] }}\n nodeTooltip={({ node }: { node: { id: string; value: number } }) => {\n const unit = effectiveTerminalNodeIds.has(node.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel\n return (\n <div className=\"rounded-md border border-border bg-card p-2 text-xs shadow-lg\">\n <span className=\"mb-1 block font-bold\">{node.id}{nodeAmounts?.[node.id] ? ` — ${nodeAmounts[node.id]}` : \"\"}</span>\n <span>{node.value} {unit}</span>\n </div>\n )\n }}\n linkTooltip={({ link }: { link: { source: { id: string }; target: { id: string }; value: number } }) => {\n const unit = effectiveTerminalNodeIds.has(link.target.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel\n return (\n <div className=\"rounded-md border border-border bg-card p-2 text-xs shadow-lg\">\n <span className=\"mb-1 block font-bold\">\n {link.source.id} → {link.target.id}\n </span>\n <span>{link.value} {unit}</span>\n </div>\n )\n }}\n />\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/charts/pipeline-overview.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { TrendingUp, Info, ArrowRight } from \"lucide-react\"\nimport { ResponsiveSankey } from \"@nivo/sankey\"\n\nimport { cn } from \"../lib/utils\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"../components/tooltip\"\n\nexport interface PipelineStage {\n id: string\n label: string\n count: number\n trend: string\n nextConversion: string | null\n}\n\nexport interface PipelineStageMetrics {\n medianTime: string\n avgTime: string\n dropOffs: { reason: string; count: number; pct: string }[]\n}\n\nexport interface PipelineStageTiming {\n median: string\n avg: string\n}\n\nexport interface PipelineFilterBreakdown {\n [stageId: string]: Record<string, number>\n}\n\nconst SEGMENT_PALETTE = [\n \"#0F4C3A\",\n \"#15803d\",\n \"#0ea5e9\",\n \"#8b5cf6\",\n \"#f59e0b\",\n \"#ef4444\",\n]\n\nconst DROP_OFF_NODES = [\n { id: \"Lost/Other\", nodeColor: \"#CBD5E1\" },\n { id: \"Coverage\", nodeColor: \"#F59E0B\" },\n { id: \"Unqualified\", nodeColor: \"#F59E0B\" },\n { id: \"No Contact\", nodeColor: \"#F59E0B\" },\n { id: \"Intake Drop\", nodeColor: \"#F59E0B\" },\n { id: \"No Show/Cancel\", nodeColor: \"#F59E0B\" },\n]\n\nfunction StageHoverCard({\n title,\n count,\n metrics,\n}: {\n title: string\n count: number | string\n metrics?: PipelineStageMetrics\n}) {\n return (\n <div className=\"w-[260px] overflow-hidden rounded-lg border border-border bg-card font-sans text-left shadow-xl\">\n <div className=\"border-b border-border p-3\">\n <div className=\"mb-0.5 text-xs font-medium text-muted-foreground\">{title}</div>\n <div className=\"text-2xl font-bold text-foreground\">{count}</div>\n </div>\n\n {metrics && (\n <div className=\"grid grid-cols-2 gap-2 border-b border-border bg-muted/30 px-3 py-2\">\n <div>\n <div className=\"text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70\">\n Median\n </div>\n <div className=\"text-xs font-bold text-foreground\">\n {metrics.medianTime}\n </div>\n </div>\n <div className=\"border-l border-border pl-2\">\n <div className=\"text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70\">\n Average\n </div>\n <div className=\"text-xs font-bold text-foreground\">\n {metrics.avgTime}\n </div>\n </div>\n </div>\n )}\n\n {metrics?.dropOffs && metrics.dropOffs.length > 0 && (\n <div className=\"p-2\">\n <div className=\"mb-1.5 px-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground\">\n Drop-off Reasons\n </div>\n <div className=\"space-y-0.5\">\n {metrics.dropOffs.map((drop, i) => (\n <div\n key={i}\n className=\"group flex cursor-pointer items-center justify-between rounded p-1.5 text-xs transition-colors hover:bg-muted\"\n >\n <span className=\"truncate pr-2 text-muted-foreground group-hover:text-foreground\">\n {drop.reason}\n </span>\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-[10px] text-muted-foreground/70\">{drop.pct}</span>\n <span className=\"inline-flex h-4 min-w-[20px] items-center justify-center rounded border border-border bg-muted px-1 text-[9px] font-semibold text-muted-foreground group-hover:bg-muted group-hover:text-foreground\">\n {drop.count}\n </span>\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n\n <div className=\"flex cursor-pointer items-center justify-between border-t border-border bg-muted px-3 py-2 text-xs font-medium text-blue-600 transition-colors hover:bg-muted hover:text-blue-700\">\n View in Work Queue <ArrowRight className=\"h-3 w-3\" />\n </div>\n </div>\n )\n}\n\nexport interface PipelineOverviewProps {\n title?: string\n stages: PipelineStage[]\n stageMetrics: Record<string, PipelineStageMetrics>\n stageTimings: (PipelineStageTiming | null)[]\n filterOptions?: string[]\n onFilterChange?: (filterOption: string) => void\n filterBreakdowns?: Record<string, PipelineFilterBreakdown>\n countingModes?: string[]\n countingModeTooltip?: string\n /** Main pipeline flow nodes (after the first stage) */\n flowNodes?: { id: string; nodeColor: string }[]\n /** Drop-off distribution from initial stage: { \"Lost/Other\": 56, \"Coverage\": 40, ... } */\n dropOffDistribution?: Record<string, number>\n /** Flow links after the first stage (middle of pipeline onward) */\n flowLinks?: { source: string; target: string; value: number }[]\n totalReceived?: number\n /** Dollar amounts to display on terminal node labels: { \"Retained\": \"$18.2M\" } */\n nodeAmounts?: Record<string, string>\n /** Unit noun for standard node/link tooltips (e.g. \"signals\", \"accounts\"). */\n unitLabel?: string\n /** Unit noun for terminal/drop-off node tooltips (e.g. \"opportunities\"). Defaults to unitLabel. */\n terminalUnitLabel?: string\n /** Node IDs that should use terminalUnitLabel. Defaults to keys of dropOffDistribution. */\n terminalNodeIds?: string[]\n /** Sankey chart margins, merged with defaults { top: 20, right: 120, bottom: 20, left: 140 }. */\n sankeyMargin?: { top?: number; right?: number; bottom?: number; left?: number }\n /** Gap between Sankey node bar and its outside label. */\n sankeyLabelPadding?: number\n /** When true, conversion badges use `flex`; when false, `hidden xl:flex`. */\n alwaysShowConversionBadges?: boolean\n /** Color for dynamically generated drop-off nodes. */\n dropOffNodeColor?: string\n onViewInWorkQueue?: (stageId: string) => void\n className?: string\n}\n\nexport function PipelineOverview({\n title = \"Pipeline Overview\",\n stages,\n stageMetrics,\n stageTimings,\n filterOptions = [\"Facility\", \"Source\", \"Lead Source\", \"Payer\", \"Channel\"],\n onFilterChange,\n filterBreakdowns,\n countingModes = [\"Unique Patients\", \"All Referrals\"],\n countingModeTooltip = \"Patients may be referred through multiple channels. 'All Referrals' shows total volume; 'Unique Patients' deduplicates to show distinct patient counts.\",\n flowNodes = [\n { id: \"Contacted\", nodeColor: \"#2A8F7A\" },\n { id: \"Intake Sent\", nodeColor: \"#3DB4A0\" },\n { id: \"Intake Done\", nodeColor: \"#4CC9B0\" },\n { id: \"Scheduled\", nodeColor: \"#5FCFBC\" },\n { id: \"Completed\", nodeColor: \"#79E2C9\" },\n ],\n dropOffDistribution = {\n \"Lost/Other\": 56,\n Coverage: 40,\n Unqualified: 30,\n },\n flowLinks = [\n { source: \"Contacted\", target: \"Intake Sent\", value: 660 },\n { source: \"Contacted\", target: \"No Contact\", value: 60 },\n { source: \"Intake Sent\", target: \"Intake Done\", value: 612 },\n { source: \"Intake Sent\", target: \"Intake Drop\", value: 48 },\n { source: \"Intake Done\", target: \"Scheduled\", value: 612 },\n { source: \"Scheduled\", target: \"Completed\", value: 520 },\n { source: \"Scheduled\", target: \"No Show/Cancel\", value: 92 },\n ],\n totalReceived = 847,\n nodeAmounts,\n unitLabel,\n terminalUnitLabel,\n terminalNodeIds,\n sankeyMargin,\n sankeyLabelPadding,\n alwaysShowConversionBadges = false,\n dropOffNodeColor,\n onViewInWorkQueue: _onViewInWorkQueue,\n className,\n}: PipelineOverviewProps) {\n const [selectedFilter, setSelectedFilter] = React.useState(filterOptions[0])\n const [countingMode, setCountingMode] = React.useState(countingModes[0])\n\n const effectiveUnitLabel = unitLabel ?? \"items\"\n const effectiveTerminalUnitLabel = terminalUnitLabel ?? effectiveUnitLabel\n const effectiveTerminalNodeIds = React.useMemo(\n () => new Set(terminalNodeIds ?? Object.keys(dropOffDistribution)),\n [terminalNodeIds, dropOffDistribution],\n )\n const effectiveMargin = React.useMemo(\n () => ({ top: 20, right: 120, bottom: 20, left: 140, ...sankeyMargin }),\n [sankeyMargin],\n )\n const effectiveLabelPadding = sankeyLabelPadding ?? 16\n\n const sankeyData = React.useMemo(() => {\n const breakdown =\n filterBreakdowns?.[selectedFilter]?.received ??\n ({ [stages[0]?.label ?? \"Received\"]: totalReceived } as Record<\n string,\n number\n >)\n\n const segments = Object.entries(breakdown).map(([name, value], index) => ({\n id: name,\n nodeColor: SEGMENT_PALETTE[index % SEGMENT_PALETTE.length],\n value,\n }))\n\n const dropOffKeys = Object.keys(dropOffDistribution)\n const dropOffNodes = dropOffKeys.length > 0\n ? dropOffKeys.map((reason) => ({\n id: reason,\n nodeColor: dropOffNodeColor ?? \"#F59E0B\",\n }))\n : DROP_OFF_NODES\n\n const nodes = [\n ...segments,\n ...flowNodes,\n ...dropOffNodes,\n ]\n\n const nodeIds = new Set(nodes.map((n) => n.id))\n for (const link of flowLinks) {\n for (const endpoint of [link.source, link.target]) {\n if (!nodeIds.has(endpoint)) {\n nodes.push({ id: endpoint, nodeColor: dropOffNodeColor ?? \"#F59E0B\" })\n nodeIds.add(endpoint)\n }\n }\n }\n\n const links: { source: string; target: string; value: number }[] = []\n\n const firstFlowNode = flowNodes[0]?.id ?? \"Contacted\"\n\n segments.forEach((segment) => {\n if (segment.value > 0) {\n links.push({\n source: segment.id,\n target: firstFlowNode,\n value: segment.value,\n })\n }\n })\n\n for (const [reason, count] of Object.entries(dropOffDistribution)) {\n if (count > 0) {\n links.push({ source: firstFlowNode, target: reason, value: count })\n }\n }\n\n flowLinks.forEach((link) => links.push({ ...link }))\n\n return { nodes, links }\n }, [\n selectedFilter,\n filterBreakdowns,\n stages,\n totalReceived,\n flowNodes,\n dropOffDistribution,\n dropOffNodeColor,\n flowLinks,\n ])\n\n return (\n <div\n className={cn(\n \"rounded-xl border border-border bg-card p-5 shadow-sm\",\n className,\n )}\n >\n {/* Header */}\n <div className=\"mb-6 flex flex-col gap-4\">\n <div className=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex items-center gap-4\">\n <h3 className=\"text-lg font-semibold text-foreground\">{title}</h3>\n\n {/* Counting Mode Toggle */}\n {countingModes.length > 1 && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border bg-muted p-1\">\n {countingModes.map((mode) => (\n <button\n key={mode}\n onClick={() => setCountingMode(mode)}\n className={cn(\n \"rounded-md px-3 py-1 text-xs font-medium transition-all\",\n countingMode === mode\n ? \"border border-border bg-background text-foreground shadow-sm\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {mode}\n </button>\n ))}\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Info className=\"ml-1 h-3.5 w-3.5 cursor-help text-muted-foreground/70\" />\n </TooltipTrigger>\n <TooltipContent className=\"max-w-[250px] text-xs\">\n {countingModeTooltip}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n )}\n </div>\n\n {/* Filter Tabs */}\n {filterOptions.length > 1 && (\n <div className=\"flex items-center gap-1 self-start rounded-lg bg-muted p-1 md:self-auto\">\n {filterOptions.map((option) => (\n <button\n key={option}\n onClick={() => { setSelectedFilter(option); onFilterChange?.(option); }}\n className={cn(\n \"h-7 rounded-md border-none bg-transparent px-3 text-xs font-medium shadow-none transition-all hover:bg-background\",\n selectedFilter === option &&\n \"bg-background text-foreground shadow-sm\",\n )}\n >\n {option}\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n\n {/* Stage Metrics Row */}\n <div className=\"mb-2 grid gap-4\" style={{ gridTemplateColumns: `repeat(${stages.length}, minmax(0, 1fr))` }}>\n {stages.map((stage, index) => {\n const details = stageMetrics[stage.id]\n const timing = stageTimings[index] ?? null\n\n return (\n <TooltipProvider key={stage.id} delayDuration={100}>\n <Tooltip>\n <TooltipTrigger asChild>\n <div className=\"group relative flex cursor-pointer flex-col items-center rounded-lg p-2 text-center transition-colors hover:bg-muted\">\n <div\n className=\"mb-1 w-full truncate text-xs font-medium text-muted-foreground\"\n title={stage.label}\n >\n {stage.label}\n </div>\n <div className=\"mb-1 text-2xl font-bold text-foreground\">\n {stage.count.toLocaleString()}\n </div>\n <div className=\"mb-1 flex items-center justify-center gap-1 text-xs font-medium text-emerald-600\">\n {stage.trend} <TrendingUp className=\"h-3 w-3\" />\n </div>\n\n {/* Conversion badge + timing between stages */}\n {index < stages.length - 1 && stage.nextConversion && (\n <div className={cn(\n alwaysShowConversionBadges ? \"flex\" : \"hidden xl:flex\",\n \"absolute -right-2 top-1/2 z-10 -translate-y-1/2 translate-x-1/2 flex-col items-center\",\n )}>\n <span className=\"z-10 whitespace-nowrap rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm\">\n {stage.nextConversion}\n </span>\n {timing && (\n <div className=\"mt-1 flex flex-col items-center\">\n <div className=\"h-2 w-px bg-border\" />\n <div className=\"whitespace-nowrap rounded bg-background/80 px-1 py-0.5 text-[9px] font-medium leading-3 text-muted-foreground/70 backdrop-blur-[2px]\">\n <span className=\"mr-1 font-semibold text-muted-foreground\">\n Med: {timing.median}\n </span>\n <span className=\"text-muted-foreground/70\">\n Avg: {timing.avg}\n </span>\n </div>\n </div>\n )}\n </div>\n )}\n\n {/* Connector line down to Sankey */}\n <div className=\"mx-auto mb-1 mt-2 h-4 w-px bg-border\" />\n </div>\n </TooltipTrigger>\n <TooltipContent\n className=\"border-none bg-transparent p-0 shadow-none\"\n sideOffset={8}\n >\n <StageHoverCard\n title={stage.label}\n count={stage.count}\n metrics={details}\n />\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n )\n })}\n </div>\n\n {/* Sankey Chart */}\n <div\n className=\"relative mt-4 w-full\"\n style={{ height: 400, minWidth: 0 }}\n >\n <ResponsiveSankey\n data={sankeyData}\n margin={effectiveMargin}\n align=\"justify\"\n colors={(node: { nodeColor?: string }) => node.nodeColor || \"#94a3b8\"}\n nodeOpacity={1}\n nodeHoverOthersOpacity={0.35}\n nodeThickness={18}\n nodeSpacing={16}\n nodeBorderWidth={0}\n nodeBorderRadius={3}\n linkOpacity={0.5}\n linkHoverOthersOpacity={0.1}\n linkContract={3}\n enableLinkGradient\n labelPosition=\"outside\"\n labelOrientation=\"horizontal\"\n labelPadding={effectiveLabelPadding}\n label={(node: { id: string }) => nodeAmounts?.[node.id] ? `${node.id} (${nodeAmounts[node.id]})` : node.id}\n labelTextColor={{ from: \"color\", modifiers: [[\"darker\", 1]] }}\n nodeTooltip={({ node }: { node: { id: string; value: number } }) => {\n const unit = effectiveTerminalNodeIds.has(node.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel\n return (\n <div className=\"rounded-md border border-border bg-card p-2 text-xs shadow-lg\">\n <span className=\"mb-1 block font-bold\">{node.id}{nodeAmounts?.[node.id] ? ` — ${nodeAmounts[node.id]}` : \"\"}</span>\n <span>{node.value} {unit}</span>\n </div>\n )\n }}\n linkTooltip={({ link }: { link: { source: { id: string }; target: { id: string }; value: number } }) => {\n const unit = effectiveTerminalNodeIds.has(link.target.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel\n return (\n <div className=\"rounded-md border border-border bg-card p-2 text-xs shadow-lg\">\n <span className=\"mb-1 block font-bold\">\n {link.source.id} → {link.target.id}\n </span>\n <span>{link.value} {unit}</span>\n </div>\n )\n }}\n />\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,12 +11,11 @@ interface FilterDefinition {
|
|
|
11
11
|
}
|
|
12
12
|
interface InsightsFilterBarProps {
|
|
13
13
|
filters: FilterDefinition[];
|
|
14
|
-
variant?: "default" | "compact";
|
|
15
14
|
values: Record<string, string>;
|
|
16
15
|
onChange: (filterId: string, value: string) => void;
|
|
17
16
|
onClearAll?: () => void;
|
|
18
17
|
className?: string;
|
|
19
18
|
}
|
|
20
|
-
declare function InsightsFilterBar({ filters, values, onChange, onClearAll, className,
|
|
19
|
+
declare function InsightsFilterBar({ filters, values, onChange, onClearAll, className, }: InsightsFilterBarProps): React.JSX.Element;
|
|
21
20
|
|
|
22
21
|
export { type FilterDefinition, InsightsFilterBar, type InsightsFilterBarProps };
|
|
@@ -24,8 +24,7 @@ function InsightsFilterBar({
|
|
|
24
24
|
values,
|
|
25
25
|
onChange,
|
|
26
26
|
onClearAll,
|
|
27
|
-
className
|
|
28
|
-
variant = "default"
|
|
27
|
+
className
|
|
29
28
|
}) {
|
|
30
29
|
const showClearAll = onClearAll && hasNonDefaultValue(filters, values);
|
|
31
30
|
return /* @__PURE__ */ jsxs(
|
|
@@ -33,12 +32,11 @@ function InsightsFilterBar({
|
|
|
33
32
|
{
|
|
34
33
|
"data-slot": "insights-filter-bar",
|
|
35
34
|
className: cn(
|
|
36
|
-
"flex flex-wrap items-center rounded-md border border-border bg-card shadow-sm",
|
|
37
|
-
variant === "compact" ? "gap-2 p-2" : "gap-3 p-4",
|
|
35
|
+
"flex flex-wrap items-center gap-3 rounded-md border border-border bg-card p-4 shadow-sm",
|
|
38
36
|
className
|
|
39
37
|
),
|
|
40
38
|
children: [
|
|
41
|
-
/* @__PURE__ */ jsxs("div", { className:
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
42
40
|
/* @__PURE__ */ jsx(FilterIcon, { className: "h-4 w-4 text-muted-foreground" }),
|
|
43
41
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-muted-foreground", children: "Filters:" })
|
|
44
42
|
] }),
|
|
@@ -53,10 +51,7 @@ function InsightsFilterBar({
|
|
|
53
51
|
{
|
|
54
52
|
variant: "outline",
|
|
55
53
|
size: "sm",
|
|
56
|
-
className:
|
|
57
|
-
"gap-1.5 text-xs font-normal shadow-none",
|
|
58
|
-
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
59
|
-
),
|
|
54
|
+
className: "h-8 gap-1.5 text-xs font-normal shadow-none",
|
|
60
55
|
children: [
|
|
61
56
|
IconComp ? /* @__PURE__ */ jsx(IconComp, { className: "h-3.5 w-3.5 text-muted-foreground" }) : null,
|
|
62
57
|
filter.label,
|
|
@@ -89,10 +84,7 @@ function InsightsFilterBar({
|
|
|
89
84
|
{
|
|
90
85
|
variant: "ghost",
|
|
91
86
|
size: "sm",
|
|
92
|
-
className:
|
|
93
|
-
"text-xs text-destructive hover:text-destructive",
|
|
94
|
-
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
95
|
-
),
|
|
87
|
+
className: "h-8 text-xs text-destructive hover:text-destructive",
|
|
96
88
|
onClick: onClearAll,
|
|
97
89
|
children: "Clear All"
|
|
98
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/insights-filter-bar.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarIcon, ChevronDownIcon, FilterIcon } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterDefinition {\n id: string\n label: string\n options: string[]\n defaultValue?: string\n icon?: \"calendar\" | React.ComponentType<{ className?: string }>\n}\n\nexport interface InsightsFilterBarProps {\n filters: FilterDefinition[]\n
|
|
1
|
+
{"version":3,"sources":["../../src/components/insights-filter-bar.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarIcon, ChevronDownIcon, FilterIcon } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterDefinition {\n id: string\n label: string\n options: string[]\n defaultValue?: string\n icon?: \"calendar\" | React.ComponentType<{ className?: string }>\n}\n\nexport interface InsightsFilterBarProps {\n filters: FilterDefinition[]\n values: Record<string, string>\n onChange: (filterId: string, value: string) => void\n onClearAll?: () => void\n className?: string\n}\n\nfunction hasNonDefaultValue(\n filters: FilterDefinition[],\n values: Record<string, string>\n) {\n return filters.some((filter) => {\n const defaultVal = filter.defaultValue ?? filter.options[0] ?? \"All\"\n return values[filter.id] !== undefined && values[filter.id] !== defaultVal\n })\n}\n\nfunction InsightsFilterBar({\n filters,\n values,\n onChange,\n onClearAll,\n className,\n}: InsightsFilterBarProps) {\n const showClearAll = onClearAll && hasNonDefaultValue(filters, values)\n\n return (\n <div\n data-slot=\"insights-filter-bar\"\n className={cn(\n \"flex flex-wrap items-center gap-3 rounded-md border border-border bg-card p-4 shadow-sm\",\n className\n )}\n >\n <div className=\"flex items-center gap-2\">\n <FilterIcon className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-sm font-medium text-muted-foreground\">\n Filters:\n </span>\n </div>\n\n {filters.map((filter) => {\n const current = values[filter.id] ?? filter.defaultValue ?? \"All\"\n const isCheckbox = filter.options.length > 0\n\n const IconComp =\n filter.icon === \"calendar\"\n ? CalendarIcon\n : typeof filter.icon === \"function\"\n ? filter.icon\n : null\n\n return (\n <DropdownMenu key={filter.id}>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"h-8 gap-1.5 text-xs font-normal shadow-none\"\n >\n {IconComp ? (\n <IconComp className=\"h-3.5 w-3.5 text-muted-foreground\" />\n ) : null}\n {filter.label}: {current}\n <ChevronDownIcon className=\"h-3.5 w-3.5 opacity-50\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {isCheckbox\n ? filter.options.map((option) => (\n <DropdownMenuCheckboxItem\n key={option}\n checked={current === option}\n onCheckedChange={() => onChange(filter.id, option)}\n >\n {option}\n </DropdownMenuCheckboxItem>\n ))\n : filter.options.map((option) => (\n <DropdownMenuItem\n key={option}\n onSelect={() => onChange(filter.id, option)}\n >\n {option}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n })}\n\n {showClearAll ? (\n <div className=\"ml-auto\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 text-xs text-destructive hover:text-destructive\"\n onClick={onClearAll}\n >\n Clear All\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\nexport { InsightsFilterBar }\n"],"mappings":";AA0DM,SACE,KADF;AAvDN,SAAS,cAAc,iBAAiB,kBAAkB;AAE1D,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAkBP,SAAS,mBACP,SACA,QACA;AACA,SAAO,QAAQ,KAAK,CAAC,WAAW;AAnClC;AAoCI,UAAM,cAAa,kBAAO,iBAAP,YAAuB,OAAO,QAAQ,CAAC,MAAvC,YAA4C;AAC/D,WAAO,OAAO,OAAO,EAAE,MAAM,UAAa,OAAO,OAAO,EAAE,MAAM;AAAA,EAClE,CAAC;AACH;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AACzB,QAAM,eAAe,cAAc,mBAAmB,SAAS,MAAM;AAErE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,6BAAC,SAAI,WAAU,2BACb;AAAA,8BAAC,cAAW,WAAU,iCAAgC;AAAA,UACtD,oBAAC,UAAK,WAAU,6CAA4C,sBAE5D;AAAA,WACF;AAAA,QAEC,QAAQ,IAAI,CAAC,WAAW;AAjE/B;AAkEQ,gBAAM,WAAU,kBAAO,OAAO,EAAE,MAAhB,YAAqB,OAAO,iBAA5B,YAA4C;AAC5D,gBAAM,aAAa,OAAO,QAAQ,SAAS;AAE3C,gBAAM,WACJ,OAAO,SAAS,aACZ,eACA,OAAO,OAAO,SAAS,aACrB,OAAO,OACP;AAER,iBACE,qBAAC,gBACC;AAAA,gCAAC,uBAAoB,SAAO,MAC1B;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,WAAU;AAAA,gBAET;AAAA,6BACC,oBAAC,YAAS,WAAU,qCAAoC,IACtD;AAAA,kBACH,OAAO;AAAA,kBAAM;AAAA,kBAAG;AAAA,kBACjB,oBAAC,mBAAgB,WAAU,0BAAyB;AAAA;AAAA;AAAA,YACtD,GACF;AAAA,YACA,oBAAC,uBAAoB,OAAM,SACxB,uBACG,OAAO,QAAQ,IAAI,CAAC,WAClB;AAAA,cAAC;AAAA;AAAA,gBAEC,SAAS,YAAY;AAAA,gBACrB,iBAAiB,MAAM,SAAS,OAAO,IAAI,MAAM;AAAA,gBAEhD;AAAA;AAAA,cAJI;AAAA,YAKP,CACD,IACD,OAAO,QAAQ,IAAI,CAAC,WAClB;AAAA,cAAC;AAAA;AAAA,gBAEC,UAAU,MAAM,SAAS,OAAO,IAAI,MAAM;AAAA,gBAEzC;AAAA;AAAA,cAHI;AAAA,YAIP,CACD,GACP;AAAA,eAjCiB,OAAO,EAkC1B;AAAA,QAEJ,CAAC;AAAA,QAEA,eACC,oBAAC,SAAI,WAAU,WACb;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS;AAAA,YACV;AAAA;AAAA,QAED,GACF,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -20,19 +20,6 @@ interface MetricCardProps {
|
|
|
20
20
|
showExternalLink?: boolean;
|
|
21
21
|
showInfo?: boolean;
|
|
22
22
|
}
|
|
23
|
-
interface KpiStripItem {
|
|
24
|
-
id?: string;
|
|
25
|
-
label: React.ReactNode;
|
|
26
|
-
value: React.ReactNode;
|
|
27
|
-
unit?: React.ReactNode;
|
|
28
|
-
subtitle?: React.ReactNode;
|
|
29
|
-
change?: MetricCardProps["change"];
|
|
30
|
-
}
|
|
31
|
-
interface KpiStripProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
32
|
-
items: KpiStripItem[];
|
|
33
|
-
columns?: 2 | 3 | 4;
|
|
34
|
-
}
|
|
35
|
-
declare function KpiStrip({ items, columns, className, ...props }: KpiStripProps): React.JSX.Element;
|
|
36
23
|
declare function MetricCard({ title, value, unit, subtitle, change, footerText, dataPoints, showExternalLink, showInfo, }: MetricCardProps): React.JSX.Element;
|
|
37
24
|
|
|
38
|
-
export {
|
|
25
|
+
export { MetricCard, type MetricCardProps, type MetricDataPoint };
|
|
@@ -1,91 +1,6 @@
|
|
|
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
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
33
2
|
import { ArrowUp, ArrowDown, Info, ExternalLink } from "lucide-react";
|
|
34
3
|
import { cn } from "../lib/utils.js";
|
|
35
|
-
function KpiStrip(_a) {
|
|
36
|
-
var _b = _a, { items, columns = 4, className } = _b, props = __objRest(_b, ["items", "columns", "className"]);
|
|
37
|
-
return /* @__PURE__ */ jsx(
|
|
38
|
-
"div",
|
|
39
|
-
__spreadProps(__spreadValues({
|
|
40
|
-
"data-slot": "kpi-strip",
|
|
41
|
-
className: cn(
|
|
42
|
-
"grid gap-3 rounded-xl border border-border bg-card p-3 shadow-sm",
|
|
43
|
-
columns === 2 && "sm:grid-cols-2",
|
|
44
|
-
columns === 3 && "sm:grid-cols-3",
|
|
45
|
-
columns === 4 && "sm:grid-cols-2 lg:grid-cols-4",
|
|
46
|
-
className
|
|
47
|
-
)
|
|
48
|
-
}, props), {
|
|
49
|
-
children: items.map((item, index) => {
|
|
50
|
-
var _a2, _b2;
|
|
51
|
-
const isGoodDirection = item.change ? item.change.isGood !== void 0 ? item.change.isGood : item.change.direction === "up" : false;
|
|
52
|
-
const ChangeIcon = ((_a2 = item.change) == null ? void 0 : _a2.direction) === "down" ? ArrowDown : ArrowUp;
|
|
53
|
-
return /* @__PURE__ */ jsxs(
|
|
54
|
-
"div",
|
|
55
|
-
{
|
|
56
|
-
"data-slot": "kpi-strip-item",
|
|
57
|
-
className: "min-w-0 rounded-lg bg-muted/40 px-3 py-2",
|
|
58
|
-
children: [
|
|
59
|
-
/* @__PURE__ */ jsx("div", { "data-slot": "kpi-strip-label", className: "truncate text-xs font-medium text-muted-foreground", children: item.label }),
|
|
60
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-baseline gap-1", children: [
|
|
61
|
-
/* @__PURE__ */ jsx("span", { "data-slot": "kpi-strip-value", className: "truncate text-2xl font-bold tracking-tight text-foreground", children: item.value }),
|
|
62
|
-
item.unit ? /* @__PURE__ */ jsx("span", { "data-slot": "kpi-strip-unit", className: "text-sm font-semibold text-muted-foreground", children: item.unit }) : null
|
|
63
|
-
] }),
|
|
64
|
-
item.subtitle || item.change ? /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2 text-xs", children: [
|
|
65
|
-
item.change ? /* @__PURE__ */ jsxs(
|
|
66
|
-
"span",
|
|
67
|
-
{
|
|
68
|
-
"data-slot": "kpi-strip-change",
|
|
69
|
-
className: cn(
|
|
70
|
-
"inline-flex items-center gap-0.5 font-semibold",
|
|
71
|
-
isGoodDirection ? "text-emerald-600" : "text-red-600"
|
|
72
|
-
),
|
|
73
|
-
children: [
|
|
74
|
-
/* @__PURE__ */ jsx(ChangeIcon, { className: "h-3 w-3 stroke-[3]" }),
|
|
75
|
-
item.change.value
|
|
76
|
-
]
|
|
77
|
-
}
|
|
78
|
-
) : null,
|
|
79
|
-
item.subtitle ? /* @__PURE__ */ jsx("span", { "data-slot": "kpi-strip-subtitle", className: "truncate text-muted-foreground", children: item.subtitle }) : null
|
|
80
|
-
] }) : null
|
|
81
|
-
]
|
|
82
|
-
},
|
|
83
|
-
(_b2 = item.id) != null ? _b2 : index
|
|
84
|
-
);
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
4
|
function MetricCard({
|
|
90
5
|
title,
|
|
91
6
|
value,
|
|
@@ -187,7 +102,6 @@ function MetricCard({
|
|
|
187
102
|
] });
|
|
188
103
|
}
|
|
189
104
|
export {
|
|
190
|
-
KpiStrip,
|
|
191
105
|
MetricCard
|
|
192
106
|
};
|
|
193
107
|
//# sourceMappingURL=metric-card.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/metric-card.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { ArrowUp, ArrowDown, Info, ExternalLink } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\nexport interface MetricDataPoint {\n label: string\n value: number | string\n color?: string\n}\n\nexport interface MetricCardProps {\n title: string\n value?: string | number\n unit?: string\n subtitle?: string\n change?: { \n value: string\n direction: \"up\" | \"down\" | \"neutral\"\n isGood?: boolean // if true, up is green. if false, up is red (e.g. churn).\n }\n footerText?: string\n dataPoints?: MetricDataPoint[]\n showExternalLink?: boolean\n showInfo?: boolean\n}\n\nexport interface KpiStripItem {\n id?: string\n label: React.ReactNode\n value: React.ReactNode\n unit?: React.ReactNode\n subtitle?: React.ReactNode\n change?: MetricCardProps[\"change\"]\n}\n\nexport interface KpiStripProps extends React.HTMLAttributes<HTMLDivElement> {\n items: KpiStripItem[]\n columns?: 2 | 3 | 4\n}\n\nexport function KpiStrip({ items, columns = 4, className, ...props }: KpiStripProps) {\n return (\n <div\n data-slot=\"kpi-strip\"\n className={cn(\n \"grid gap-3 rounded-xl border border-border bg-card p-3 shadow-sm\",\n columns === 2 && \"sm:grid-cols-2\",\n columns === 3 && \"sm:grid-cols-3\",\n columns === 4 && \"sm:grid-cols-2 lg:grid-cols-4\",\n className\n )}\n {...props}\n >\n {items.map((item, index) => {\n const isGoodDirection = item.change\n ? item.change.isGood !== undefined\n ? item.change.isGood\n : item.change.direction === \"up\"\n : false\n const ChangeIcon = item.change?.direction === \"down\" ? ArrowDown : ArrowUp\n\n return (\n <div\n key={item.id ?? index}\n data-slot=\"kpi-strip-item\"\n className=\"min-w-0 rounded-lg bg-muted/40 px-3 py-2\"\n >\n <div data-slot=\"kpi-strip-label\" className=\"truncate text-xs font-medium text-muted-foreground\">\n {item.label}\n </div>\n <div className=\"mt-1 flex items-baseline gap-1\">\n <span data-slot=\"kpi-strip-value\" className=\"truncate text-2xl font-bold tracking-tight text-foreground\">\n {item.value}\n </span>\n {item.unit ? (\n <span data-slot=\"kpi-strip-unit\" className=\"text-sm font-semibold text-muted-foreground\">\n {item.unit}\n </span>\n ) : null}\n </div>\n {item.subtitle || item.change ? (\n <div className=\"mt-1 flex items-center gap-2 text-xs\">\n {item.change ? (\n <span\n data-slot=\"kpi-strip-change\"\n className={cn(\n \"inline-flex items-center gap-0.5 font-semibold\",\n isGoodDirection ? \"text-emerald-600\" : \"text-red-600\"\n )}\n >\n <ChangeIcon className=\"h-3 w-3 stroke-[3]\" />\n {item.change.value}\n </span>\n ) : null}\n {item.subtitle ? (\n <span data-slot=\"kpi-strip-subtitle\" className=\"truncate text-muted-foreground\">\n {item.subtitle}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n )\n}\n\nexport function MetricCard({\n title,\n value,\n unit,\n subtitle,\n change,\n footerText,\n dataPoints,\n showExternalLink,\n showInfo = true,\n}: MetricCardProps) {\n // SVG Donut Chart logic for variants with dataPoints\n const renderDonut = () => {\n if (!dataPoints || dataPoints.length === 0 || value === undefined) return null\n\n // Simple pseudo-donut chart logic assuming specific colors from the image\n // In a real prod environment we'd use recharts/visx, but for this standalone component\n // we can draw an SVG circle with stroke-dasharray based on the data\n const size = 80\n const strokeWidth = 12\n const radius = (size - strokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n \n // Calculate total to distribute the circle\n const total = dataPoints.reduce((sum, dp) => sum + (typeof dp.value === 'number' ? dp.value : 0), 0)\n let currentOffset = 0\n\n return (\n <div className=\"relative\" style={{ width: size, height: size }}>\n <svg width={size} height={size} className=\"transform -rotate-90\">\n {dataPoints.map((dp, i) => {\n const val = typeof dp.value === 'number' ? dp.value : 0\n const percentage = val / total\n const strokeLength = percentage * circumference\n const offset = currentOffset\n currentOffset += strokeLength\n\n // Fallback colors matching the image's teal/green palette\n const colors = [\"#166534\", \"#22c55e\", \"#6ee7b7\", \"#ccfbf1\", \"#f1f5f9\"]\n const color = dp.color || colors[i % colors.length]\n\n return (\n <circle\n key={dp.label}\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeDasharray={`${Math.max(strokeLength - 2, 0)} ${circumference}`}\n strokeDashoffset={-offset}\n className=\"transition-all duration-300\"\n />\n )\n })}\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center flex-col\">\n <span className=\"text-xl font-bold text-foreground leading-none\">{value}</span>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col rounded-xl border border-border bg-card p-5 shadow-sm h-full w-full\">\n <div className={cn(\"flex justify-between items-start\", title ? \"mb-4\" : \"mb-4\")}>\n {title ? (\n <h3 className=\"font-semibold text-sm text-foreground/80\">{title}</h3>\n ) : (\n <div className=\"flex flex-col\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-3xl font-bold tracking-tight text-foreground\">{value}</span>\n {unit && <span className=\"text-2xl font-bold tracking-tight text-foreground\">{unit}</span>}\n </div>\n {subtitle && (\n <p className=\"text-sm font-medium text-muted-foreground mt-2\">{subtitle}</p>\n )}\n </div>\n )}\n <div className=\"flex items-center gap-1.5 text-muted-foreground shrink-0 mt-0.5\">\n {showExternalLink && <ExternalLink className=\"w-3.5 h-3.5 cursor-pointer hover:text-foreground transition-colors\" />}\n {showInfo && <Info className=\"w-3.5 h-3.5 cursor-pointer hover:text-foreground transition-colors\" />}\n </div>\n </div>\n\n <div className=\"flex-1 flex flex-col min-w-0\">\n {dataPoints && dataPoints.length > 0 ? (\n // Donut Chart Variant\n <div className=\"flex items-center gap-4 mt-2 mb-6\">\n <div className=\"shrink-0\">\n {renderDonut()}\n </div>\n <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n {dataPoints.slice(0, 5).map((dp, i) => {\n const colors = [\"bg-[#166534]\", \"bg-[#22c55e]\", \"bg-[#6ee7b7]\", \"bg-[#ccfbf1]\", \"bg-[#f1f5f9]\"]\n return (\n <div key={dp.label} className=\"flex items-center justify-between gap-2 text-[11px] font-medium min-w-0\">\n <div className=\"flex items-center gap-1.5 text-muted-foreground min-w-0\">\n <div className={cn(\"w-1.5 h-1.5 rounded-full shrink-0\", dp.color ? \"\" : colors[i % colors.length])} style={dp.color ? { backgroundColor: dp.color } : {}} />\n <span className=\"whitespace-nowrap\">{dp.label}</span>\n </div>\n <span className=\"text-foreground font-semibold shrink-0\">{dp.value}</span>\n </div>\n )\n })}\n </div>\n </div>\n ) : title && (\n // Standard Big Number Variant (only if title exists)\n <div className=\"mb-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold tracking-tight text-foreground\">{value}</span>\n {unit && <span className=\"text-2xl font-bold tracking-tight text-foreground\">{unit}</span>}\n </div>\n {subtitle && (\n <p className=\"text-sm font-medium text-muted-foreground mt-1\">{subtitle}</p>\n )}\n </div>\n )}\n\n {/* Footer section (Change indicator & extra text) */}\n <div className=\"mt-auto flex flex-col gap-1.5\">\n {change && (\n <div className=\"flex items-center gap-1\">\n {(() => {\n // Determine color based on isGood property\n // By default, up is green (good), down is red (bad)\n const isGoodDirection = change.isGood !== undefined \n ? change.isGood \n : change.direction === \"up\";\n \n const colorClass = isGoodDirection ? \"text-emerald-600\" : \"text-red-600\";\n const Icon = change.direction === \"down\" ? ArrowDown : ArrowUp;\n \n return (\n <span className={cn(\"text-xs font-semibold flex items-center gap-0.5\", colorClass)}>\n <Icon className=\"w-3 h-3 stroke-[3]\" />\n {change.value}\n </span>\n )\n })()}\n </div>\n )}\n {footerText && (\n <span className=\"text-[11px] text-muted-foreground font-medium\">{footerText}</span>\n )}\n </div>\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/metric-card.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { ArrowUp, ArrowDown, Info, ExternalLink } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\nexport interface MetricDataPoint {\n label: string\n value: number | string\n color?: string\n}\n\nexport interface MetricCardProps {\n title: string\n value?: string | number\n unit?: string\n subtitle?: string\n change?: { \n value: string\n direction: \"up\" | \"down\" | \"neutral\"\n isGood?: boolean // if true, up is green. if false, up is red (e.g. churn).\n }\n footerText?: string\n dataPoints?: MetricDataPoint[]\n showExternalLink?: boolean\n showInfo?: boolean\n}\n\nexport function MetricCard({\n title,\n value,\n unit,\n subtitle,\n change,\n footerText,\n dataPoints,\n showExternalLink,\n showInfo = true,\n}: MetricCardProps) {\n // SVG Donut Chart logic for variants with dataPoints\n const renderDonut = () => {\n if (!dataPoints || dataPoints.length === 0 || value === undefined) return null\n\n // Simple pseudo-donut chart logic assuming specific colors from the image\n // In a real prod environment we'd use recharts/visx, but for this standalone component\n // we can draw an SVG circle with stroke-dasharray based on the data\n const size = 80\n const strokeWidth = 12\n const radius = (size - strokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n \n // Calculate total to distribute the circle\n const total = dataPoints.reduce((sum, dp) => sum + (typeof dp.value === 'number' ? dp.value : 0), 0)\n let currentOffset = 0\n\n return (\n <div className=\"relative\" style={{ width: size, height: size }}>\n <svg width={size} height={size} className=\"transform -rotate-90\">\n {dataPoints.map((dp, i) => {\n const val = typeof dp.value === 'number' ? dp.value : 0\n const percentage = val / total\n const strokeLength = percentage * circumference\n const offset = currentOffset\n currentOffset += strokeLength\n\n // Fallback colors matching the image's teal/green palette\n const colors = [\"#166534\", \"#22c55e\", \"#6ee7b7\", \"#ccfbf1\", \"#f1f5f9\"]\n const color = dp.color || colors[i % colors.length]\n\n return (\n <circle\n key={dp.label}\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeDasharray={`${Math.max(strokeLength - 2, 0)} ${circumference}`}\n strokeDashoffset={-offset}\n className=\"transition-all duration-300\"\n />\n )\n })}\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center flex-col\">\n <span className=\"text-xl font-bold text-foreground leading-none\">{value}</span>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col rounded-xl border border-border bg-card p-5 shadow-sm h-full w-full\">\n <div className={cn(\"flex justify-between items-start\", title ? \"mb-4\" : \"mb-4\")}>\n {title ? (\n <h3 className=\"font-semibold text-sm text-foreground/80\">{title}</h3>\n ) : (\n <div className=\"flex flex-col\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-3xl font-bold tracking-tight text-foreground\">{value}</span>\n {unit && <span className=\"text-2xl font-bold tracking-tight text-foreground\">{unit}</span>}\n </div>\n {subtitle && (\n <p className=\"text-sm font-medium text-muted-foreground mt-2\">{subtitle}</p>\n )}\n </div>\n )}\n <div className=\"flex items-center gap-1.5 text-muted-foreground shrink-0 mt-0.5\">\n {showExternalLink && <ExternalLink className=\"w-3.5 h-3.5 cursor-pointer hover:text-foreground transition-colors\" />}\n {showInfo && <Info className=\"w-3.5 h-3.5 cursor-pointer hover:text-foreground transition-colors\" />}\n </div>\n </div>\n\n <div className=\"flex-1 flex flex-col min-w-0\">\n {dataPoints && dataPoints.length > 0 ? (\n // Donut Chart Variant\n <div className=\"flex items-center gap-4 mt-2 mb-6\">\n <div className=\"shrink-0\">\n {renderDonut()}\n </div>\n <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n {dataPoints.slice(0, 5).map((dp, i) => {\n const colors = [\"bg-[#166534]\", \"bg-[#22c55e]\", \"bg-[#6ee7b7]\", \"bg-[#ccfbf1]\", \"bg-[#f1f5f9]\"]\n return (\n <div key={dp.label} className=\"flex items-center justify-between gap-2 text-[11px] font-medium min-w-0\">\n <div className=\"flex items-center gap-1.5 text-muted-foreground min-w-0\">\n <div className={cn(\"w-1.5 h-1.5 rounded-full shrink-0\", dp.color ? \"\" : colors[i % colors.length])} style={dp.color ? { backgroundColor: dp.color } : {}} />\n <span className=\"whitespace-nowrap\">{dp.label}</span>\n </div>\n <span className=\"text-foreground font-semibold shrink-0\">{dp.value}</span>\n </div>\n )\n })}\n </div>\n </div>\n ) : title && (\n // Standard Big Number Variant (only if title exists)\n <div className=\"mb-6\">\n <div className=\"flex items-baseline gap-1\">\n <span className=\"text-4xl font-bold tracking-tight text-foreground\">{value}</span>\n {unit && <span className=\"text-2xl font-bold tracking-tight text-foreground\">{unit}</span>}\n </div>\n {subtitle && (\n <p className=\"text-sm font-medium text-muted-foreground mt-1\">{subtitle}</p>\n )}\n </div>\n )}\n\n {/* Footer section (Change indicator & extra text) */}\n <div className=\"mt-auto flex flex-col gap-1.5\">\n {change && (\n <div className=\"flex items-center gap-1\">\n {(() => {\n // Determine color based on isGood property\n // By default, up is green (good), down is red (bad)\n const isGoodDirection = change.isGood !== undefined \n ? change.isGood \n : change.direction === \"up\";\n \n const colorClass = isGoodDirection ? \"text-emerald-600\" : \"text-red-600\";\n const Icon = change.direction === \"down\" ? ArrowDown : ArrowUp;\n \n return (\n <span className={cn(\"text-xs font-semibold flex items-center gap-0.5\", colorClass)}>\n <Icon className=\"w-3 h-3 stroke-[3]\" />\n {change.value}\n </span>\n )\n })()}\n </div>\n )}\n {footerText && (\n <span className=\"text-[11px] text-muted-foreground font-medium\">{footerText}</span>\n )}\n </div>\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
+
type TimelineEventTone = "red" | "amber" | "emerald" | "violet" | "blue" | "slate" | "salesforce" | "gmail";
|
|
4
|
+
interface TimelineEventActor {
|
|
5
|
+
kind: "user" | "integration" | "system";
|
|
6
|
+
name?: string;
|
|
7
|
+
initials?: string;
|
|
8
|
+
avatarUrl?: string;
|
|
9
|
+
verb?: string;
|
|
10
|
+
}
|
|
3
11
|
interface TimelineEvent {
|
|
4
12
|
id: string;
|
|
5
13
|
icon: React.ReactNode;
|
|
@@ -24,11 +32,18 @@ interface TimelineEvent {
|
|
|
24
32
|
defaultExpanded?: boolean;
|
|
25
33
|
isInteractive?: boolean;
|
|
26
34
|
onSourceClick?: () => void;
|
|
35
|
+
tone?: TimelineEventTone;
|
|
36
|
+
actor?: TimelineEventActor;
|
|
37
|
+
isSystemNoise?: boolean;
|
|
27
38
|
}
|
|
39
|
+
declare const TONE_CLASSES: Record<TimelineEventTone, {
|
|
40
|
+
dot: string;
|
|
41
|
+
icon: string;
|
|
42
|
+
}>;
|
|
28
43
|
interface TimelineActivityProps {
|
|
29
44
|
events: TimelineEvent[];
|
|
30
45
|
className?: string;
|
|
31
46
|
}
|
|
32
47
|
declare function TimelineActivity({ events, className }: TimelineActivityProps): React.JSX.Element;
|
|
33
48
|
|
|
34
|
-
export { TimelineActivity, type TimelineActivityProps, type TimelineEvent };
|
|
49
|
+
export { TONE_CLASSES, TimelineActivity, type TimelineActivityProps, type TimelineEvent, type TimelineEventActor, type TimelineEventTone };
|