@handled-ai/design-system 0.18.1 → 0.18.2
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/chart.d.ts +1 -1
- 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/feedback-primitives.d.ts +21 -2
- package/dist/components/feedback-primitives.js +90 -6
- package/dist/components/feedback-primitives.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/score-why-chips.d.ts +1 -1
- package/dist/components/score-why-chips.js +26 -5
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +172 -7
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/index.d.ts +3 -9
- package/dist/index.js +0 -5
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +4 -1
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DQ_VuHac.d.ts → signal-priority-popover-DWaAMhPI.d.ts} +26 -2
- package/package.json +3 -1
- package/src/charts/index.ts +0 -1
- package/src/charts/pipeline-overview.tsx +1 -38
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +546 -0
- package/src/components/feedback-primitives.tsx +148 -26
- package/src/components/insights-filter-bar.tsx +4 -13
- package/src/components/metric-card.tsx +0 -82
- package/src/components/score-why-chips.tsx +28 -2
- package/src/components/signal-priority-popover.tsx +194 -3
- package/src/index.ts +1 -6
- package/src/prototype/prototype-config.ts +11 -1
- package/src/prototype/prototype-inbox-view.tsx +3 -0
- 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/chart.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ declare function ChartTooltipContent({ active, payload, className, indicator, hi
|
|
|
48
48
|
labelClassName?: string;
|
|
49
49
|
color?: string;
|
|
50
50
|
}): React.JSX.Element | null;
|
|
51
|
-
declare const ChartLegend:
|
|
51
|
+
declare const ChartLegend: React.MemoExoticComponent<(outsideProps: RechartsPrimitive.LegendProps) => React.ReactPortal | null>;
|
|
52
52
|
type LegendPayloadItem = {
|
|
53
53
|
value?: string;
|
|
54
54
|
dataKey?: string;
|
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":";;;;;;;;;;;;;;;;;AAkEM,SACE,KADF;AAhEN,YAAY,WAAW;AACvB,SAAS,YAAY,MAAM,kBAAkB;AAC7C,SAAS,wBAAwB;AAEjC,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAyBP,MAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,iBAAiB;AAAA,EACrB,EAAE,IAAI,cAAc,WAAW,UAAU;AAAA,EACzC,EAAE,IAAI,YAAY,WAAW,UAAU;AAAA,EACvC,EAAE,IAAI,eAAe,WAAW,UAAU;AAAA,EAC1C,EAAE,IAAI,cAAc,WAAW,UAAU;AAAA,EACzC,EAAE,IAAI,eAAe,WAAW,UAAU;AAAA,EAC1C,EAAE,IAAI,kBAAkB,WAAW,UAAU;AAC/C;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,SAAI,WAAU,mGACb;AAAA,yBAAC,SAAI,WAAU,8BACb;AAAA,0BAAC,SAAI,WAAU,oDAAoD,iBAAM;AAAA,MACzE,oBAAC,SAAI,WAAU,sCAAsC,iBAAM;AAAA,OAC7D;AAAA,IAEC,WACC,qBAAC,SAAI,WAAU,uEACb;AAAA,2BAAC,SACC;AAAA,4BAAC,SAAI,WAAU,8EAA6E,oBAE5F;AAAA,QACA,oBAAC,SAAI,WAAU,qCACZ,kBAAQ,YACX;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,+BACb;AAAA,4BAAC,SAAI,WAAU,8EAA6E,qBAE5F;AAAA,QACA,oBAAC,SAAI,WAAU,qCACZ,kBAAQ,SACX;AAAA,SACF;AAAA,OACF;AAAA,KAGD,mCAAS,aAAY,QAAQ,SAAS,SAAS,KAC9C,qBAAC,SAAI,WAAU,OACb;AAAA,0BAAC,SAAI,WAAU,uFAAsF,8BAErG;AAAA,MACA,oBAAC,SAAI,WAAU,eACZ,kBAAQ,SAAS,IAAI,CAAC,MAAM,MAC3B;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UAEV;AAAA,gCAAC,UAAK,WAAU,mEACb,eAAK,QACR;AAAA,YACA,qBAAC,SAAI,WAAU,6BACb;AAAA,kCAAC,UAAK,WAAU,wCAAwC,eAAK,KAAI;AAAA,cACjE,oBAAC,UAAK,WAAU,uMACb,eAAK,OACR;AAAA,eACF;AAAA;AAAA;AAAA,QAXK;AAAA,MAYP,CACD,GACH;AAAA,OACF;AAAA,IAGF,qBAAC,SAAI,WAAU,qLAAoL;AAAA;AAAA,MAC9K,oBAAC,cAAW,WAAU,WAAU;AAAA,OACrD;AAAA,KACF;AAEJ;AAwCO,SAAS,iBAAiB;AAAA,EAC/B,QAAQ;AAAA,EACR,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB,CAAC,YAAY,UAAU,eAAe,SAAS,SAAS;AAAA,EACxE;AAAA,EACA;AAAA,EACA,gBAAgB,CAAC,mBAAmB,eAAe;AAAA,EACnD,sBAAsB;AAAA,EACtB,YAAY;AAAA,IACV,EAAE,IAAI,aAAa,WAAW,UAAU;AAAA,IACxC,EAAE,IAAI,eAAe,WAAW,UAAU;AAAA,IAC1C,EAAE,IAAI,eAAe,WAAW,UAAU;AAAA,IAC1C,EAAE,IAAI,aAAa,WAAW,UAAU;AAAA,IACxC,EAAE,IAAI,aAAa,WAAW,UAAU;AAAA,EAC1C;AAAA,EACA,sBAAsB;AAAA,IACpB,cAAc;AAAA,IACd,UAAU;AAAA,IACV,aAAa;AAAA,EACf;AAAA,EACA,YAAY;AAAA,IACV,EAAE,QAAQ,aAAa,QAAQ,eAAe,OAAO,IAAI;AAAA,IACzD,EAAE,QAAQ,aAAa,QAAQ,cAAc,OAAO,GAAG;AAAA,IACvD,EAAE,QAAQ,eAAe,QAAQ,eAAe,OAAO,IAAI;AAAA,IAC3D,EAAE,QAAQ,eAAe,QAAQ,eAAe,OAAO,GAAG;AAAA,IAC1D,EAAE,QAAQ,eAAe,QAAQ,aAAa,OAAO,IAAI;AAAA,IACzD,EAAE,QAAQ,aAAa,QAAQ,aAAa,OAAO,IAAI;AAAA,IACvD,EAAE,QAAQ,aAAa,QAAQ,kBAAkB,OAAO,GAAG;AAAA,EAC7D;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,6BAA6B;AAAA,EAC7B;AAAA,EACA,mBAAmB;AAAA,EACnB;AACF,GAA0B;AACxB,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,cAAc,CAAC,CAAC;AAC3E,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,cAAc,CAAC,CAAC;AAEvE,QAAM,qBAAqB,gCAAa;AACxC,QAAM,6BAA6B,gDAAqB;AACxD,QAAM,2BAA2B,MAAM;AAAA,IACrC,MAAM,IAAI,IAAI,4CAAmB,OAAO,KAAK,mBAAmB,CAAC;AAAA,IACjE,CAAC,iBAAiB,mBAAmB;AAAA,EACvC;AACA,QAAM,kBAAkB,MAAM;AAAA,IAC5B,MAAO,iBAAE,KAAK,IAAI,OAAO,KAAK,QAAQ,IAAI,MAAM,OAAQ;AAAA,IACxD,CAAC,YAAY;AAAA,EACf;AACA,QAAM,wBAAwB,kDAAsB;AAEpD,QAAM,aAAa,MAAM,QAAQ,MAAM;AA9NzC;AA+NI,UAAM,aACJ,gEAAmB,oBAAnB,mBAAoC,aAApC,YACC,EAAE,EAAC,kBAAO,CAAC,MAAR,mBAAW,UAAX,YAAoB,UAAU,GAAG,cAAc;AAKrD,UAAM,WAAW,OAAO,QAAQ,SAAS,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG,WAAW;AAAA,MACxE,IAAI;AAAA,MACJ,WAAW,gBAAgB,QAAQ,gBAAgB,MAAM;AAAA,MACzD;AAAA,IACF,EAAE;AAEF,UAAM,cAAc,OAAO,KAAK,mBAAmB;AACnD,UAAM,eAAe,YAAY,SAAS,IACtC,YAAY,IAAI,CAAC,YAAY;AAAA,MAC3B,IAAI;AAAA,MACJ,WAAW,8CAAoB;AAAA,IACjC,EAAE,IACF;AAEJ,UAAM,QAAQ;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAEA,UAAM,UAAU,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC9C,eAAW,QAAQ,WAAW;AAC5B,iBAAW,YAAY,CAAC,KAAK,QAAQ,KAAK,MAAM,GAAG;AACjD,YAAI,CAAC,QAAQ,IAAI,QAAQ,GAAG;AAC1B,gBAAM,KAAK,EAAE,IAAI,UAAU,WAAW,8CAAoB,UAAU,CAAC;AACrE,kBAAQ,IAAI,QAAQ;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAA6D,CAAC;AAEpE,UAAM,iBAAgB,qBAAU,CAAC,MAAX,mBAAc,OAAd,YAAoB;AAE1C,aAAS,QAAQ,CAAC,YAAY;AAC5B,UAAI,QAAQ,QAAQ,GAAG;AACrB,cAAM,KAAK;AAAA,UACT,QAAQ,QAAQ;AAAA,UAChB,QAAQ;AAAA,UACR,OAAO,QAAQ;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAED,eAAW,CAAC,QAAQ,KAAK,KAAK,OAAO,QAAQ,mBAAmB,GAAG;AACjE,UAAI,QAAQ,GAAG;AACb,cAAM,KAAK,EAAE,QAAQ,eAAe,QAAQ,QAAQ,OAAO,MAAM,CAAC;AAAA,MACpE;AAAA,IACF;AAEA,cAAU,QAAQ,CAAC,SAAS,MAAM,KAAK,mBAAK,KAAM,CAAC;AAEnD,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAGA;AAAA,4BAAC,SAAI,WAAU,4BACb,+BAAC,SAAI,WAAU,sEACb;AAAA,+BAAC,SAAI,WAAU,2BACb;AAAA,gCAAC,QAAG,WAAU,yCAAyC,iBAAM;AAAA,YAG5D,cAAc,SAAS,KACtB,qBAAC,SAAI,WAAU,wEACZ;AAAA,4BAAc,IAAI,CAAC,SAClB;AAAA,gBAAC;AAAA;AAAA,kBAEC,SAAS,MAAM,gBAAgB,IAAI;AAAA,kBACnC,WAAW;AAAA,oBACT;AAAA,oBACA,iBAAiB,OACb,iEACA;AAAA,kBACN;AAAA,kBAEC;AAAA;AAAA,gBATI;AAAA,cAUP,CACD;AAAA,cACD,oBAAC,mBACC,+BAAC,WACC;AAAA,oCAAC,kBAAe,SAAO,MACrB,8BAAC,QAAK,WAAU,yDAAwD,GAC1E;AAAA,gBACA,oBAAC,kBAAe,WAAU,yBACvB,+BACH;AAAA,iBACF,GACF;AAAA,eACF;AAAA,aAEJ;AAAA,UAGC,cAAc,SAAS,KACtB,oBAAC,SAAI,WAAU,2EACZ,wBAAc,IAAI,CAAC,WAClB;AAAA,YAAC;AAAA;AAAA,cAEC,SAAS,MAAM;AAAE,kCAAkB,MAAM;AAAG,iEAAiB;AAAA,cAAS;AAAA,cACtE,WAAW;AAAA,gBACT;AAAA,gBACA,mBAAmB,UACjB;AAAA,cACJ;AAAA,cAEC;AAAA;AAAA,YARI;AAAA,UASP,CACD,GACH;AAAA,WAEJ,GACF;AAAA,QAGA,oBAAC,SAAI,WAAU,mBAAkB,OAAO,EAAE,qBAAqB,UAAU,OAAO,MAAM,oBAAoB,GACvG,iBAAO,IAAI,CAAC,OAAO,UAAU;AAzWtC;AA0WU,gBAAM,UAAU,aAAa,MAAM,EAAE;AACrC,gBAAM,UAAS,kBAAa,KAAK,MAAlB,YAAuB;AAEtC,iBACE,oBAAC,mBAA+B,eAAe,KAC7C,+BAAC,WACC;AAAA,gCAAC,kBAAe,SAAO,MACrB,+BAAC,SAAI,WAAU,wHACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,WAAU;AAAA,kBACV,OAAO,MAAM;AAAA,kBAEZ,gBAAM;AAAA;AAAA,cACT;AAAA,cACA,oBAAC,SAAI,WAAU,2CACZ,gBAAM,MAAM,eAAe,GAC9B;AAAA,cACA,qBAAC,SAAI,WAAU,oFACZ;AAAA,sBAAM;AAAA,gBAAM;AAAA,gBAAC,oBAAC,cAAW,WAAU,WAAU;AAAA,iBAChD;AAAA,cAGC,QAAQ,OAAO,SAAS,KAAK,MAAM,kBAClC,qBAAC,SAAI,WAAW;AAAA,gBACd,6BAA6B,SAAS;AAAA,gBACtC;AAAA,cACF,GACE;AAAA,oCAAC,UAAK,WAAU,2IACb,gBAAM,gBACT;AAAA,gBACC,UACC,qBAAC,SAAI,WAAU,mCACb;AAAA,sCAAC,SAAI,WAAU,sBAAqB;AAAA,kBACpC,qBAAC,SAAI,WAAU,wIACb;AAAA,yCAAC,UAAK,WAAU,4CAA2C;AAAA;AAAA,sBACnD,OAAO;AAAA,uBACf;AAAA,oBACA,qBAAC,UAAK,WAAU,4BAA2B;AAAA;AAAA,sBACnC,OAAO;AAAA,uBACf;AAAA,qBACF;AAAA,mBACF;AAAA,iBAEJ;AAAA,cAIF,oBAAC,SAAI,WAAU,wCAAuC;AAAA,eACxD,GACF;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,YAAY;AAAA,gBAEZ;AAAA,kBAAC;AAAA;AAAA,oBACC,OAAO,MAAM;AAAA,oBACb,OAAO,MAAM;AAAA,oBACb,SAAS;AAAA;AAAA,gBACX;AAAA;AAAA,YACF;AAAA,aACF,KAxDoB,MAAM,EAyD5B;AAAA,QAEJ,CAAC,GACH;AAAA,QAEC,YAAY,YACX,oBAAC,SAAI,aAAU,6BAA4B,WAAU,kBAClD,iBAAO,IAAI,CAAC,OAAO,UAAU;AA9axC;AA+aY,gBAAM,WAAW,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,SAAS,KAAK,KAAK,GAAG,CAAC;AAChE,gBAAM,QAAQ,GAAG,KAAK,IAAK,MAAM,QAAQ,WAAY,KAAK,CAAC,CAAC;AAE5D,iBACE,qBAAC,SAAmB,WAAU,qDAC5B;AAAA,iCAAC,SAAI,WAAU,gDACb;AAAA,mCAAC,SAAI,WAAU,WACb;AAAA,oCAAC,SAAI,WAAU,gDAAgD,gBAAM,OAAM;AAAA,gBAC3E,oBAAC,SAAI,WAAU,iCAAiC,gBAAM,OAAM;AAAA,iBAC9D;AAAA,cACA,oBAAC,SAAI,WAAU,gDACZ,gBAAM,MAAM,eAAe,GAC9B;AAAA,eACF;AAAA,YACA,oBAAC,SAAI,WAAU,6CACb;AAAA,cAAC;AAAA;AAAA,gBACC,aAAU;AAAA,gBACV,WAAU;AAAA,gBACV,OAAO,EAAE,MAAM;AAAA;AAAA,YACjB,GACF;AAAA,YACC,QAAQ,OAAO,SAAS,KAAK,MAAM,iBAClC,qBAAC,SAAI,WAAU,sCACZ;AAAA,oBAAM;AAAA,cAAe;AAAA,eAAK,YAAO,QAAQ,CAAC,MAAhB,mBAAmB;AAAA,eAChD,IACE;AAAA,eArBI,MAAM,EAsBhB;AAAA,QAEJ,CAAC,GACH,IAEF;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAU;AAAA,YACV,OAAO,EAAE,QAAQ,KAAK,UAAU,EAAE;AAAA,YAElC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,QAAQ;AAAA,gBACR,OAAM;AAAA,gBACN,QAAQ,CAAC,SAAiC,KAAK,aAAa;AAAA,gBAC5D,aAAa;AAAA,gBACb,wBAAwB;AAAA,gBACxB,eAAe;AAAA,gBACf,aAAa;AAAA,gBACb,iBAAiB;AAAA,gBACjB,kBAAkB;AAAA,gBAClB,aAAa;AAAA,gBACb,wBAAwB;AAAA,gBACxB,cAAc;AAAA,gBACd,oBAAkB;AAAA,gBAClB,eAAc;AAAA,gBACd,kBAAiB;AAAA,gBACjB,cAAc;AAAA,gBACd,OAAO,CAAC,UAAyB,2CAAc,KAAK,OAAM,GAAG,KAAK,EAAE,KAAK,YAAY,KAAK,EAAE,CAAC,MAAM,KAAK;AAAA,gBACxG,gBAAgB,EAAE,MAAM,SAAS,WAAW,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE;AAAA,gBAC5D,aAAa,CAAC,EAAE,KAAK,MAA+C;AAClE,wBAAM,OAAO,yBAAyB,IAAI,KAAK,EAAE,IAAI,6BAA6B;AAClF,yBACE,qBAAC,SAAI,WAAU,iEACb;AAAA,yCAAC,UAAK,WAAU,wBAAwB;AAAA,2BAAK;AAAA,uBAAI,2CAAc,KAAK,OAAM,WAAM,YAAY,KAAK,EAAE,CAAC,KAAK;AAAA,uBAAG;AAAA,oBAC5G,qBAAC,UAAM;AAAA,2BAAK;AAAA,sBAAM;AAAA,sBAAE;AAAA,uBAAK;AAAA,qBAC3B;AAAA,gBAEJ;AAAA,gBACA,aAAa,CAAC,EAAE,KAAK,MAAmF;AACtG,wBAAM,OAAO,yBAAyB,IAAI,KAAK,OAAO,EAAE,IAAI,6BAA6B;AACzF,yBACE,qBAAC,SAAI,WAAU,iEACb;AAAA,yCAAC,UAAK,WAAU,wBACb;AAAA,2BAAK,OAAO;AAAA,sBAAG;AAAA,sBAAI,KAAK,OAAO;AAAA,uBAClC;AAAA,oBACA,qBAAC,UAAM;AAAA,2BAAK;AAAA,sBAAM;AAAA,sBAAE;AAAA,uBAAK;AAAA,qBAC3B;AAAA,gBAEJ;AAAA;AAAA,YACF;AAAA;AAAA,QACF;AAAA;AAAA;AAAA,EAEF;AAEJ;","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":[]}
|
|
@@ -16,6 +16,18 @@ interface FeedbackSubmitData {
|
|
|
16
16
|
pills: string[];
|
|
17
17
|
detail: string;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Persisted feedback data from a previous submission, used to hydrate the
|
|
21
|
+
* footer into its "already submitted" visual state.
|
|
22
|
+
*/
|
|
23
|
+
interface PersistedFeedbackData {
|
|
24
|
+
sentiment: "positive" | "negative";
|
|
25
|
+
reasonTop?: string;
|
|
26
|
+
reasonSub?: string;
|
|
27
|
+
pills?: string[];
|
|
28
|
+
detail?: string;
|
|
29
|
+
ownershipLabel: "Your feedback" | "Team feedback";
|
|
30
|
+
}
|
|
19
31
|
/**
|
|
20
32
|
* Defines a tier-1 chip that may have tier-2 sub-chips.
|
|
21
33
|
*/
|
|
@@ -60,7 +72,14 @@ interface FeedbackFooterProps {
|
|
|
60
72
|
negativeChips?: FeedbackChipTree[];
|
|
61
73
|
positiveChips?: string[];
|
|
62
74
|
className?: string;
|
|
75
|
+
/** Pre-existing feedback to hydrate from (e.g. after page reload). */
|
|
76
|
+
initialFeedback?: PersistedFeedbackData | null;
|
|
77
|
+
/** Label shown in the transient confirmation pill after submit. */
|
|
78
|
+
submittedLabel?: string;
|
|
79
|
+
/** Stable key for syncing initialFeedback into local state. When this
|
|
80
|
+
* changes, the component resets to the new initialFeedback value. */
|
|
81
|
+
feedbackKey?: string;
|
|
63
82
|
}
|
|
64
|
-
declare function FeedbackFooter({ feedback, onFeedbackChange, onSubmit, metaText, positivePrompt, negativePrompt, negativeChips, positiveChips, className, }: FeedbackFooterProps): React.JSX.Element;
|
|
83
|
+
declare function FeedbackFooter({ feedback, onFeedbackChange, onSubmit, metaText, positivePrompt, negativePrompt, negativeChips, positiveChips, className, initialFeedback, submittedLabel, feedbackKey, }: FeedbackFooterProps): React.JSX.Element;
|
|
65
84
|
|
|
66
|
-
export { FeedbackActions, type FeedbackActionsProps, FeedbackChipGroup, type FeedbackChipGroupProps, type FeedbackChipTree, FeedbackFooter, type FeedbackFooterProps, FeedbackInput, type FeedbackInputProps, type FeedbackSubmitData };
|
|
85
|
+
export { FeedbackActions, type FeedbackActionsProps, FeedbackChipGroup, type FeedbackChipGroupProps, type FeedbackChipTree, FeedbackFooter, type FeedbackFooterProps, FeedbackInput, type FeedbackInputProps, type FeedbackSubmitData, type PersistedFeedbackData };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"use client";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
|
6
|
+
import { ThumbsUp, ThumbsDown, Check, Pencil } from "lucide-react";
|
|
7
7
|
import { cn } from "../lib/utils.js";
|
|
8
8
|
const CHIP_SELECTED_CLASSES = {
|
|
9
9
|
negative: "bg-red-50 text-red-700 border-red-200",
|
|
@@ -107,7 +107,10 @@ function FeedbackFooter({
|
|
|
107
107
|
negativePrompt = "What's the issue?",
|
|
108
108
|
negativeChips = [],
|
|
109
109
|
positiveChips = [],
|
|
110
|
-
className
|
|
110
|
+
className,
|
|
111
|
+
initialFeedback,
|
|
112
|
+
submittedLabel = "Saved",
|
|
113
|
+
feedbackKey
|
|
111
114
|
}) {
|
|
112
115
|
const [expanded, setExpanded] = React.useState(false);
|
|
113
116
|
const [selectedTier1, setSelectedTier1] = React.useState(null);
|
|
@@ -117,6 +120,32 @@ function FeedbackFooter({
|
|
|
117
120
|
const [activeTreeIndex, setActiveTreeIndex] = React.useState(
|
|
118
121
|
null
|
|
119
122
|
);
|
|
123
|
+
const [submitted, setSubmitted] = React.useState(false);
|
|
124
|
+
const [persisted, setPersisted] = React.useState(
|
|
125
|
+
initialFeedback != null ? initialFeedback : null
|
|
126
|
+
);
|
|
127
|
+
const [isEditing, setIsEditing] = React.useState(false);
|
|
128
|
+
const lastKeyRef = React.useRef(feedbackKey);
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
const keyChanged = feedbackKey !== lastKeyRef.current;
|
|
131
|
+
lastKeyRef.current = feedbackKey;
|
|
132
|
+
if (keyChanged) {
|
|
133
|
+
setPersisted(initialFeedback != null ? initialFeedback : null);
|
|
134
|
+
setSubmitted(false);
|
|
135
|
+
setExpanded(false);
|
|
136
|
+
setIsEditing(false);
|
|
137
|
+
if (initialFeedback) {
|
|
138
|
+
onFeedbackChange(initialFeedback.sentiment);
|
|
139
|
+
} else {
|
|
140
|
+
onFeedbackChange(null);
|
|
141
|
+
}
|
|
142
|
+
} else if (!isEditing) {
|
|
143
|
+
setPersisted(initialFeedback != null ? initialFeedback : null);
|
|
144
|
+
if (initialFeedback) {
|
|
145
|
+
onFeedbackChange(initialFeedback.sentiment);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, [initialFeedback, feedbackKey]);
|
|
120
149
|
const resetState = React.useCallback(() => {
|
|
121
150
|
setExpanded(false);
|
|
122
151
|
setSelectedTier1(null);
|
|
@@ -124,15 +153,31 @@ function FeedbackFooter({
|
|
|
124
153
|
setAdditionalPills([]);
|
|
125
154
|
setDetailText("");
|
|
126
155
|
setActiveTreeIndex(null);
|
|
156
|
+
setIsEditing(false);
|
|
127
157
|
}, []);
|
|
128
158
|
const handleSentimentClick = React.useCallback(
|
|
129
159
|
(sentiment) => {
|
|
130
160
|
onFeedbackChange(sentiment);
|
|
131
161
|
resetState();
|
|
132
162
|
setExpanded(true);
|
|
163
|
+
setSubmitted(false);
|
|
164
|
+
setPersisted(null);
|
|
165
|
+
setIsEditing(true);
|
|
133
166
|
},
|
|
134
167
|
[onFeedbackChange, resetState]
|
|
135
168
|
);
|
|
169
|
+
const handlePersistedClick = React.useCallback(() => {
|
|
170
|
+
var _a, _b, _c, _d;
|
|
171
|
+
if (!persisted) return;
|
|
172
|
+
onFeedbackChange(persisted.sentiment);
|
|
173
|
+
setSelectedTier1((_a = persisted.reasonTop) != null ? _a : null);
|
|
174
|
+
setSelectedTier2((_b = persisted.reasonSub) != null ? _b : null);
|
|
175
|
+
setAdditionalPills((_c = persisted.pills) != null ? _c : []);
|
|
176
|
+
setDetailText((_d = persisted.detail) != null ? _d : "");
|
|
177
|
+
setExpanded(true);
|
|
178
|
+
setSubmitted(false);
|
|
179
|
+
setIsEditing(true);
|
|
180
|
+
}, [persisted, onFeedbackChange]);
|
|
136
181
|
const handleTier1Toggle = React.useCallback(
|
|
137
182
|
(chipLabel) => {
|
|
138
183
|
if (selectedTier1 === chipLabel) {
|
|
@@ -182,15 +227,21 @@ function FeedbackFooter({
|
|
|
182
227
|
pills: additionalPills,
|
|
183
228
|
detail: detailText
|
|
184
229
|
});
|
|
185
|
-
|
|
230
|
+
setSubmitted(true);
|
|
231
|
+
setExpanded(false);
|
|
232
|
+
setSelectedTier1(null);
|
|
233
|
+
setSelectedTier2(null);
|
|
234
|
+
setAdditionalPills([]);
|
|
235
|
+
setDetailText("");
|
|
236
|
+
setActiveTreeIndex(null);
|
|
237
|
+
setIsEditing(false);
|
|
186
238
|
}, [
|
|
187
239
|
feedback,
|
|
188
240
|
selectedTier1,
|
|
189
241
|
selectedTier2,
|
|
190
242
|
additionalPills,
|
|
191
243
|
detailText,
|
|
192
|
-
onSubmit
|
|
193
|
-
resetState
|
|
244
|
+
onSubmit
|
|
194
245
|
]);
|
|
195
246
|
const handleCancel = React.useCallback(() => {
|
|
196
247
|
resetState();
|
|
@@ -203,9 +254,30 @@ function FeedbackFooter({
|
|
|
203
254
|
return result;
|
|
204
255
|
}, [selectedTier1, additionalPills]);
|
|
205
256
|
const activeTree = activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null;
|
|
257
|
+
const showPersistedIndicator = persisted && !expanded && !submitted;
|
|
206
258
|
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
|
|
207
259
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
208
|
-
|
|
260
|
+
showPersistedIndicator ? (
|
|
261
|
+
/* Persisted feedback indicator — clickable to reopen editor */
|
|
262
|
+
/* @__PURE__ */ jsxs(
|
|
263
|
+
"button",
|
|
264
|
+
{
|
|
265
|
+
type: "button",
|
|
266
|
+
onClick: handlePersistedClick,
|
|
267
|
+
className: "group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors",
|
|
268
|
+
"data-testid": "persisted-feedback-indicator",
|
|
269
|
+
children: [
|
|
270
|
+
/* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
|
|
271
|
+
persisted.ownershipLabel,
|
|
272
|
+
":"
|
|
273
|
+
] }),
|
|
274
|
+
persisted.sentiment === "positive" ? /* @__PURE__ */ jsx(ThumbsUp, { className: "h-[11px] w-[11px]" }) : /* @__PURE__ */ jsx(ThumbsDown, { className: "h-[11px] w-[11px]" }),
|
|
275
|
+
persisted.detail && /* @__PURE__ */ jsx("span", { className: "max-w-[200px] truncate text-muted-foreground/70", children: persisted.detail }),
|
|
276
|
+
/* @__PURE__ */ jsx(Pencil, { className: "h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity" })
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
209
281
|
/* @__PURE__ */ jsxs(
|
|
210
282
|
"button",
|
|
211
283
|
{
|
|
@@ -235,6 +307,18 @@ function FeedbackFooter({
|
|
|
235
307
|
"Not helpful"
|
|
236
308
|
]
|
|
237
309
|
}
|
|
310
|
+
),
|
|
311
|
+
submitted && feedback && /* @__PURE__ */ jsxs(
|
|
312
|
+
"span",
|
|
313
|
+
{
|
|
314
|
+
className: "inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600",
|
|
315
|
+
role: "status",
|
|
316
|
+
"data-testid": "feedback-submitted-pill",
|
|
317
|
+
children: [
|
|
318
|
+
/* @__PURE__ */ jsx(Check, { className: "h-[11px] w-[11px]" }),
|
|
319
|
+
submittedLabel
|
|
320
|
+
]
|
|
321
|
+
}
|
|
238
322
|
)
|
|
239
323
|
] }),
|
|
240
324
|
metaText && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: metaText })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/feedback-primitives.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ThumbsUp, ThumbsDown } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Structured feedback data shape.\n *\n * Preserves the tree structure for DB persistence:\n * reasonTop -> tier-1 chip label (maps to case_feedback.reason_top)\n * reasonSub -> tier-2 sub-chip label (maps to case_feedback.reason_sub)\n * pills -> any additional selected chips (maps to case_feedback.pills)\n * detail -> free-text input (maps to case_feedback.free_text)\n */\nexport interface FeedbackSubmitData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills: string[]\n detail: string\n}\n\n/**\n * Defines a tier-1 chip that may have tier-2 sub-chips.\n */\nexport interface FeedbackChipTree {\n label: string\n subPrompt?: string\n subChips?: string[]\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackChipGroup\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackChipGroupProps {\n chips: string[]\n selected: string[]\n onToggle: (chip: string) => void\n flavor: \"positive\" | \"negative\"\n className?: string\n}\n\nconst CHIP_SELECTED_CLASSES: Record<\"positive\" | \"negative\", string> = {\n negative: \"bg-red-50 text-red-700 border-red-200\",\n positive: \"bg-muted text-foreground border-border\",\n}\n\nconst CHIP_IDLE_CLASS =\n \"bg-background text-muted-foreground border-border hover:bg-muted/50\"\n\nexport function FeedbackChipGroup({\n chips,\n selected,\n onToggle,\n flavor,\n className,\n}: FeedbackChipGroupProps) {\n return (\n <div className={cn(\"flex flex-wrap gap-1.5\", className)}>\n {chips.map((chip) => {\n const isSelected = selected.includes(chip)\n return (\n <button\n key={chip}\n type=\"button\"\n onClick={() => onToggle(chip)}\n className={cn(\n \"rounded-md px-2.5 py-1 text-[11px] font-medium border transition-colors\",\n isSelected ? CHIP_SELECTED_CLASSES[flavor] : CHIP_IDLE_CLASS,\n )}\n >\n {chip}\n </button>\n )\n })}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackInput\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackInputProps {\n placeholder?: string\n value: string\n onChange: (value: string) => void\n onSubmit?: () => void\n className?: string\n}\n\nexport function FeedbackInput({\n placeholder,\n value,\n onChange,\n onSubmit,\n className,\n}: FeedbackInputProps) {\n return (\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && onSubmit) {\n e.preventDefault()\n onSubmit()\n }\n }}\n placeholder={placeholder}\n className={cn(\n \"w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring\",\n className,\n )}\n />\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackActions\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackActionsProps {\n onSubmit: () => void\n onCancel: () => void\n submitDisabled?: boolean\n submitLabel?: string\n cancelLabel?: string\n hint?: string\n className?: string\n}\n\nexport function FeedbackActions({\n onSubmit,\n onCancel,\n submitDisabled = false,\n submitLabel = \"Submit\",\n cancelLabel = \"Cancel\",\n hint,\n className,\n}: FeedbackActionsProps) {\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <button\n type=\"button\"\n onClick={onSubmit}\n disabled={submitDisabled}\n className=\"bg-foreground text-background rounded-md px-3 py-1.5 text-xs font-semibold disabled:opacity-50\"\n >\n {submitLabel}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"border border-border rounded-md px-3 py-1.5 text-xs font-medium\"\n >\n {cancelLabel}\n </button>\n {hint && (\n <span className=\"ml-auto text-[11px] text-muted-foreground\">\n {hint}\n </span>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackFooter\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackFooterProps {\n feedback: \"positive\" | \"negative\" | null\n onFeedbackChange: (value: \"positive\" | \"negative\" | null) => void\n onSubmit: (data: FeedbackSubmitData) => void\n metaText?: string\n positivePrompt?: string\n negativePrompt?: string\n negativeChips?: FeedbackChipTree[]\n positiveChips?: string[]\n className?: string\n}\n\nconst SENTIMENT_BUTTON_ACTIVE: Record<\"positive\" | \"negative\", string> = {\n negative: \"text-red-600 bg-red-50 border-red-200\",\n positive: \"text-foreground bg-muted border-border\",\n}\n\nconst SENTIMENT_BUTTON_IDLE =\n \"text-muted-foreground hover:text-foreground\"\n\nexport function FeedbackFooter({\n feedback,\n onFeedbackChange,\n onSubmit,\n metaText,\n positivePrompt = \"Thanks! Anything to keep about this score?\",\n negativePrompt = \"What's the issue?\",\n negativeChips = [],\n positiveChips = [],\n className,\n}: FeedbackFooterProps) {\n const [expanded, setExpanded] = React.useState(false)\n const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)\n const [selectedTier2, setSelectedTier2] = React.useState<string | null>(null)\n const [additionalPills, setAdditionalPills] = React.useState<string[]>([])\n const [detailText, setDetailText] = React.useState(\"\")\n const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(\n null,\n )\n\n // Reset state when feedback collapses\n const resetState = React.useCallback(() => {\n setExpanded(false)\n setSelectedTier1(null)\n setSelectedTier2(null)\n setAdditionalPills([])\n setDetailText(\"\")\n setActiveTreeIndex(null)\n }, [])\n\n const handleSentimentClick = React.useCallback(\n (sentiment: \"positive\" | \"negative\") => {\n onFeedbackChange(sentiment)\n // Reset chip state when switching sentiment, then expand\n resetState()\n setExpanded(true)\n },\n [onFeedbackChange, resetState],\n )\n\n const handleTier1Toggle = React.useCallback(\n (chipLabel: string) => {\n if (selectedTier1 === chipLabel) {\n // Deselect the tier-1 chip\n setSelectedTier1(null)\n setSelectedTier2(null)\n setActiveTreeIndex(null)\n } else if (selectedTier1 === null) {\n // First selection becomes the primary reasonTop\n setSelectedTier1(chipLabel)\n setSelectedTier2(null)\n // Find the chip's tree index to show sub-chips\n const idx = negativeChips.findIndex((c) => c.label === chipLabel)\n if (idx !== -1 && negativeChips[idx].subChips) {\n setActiveTreeIndex(idx)\n } else {\n setActiveTreeIndex(null)\n }\n } else {\n // Additional selections become pills\n setAdditionalPills((prev) =>\n prev.includes(chipLabel)\n ? prev.filter((p) => p !== chipLabel)\n : [...prev, chipLabel],\n )\n }\n },\n [selectedTier1, negativeChips],\n )\n\n const handleTier2Toggle = React.useCallback((subChip: string) => {\n setSelectedTier2((prev) => (prev === subChip ? null : subChip))\n }, [])\n\n const handlePositiveChipToggle = React.useCallback(\n (chip: string) => {\n if (selectedTier1 === chip) {\n setSelectedTier1(null)\n } else if (selectedTier1 === null) {\n setSelectedTier1(chip)\n } else {\n setAdditionalPills((prev) =>\n prev.includes(chip)\n ? prev.filter((p) => p !== chip)\n : [...prev, chip],\n )\n }\n },\n [selectedTier1],\n )\n\n const handleSubmit = React.useCallback(() => {\n if (!feedback) return\n onSubmit({\n sentiment: feedback,\n reasonTop: selectedTier1 ?? undefined,\n reasonSub: selectedTier2 ?? undefined,\n pills: additionalPills,\n detail: detailText,\n })\n resetState()\n }, [\n feedback,\n selectedTier1,\n selectedTier2,\n additionalPills,\n detailText,\n onSubmit,\n resetState,\n ])\n\n const handleCancel = React.useCallback(() => {\n resetState()\n onFeedbackChange(null)\n }, [resetState, onFeedbackChange])\n\n // Determine which chips are selected (combining tier1 + additionalPills)\n const allSelectedChips = React.useMemo(() => {\n const result: string[] = []\n if (selectedTier1) result.push(selectedTier1)\n result.push(...additionalPills)\n return result\n }, [selectedTier1, additionalPills])\n\n // Active tier-1 chip tree (for showing sub-chips)\n const activeTree =\n activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Sentiment buttons + meta text bar */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"positive\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"positive\"\n ? SENTIMENT_BUTTON_ACTIVE.positive\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n Helpful\n </button>\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"negative\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"negative\"\n ? SENTIMENT_BUTTON_ACTIVE.negative\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n Not helpful\n </button>\n </div>\n {metaText && (\n <span className=\"text-[11px] text-muted-foreground\">{metaText}</span>\n )}\n </div>\n\n {/* Expanded feedback area */}\n {expanded && feedback && (\n <div className=\"space-y-3\">\n {/* Prompt text */}\n <p className=\"text-xs text-muted-foreground\">\n {feedback === \"negative\" ? negativePrompt : positivePrompt}\n </p>\n\n {/* Chip area */}\n {feedback === \"negative\" && negativeChips.length > 0 && (\n <div className=\"space-y-2\">\n {/* Tier-1 chips */}\n <FeedbackChipGroup\n chips={negativeChips.map((c) => c.label)}\n selected={allSelectedChips}\n onToggle={handleTier1Toggle}\n flavor=\"negative\"\n />\n\n {/* Tier-2 sub-chips (shown when a tier-1 with sub-chips is active) */}\n {activeTree && activeTree.subChips && (\n <div className=\"pl-3 space-y-1.5\">\n {activeTree.subPrompt && (\n <p className=\"text-[11px] text-muted-foreground\">\n {activeTree.subPrompt}\n </p>\n )}\n <FeedbackChipGroup\n chips={activeTree.subChips}\n selected={selectedTier2 ? [selectedTier2] : []}\n onToggle={handleTier2Toggle}\n flavor=\"negative\"\n />\n </div>\n )}\n </div>\n )}\n\n {feedback === \"positive\" && positiveChips.length > 0 && (\n <FeedbackChipGroup\n chips={positiveChips}\n selected={allSelectedChips}\n onToggle={handlePositiveChipToggle}\n flavor=\"positive\"\n />\n )}\n\n {/* Detail text input */}\n <FeedbackInput\n placeholder=\"Add optional detail…\"\n value={detailText}\n onChange={setDetailText}\n onSubmit={handleSubmit}\n />\n\n {/* Action buttons */}\n <FeedbackActions onSubmit={handleSubmit} onCancel={handleCancel} />\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/feedback-primitives.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ThumbsUp, ThumbsDown, Check, Pencil } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Structured feedback data shape.\n *\n * Preserves the tree structure for DB persistence:\n * reasonTop -> tier-1 chip label (maps to case_feedback.reason_top)\n * reasonSub -> tier-2 sub-chip label (maps to case_feedback.reason_sub)\n * pills -> any additional selected chips (maps to case_feedback.pills)\n * detail -> free-text input (maps to case_feedback.free_text)\n */\nexport interface FeedbackSubmitData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills: string[]\n detail: string\n}\n\n/**\n * Persisted feedback data from a previous submission, used to hydrate the\n * footer into its \"already submitted\" visual state.\n */\nexport interface PersistedFeedbackData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills?: string[]\n detail?: string\n ownershipLabel: \"Your feedback\" | \"Team feedback\"\n}\n\n/**\n * Defines a tier-1 chip that may have tier-2 sub-chips.\n */\nexport interface FeedbackChipTree {\n label: string\n subPrompt?: string\n subChips?: string[]\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackChipGroup\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackChipGroupProps {\n chips: string[]\n selected: string[]\n onToggle: (chip: string) => void\n flavor: \"positive\" | \"negative\"\n className?: string\n}\n\nconst CHIP_SELECTED_CLASSES: Record<\"positive\" | \"negative\", string> = {\n negative: \"bg-red-50 text-red-700 border-red-200\",\n positive: \"bg-muted text-foreground border-border\",\n}\n\nconst CHIP_IDLE_CLASS =\n \"bg-background text-muted-foreground border-border hover:bg-muted/50\"\n\nexport function FeedbackChipGroup({\n chips,\n selected,\n onToggle,\n flavor,\n className,\n}: FeedbackChipGroupProps) {\n return (\n <div className={cn(\"flex flex-wrap gap-1.5\", className)}>\n {chips.map((chip) => {\n const isSelected = selected.includes(chip)\n return (\n <button\n key={chip}\n type=\"button\"\n onClick={() => onToggle(chip)}\n className={cn(\n \"rounded-md px-2.5 py-1 text-[11px] font-medium border transition-colors\",\n isSelected ? CHIP_SELECTED_CLASSES[flavor] : CHIP_IDLE_CLASS,\n )}\n >\n {chip}\n </button>\n )\n })}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackInput\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackInputProps {\n placeholder?: string\n value: string\n onChange: (value: string) => void\n onSubmit?: () => void\n className?: string\n}\n\nexport function FeedbackInput({\n placeholder,\n value,\n onChange,\n onSubmit,\n className,\n}: FeedbackInputProps) {\n return (\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && onSubmit) {\n e.preventDefault()\n onSubmit()\n }\n }}\n placeholder={placeholder}\n className={cn(\n \"w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring\",\n className,\n )}\n />\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackActions\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackActionsProps {\n onSubmit: () => void\n onCancel: () => void\n submitDisabled?: boolean\n submitLabel?: string\n cancelLabel?: string\n hint?: string\n className?: string\n}\n\nexport function FeedbackActions({\n onSubmit,\n onCancel,\n submitDisabled = false,\n submitLabel = \"Submit\",\n cancelLabel = \"Cancel\",\n hint,\n className,\n}: FeedbackActionsProps) {\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <button\n type=\"button\"\n onClick={onSubmit}\n disabled={submitDisabled}\n className=\"bg-foreground text-background rounded-md px-3 py-1.5 text-xs font-semibold disabled:opacity-50\"\n >\n {submitLabel}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"border border-border rounded-md px-3 py-1.5 text-xs font-medium\"\n >\n {cancelLabel}\n </button>\n {hint && (\n <span className=\"ml-auto text-[11px] text-muted-foreground\">\n {hint}\n </span>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackFooter\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackFooterProps {\n feedback: \"positive\" | \"negative\" | null\n onFeedbackChange: (value: \"positive\" | \"negative\" | null) => void\n onSubmit: (data: FeedbackSubmitData) => void\n metaText?: string\n positivePrompt?: string\n negativePrompt?: string\n negativeChips?: FeedbackChipTree[]\n positiveChips?: string[]\n className?: string\n /** Pre-existing feedback to hydrate from (e.g. after page reload). */\n initialFeedback?: PersistedFeedbackData | null\n /** Label shown in the transient confirmation pill after submit. */\n submittedLabel?: string\n /** Stable key for syncing initialFeedback into local state. When this\n * changes, the component resets to the new initialFeedback value. */\n feedbackKey?: string\n}\n\nconst SENTIMENT_BUTTON_ACTIVE: Record<\"positive\" | \"negative\", string> = {\n negative: \"text-red-600 bg-red-50 border-red-200\",\n positive: \"text-foreground bg-muted border-border\",\n}\n\nconst SENTIMENT_BUTTON_IDLE =\n \"text-muted-foreground hover:text-foreground\"\n\nexport function FeedbackFooter({\n feedback,\n onFeedbackChange,\n onSubmit,\n metaText,\n positivePrompt = \"Thanks! Anything to keep about this score?\",\n negativePrompt = \"What's the issue?\",\n negativeChips = [],\n positiveChips = [],\n className,\n initialFeedback,\n submittedLabel = \"Saved\",\n feedbackKey,\n}: FeedbackFooterProps) {\n const [expanded, setExpanded] = React.useState(false)\n const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)\n const [selectedTier2, setSelectedTier2] = React.useState<string | null>(null)\n const [additionalPills, setAdditionalPills] = React.useState<string[]>([])\n const [detailText, setDetailText] = React.useState(\"\")\n const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(\n null,\n )\n /** Transient \"Saved\" confirmation — shown after successful submit. */\n const [submitted, setSubmitted] = React.useState(false)\n /** Persisted feedback shown as a clickable indicator (survives reload). */\n const [persisted, setPersisted] = React.useState<PersistedFeedbackData | null>(\n initialFeedback ?? null,\n )\n /** Tracks whether the user is actively editing (to guard against prop overwrites). */\n const [isEditing, setIsEditing] = React.useState(false)\n /** Track the last synced feedbackKey to detect key changes. */\n const lastKeyRef = React.useRef<string | undefined>(feedbackKey)\n\n // Sync initialFeedback into local state via useEffect keyed on feedbackKey.\n // When feedbackKey changes, reset to new target. Preserve active edits\n // when feedbackKey stays the same.\n React.useEffect(() => {\n const keyChanged = feedbackKey !== lastKeyRef.current\n lastKeyRef.current = feedbackKey\n\n if (keyChanged) {\n // Key changed — full reset to new target\n setPersisted(initialFeedback ?? null)\n setSubmitted(false)\n setExpanded(false)\n setIsEditing(false)\n if (initialFeedback) {\n onFeedbackChange(initialFeedback.sentiment)\n } else {\n onFeedbackChange(null)\n }\n } else if (!isEditing) {\n // Same key, not actively editing — safe to sync\n setPersisted(initialFeedback ?? null)\n if (initialFeedback) {\n onFeedbackChange(initialFeedback.sentiment)\n }\n }\n }, [initialFeedback, feedbackKey]) // eslint-disable-line react-hooks/exhaustive-deps -- reads isEditing as guard, not trigger\n\n // Reset state when feedback collapses\n const resetState = React.useCallback(() => {\n setExpanded(false)\n setSelectedTier1(null)\n setSelectedTier2(null)\n setAdditionalPills([])\n setDetailText(\"\")\n setActiveTreeIndex(null)\n setIsEditing(false)\n }, [])\n\n const handleSentimentClick = React.useCallback(\n (sentiment: \"positive\" | \"negative\") => {\n onFeedbackChange(sentiment)\n // Reset chip state when switching sentiment, then expand\n resetState()\n setExpanded(true)\n setSubmitted(false)\n setPersisted(null)\n setIsEditing(true)\n },\n [onFeedbackChange, resetState],\n )\n\n /** Open the persisted indicator for editing. */\n const handlePersistedClick = React.useCallback(() => {\n if (!persisted) return\n onFeedbackChange(persisted.sentiment)\n setSelectedTier1(persisted.reasonTop ?? null)\n setSelectedTier2(persisted.reasonSub ?? null)\n setAdditionalPills(persisted.pills ?? [])\n setDetailText(persisted.detail ?? \"\")\n setExpanded(true)\n setSubmitted(false)\n setIsEditing(true)\n }, [persisted, onFeedbackChange])\n\n const handleTier1Toggle = React.useCallback(\n (chipLabel: string) => {\n if (selectedTier1 === chipLabel) {\n // Deselect the tier-1 chip\n setSelectedTier1(null)\n setSelectedTier2(null)\n setActiveTreeIndex(null)\n } else if (selectedTier1 === null) {\n // First selection becomes the primary reasonTop\n setSelectedTier1(chipLabel)\n setSelectedTier2(null)\n // Find the chip's tree index to show sub-chips\n const idx = negativeChips.findIndex((c) => c.label === chipLabel)\n if (idx !== -1 && negativeChips[idx].subChips) {\n setActiveTreeIndex(idx)\n } else {\n setActiveTreeIndex(null)\n }\n } else {\n // Additional selections become pills\n setAdditionalPills((prev) =>\n prev.includes(chipLabel)\n ? prev.filter((p) => p !== chipLabel)\n : [...prev, chipLabel],\n )\n }\n },\n [selectedTier1, negativeChips],\n )\n\n const handleTier2Toggle = React.useCallback((subChip: string) => {\n setSelectedTier2((prev) => (prev === subChip ? null : subChip))\n }, [])\n\n const handlePositiveChipToggle = React.useCallback(\n (chip: string) => {\n if (selectedTier1 === chip) {\n setSelectedTier1(null)\n } else if (selectedTier1 === null) {\n setSelectedTier1(chip)\n } else {\n setAdditionalPills((prev) =>\n prev.includes(chip)\n ? prev.filter((p) => p !== chip)\n : [...prev, chip],\n )\n }\n },\n [selectedTier1],\n )\n\n const handleSubmit = React.useCallback(() => {\n if (!feedback) return\n onSubmit({\n sentiment: feedback,\n reasonTop: selectedTier1 ?? undefined,\n reasonSub: selectedTier2 ?? undefined,\n pills: additionalPills,\n detail: detailText,\n })\n // Show transient \"Saved\" confirmation\n setSubmitted(true)\n // Collapse expansion but keep sentiment visible\n setExpanded(false)\n setSelectedTier1(null)\n setSelectedTier2(null)\n setAdditionalPills([])\n setDetailText(\"\")\n setActiveTreeIndex(null)\n setIsEditing(false)\n }, [\n feedback,\n selectedTier1,\n selectedTier2,\n additionalPills,\n detailText,\n onSubmit,\n ])\n\n const handleCancel = React.useCallback(() => {\n resetState()\n onFeedbackChange(null)\n }, [resetState, onFeedbackChange])\n\n // Determine which chips are selected (combining tier1 + additionalPills)\n const allSelectedChips = React.useMemo(() => {\n const result: string[] = []\n if (selectedTier1) result.push(selectedTier1)\n result.push(...additionalPills)\n return result\n }, [selectedTier1, additionalPills])\n\n // Active tier-1 chip tree (for showing sub-chips)\n const activeTree =\n activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null\n\n // Determine if we should show the persisted indicator instead of bare buttons\n const showPersistedIndicator = persisted && !expanded && !submitted\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Sentiment buttons + meta text bar */}\n <div className=\"flex items-center justify-between\">\n {showPersistedIndicator ? (\n /* Persisted feedback indicator — clickable to reopen editor */\n <button\n type=\"button\"\n onClick={handlePersistedClick}\n className=\"group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors\"\n data-testid=\"persisted-feedback-indicator\"\n >\n <span className=\"font-medium\">{persisted.ownershipLabel}:</span>\n {persisted.sentiment === \"positive\" ? (\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n ) : (\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n )}\n {persisted.detail && (\n <span className=\"max-w-[200px] truncate text-muted-foreground/70\">\n {persisted.detail}\n </span>\n )}\n <Pencil className=\"h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity\" />\n </button>\n ) : (\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"positive\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"positive\"\n ? SENTIMENT_BUTTON_ACTIVE.positive\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n Helpful\n </button>\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"negative\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"negative\"\n ? SENTIMENT_BUTTON_ACTIVE.negative\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n Not helpful\n </button>\n {/* Transient \"Saved\" confirmation pill */}\n {submitted && feedback && (\n <span\n className=\"inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600\"\n role=\"status\"\n data-testid=\"feedback-submitted-pill\"\n >\n <Check className=\"h-[11px] w-[11px]\" />\n {submittedLabel}\n </span>\n )}\n </div>\n )}\n {metaText && (\n <span className=\"text-[11px] text-muted-foreground\">{metaText}</span>\n )}\n </div>\n\n {/* Expanded feedback area */}\n {expanded && feedback && (\n <div className=\"space-y-3\">\n {/* Prompt text */}\n <p className=\"text-xs text-muted-foreground\">\n {feedback === \"negative\" ? negativePrompt : positivePrompt}\n </p>\n\n {/* Chip area */}\n {feedback === \"negative\" && negativeChips.length > 0 && (\n <div className=\"space-y-2\">\n {/* Tier-1 chips */}\n <FeedbackChipGroup\n chips={negativeChips.map((c) => c.label)}\n selected={allSelectedChips}\n onToggle={handleTier1Toggle}\n flavor=\"negative\"\n />\n\n {/* Tier-2 sub-chips (shown when a tier-1 with sub-chips is active) */}\n {activeTree && activeTree.subChips && (\n <div className=\"pl-3 space-y-1.5\">\n {activeTree.subPrompt && (\n <p className=\"text-[11px] text-muted-foreground\">\n {activeTree.subPrompt}\n </p>\n )}\n <FeedbackChipGroup\n chips={activeTree.subChips}\n selected={selectedTier2 ? [selectedTier2] : []}\n onToggle={handleTier2Toggle}\n flavor=\"negative\"\n />\n </div>\n )}\n </div>\n )}\n\n {feedback === \"positive\" && positiveChips.length > 0 && (\n <FeedbackChipGroup\n chips={positiveChips}\n selected={allSelectedChips}\n onToggle={handlePositiveChipToggle}\n flavor=\"positive\"\n />\n )}\n\n {/* Detail text input */}\n <FeedbackInput\n placeholder=\"Add optional detail…\"\n value={detailText}\n onChange={setDetailText}\n onSubmit={handleSubmit}\n />\n\n {/* Action buttons */}\n <FeedbackActions onSubmit={handleSubmit} onCancel={handleCancel} />\n </div>\n )}\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
|
}
|