ai-design-system 0.1.70 → 0.1.71
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/components/blocks/EvalSessionDetailsPanel/EvalSessionDetailsPanel.tsx +29 -4
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +2 -2
- package/components/blocks/WorkflowCanvas/layout-engine.ts +23 -5
- package/components/composites/AppHeader/AppHeader.stories.tsx +30 -0
- package/components/composites/AppHeader/AppHeader.tsx +4 -4
- package/components/composites/AppHeader/interfaces.ts +2 -2
- package/components/composites/{WorkflowSwitcher/WorkflowSwitcher.stories.tsx → ButtonSwitcher/ButtonSwitcher.stories.tsx} +9 -11
- package/components/composites/{WorkflowSwitcher/WorkflowSwitcher.tsx → ButtonSwitcher/ButtonSwitcher.tsx} +16 -15
- package/components/composites/ButtonSwitcher/index.ts +1 -0
- package/components/composites/DashboardChart/DashboardChart.tsx +108 -66
- package/components/composites/DefaultSwitcher/DefaultSwitcher.stories.tsx +46 -0
- package/components/composites/{ThemeSelector/ThemeSelector.tsx → DefaultSwitcher/DefaultSwitcher.tsx} +8 -8
- package/components/composites/DefaultSwitcher/index.ts +2 -0
- package/components/composites/IconButton/IconButton.stories.tsx +32 -0
- package/components/composites/IconButton/IconButton.tsx +20 -0
- package/components/composites/IconButton/index.ts +2 -0
- package/components/composites/index.ts +10 -6
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.mocks.ts +1 -0
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.tsx +22 -9
- package/components/features/EvalDashboardFeature/useEvalDashboardFeature.d.ts +9 -0
- package/components/index.ts +2 -2
- package/dist/index.cjs +147 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +3 -0
- package/dist/index.d.ts +87 -66
- package/dist/index.js +147 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/components/composites/ThemeSelector/ThemeSelector.stories.tsx +0 -48
- package/components/composites/ThemeSelector/index.ts +0 -2
- package/components/composites/WorkflowSwitcher/index.ts +0 -1
|
@@ -11,6 +11,7 @@ export interface EvalSessionDetails {
|
|
|
11
11
|
id: string
|
|
12
12
|
date: string
|
|
13
13
|
goldenEvals: DashboardRow[]
|
|
14
|
+
recommendations: Array<{ id: number; name: string; rationale: string }>
|
|
14
15
|
systemPrompt?: string
|
|
15
16
|
outputTranscript?: string
|
|
16
17
|
}
|
|
@@ -23,12 +24,23 @@ export interface EvalSessionDetailsPanelProps {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export const EvalTriggerButton = ({ onClick, loading }: { onClick: () => void; loading?: boolean }) => (
|
|
26
|
-
<Button onClick={onClick} variant="default" size="sm" className="h-8" disabled={loading}>
|
|
27
|
-
<Icon name={loading ? 'loader-2' : 'play'} className={`w-4 h-4
|
|
28
|
-
{loading ? 'Evaluating...' : 'Trigger'}
|
|
27
|
+
<Button onClick={onClick} variant="default" size="sm" className="h-8 w-8 p-0" disabled={loading} title={loading ? 'Evaluating...' : 'Trigger'}>
|
|
28
|
+
<Icon name={loading ? 'loader-2' : 'play'} className={`w-4 h-4${loading ? ' animate-spin' : ''}`} />
|
|
29
29
|
</Button>
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
const recommendationTableSchema = dynamicTableSchema.parse({
|
|
33
|
+
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
34
|
+
rowKey: "name",
|
|
35
|
+
enableFiltering: true,
|
|
36
|
+
enablePagination: false,
|
|
37
|
+
enableRowSelection: false,
|
|
38
|
+
columns: [
|
|
39
|
+
{ key: "name", label: "Recommendation", renderType: "text" },
|
|
40
|
+
{ key: "rationale", label: "Rationale", renderType: "text" },
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
32
44
|
const goldenEvalTableSchema = dynamicTableSchema.parse({
|
|
33
45
|
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
34
46
|
rowKey: "id",
|
|
@@ -44,7 +56,7 @@ const goldenEvalTableSchema = dynamicTableSchema.parse({
|
|
|
44
56
|
})
|
|
45
57
|
|
|
46
58
|
export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
47
|
-
({ sessionDetails, activeTab, actionHandlers, workflowContent }) => {
|
|
59
|
+
({ sessionDetails, activeTab, actionHandlers: _actionHandlers, workflowContent }) => {
|
|
48
60
|
return (
|
|
49
61
|
<div className="flex flex-col h-full bg-background">
|
|
50
62
|
<Tabs value={activeTab} className="flex-1 flex flex-col min-h-0">
|
|
@@ -62,6 +74,19 @@ export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
|
62
74
|
</div>
|
|
63
75
|
</TabsContent>
|
|
64
76
|
|
|
77
|
+
<TabsContent value="recommendations" className="absolute inset-0 m-0 p-0 overflow-hidden flex flex-col">
|
|
78
|
+
<div className="p-4 flex-1 min-h-0 flex flex-col">
|
|
79
|
+
{sessionDetails.recommendations && sessionDetails.recommendations.length > 0 ? (
|
|
80
|
+
<EnhancedDataTable
|
|
81
|
+
data={sessionDetails.recommendations}
|
|
82
|
+
tableSchema={recommendationTableSchema}
|
|
83
|
+
/>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="text-sm text-muted-foreground flex-1 flex items-center justify-center">No recommendations available.</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</TabsContent>
|
|
89
|
+
|
|
65
90
|
<TabsContent value="prompts" className="absolute inset-0 m-0 p-0">
|
|
66
91
|
<ScrollArea className="h-full">
|
|
67
92
|
<div className="p-4">
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ConnectionMode,
|
|
5
5
|
ReactFlowProvider,
|
|
6
6
|
type Connection,
|
|
7
|
+
type Node,
|
|
7
8
|
useReactFlow,
|
|
8
9
|
} from "@xyflow/react";
|
|
9
10
|
import { useCallback, useEffect } from "react";
|
|
@@ -95,8 +96,7 @@ function WorkflowCanvasInner({
|
|
|
95
96
|
onEdgesChange={onEdgesChange}
|
|
96
97
|
onNodesChange={onNodesChange}
|
|
97
98
|
onPaneClick={onPaneClick}
|
|
98
|
-
|
|
99
|
-
onNodeClick={onNodeClick as any}
|
|
99
|
+
onNodeClick={onNodeClick as (event: React.MouseEvent, node: Node) => void}
|
|
100
100
|
onEdgeClick={onEdgeClick}
|
|
101
101
|
>
|
|
102
102
|
{topLeft && (
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import ELK from 'elkjs';
|
|
2
2
|
import type { WorkflowNode, WorkflowEdge } from './interfaces';
|
|
3
3
|
|
|
4
|
+
interface ElkNode {
|
|
5
|
+
id: string;
|
|
6
|
+
x?: number;
|
|
7
|
+
y?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
children?: ElkNode[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ElkGraph {
|
|
14
|
+
id: string;
|
|
15
|
+
layoutOptions?: Record<string, string>;
|
|
16
|
+
children?: ElkNode[];
|
|
17
|
+
edges?: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
sources: string[];
|
|
20
|
+
targets: string[];
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
const NODE_WIDTH = 180;
|
|
5
25
|
const NODE_HEIGHT = 52;
|
|
6
26
|
|
|
@@ -10,7 +30,7 @@ export async function getLayoutedElements(
|
|
|
10
30
|
nodes: WorkflowNode[],
|
|
11
31
|
edges: WorkflowEdge[],
|
|
12
32
|
): Promise<{ nodes: WorkflowNode[]; edges: WorkflowEdge[] }> {
|
|
13
|
-
const graph = {
|
|
33
|
+
const graph: ElkGraph = {
|
|
14
34
|
id: 'root',
|
|
15
35
|
layoutOptions: {
|
|
16
36
|
'elk.algorithm': 'layered',
|
|
@@ -36,12 +56,10 @@ export async function getLayoutedElements(
|
|
|
36
56
|
}),
|
|
37
57
|
};
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
const layout = await elk.layout(graph as any);
|
|
59
|
+
const layout = await elk.layout(graph);
|
|
41
60
|
|
|
42
61
|
const layoutedNodes: WorkflowNode[] = nodes.map((n) => {
|
|
43
|
-
|
|
44
|
-
const elkNode = (layout.children ?? []).find((c: any) => c.id === n.id);
|
|
62
|
+
const elkNode = (layout.children ?? []).find((c: ElkNode) => c.id === n.id);
|
|
45
63
|
if (!elkNode || elkNode.x == null || elkNode.y == null) return n;
|
|
46
64
|
return {
|
|
47
65
|
...n,
|
|
@@ -144,3 +144,33 @@ export const ManyTabs: Story = {
|
|
|
144
144
|
</SidebarProvider>
|
|
145
145
|
),
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* With Button Switcher
|
|
150
|
+
*
|
|
151
|
+
* Header with a button switcher (dropdown) instead of a title.
|
|
152
|
+
*/
|
|
153
|
+
export const WithButtonSwitcher: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<SidebarProvider>
|
|
156
|
+
<Sidebar />
|
|
157
|
+
<SidebarInset>
|
|
158
|
+
<AppHeader
|
|
159
|
+
buttonSwitcherProps={{
|
|
160
|
+
items: [
|
|
161
|
+
{ id: "1", name: "Alpha Workflow" },
|
|
162
|
+
{ id: "2", name: "Beta Workflow" },
|
|
163
|
+
],
|
|
164
|
+
activeId: "1",
|
|
165
|
+
onSelect: fn(),
|
|
166
|
+
}}
|
|
167
|
+
actions={
|
|
168
|
+
<Button size="sm">
|
|
169
|
+
Deploy
|
|
170
|
+
</Button>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
</SidebarInset>
|
|
174
|
+
</SidebarProvider>
|
|
175
|
+
),
|
|
176
|
+
}
|
|
@@ -3,7 +3,7 @@ import { SidebarTrigger } from "@/components/primitives/Sidebar"
|
|
|
3
3
|
import { Separator } from "@/components/primitives/Separator"
|
|
4
4
|
import { Tabs, TabsList, TabsTrigger } from "@/components/primitives/Tabs"
|
|
5
5
|
import { ChatToggleButton } from "@/components/composites/ChatToggleButton"
|
|
6
|
-
import {
|
|
6
|
+
import { ButtonSwitcher } from "@/components/composites/ButtonSwitcher"
|
|
7
7
|
import type { AppHeaderProps } from "./interfaces"
|
|
8
8
|
|
|
9
9
|
export const AppHeader = React.memo<AppHeaderProps>(({
|
|
@@ -16,7 +16,7 @@ export const AppHeader = React.memo<AppHeaderProps>(({
|
|
|
16
16
|
tabsPosition = 'center',
|
|
17
17
|
showSidebarToggle = true,
|
|
18
18
|
showTitle = true,
|
|
19
|
-
|
|
19
|
+
buttonSwitcherProps,
|
|
20
20
|
chatToggleProps
|
|
21
21
|
}) => {
|
|
22
22
|
return (
|
|
@@ -24,7 +24,7 @@ export const AppHeader = React.memo<AppHeaderProps>(({
|
|
|
24
24
|
<div className="grid w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-4 lg:px-6">
|
|
25
25
|
<div className="min-w-0 flex items-center gap-1 lg:gap-2">
|
|
26
26
|
{showSidebarToggle && <SidebarTrigger className="-ml-1" />}
|
|
27
|
-
{showSidebarToggle && (showTitle && title ||
|
|
27
|
+
{showSidebarToggle && (showTitle && title || buttonSwitcherProps || chatToggleProps) && (
|
|
28
28
|
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
|
29
29
|
)}
|
|
30
30
|
{showTitle && title && (
|
|
@@ -44,7 +44,7 @@ export const AppHeader = React.memo<AppHeaderProps>(({
|
|
|
44
44
|
</TabsList>
|
|
45
45
|
</Tabs>
|
|
46
46
|
)}
|
|
47
|
-
{
|
|
47
|
+
{buttonSwitcherProps && <ButtonSwitcher {...buttonSwitcherProps} />}
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<div className="justify-self-center">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ButtonSwitcherProps } from "@/components/composites/ButtonSwitcher/ButtonSwitcher";
|
|
3
3
|
import type { ChatToggleButtonProps } from "@/components/composites/ChatToggleButton/ChatToggleButton";
|
|
4
4
|
|
|
5
5
|
export interface TabItem {
|
|
@@ -17,6 +17,6 @@ export interface AppHeaderProps {
|
|
|
17
17
|
className?: string;
|
|
18
18
|
showSidebarToggle?: boolean;
|
|
19
19
|
showTitle?: boolean;
|
|
20
|
-
|
|
20
|
+
buttonSwitcherProps?: ButtonSwitcherProps;
|
|
21
21
|
chatToggleProps?: ChatToggleButtonProps;
|
|
22
22
|
}
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
-
import {
|
|
3
|
+
import { ButtonSwitcher } from "./ButtonSwitcher"
|
|
4
4
|
|
|
5
|
-
const meta: Meta<typeof
|
|
6
|
-
title: "Composites/
|
|
7
|
-
component:
|
|
8
|
-
|
|
9
|
-
layout: "centered",
|
|
10
|
-
},
|
|
5
|
+
const meta: Meta<typeof ButtonSwitcher> = {
|
|
6
|
+
title: "Composites/ButtonSwitcher",
|
|
7
|
+
component: ButtonSwitcher,
|
|
8
|
+
tags: ["autodocs"],
|
|
11
9
|
args: {
|
|
12
|
-
|
|
10
|
+
items: [
|
|
13
11
|
{ id: "1", name: "Alpha Workflow" },
|
|
14
12
|
{ id: "2", name: "Beta Workflow" },
|
|
15
13
|
{ id: "3", name: "Gamma Workflow" },
|
|
16
14
|
],
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
activeId: "1",
|
|
16
|
+
onSelect: () => {},
|
|
19
17
|
},
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export default meta
|
|
23
|
-
type Story = StoryObj<typeof
|
|
21
|
+
type Story = StoryObj<typeof ButtonSwitcher>
|
|
24
22
|
|
|
25
23
|
export const Default: Story = {}
|
|
26
24
|
|
|
@@ -8,21 +8,22 @@ import {
|
|
|
8
8
|
} from "@/components/primitives/DropdownMenu"
|
|
9
9
|
import { Icon } from "@/components/primitives/Icon"
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface ButtonSwitcherItem {
|
|
12
12
|
id: string
|
|
13
13
|
name: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export interface ButtonSwitcherProps {
|
|
17
|
+
items: ButtonSwitcherItem[]
|
|
18
|
+
activeId?: string | null
|
|
19
|
+
onSelect: (id: string) => void
|
|
20
20
|
className?: string
|
|
21
|
+
placeholder?: string
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export const
|
|
24
|
-
({
|
|
25
|
-
const
|
|
24
|
+
export const ButtonSwitcher = React.memo<ButtonSwitcherProps>(
|
|
25
|
+
({ items, activeId, onSelect, className, placeholder = "Select Item" }) => {
|
|
26
|
+
const currentItem = items.find((w) => w.id === activeId)
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<div className={className}>
|
|
@@ -35,25 +36,25 @@ export const WorkflowSwitcher = React.memo<WorkflowSwitcherProps>(
|
|
|
35
36
|
variant="secondary"
|
|
36
37
|
>
|
|
37
38
|
<span className="truncate max-w-[150px]">
|
|
38
|
-
{
|
|
39
|
+
{currentItem?.name ?? placeholder}
|
|
39
40
|
</span>
|
|
40
41
|
<Icon name="chevron-down" size="xs" className="ml-1 opacity-50 shrink-0" />
|
|
41
42
|
</Button>
|
|
42
43
|
</DropdownMenuTrigger>
|
|
43
44
|
<DropdownMenuContent align="start">
|
|
44
|
-
{
|
|
45
|
+
{items.map((w) => (
|
|
45
46
|
<DropdownMenuItem
|
|
46
47
|
className="flex items-center justify-between"
|
|
47
48
|
key={w.id}
|
|
48
|
-
onClick={() =>
|
|
49
|
+
onClick={() => onSelect(w.id)}
|
|
49
50
|
>
|
|
50
51
|
<span className="truncate pr-4">{w.name}</span>
|
|
51
|
-
{w.id ===
|
|
52
|
+
{w.id === activeId && <Icon name="check" size="sm" className="ml-auto shrink-0" />}
|
|
52
53
|
</DropdownMenuItem>
|
|
53
54
|
))}
|
|
54
|
-
{
|
|
55
|
+
{items.length === 0 && (
|
|
55
56
|
<div className="px-2 py-1.5 text-sm text-muted-foreground text-center">
|
|
56
|
-
No
|
|
57
|
+
No items found
|
|
57
58
|
</div>
|
|
58
59
|
)}
|
|
59
60
|
</DropdownMenuContent>
|
|
@@ -63,4 +64,4 @@ export const WorkflowSwitcher = React.memo<WorkflowSwitcherProps>(
|
|
|
63
64
|
}
|
|
64
65
|
)
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
ButtonSwitcher.displayName = "ButtonSwitcher"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ButtonSwitcher"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { Area,
|
|
3
|
-
import type { TooltipContentProps } from "recharts"
|
|
2
|
+
import { Area, CartesianGrid, ComposedChart, Line, XAxis } from "recharts"
|
|
4
3
|
|
|
5
4
|
import {
|
|
6
5
|
ChartContainer,
|
|
@@ -31,10 +30,11 @@ export interface DashboardChartPoint {
|
|
|
31
30
|
mobile: number
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export type DashboardChartTimeRange = string
|
|
33
|
+
export type DashboardChartTimeRange = string;
|
|
35
34
|
|
|
36
35
|
export interface DashboardChartProps {
|
|
37
36
|
series: DashboardChartPoint[]
|
|
37
|
+
timeRange?: DashboardChartTimeRange
|
|
38
38
|
onTimeRangeChange?: (range: DashboardChartTimeRange) => void
|
|
39
39
|
title: string
|
|
40
40
|
description: string
|
|
@@ -47,8 +47,100 @@ export interface DashboardChartProps {
|
|
|
47
47
|
chartClassName?: string
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
interface DashboardTooltipPayloadItem {
|
|
51
|
+
dataKey?: string | number;
|
|
52
|
+
value?: number;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DashboardTooltipContent = React.memo((props: {
|
|
57
|
+
active?: boolean;
|
|
58
|
+
payload?: DashboardTooltipPayloadItem[];
|
|
59
|
+
label?: string;
|
|
60
|
+
desktopLabel?: string;
|
|
61
|
+
mobileLabel?: string;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}): React.ReactElement | null => {
|
|
64
|
+
const { active, payload, desktopLabel, mobileLabel, content: _content, label, ...rest } = props;
|
|
65
|
+
void _content;
|
|
66
|
+
if (!active || !payload?.length) return null;
|
|
67
|
+
|
|
68
|
+
const uniquePayload = payload.filter(
|
|
69
|
+
(item: DashboardTooltipPayloadItem, index: number, self: DashboardTooltipPayloadItem[]) =>
|
|
70
|
+
index === self.findIndex((t: DashboardTooltipPayloadItem) => String(t.dataKey) === String(item.dataKey)),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ChartTooltipContent
|
|
75
|
+
{...rest}
|
|
76
|
+
active={active}
|
|
77
|
+
label={label as string}
|
|
78
|
+
payload={uniquePayload}
|
|
79
|
+
labelFormatter={(value, tooltipPayload) => {
|
|
80
|
+
const typedPayload = tooltipPayload as Array<{ payload: { timestamp?: number } }>;
|
|
81
|
+
const timestamp = typedPayload?.[0]?.payload?.timestamp;
|
|
82
|
+
if (!timestamp) return null;
|
|
83
|
+
const date = new Date(timestamp);
|
|
84
|
+
if (isNaN(date.getTime())) return ""
|
|
85
|
+
return date.toLocaleDateString("en-US", {
|
|
86
|
+
month: "short",
|
|
87
|
+
year: "numeric",
|
|
88
|
+
})
|
|
89
|
+
}}
|
|
90
|
+
formatter={(value, name, item, index, tooltipPayload) => {
|
|
91
|
+
const typedPayload = tooltipPayload as { timestamp?: number };
|
|
92
|
+
const typedItem = item as { color?: string };
|
|
93
|
+
const labelText = name === "desktop" ? desktopLabel : (name === "mobile" ? mobileLabel : name);
|
|
94
|
+
const timeStr = typedPayload?.timestamp ? new Date(typedPayload.timestamp).toLocaleTimeString("en-US", {
|
|
95
|
+
hour: "numeric",
|
|
96
|
+
minute: "2-digit"
|
|
97
|
+
}) : null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="flex flex-col w-full">
|
|
101
|
+
<div className="flex w-full items-center gap-2">
|
|
102
|
+
<div
|
|
103
|
+
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
|
|
104
|
+
style={{ backgroundColor: typedItem.color }}
|
|
105
|
+
/>
|
|
106
|
+
<div className="flex flex-1 justify-between items-center gap-4">
|
|
107
|
+
<span className="text-muted-foreground">{labelText as React.ReactNode}</span>
|
|
108
|
+
<span className="font-mono font-medium tabular-nums text-foreground">{value as React.ReactNode}</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{timeStr && (
|
|
112
|
+
<div className="text-[10px] text-muted-foreground self-end mt-0.5">
|
|
113
|
+
{timeStr}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}}
|
|
119
|
+
indicator="dot"
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
DashboardTooltipContent.displayName = "DashboardTooltipContent";
|
|
124
|
+
|
|
125
|
+
const CustomDot = React.memo((props: { cx?: number; cy?: number; stroke?: string }): React.ReactElement => {
|
|
126
|
+
const { cx, cy, stroke } = props;
|
|
127
|
+
return (
|
|
128
|
+
<circle
|
|
129
|
+
cx={cx}
|
|
130
|
+
cy={cy}
|
|
131
|
+
r={4}
|
|
132
|
+
fill="var(--background)"
|
|
133
|
+
stroke={stroke}
|
|
134
|
+
strokeWidth={2}
|
|
135
|
+
pointerEvents="none"
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
CustomDot.displayName = "CustomDot";
|
|
140
|
+
|
|
50
141
|
export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
51
142
|
series,
|
|
143
|
+
timeRange: propsTimeRange,
|
|
52
144
|
onTimeRangeChange,
|
|
53
145
|
title,
|
|
54
146
|
description,
|
|
@@ -60,11 +152,12 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
60
152
|
className,
|
|
61
153
|
chartClassName
|
|
62
154
|
}) => {
|
|
63
|
-
const [
|
|
155
|
+
const [localTimeRange, setLocalTimeRange] = React.useState<DashboardChartTimeRange>(timeRanges[0]?.value)
|
|
156
|
+
const timeRange = propsTimeRange ?? localTimeRange
|
|
64
157
|
|
|
65
158
|
const handleTimeRangeChange = React.useCallback(
|
|
66
159
|
(range: DashboardChartTimeRange) => {
|
|
67
|
-
|
|
160
|
+
setLocalTimeRange(range)
|
|
68
161
|
onTimeRangeChange?.(range)
|
|
69
162
|
},
|
|
70
163
|
[onTimeRangeChange]
|
|
@@ -181,63 +274,12 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
181
274
|
/>
|
|
182
275
|
<ChartTooltip
|
|
183
276
|
cursor={false}
|
|
184
|
-
content={
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
index === self.findIndex((t) => String(t.dataKey) === String(item.dataKey)),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<ChartTooltipContent
|
|
195
|
-
{...rest}
|
|
196
|
-
payload={uniquePayload}
|
|
197
|
-
labelFormatter={(value, tooltipPayload) => {
|
|
198
|
-
const typedPayload = tooltipPayload as Array<{ payload: { timestamp?: number } }>;
|
|
199
|
-
const timestamp = typedPayload?.[0]?.payload?.timestamp;
|
|
200
|
-
if (!timestamp) return null;
|
|
201
|
-
const date = new Date(timestamp);
|
|
202
|
-
if (isNaN(date.getTime())) return ""
|
|
203
|
-
return date.toLocaleDateString("en-US", {
|
|
204
|
-
month: "short",
|
|
205
|
-
year: "numeric",
|
|
206
|
-
})
|
|
207
|
-
}}
|
|
208
|
-
formatter={(value, name, item, index, tooltipPayload) => {
|
|
209
|
-
const typedPayload = tooltipPayload as { timestamp?: number };
|
|
210
|
-
const typedItem = item as { color?: string };
|
|
211
|
-
const labelText = name === "desktop" ? desktopLabel : (name === "mobile" ? mobileLabel : name);
|
|
212
|
-
const timeStr = typedPayload?.timestamp ? new Date(typedPayload.timestamp).toLocaleTimeString("en-US", {
|
|
213
|
-
hour: "numeric",
|
|
214
|
-
minute: "2-digit"
|
|
215
|
-
}) : null;
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<div className="flex flex-col w-full">
|
|
219
|
-
<div className="flex w-full items-center gap-2">
|
|
220
|
-
<div
|
|
221
|
-
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
|
|
222
|
-
style={{ backgroundColor: typedItem.color }}
|
|
223
|
-
/>
|
|
224
|
-
<div className="flex flex-1 justify-between items-center gap-4">
|
|
225
|
-
<span className="text-muted-foreground">{labelText as React.ReactNode}</span>
|
|
226
|
-
<span className="font-mono font-medium tabular-nums text-foreground">{value as React.ReactNode}</span>
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
{timeStr && (
|
|
230
|
-
<div className="text-[10px] text-muted-foreground self-end mt-0.5">
|
|
231
|
-
{timeStr}
|
|
232
|
-
</div>
|
|
233
|
-
)}
|
|
234
|
-
</div>
|
|
235
|
-
)
|
|
236
|
-
}}
|
|
237
|
-
indicator="dot"
|
|
238
|
-
/>
|
|
239
|
-
);
|
|
240
|
-
}}
|
|
277
|
+
content={
|
|
278
|
+
<DashboardTooltipContent
|
|
279
|
+
desktopLabel={desktopLabel}
|
|
280
|
+
mobileLabel={mobileLabel}
|
|
281
|
+
/>
|
|
282
|
+
}
|
|
241
283
|
/>
|
|
242
284
|
{showMobile && (
|
|
243
285
|
<>
|
|
@@ -252,8 +294,8 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
252
294
|
type="monotone"
|
|
253
295
|
stroke="var(--color-mobile)"
|
|
254
296
|
strokeWidth={2}
|
|
255
|
-
dot={
|
|
256
|
-
activeDot={{ r:
|
|
297
|
+
dot={<CustomDot />}
|
|
298
|
+
activeDot={{ r: 6 }}
|
|
257
299
|
/>
|
|
258
300
|
</>
|
|
259
301
|
)}
|
|
@@ -268,8 +310,8 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
268
310
|
type="monotone"
|
|
269
311
|
stroke="var(--color-desktop)"
|
|
270
312
|
strokeWidth={2}
|
|
271
|
-
dot={
|
|
272
|
-
activeDot={{ r:
|
|
313
|
+
dot={<CustomDot />}
|
|
314
|
+
activeDot={{ r: 6 }}
|
|
273
315
|
/>
|
|
274
316
|
</ComposedChart>
|
|
275
317
|
</ChartContainer>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { DefaultSwitcher, DefaultSwitcherItem } from "./DefaultSwitcher"
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Composites/DefaultSwitcher",
|
|
7
|
+
component: DefaultSwitcher,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
} satisfies Meta<typeof DefaultSwitcher>
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof meta>
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
themes: [
|
|
20
|
+
{ label: "Light", value: "light" },
|
|
21
|
+
{ label: "Dark", value: "dark" },
|
|
22
|
+
{ label: "System", value: "system" },
|
|
23
|
+
],
|
|
24
|
+
placeholder: "Select theme...",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Controlled: Story = {
|
|
29
|
+
render: () => {
|
|
30
|
+
const [value, setValue] = React.useState<string>()
|
|
31
|
+
const themes: DefaultSwitcherItem[] = [
|
|
32
|
+
{ label: "Blue", value: "blue" },
|
|
33
|
+
{ label: "Green", value: "green" },
|
|
34
|
+
{ label: "Purple", value: "purple" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex flex-col gap-4 min-w-[200px]">
|
|
39
|
+
<div className="text-sm text-muted-foreground">Selected: {value || "None"}</div>
|
|
40
|
+
<DefaultSwitcher themes={themes} value={value} onValueChange={setValue} />
|
|
41
|
+
|
|
42
|
+
<p style={{ marginTop: '16px', fontSize: '14px' }}>Selected: {value}</p>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "@/components/primitives/Select"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* DefaultSwitcher Composite
|
|
12
12
|
*
|
|
13
13
|
* A theme variant selector component that allows users to choose from multiple theme options.
|
|
14
14
|
* Composes Select primitive.
|
|
@@ -20,20 +20,20 @@ import {
|
|
|
20
20
|
* { label: "Ocean", value: "ocean" },
|
|
21
21
|
* { label: "Forest", value: "forest" },
|
|
22
22
|
* ]
|
|
23
|
-
* <
|
|
23
|
+
* <DefaultSwitcher themes={themes} value="default" onValueChange={setTheme} />
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
export interface
|
|
27
|
+
export interface DefaultSwitcherItem {
|
|
28
28
|
label: string
|
|
29
29
|
value: string
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export interface
|
|
32
|
+
export interface DefaultSwitcherProps {
|
|
33
33
|
/**
|
|
34
34
|
* Available theme options
|
|
35
35
|
*/
|
|
36
|
-
themes:
|
|
36
|
+
themes: DefaultSwitcherItem[]
|
|
37
37
|
/**
|
|
38
38
|
* Currently selected theme value
|
|
39
39
|
*/
|
|
@@ -53,11 +53,11 @@ export interface ThemeSelectorProps {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* DefaultSwitcher component
|
|
57
57
|
*
|
|
58
58
|
* Provides a select dropdown to choose from multiple theme variants.
|
|
59
59
|
*/
|
|
60
|
-
export const
|
|
60
|
+
export const DefaultSwitcher = React.memo<DefaultSwitcherProps>(
|
|
61
61
|
({ themes, value, onValueChange, placeholder = "Select theme", className }) => {
|
|
62
62
|
return (
|
|
63
63
|
<Select value={value} onValueChange={onValueChange}>
|
|
@@ -76,4 +76,4 @@ export const ThemeSelector = React.memo<ThemeSelectorProps>(
|
|
|
76
76
|
}
|
|
77
77
|
)
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
DefaultSwitcher.displayName = "DefaultSwitcher"
|