@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.
- package/dist/charts/chart.d.ts +1 -1
- package/dist/charts/empty-chart-state.d.ts +11 -0
- package/dist/charts/empty-chart-state.js +70 -0
- package/dist/charts/empty-chart-state.js.map +1 -0
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +2 -1
- package/dist/charts/pipeline-overview.js +32 -1
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/days-open-cell.d.ts +16 -0
- package/dist/components/days-open-cell.js +73 -0
- package/dist/components/days-open-cell.js.map +1 -0
- package/dist/components/detail-drawer.d.ts +16 -0
- package/dist/components/detail-drawer.js +45 -0
- package/dist/components/detail-drawer.js.map +1 -0
- package/dist/components/insights-filter-bar.d.ts +2 -1
- package/dist/components/insights-filter-bar.js +13 -5
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +14 -0
- package/dist/components/linked-entity-cell.js +96 -0
- package/dist/components/linked-entity-cell.js.map +1 -0
- package/dist/components/metric-card.d.ts +14 -1
- package/dist/components/metric-card.js +97 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/pill.d.ts +26 -0
- package/dist/components/pill.js +77 -0
- package/dist/components/pill.js.map +1 -0
- package/dist/components/quick-segment.d.ts +13 -0
- package/dist/components/quick-segment.js +96 -0
- package/dist/components/quick-segment.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -3
- package/src/charts/__tests__/insights-charts.test.tsx +62 -0
- package/src/charts/empty-chart-state.tsx +44 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/pipeline-overview.tsx +41 -1
- package/src/components/__tests__/insights-primitives.test.tsx +135 -0
- package/src/components/days-open-cell.tsx +50 -0
- package/src/components/detail-drawer.tsx +60 -0
- package/src/components/insights-filter-bar.tsx +13 -4
- package/src/components/linked-entity-cell.tsx +74 -0
- package/src/components/metric-card.tsx +98 -0
- package/src/components/pill.tsx +67 -0
- package/src/components/quick-segment.tsx +68 -0
- package/src/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.
|
|
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
|
+
}
|
package/src/charts/index.ts
CHANGED
|
@@ -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
|
-
{
|
|
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
|
|
57
|
+
"flex flex-wrap items-center rounded-md border border-border bg-card shadow-sm",
|
|
58
|
+
variant === "compact" ? "gap-2 p-2" : "gap-3 p-4",
|
|
56
59
|
className
|
|
57
60
|
)}
|
|
58
61
|
>
|
|
59
|
-
<div className="flex items-center gap-2">
|
|
62
|
+
<div className={cn("flex items-center gap-2", variant === "compact" && "sr-only")}>
|
|
60
63
|
<FilterIcon className="h-4 w-4 text-muted-foreground" />
|
|
61
64
|
<span className="text-sm font-medium text-muted-foreground">
|
|
62
65
|
Filters:
|
|
@@ -80,7 +83,10 @@ function InsightsFilterBar({
|
|
|
80
83
|
<Button
|
|
81
84
|
variant="outline"
|
|
82
85
|
size="sm"
|
|
83
|
-
className=
|
|
86
|
+
className={cn(
|
|
87
|
+
"gap-1.5 text-xs font-normal shadow-none",
|
|
88
|
+
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
89
|
+
)}
|
|
84
90
|
>
|
|
85
91
|
{IconComp ? (
|
|
86
92
|
<IconComp className="h-3.5 w-3.5 text-muted-foreground" />
|
|
@@ -118,7 +124,10 @@ function InsightsFilterBar({
|
|
|
118
124
|
<Button
|
|
119
125
|
variant="ghost"
|
|
120
126
|
size="sm"
|
|
121
|
-
className=
|
|
127
|
+
className={cn(
|
|
128
|
+
"text-xs text-destructive hover:text-destructive",
|
|
129
|
+
variant === "compact" ? "h-7 px-2" : "h-8"
|
|
130
|
+
)}
|
|
122
131
|
onClick={onClearAll}
|
|
123
132
|
>
|
|
124
133
|
Clear All
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ExternalLink } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
export interface LinkedEntityCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
name: React.ReactNode
|
|
10
|
+
href?: string
|
|
11
|
+
subtitle?: React.ReactNode
|
|
12
|
+
meta?: React.ReactNode
|
|
13
|
+
icon?: React.ReactNode
|
|
14
|
+
external?: boolean
|
|
15
|
+
onNavigate?: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function LinkedEntityCell({
|
|
19
|
+
name,
|
|
20
|
+
href,
|
|
21
|
+
subtitle,
|
|
22
|
+
meta,
|
|
23
|
+
icon,
|
|
24
|
+
external = false,
|
|
25
|
+
onNavigate,
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: LinkedEntityCellProps) {
|
|
29
|
+
const content = (
|
|
30
|
+
<>
|
|
31
|
+
<span className="truncate">{name}</span>
|
|
32
|
+
{external ? <ExternalLink className="h-3 w-3 shrink-0 opacity-60" aria-hidden="true" /> : null}
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="linked-entity-cell"
|
|
39
|
+
className={cn("flex min-w-0 items-center gap-2", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{icon ? (
|
|
43
|
+
<span data-slot="linked-entity-cell-icon" className="shrink-0 text-muted-foreground">
|
|
44
|
+
{icon}
|
|
45
|
+
</span>
|
|
46
|
+
) : null}
|
|
47
|
+
<div className="min-w-0 flex-1">
|
|
48
|
+
{href ? (
|
|
49
|
+
<a
|
|
50
|
+
data-slot="linked-entity-cell-link"
|
|
51
|
+
href={href}
|
|
52
|
+
target={external ? "_blank" : undefined}
|
|
53
|
+
rel={external ? "noreferrer" : undefined}
|
|
54
|
+
onClick={onNavigate}
|
|
55
|
+
className="inline-flex max-w-full items-center gap-1 truncate font-medium text-foreground underline-offset-4 hover:text-primary hover:underline"
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</a>
|
|
59
|
+
) : (
|
|
60
|
+
<span data-slot="linked-entity-cell-name" className="block truncate font-medium text-foreground">
|
|
61
|
+
{name}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
{subtitle || meta ? (
|
|
65
|
+
<div data-slot="linked-entity-cell-meta" className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
66
|
+
{subtitle}
|
|
67
|
+
{subtitle && meta ? <span className="px-1">·</span> : null}
|
|
68
|
+
{meta}
|
|
69
|
+
</div>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|