@handled-ai/design-system 0.18.4 → 0.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/charts/chart.d.ts +1 -1
  2. package/dist/charts/empty-chart-state.d.ts +11 -0
  3. package/dist/charts/empty-chart-state.js +70 -0
  4. package/dist/charts/empty-chart-state.js.map +1 -0
  5. package/dist/charts/index.d.ts +1 -0
  6. package/dist/charts/index.js +1 -0
  7. package/dist/charts/index.js.map +1 -1
  8. package/dist/charts/pipeline-overview.d.ts +2 -1
  9. package/dist/charts/pipeline-overview.js +32 -1
  10. package/dist/charts/pipeline-overview.js.map +1 -1
  11. package/dist/components/days-open-cell.d.ts +16 -0
  12. package/dist/components/days-open-cell.js +73 -0
  13. package/dist/components/days-open-cell.js.map +1 -0
  14. package/dist/components/detail-drawer.d.ts +16 -0
  15. package/dist/components/detail-drawer.js +45 -0
  16. package/dist/components/detail-drawer.js.map +1 -0
  17. package/dist/components/insights-filter-bar.d.ts +2 -1
  18. package/dist/components/insights-filter-bar.js +13 -5
  19. package/dist/components/insights-filter-bar.js.map +1 -1
  20. package/dist/components/linked-entity-cell.d.ts +14 -0
  21. package/dist/components/linked-entity-cell.js +96 -0
  22. package/dist/components/linked-entity-cell.js.map +1 -0
  23. package/dist/components/metric-card.d.ts +14 -1
  24. package/dist/components/metric-card.js +97 -0
  25. package/dist/components/metric-card.js.map +1 -1
  26. package/dist/components/pill.d.ts +26 -0
  27. package/dist/components/pill.js +77 -0
  28. package/dist/components/pill.js.map +1 -0
  29. package/dist/components/quick-segment.d.ts +13 -0
  30. package/dist/components/quick-segment.js +96 -0
  31. package/dist/components/quick-segment.js.map +1 -0
  32. package/dist/index.d.ts +7 -1
  33. package/dist/index.js +5 -0
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -3
  36. package/src/charts/__tests__/insights-charts.test.tsx +62 -0
  37. package/src/charts/empty-chart-state.tsx +44 -0
  38. package/src/charts/index.ts +1 -0
  39. package/src/charts/pipeline-overview.tsx +41 -1
  40. package/src/components/__tests__/insights-primitives.test.tsx +135 -0
  41. package/src/components/days-open-cell.tsx +50 -0
  42. package/src/components/detail-drawer.tsx +60 -0
  43. package/src/components/insights-filter-bar.tsx +13 -4
  44. package/src/components/linked-entity-cell.tsx +74 -0
  45. package/src/components/metric-card.tsx +98 -0
  46. package/src/components/pill.tsx +67 -0
  47. package/src/components/quick-segment.tsx +68 -0
  48. package/src/index.ts +5 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system\n * UI components and utilities (shadcn-style, New York)\n */\n\n// Utilities\nexport { cn } from \"./lib/utils\"\nexport { BRAND_ICONS, BRAND_GRAPHICS } from \"./lib/icons\"\nexport { displayName, getInitials, shortName, type ProfileLike } from \"./lib/user-display\"\n\n// Hooks\nexport { useIsMobile } from \"./hooks/use-mobile\"\n\n// Components (light — no recharts/nivo/three transitive deps)\nexport * from \"./components/activity-detail\"\nexport * from \"./components/activity-log\"\nexport * from \"./components/agent-popover\"\nexport * from \"./components/agent-widget\"\nexport * from \"./components/avatar\"\nexport * from \"./components/badge\"\nexport * from \"./components/button\"\nexport * from \"./components/card\"\nexport { CollapsibleSection, type CollapsibleSectionProps } from \"./components/collapsible-section\"\nexport * from \"./components/compliance-badge\"\nexport * from \"./components/contact-chip\"\nexport * from \"./components/contact-list\"\nexport * from \"./components/contextual-quick-action-launcher\"\nexport * from \"./components/dashboard-cards\"\nexport * from \"./components/data-table\"\nexport * from \"./components/data-table-condition-filter\"\nexport * from \"./components/data-table-display\"\nexport * from \"./components/data-table-filter\"\nexport * from \"./components/data-table-quick-views\"\nexport * from \"./components/data-table-toolbar\"\nexport * from \"./components/detail-view\"\nexport * from \"./components/dialog\"\nexport * from \"./components/dropdown-menu\"\nexport * from \"./components/empty-state\"\nexport * from \"./components/entity-panel\"\nexport { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from \"./components/feedback-primitives\"\nexport type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from \"./components/feedback-primitives\"\nexport { SignalPriorityPopover } from \"./components/signal-priority-popover\"\nexport type { SignalPriorityPopoverProps, PriorityFactor } from \"./components/signal-priority-popover\"\nexport * from \"./components/filter-chip\"\nexport * from \"./components/inbox-row\"\nexport * from \"./components/inbox-toolbar\"\nexport * from \"./components/inline-banner\"\nexport * from \"./components/input\"\nexport * from \"./components/insights-filter-bar\"\nexport * from \"./components/item-list\"\nexport * from \"./components/item-list-display\"\nexport * from \"./components/item-list-filter\"\nexport * from \"./components/item-list-toolbar\"\nexport * from \"./components/kbd-hint\"\nexport * from \"./components/label\"\nexport * from \"./components/message\"\nexport * from \"./components/metric-card\"\nexport * from \"./components/performance-metrics-table\"\nexport * from \"./components/preview-list\"\nexport * from \"./components/progress\"\nexport * from \"./components/quick-action-chat-area\"\nexport {\n QuickActionModal,\n type QuickActionPriority,\n type QuickActionTaskDraft,\n type QuickActionTemplate,\n} from \"./components/quick-action-modal\"\nexport * from \"./components/quick-action-sidebar-nav\"\nexport * from \"./components/recommended-actions-section\"\nexport * from \"./components/report-card\"\nexport * from \"./components/rich-text-toolbar\"\nexport * from \"./components/score-analysis-modal\"\nexport * from \"./components/score-breakdown\"\nexport * from \"./components/score-feedback\"\nexport * from \"./components/score-why-chips\"\nexport * from \"./components/score-ring\"\nexport * from \"./components/scroll-area\"\nexport * from \"./components/select\"\nexport * from \"./components/separator\"\nexport * from \"./components/sheet\"\nexport * from \"./components/sidebar\"\nexport * from \"./components/signal-feedback-inline\"\nexport * from \"./components/simple-data-table\"\nexport * from \"./components/skeleton\"\nexport * from \"./components/status-badge\"\nexport * from \"./components/step-timeline\"\nexport * from \"./components/sticky-action-bar\"\nexport * from \"./components/styled-bar-list\"\nexport { DraftFeedbackInline } from \"./components/draft-feedback-inline\"\nexport type { DraftFeedbackInlineProps } from \"./components/draft-feedback-inline\"\nexport { AccountContactsPopover, BrandIcon } from \"./components/account-contacts-popover\"\nexport type { AccountContactsPopoverProps } from \"./components/account-contacts-popover\"\nexport * from \"./components/suggested-actions\"\nexport * from \"./components/switch\"\nexport * from \"./components/table\"\nexport * from \"./components/tabs\"\nexport * from \"./components/textarea\"\nexport * from \"./components/timeline-activity\"\nexport * from \"./components/tooltip\"\nexport * from \"./components/user-display\"\nexport * from \"./components/variable-autocomplete\"\nexport * from \"./components/view-mode-toggle\"\nexport * from \"./components/virtualized-data-table\"\nexport type { ColumnSizingState } from \"@tanstack/react-table\"\n\n// Charts (re-exported for backward compatibility with root imports)\nexport * from \"./charts/index\"\n\n// Prototype template system (re-exported for backward compatibility)\nexport * from \"./prototype/prototype-config\"\nexport * from \"./prototype/prototype-shell\"\nexport * from \"./prototype/prototype-inbox-view\"\nexport * from \"./prototype/prototype-insights-view\"\nexport * from \"./prototype/prototype-accounts-view\"\nexport * from \"./prototype/prototype-admin-view\"\nexport * from \"./prototype/prototype-work-queue-view\"\n"],"mappings":"AAMA,SAAS,UAAU;AACnB,SAAS,aAAa,sBAAsB;AAC5C,SAAS,aAAa,aAAa,iBAAmC;AAGtE,SAAS,mBAAmB;AAG5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,0BAAwD;AACjE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,gBAAgB,mBAAmB,eAAe,iBAAiB,6BAA6B;AAEzG,SAAS,6BAA6B;AAEtC,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,2BAA2B;AAEpC,SAAS,wBAAwB,iBAAiB;AAElD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @handled-ai/design-system\n * UI components and utilities (shadcn-style, New York)\n */\n\n// Utilities\nexport { cn } from \"./lib/utils\"\nexport { BRAND_ICONS, BRAND_GRAPHICS } from \"./lib/icons\"\nexport { displayName, getInitials, shortName, type ProfileLike } from \"./lib/user-display\"\n\n// Hooks\nexport { useIsMobile } from \"./hooks/use-mobile\"\n\n// Components (light — no recharts/nivo/three transitive deps)\nexport * from \"./components/activity-detail\"\nexport * from \"./components/activity-log\"\nexport * from \"./components/agent-popover\"\nexport * from \"./components/agent-widget\"\nexport * from \"./components/avatar\"\nexport * from \"./components/badge\"\nexport * from \"./components/button\"\nexport * from \"./components/card\"\nexport { CollapsibleSection, type CollapsibleSectionProps } from \"./components/collapsible-section\"\nexport * from \"./components/compliance-badge\"\nexport * from \"./components/contact-chip\"\nexport * from \"./components/contact-list\"\nexport * from \"./components/contextual-quick-action-launcher\"\nexport * from \"./components/dashboard-cards\"\nexport * from \"./components/data-table\"\nexport * from \"./components/data-table-condition-filter\"\nexport * from \"./components/data-table-display\"\nexport * from \"./components/data-table-filter\"\nexport * from \"./components/data-table-quick-views\"\nexport * from \"./components/data-table-toolbar\"\nexport * from \"./components/detail-view\"\nexport * from \"./components/detail-drawer\"\nexport * from \"./components/dialog\"\nexport * from \"./components/dropdown-menu\"\nexport * from \"./components/empty-state\"\nexport * from \"./components/entity-panel\"\nexport { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from \"./components/feedback-primitives\"\nexport type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from \"./components/feedback-primitives\"\nexport { SignalPriorityPopover } from \"./components/signal-priority-popover\"\nexport type { SignalPriorityPopoverProps, PriorityFactor } from \"./components/signal-priority-popover\"\nexport * from \"./components/filter-chip\"\nexport * from \"./components/inbox-row\"\nexport * from \"./components/inbox-toolbar\"\nexport * from \"./components/inline-banner\"\nexport * from \"./components/input\"\nexport * from \"./components/insights-filter-bar\"\nexport * from \"./components/days-open-cell\"\nexport * from \"./components/linked-entity-cell\"\nexport * from \"./components/item-list\"\nexport * from \"./components/item-list-display\"\nexport * from \"./components/item-list-filter\"\nexport * from \"./components/item-list-toolbar\"\nexport * from \"./components/kbd-hint\"\nexport * from \"./components/label\"\nexport * from \"./components/message\"\nexport * from \"./components/metric-card\"\nexport * from \"./components/performance-metrics-table\"\nexport * from \"./components/pill\"\nexport * from \"./components/preview-list\"\nexport * from \"./components/progress\"\nexport * from \"./components/quick-action-chat-area\"\nexport * from \"./components/quick-segment\"\nexport {\n QuickActionModal,\n type QuickActionPriority,\n type QuickActionTaskDraft,\n type QuickActionTemplate,\n} from \"./components/quick-action-modal\"\nexport * from \"./components/quick-action-sidebar-nav\"\nexport * from \"./components/recommended-actions-section\"\nexport * from \"./components/report-card\"\nexport * from \"./components/rich-text-toolbar\"\nexport * from \"./components/score-analysis-modal\"\nexport * from \"./components/score-breakdown\"\nexport * from \"./components/score-feedback\"\nexport * from \"./components/score-why-chips\"\nexport * from \"./components/score-ring\"\nexport * from \"./components/scroll-area\"\nexport * from \"./components/select\"\nexport * from \"./components/separator\"\nexport * from \"./components/sheet\"\nexport * from \"./components/sidebar\"\nexport * from \"./components/signal-feedback-inline\"\nexport * from \"./components/simple-data-table\"\nexport * from \"./components/skeleton\"\nexport * from \"./components/status-badge\"\nexport * from \"./components/step-timeline\"\nexport * from \"./components/sticky-action-bar\"\nexport * from \"./components/styled-bar-list\"\nexport { DraftFeedbackInline } from \"./components/draft-feedback-inline\"\nexport type { DraftFeedbackInlineProps } from \"./components/draft-feedback-inline\"\nexport { AccountContactsPopover, BrandIcon } from \"./components/account-contacts-popover\"\nexport type { AccountContactsPopoverProps } from \"./components/account-contacts-popover\"\nexport * from \"./components/suggested-actions\"\nexport * from \"./components/switch\"\nexport * from \"./components/table\"\nexport * from \"./components/tabs\"\nexport * from \"./components/textarea\"\nexport * from \"./components/timeline-activity\"\nexport * from \"./components/tooltip\"\nexport * from \"./components/user-display\"\nexport * from \"./components/variable-autocomplete\"\nexport * from \"./components/view-mode-toggle\"\nexport * from \"./components/virtualized-data-table\"\nexport type { ColumnSizingState } from \"@tanstack/react-table\"\n\n// Charts (re-exported for backward compatibility with root imports)\nexport * from \"./charts/index\"\n\n// Prototype template system (re-exported for backward compatibility)\nexport * from \"./prototype/prototype-config\"\nexport * from \"./prototype/prototype-shell\"\nexport * from \"./prototype/prototype-inbox-view\"\nexport * from \"./prototype/prototype-insights-view\"\nexport * from \"./prototype/prototype-accounts-view\"\nexport * from \"./prototype/prototype-admin-view\"\nexport * from \"./prototype/prototype-work-queue-view\"\n"],"mappings":"AAMA,SAAS,UAAU;AACnB,SAAS,aAAa,sBAAsB;AAC5C,SAAS,aAAa,aAAa,iBAAmC;AAGtE,SAAS,mBAAmB;AAG5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,0BAAwD;AACjE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,gBAAgB,mBAAmB,eAAe,iBAAiB,6BAA6B;AAEzG,SAAS,6BAA6B;AAEtC,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,2BAA2B;AAEpC,SAAS,wBAAwB,iBAAiB;AAElD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handled-ai/design-system",
3
- "version": "0.18.4",
3
+ "version": "0.18.6",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -171,11 +171,9 @@
171
171
  "eslint": "^9.32.0",
172
172
  "eslint-config-next": "15.3.1",
173
173
  "happy-dom": "^20.9.0",
174
- "lucide-react": "^1.16.0",
175
174
  "next": "15.5.9",
176
175
  "react": "19.1.0",
177
176
  "react-dom": "19.1.0",
178
- "recharts": "^3.8.1",
179
177
  "shadcn": "^3.0.0",
180
178
  "tailwindcss": "^4.1.11",
181
179
  "three": "^0.183.1",
@@ -0,0 +1,62 @@
1
+ import React from "react"
2
+ import { describe, expect, it, vi } from "vitest"
3
+ import { render, screen, fireEvent } from "@testing-library/react"
4
+
5
+ import { EmptyChartState } from "../empty-chart-state"
6
+ import { PipelineOverview, type PipelineStage, type PipelineStageMetrics } from "../pipeline-overview"
7
+
8
+ vi.mock("@nivo/sankey", () => ({
9
+ ResponsiveSankey: ({ data }: { data: { nodes: unknown[]; links: unknown[] } }) => (
10
+ <div data-testid="mock-sankey">{data.nodes.length}:{data.links.length}</div>
11
+ ),
12
+ }))
13
+
14
+ const stages: PipelineStage[] = [
15
+ { id: "received", label: "Received", count: 100, trend: "+5%", nextConversion: "80%" },
16
+ { id: "contacted", label: "Contacted", count: 80, trend: "+3%", nextConversion: null },
17
+ ]
18
+
19
+ const metrics: Record<string, PipelineStageMetrics> = {
20
+ received: { medianTime: "1d", avgTime: "2d", dropOffs: [] },
21
+ contacted: { medianTime: "2d", avgTime: "3d", dropOffs: [] },
22
+ }
23
+
24
+ function renderPipeline(variant?: "sankey" | "compact", onFilterChange = vi.fn()) {
25
+ return render(
26
+ <PipelineOverview
27
+ variant={variant}
28
+ stages={stages}
29
+ stageMetrics={metrics}
30
+ stageTimings={[{ median: "1d", avg: "2d" }, null]}
31
+ filterOptions={["Facility", "Source"]}
32
+ onFilterChange={onFilterChange}
33
+ />
34
+ )
35
+ }
36
+
37
+ describe("Insights chart primitives", () => {
38
+ it("EmptyChartState renders defaults and action", () => {
39
+ const { container } = render(<EmptyChartState action={<button type="button">Reset</button>} />)
40
+
41
+ expect(container.querySelector('[data-slot="empty-chart-state"]')).not.toBeNull()
42
+ expect(screen.getByText("No chart data")).not.toBeNull()
43
+ expect(screen.getByRole("button", { name: "Reset" })).not.toBeNull()
44
+ })
45
+
46
+ it("PipelineOverview default variant preserves Sankey rendering and callbacks", () => {
47
+ const onFilterChange = vi.fn()
48
+ renderPipeline(undefined, onFilterChange)
49
+
50
+ expect(screen.getByTestId("mock-sankey")).not.toBeNull()
51
+ fireEvent.click(screen.getByRole("button", { name: "Source" }))
52
+ expect(onFilterChange).toHaveBeenCalledWith("Source")
53
+ })
54
+
55
+ it("PipelineOverview compact variant renders compact bars instead of Sankey", () => {
56
+ const { container } = renderPipeline("compact")
57
+
58
+ expect(screen.queryByTestId("mock-sankey")).toBeNull()
59
+ expect(container.querySelector('[data-slot="pipeline-overview-compact"]')).not.toBeNull()
60
+ expect(container.querySelectorAll('[data-slot="pipeline-overview-compact-bar"]')).toHaveLength(2)
61
+ })
62
+ })
@@ -0,0 +1,44 @@
1
+ import * as React from "react"
2
+ import { BarChart3 } from "lucide-react"
3
+
4
+ import { cn } from "../lib/utils"
5
+
6
+ export interface EmptyChartStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
7
+ title?: React.ReactNode
8
+ description?: React.ReactNode
9
+ icon?: React.ReactNode
10
+ action?: React.ReactNode
11
+ }
12
+
13
+ export function EmptyChartState({
14
+ title = "No chart data",
15
+ description = "Try adjusting filters or selecting a different time range.",
16
+ icon,
17
+ action,
18
+ className,
19
+ ...props
20
+ }: EmptyChartStateProps) {
21
+ return (
22
+ <div
23
+ data-slot="empty-chart-state"
24
+ className={cn(
25
+ "flex min-h-[240px] flex-col items-center justify-center rounded-xl border border-dashed border-border bg-muted/30 p-8 text-center",
26
+ className
27
+ )}
28
+ {...props}
29
+ >
30
+ <div data-slot="empty-chart-state-icon" className="mb-3 text-muted-foreground [&>svg]:h-10 [&>svg]:w-10">
31
+ {icon ?? <BarChart3 aria-hidden="true" />}
32
+ </div>
33
+ <div data-slot="empty-chart-state-title" className="text-sm font-semibold text-foreground">
34
+ {title}
35
+ </div>
36
+ {description ? (
37
+ <div data-slot="empty-chart-state-description" className="mt-1 max-w-sm text-sm text-muted-foreground">
38
+ {description}
39
+ </div>
40
+ ) : null}
41
+ {action ? <div data-slot="empty-chart-state-action" className="mt-4">{action}</div> : null}
42
+ </div>
43
+ )
44
+ }
@@ -11,3 +11,4 @@ export * from "./volume-analysis-chart"
11
11
  export * from "./top-line-metrics"
12
12
  export * from "./pipeline-overview"
13
13
  export * from "./sankey-chart"
14
+ export * from "./empty-chart-state"
@@ -125,6 +125,7 @@ function StageHoverCard({
125
125
 
126
126
  export interface PipelineOverviewProps {
127
127
  title?: string
128
+ variant?: "sankey" | "compact"
128
129
  stages: PipelineStage[]
129
130
  stageMetrics: Record<string, PipelineStageMetrics>
130
131
  stageTimings: (PipelineStageTiming | null)[]
@@ -162,6 +163,7 @@ export interface PipelineOverviewProps {
162
163
 
163
164
  export function PipelineOverview({
164
165
  title = "Pipeline Overview",
166
+ variant = "sankey",
165
167
  stages,
166
168
  stageMetrics,
167
169
  stageTimings,
@@ -217,6 +219,10 @@ export function PipelineOverview({
217
219
  [sankeyMargin],
218
220
  )
219
221
  const effectiveLabelPadding = sankeyLabelPadding ?? 16
222
+ const compactMaxCount = React.useMemo(
223
+ () => Math.max(...stages.map((stage) => stage.count), 1),
224
+ [stages],
225
+ )
220
226
 
221
227
  const sankeyData = React.useMemo(() => {
222
228
  const breakdown =
@@ -424,8 +430,41 @@ export function PipelineOverview({
424
430
  })}
425
431
  </div>
426
432
 
427
- {/* Sankey Chart */}
433
+ {variant === "compact" ? (
434
+ <div data-slot="pipeline-overview-compact" className="mt-4 space-y-2">
435
+ {stages.map((stage, index) => {
436
+ const width = `${Math.max((stage.count / compactMaxCount) * 100, 4)}%`
437
+
438
+ return (
439
+ <div key={stage.id} className="rounded-lg border border-border bg-background p-3">
440
+ <div className="mb-2 flex items-center justify-between gap-3">
441
+ <div className="min-w-0">
442
+ <div className="truncate text-sm font-medium text-foreground">{stage.label}</div>
443
+ <div className="text-xs text-muted-foreground">{stage.trend}</div>
444
+ </div>
445
+ <div className="text-right text-lg font-bold text-foreground">
446
+ {stage.count.toLocaleString()}
447
+ </div>
448
+ </div>
449
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
450
+ <div
451
+ data-slot="pipeline-overview-compact-bar"
452
+ className="h-full rounded-full bg-emerald-600"
453
+ style={{ width }}
454
+ />
455
+ </div>
456
+ {index < stages.length - 1 && stage.nextConversion ? (
457
+ <div className="mt-2 text-xs text-muted-foreground">
458
+ {stage.nextConversion} to {stages[index + 1]?.label}
459
+ </div>
460
+ ) : null}
461
+ </div>
462
+ )
463
+ })}
464
+ </div>
465
+ ) : (
428
466
  <div
467
+ data-slot="pipeline-overview-sankey"
429
468
  className="relative mt-4 w-full"
430
469
  style={{ height: 400, minWidth: 0 }}
431
470
  >
@@ -471,6 +510,7 @@ export function PipelineOverview({
471
510
  }}
472
511
  />
473
512
  </div>
513
+ )}
474
514
  </div>
475
515
  )
476
516
  }
@@ -0,0 +1,135 @@
1
+ import React from "react"
2
+ import { describe, expect, it, vi } from "vitest"
3
+ import { render, screen, fireEvent } from "@testing-library/react"
4
+
5
+ import { DaysOpenCell, getDaysOpenIntent } from "../days-open-cell"
6
+ import { DetailDrawer } from "../detail-drawer"
7
+ import { InsightsFilterBar } from "../insights-filter-bar"
8
+ import { LinkedEntityCell } from "../linked-entity-cell"
9
+ import { KpiStrip } from "../metric-card"
10
+ import { Pill, StatusPill } from "../pill"
11
+ import { QuickSegment } from "../quick-segment"
12
+
13
+ describe("Insights primitives", () => {
14
+ it("renders compact InsightsFilterBar without changing default API", () => {
15
+ const { container } = render(
16
+ <InsightsFilterBar
17
+ variant="compact"
18
+ filters={[{ id: "status", label: "Status", options: ["All", "Open"], defaultValue: "All" }]}
19
+ values={{ status: "Open" }}
20
+ onChange={() => {}}
21
+ onClearAll={() => {}}
22
+ />
23
+ )
24
+
25
+ const bar = container.querySelector('[data-slot="insights-filter-bar"]')!
26
+ expect(bar.className).toContain("p-2")
27
+ expect(bar.className).toContain("gap-2")
28
+ expect(screen.getByRole("button", { name: /Status: Open/i }).className).toContain("h-7")
29
+ })
30
+
31
+ it("renders KpiStrip items and changes", () => {
32
+ const { container } = render(
33
+ <KpiStrip
34
+ items={[
35
+ { label: "New", value: 42, unit: "leads", change: { value: "8%", direction: "up" } },
36
+ { label: "Aging", value: 12, subtitle: "over SLA", change: { value: "3%", direction: "down", isGood: true } },
37
+ ]}
38
+ />
39
+ )
40
+
41
+ expect(container.querySelectorAll('[data-slot="kpi-strip-item"]')).toHaveLength(2)
42
+ expect(screen.getByText("New")).not.toBeNull()
43
+ expect(screen.getByText("42")).not.toBeNull()
44
+ expect(screen.getByText("8%")).not.toBeNull()
45
+ })
46
+
47
+ it("renders neutral KpiStrip changes without an up/down icon or red/green intent", () => {
48
+ const { container } = render(
49
+ <KpiStrip
50
+ items={[
51
+ { label: "Stable", value: 10, change: { value: "0%", direction: "neutral" } },
52
+ ]}
53
+ />
54
+ )
55
+
56
+ const change = container.querySelector('[data-slot="kpi-strip-change"]')
57
+ expect(change?.className).toContain("text-muted-foreground")
58
+ expect(change?.className).not.toContain("text-red-600")
59
+ expect(change?.className).not.toContain("text-emerald-600")
60
+ expect(change?.querySelector("svg")).toBeNull()
61
+ })
62
+
63
+ it("QuickSegment exposes selected state and invokes onSelect", () => {
64
+ const onSelect = vi.fn()
65
+ render(<QuickSegment label="At risk" value="risk" count={5} selected onSelect={onSelect} />)
66
+
67
+ const button = screen.getByRole("button", { name: /At risk/i })
68
+ expect(button.getAttribute("aria-pressed")).toBe("true")
69
+ expect(screen.getByText("5")).not.toBeNull()
70
+ fireEvent.click(button)
71
+ expect(onSelect).toHaveBeenCalledWith("risk")
72
+ })
73
+
74
+ it("LinkedEntityCell renders entity links and metadata", () => {
75
+ const onNavigate = vi.fn()
76
+ render(
77
+ <LinkedEntityCell
78
+ name="Acme Health"
79
+ href="/accounts/acme"
80
+ subtitle="Account"
81
+ meta="Tier 1"
82
+ onNavigate={onNavigate}
83
+ />
84
+ )
85
+
86
+ const link = screen.getByRole("link", { name: "Acme Health" })
87
+ expect(link.getAttribute("href")).toBe("/accounts/acme")
88
+ expect(screen.getByText(/Tier 1/)).not.toBeNull()
89
+ fireEvent.click(link)
90
+ expect(onNavigate).toHaveBeenCalled()
91
+ })
92
+
93
+ it("DaysOpenCell maps thresholds to status intents", () => {
94
+ expect(getDaysOpenIntent(2, 7, 30)).toBe("success")
95
+ expect(getDaysOpenIntent(10, 7, 30)).toBe("warning")
96
+ expect(getDaysOpenIntent(45, 7, 30)).toBe("error")
97
+
98
+ render(<DaysOpenCell days={45} />)
99
+ expect(screen.getByTestId("days-open-pill").getAttribute("data-variant")).toBe("error")
100
+ })
101
+
102
+ it("Pill and StatusPill render wrapper variants", () => {
103
+ const { container } = render(
104
+ <div>
105
+ <Pill variant="info">Info</Pill>
106
+ <StatusPill status="Blocked" intent="error" />
107
+ </div>
108
+ )
109
+
110
+ expect(container.querySelector('[data-slot="pill"]')?.getAttribute("data-variant")).toBe("info")
111
+ expect(container.querySelector('[data-slot="status-pill"]')?.getAttribute("data-variant")).toBe("error")
112
+ expect(screen.getByText("Blocked")).not.toBeNull()
113
+ })
114
+
115
+ it("DetailDrawer renders title, content, and footer when open", () => {
116
+ render(
117
+ <DetailDrawer
118
+ open
119
+ onOpenChange={() => {}}
120
+ title="Referral details"
121
+ description="A drawer for insights"
122
+ footer={<button type="button">Done</button>}
123
+ >
124
+ <div>Drawer body</div>
125
+ </DetailDrawer>
126
+ )
127
+
128
+ expect(screen.getByText("Referral details")).not.toBeNull()
129
+ expect(screen.getByText("A drawer for insights")).not.toBeNull()
130
+ expect(screen.getByText("Drawer body")).not.toBeNull()
131
+ expect(screen.getByRole("button", { name: "Done" })).not.toBeNull()
132
+ expect(document.querySelector('[data-slot="detail-drawer"]')?.className).toContain("flex")
133
+ expect(document.querySelector('[data-slot="detail-drawer"]')?.className).toContain("flex-col")
134
+ })
135
+ })
@@ -0,0 +1,50 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { StatusPill, type PillStatus } from "./pill"
7
+
8
+ export interface DaysOpenCellProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ days: number | null | undefined
10
+ warningAt?: number
11
+ criticalAt?: number
12
+ emptyLabel?: string
13
+ suffix?: string
14
+ }
15
+
16
+ function getDaysOpenIntent(days: number, warningAt: number, criticalAt: number): PillStatus {
17
+ if (days >= criticalAt) return "error"
18
+ if (days >= warningAt) return "warning"
19
+ return "success"
20
+ }
21
+
22
+ export function DaysOpenCell({
23
+ days,
24
+ warningAt = 7,
25
+ criticalAt = 30,
26
+ emptyLabel = "—",
27
+ suffix = "d open",
28
+ className,
29
+ ...props
30
+ }: DaysOpenCellProps) {
31
+ if (days === null || days === undefined) {
32
+ return (
33
+ <div data-slot="days-open-cell" className={cn("text-sm text-muted-foreground", className)} {...props}>
34
+ {emptyLabel}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ const intent = getDaysOpenIntent(days, warningAt, criticalAt)
40
+
41
+ return (
42
+ <div data-slot="days-open-cell" className={cn("inline-flex items-center", className)} {...props}>
43
+ <StatusPill data-testid="days-open-pill" status={`${days} ${suffix}`} intent={intent}>
44
+ {days} {suffix}
45
+ </StatusPill>
46
+ </div>
47
+ )
48
+ }
49
+
50
+ export { getDaysOpenIntent }
@@ -0,0 +1,60 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetDescription,
10
+ SheetFooter,
11
+ SheetHeader,
12
+ SheetTitle,
13
+ } from "./sheet"
14
+
15
+ export interface DetailDrawerProps {
16
+ open: boolean
17
+ onOpenChange: (open: boolean) => void
18
+ title: React.ReactNode
19
+ description?: React.ReactNode
20
+ children: React.ReactNode
21
+ footer?: React.ReactNode
22
+ side?: "right" | "left"
23
+ className?: string
24
+ contentClassName?: string
25
+ }
26
+
27
+ export function DetailDrawer({
28
+ open,
29
+ onOpenChange,
30
+ title,
31
+ description,
32
+ children,
33
+ footer,
34
+ side = "right",
35
+ className,
36
+ contentClassName,
37
+ }: DetailDrawerProps) {
38
+ return (
39
+ <Sheet open={open} onOpenChange={onOpenChange}>
40
+ <SheetContent
41
+ data-slot="detail-drawer"
42
+ side={side}
43
+ className={cn("flex w-full flex-col gap-0 p-0 sm:max-w-xl", className)}
44
+ >
45
+ <SheetHeader data-slot="detail-drawer-header" className="border-b border-border p-5">
46
+ <SheetTitle>{title}</SheetTitle>
47
+ {description ? <SheetDescription>{description}</SheetDescription> : null}
48
+ </SheetHeader>
49
+ <div data-slot="detail-drawer-content" className={cn("flex-1 overflow-y-auto p-5", contentClassName)}>
50
+ {children}
51
+ </div>
52
+ {footer ? (
53
+ <SheetFooter data-slot="detail-drawer-footer" className="border-t border-border p-5">
54
+ {footer}
55
+ </SheetFooter>
56
+ ) : null}
57
+ </SheetContent>
58
+ </Sheet>
59
+ )
60
+ }
@@ -23,6 +23,7 @@ export interface FilterDefinition {
23
23
 
24
24
  export interface InsightsFilterBarProps {
25
25
  filters: FilterDefinition[]
26
+ variant?: "default" | "compact"
26
27
  values: Record<string, string>
27
28
  onChange: (filterId: string, value: string) => void
28
29
  onClearAll?: () => void
@@ -45,6 +46,7 @@ function InsightsFilterBar({
45
46
  onChange,
46
47
  onClearAll,
47
48
  className,
49
+ variant = "default",
48
50
  }: InsightsFilterBarProps) {
49
51
  const showClearAll = onClearAll && hasNonDefaultValue(filters, values)
50
52
 
@@ -52,11 +54,12 @@ function InsightsFilterBar({
52
54
  <div
53
55
  data-slot="insights-filter-bar"
54
56
  className={cn(
55
- "flex flex-wrap items-center gap-3 rounded-md border border-border bg-card p-4 shadow-sm",
57
+ "flex flex-wrap items-center rounded-md border border-border bg-card shadow-sm",
58
+ variant === "compact" ? "gap-2 p-2" : "gap-3 p-4",
56
59
  className
57
60
  )}
58
61
  >
59
- <div className="flex items-center gap-2">
62
+ <div className={cn("flex items-center gap-2", variant === "compact" && "sr-only")}>
60
63
  <FilterIcon className="h-4 w-4 text-muted-foreground" />
61
64
  <span className="text-sm font-medium text-muted-foreground">
62
65
  Filters:
@@ -80,7 +83,10 @@ function InsightsFilterBar({
80
83
  <Button
81
84
  variant="outline"
82
85
  size="sm"
83
- className="h-8 gap-1.5 text-xs font-normal shadow-none"
86
+ className={cn(
87
+ "gap-1.5 text-xs font-normal shadow-none",
88
+ variant === "compact" ? "h-7 px-2" : "h-8"
89
+ )}
84
90
  >
85
91
  {IconComp ? (
86
92
  <IconComp className="h-3.5 w-3.5 text-muted-foreground" />
@@ -118,7 +124,10 @@ function InsightsFilterBar({
118
124
  <Button
119
125
  variant="ghost"
120
126
  size="sm"
121
- className="h-8 text-xs text-destructive hover:text-destructive"
127
+ className={cn(
128
+ "text-xs text-destructive hover:text-destructive",
129
+ variant === "compact" ? "h-7 px-2" : "h-8"
130
+ )}
122
131
  onClick={onClearAll}
123
132
  >
124
133
  Clear All
@@ -0,0 +1,74 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ExternalLink } from "lucide-react"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ export interface LinkedEntityCellProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ name: React.ReactNode
10
+ href?: string
11
+ subtitle?: React.ReactNode
12
+ meta?: React.ReactNode
13
+ icon?: React.ReactNode
14
+ external?: boolean
15
+ onNavigate?: () => void
16
+ }
17
+
18
+ export function LinkedEntityCell({
19
+ name,
20
+ href,
21
+ subtitle,
22
+ meta,
23
+ icon,
24
+ external = false,
25
+ onNavigate,
26
+ className,
27
+ ...props
28
+ }: LinkedEntityCellProps) {
29
+ const content = (
30
+ <>
31
+ <span className="truncate">{name}</span>
32
+ {external ? <ExternalLink className="h-3 w-3 shrink-0 opacity-60" aria-hidden="true" /> : null}
33
+ </>
34
+ )
35
+
36
+ return (
37
+ <div
38
+ data-slot="linked-entity-cell"
39
+ className={cn("flex min-w-0 items-center gap-2", className)}
40
+ {...props}
41
+ >
42
+ {icon ? (
43
+ <span data-slot="linked-entity-cell-icon" className="shrink-0 text-muted-foreground">
44
+ {icon}
45
+ </span>
46
+ ) : null}
47
+ <div className="min-w-0 flex-1">
48
+ {href ? (
49
+ <a
50
+ data-slot="linked-entity-cell-link"
51
+ href={href}
52
+ target={external ? "_blank" : undefined}
53
+ rel={external ? "noreferrer" : undefined}
54
+ onClick={onNavigate}
55
+ className="inline-flex max-w-full items-center gap-1 truncate font-medium text-foreground underline-offset-4 hover:text-primary hover:underline"
56
+ >
57
+ {content}
58
+ </a>
59
+ ) : (
60
+ <span data-slot="linked-entity-cell-name" className="block truncate font-medium text-foreground">
61
+ {name}
62
+ </span>
63
+ )}
64
+ {subtitle || meta ? (
65
+ <div data-slot="linked-entity-cell-meta" className="mt-0.5 truncate text-xs text-muted-foreground">
66
+ {subtitle}
67
+ {subtitle && meta ? <span className="px-1">·</span> : null}
68
+ {meta}
69
+ </div>
70
+ ) : null}
71
+ </div>
72
+ </div>
73
+ )
74
+ }