ai-design-system 0.1.69 → 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.
Files changed (32) hide show
  1. package/components/blocks/EvalSessionDetailsPanel/EvalSessionDetailsPanel.tsx +29 -4
  2. package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +2 -2
  3. package/components/blocks/WorkflowCanvas/layout-engine.ts +23 -5
  4. package/components/composites/AppHeader/AppHeader.stories.tsx +30 -0
  5. package/components/composites/AppHeader/AppHeader.tsx +4 -4
  6. package/components/composites/AppHeader/interfaces.ts +2 -2
  7. package/components/composites/{WorkflowSwitcher/WorkflowSwitcher.stories.tsx → ButtonSwitcher/ButtonSwitcher.stories.tsx} +9 -11
  8. package/components/composites/{WorkflowSwitcher/WorkflowSwitcher.tsx → ButtonSwitcher/ButtonSwitcher.tsx} +16 -15
  9. package/components/composites/ButtonSwitcher/index.ts +1 -0
  10. package/components/composites/DashboardChart/DashboardChart.tsx +149 -31
  11. package/components/composites/DefaultSwitcher/DefaultSwitcher.stories.tsx +46 -0
  12. package/components/composites/{ThemeSelector/ThemeSelector.tsx → DefaultSwitcher/DefaultSwitcher.tsx} +8 -8
  13. package/components/composites/DefaultSwitcher/index.ts +2 -0
  14. package/components/composites/IconButton/IconButton.stories.tsx +32 -0
  15. package/components/composites/IconButton/IconButton.tsx +20 -0
  16. package/components/composites/IconButton/index.ts +2 -0
  17. package/components/composites/index.ts +10 -6
  18. package/components/features/EvalDashboardFeature/EvalDashboardFeature.mocks.ts +1 -0
  19. package/components/features/EvalDashboardFeature/EvalDashboardFeature.tsx +38 -23
  20. package/components/features/EvalDashboardFeature/useEvalDashboardFeature.d.ts +16 -0
  21. package/components/features/NodeEditor/NodeEditor.tsx +25 -1
  22. package/components/index.ts +2 -2
  23. package/dist/index.cjs +226 -74
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.css +3 -0
  26. package/dist/index.d.ts +96 -65
  27. package/dist/index.js +227 -75
  28. package/dist/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/components/composites/ThemeSelector/ThemeSelector.stories.tsx +0 -48
  31. package/components/composites/ThemeSelector/index.ts +0 -2
  32. 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 mr-2${loading ? ' animate-spin' : ''}`} />
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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 { WorkflowSwitcher } from "@/components/composites/WorkflowSwitcher"
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
- workflowSwitcherProps,
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 || workflowSwitcherProps || chatToggleProps) && (
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
- {workflowSwitcherProps && <WorkflowSwitcher {...workflowSwitcherProps} />}
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 { WorkflowSwitcherProps } from "@/components/composites/WorkflowSwitcher/WorkflowSwitcher";
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
- workflowSwitcherProps?: WorkflowSwitcherProps;
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 { WorkflowSwitcher } from "./WorkflowSwitcher"
3
+ import { ButtonSwitcher } from "./ButtonSwitcher"
4
4
 
5
- const meta: Meta<typeof WorkflowSwitcher> = {
6
- title: "Composites/WorkflowSwitcher",
7
- component: WorkflowSwitcher,
8
- parameters: {
9
- layout: "centered",
10
- },
5
+ const meta: Meta<typeof ButtonSwitcher> = {
6
+ title: "Composites/ButtonSwitcher",
7
+ component: ButtonSwitcher,
8
+ tags: ["autodocs"],
11
9
  args: {
12
- workflows: [
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
- currentWorkflowId: "1",
18
- onSelectWorkflow: () => {},
15
+ activeId: "1",
16
+ onSelect: () => {},
19
17
  },
20
18
  }
21
19
 
22
20
  export default meta
23
- type Story = StoryObj<typeof WorkflowSwitcher>
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 WorkflowItem {
11
+ export interface ButtonSwitcherItem {
12
12
  id: string
13
13
  name: string
14
14
  }
15
15
 
16
- export interface WorkflowSwitcherProps {
17
- workflows: WorkflowItem[]
18
- currentWorkflowId?: string | null
19
- onSelectWorkflow: (id: string) => void
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 WorkflowSwitcher = React.memo<WorkflowSwitcherProps>(
24
- ({ workflows, currentWorkflowId, onSelectWorkflow, className }) => {
25
- const currentWorkflow = workflows.find((w) => w.id === currentWorkflowId)
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
- {currentWorkflow?.name ?? "Select Workflow"}
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
- {workflows.map((w) => (
45
+ {items.map((w) => (
45
46
  <DropdownMenuItem
46
47
  className="flex items-center justify-between"
47
48
  key={w.id}
48
- onClick={() => onSelectWorkflow(w.id)}
49
+ onClick={() => onSelect(w.id)}
49
50
  >
50
51
  <span className="truncate pr-4">{w.name}</span>
51
- {w.id === currentWorkflowId && <Icon name="check" size="sm" className="ml-auto shrink-0" />}
52
+ {w.id === activeId && <Icon name="check" size="sm" className="ml-auto shrink-0" />}
52
53
  </DropdownMenuItem>
53
54
  ))}
54
- {workflows.length === 0 && (
55
+ {items.length === 0 && (
55
56
  <div className="px-2 py-1.5 text-sm text-muted-foreground text-center">
56
- No workflows found
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
- WorkflowSwitcher.displayName = "WorkflowSwitcher"
67
+ ButtonSwitcher.displayName = "ButtonSwitcher"
@@ -0,0 +1 @@
1
+ export * from "./ButtonSwitcher"
@@ -1,5 +1,5 @@
1
1
  import * as React from "react"
2
- import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
2
+ import { Area, CartesianGrid, ComposedChart, Line, XAxis } from "recharts"
3
3
 
4
4
  import {
5
5
  ChartContainer,
@@ -30,10 +30,11 @@ export interface DashboardChartPoint {
30
30
  mobile: number
31
31
  }
32
32
 
33
- export type DashboardChartTimeRange = string
33
+ export type DashboardChartTimeRange = string;
34
34
 
35
35
  export interface DashboardChartProps {
36
36
  series: DashboardChartPoint[]
37
+ timeRange?: DashboardChartTimeRange
37
38
  onTimeRangeChange?: (range: DashboardChartTimeRange) => void
38
39
  title: string
39
40
  description: string
@@ -46,8 +47,100 @@ export interface DashboardChartProps {
46
47
  chartClassName?: string
47
48
  }
48
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
+
49
141
  export const DashboardChart = React.memo<DashboardChartProps>(({
50
142
  series,
143
+ timeRange: propsTimeRange,
51
144
  onTimeRangeChange,
52
145
  title,
53
146
  description,
@@ -59,32 +152,41 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
59
152
  className,
60
153
  chartClassName
61
154
  }) => {
62
- const [timeRange, setTimeRange] = React.useState<DashboardChartTimeRange>(timeRanges[0]?.value)
155
+ const [localTimeRange, setLocalTimeRange] = React.useState<DashboardChartTimeRange>(timeRanges[0]?.value)
156
+ const timeRange = propsTimeRange ?? localTimeRange
63
157
 
64
158
  const handleTimeRangeChange = React.useCallback(
65
159
  (range: DashboardChartTimeRange) => {
66
- setTimeRange(range)
160
+ setLocalTimeRange(range)
67
161
  onTimeRangeChange?.(range)
68
162
  },
69
163
  [onTimeRangeChange]
70
164
  )
71
165
 
72
166
  const filteredSeries = React.useMemo(() => {
73
- const referenceDate = series.length > 0 ? series[series.length - 1].date : new Date().toISOString()
167
+ // 1. Sort ascending chronologically (oldest to newest)
168
+ const sortedSeries = [...series].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
169
+
170
+ const referenceDate = sortedSeries.length > 0 ? sortedSeries[sortedSeries.length - 1].date : new Date().toISOString()
74
171
  let daysToSubtract = 90
75
172
  if (timeRange === "30d") {
76
173
  daysToSubtract = 30
77
174
  } else if (timeRange === "7d") {
78
175
  daysToSubtract = 7
79
- } else if (timeRange === "10" || timeRange === "20" || timeRange === "30") {
176
+ }
177
+
178
+ let filtered = sortedSeries;
179
+ if (timeRange === "10" || timeRange === "20" || timeRange === "30") {
80
180
  // Special logic for count-based limits if passed as timeRange
81
181
  const limit = parseInt(timeRange, 10)
82
- return series.slice(-limit)
182
+ filtered = sortedSeries.slice(-limit)
183
+ } else {
184
+ const startDate = new Date(referenceDate)
185
+ startDate.setDate(startDate.getDate() - daysToSubtract)
186
+ filtered = sortedSeries.filter(item => new Date(item.date) >= startDate)
83
187
  }
84
188
 
85
- const startDate = new Date(referenceDate)
86
- startDate.setDate(startDate.getDate() - daysToSubtract)
87
- return series.filter(item => new Date(item.date) >= startDate)
189
+ return filtered.map(item => ({ ...item, timestamp: new Date(item.date).getTime() }))
88
190
  }, [series, timeRange])
89
191
 
90
192
  return (
@@ -137,7 +239,7 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
137
239
  }}
138
240
  className={cn("aspect-auto w-full", chartClassName || "h-[250px]")}
139
241
  >
140
- <AreaChart data={filteredSeries}>
242
+ <ComposedChart data={filteredSeries}>
141
243
  <defs>
142
244
  <linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
143
245
  <stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={1} />
@@ -152,50 +254,66 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
152
254
  </defs>
153
255
  <CartesianGrid vertical={false} />
154
256
  <XAxis
155
- dataKey="date"
257
+ dataKey="timestamp"
258
+ type="number"
259
+ scale="time"
260
+ domain={['dataMin', 'dataMax']}
156
261
  tickLine={false}
157
262
  axisLine={false}
158
263
  tickMargin={8}
159
264
  minTickGap={32}
160
265
  tickFormatter={(value) => {
161
266
  const date = new Date(value)
162
- return date.toLocaleDateString("en-US", {
267
+ return date.toLocaleString("en-US", {
163
268
  month: "short",
164
269
  day: "numeric",
270
+ hour: "numeric",
271
+ minute: "2-digit"
165
272
  })
166
273
  }}
167
274
  />
168
275
  <ChartTooltip
169
276
  cursor={false}
170
277
  content={
171
- <ChartTooltipContent
172
- labelFormatter={(value) => {
173
- return new Date(String(value)).toLocaleDateString("en-US", {
174
- month: "short",
175
- day: "numeric",
176
- })
177
- }}
178
- indicator="dot"
278
+ <DashboardTooltipContent
279
+ desktopLabel={desktopLabel}
280
+ mobileLabel={mobileLabel}
179
281
  />
180
282
  }
181
283
  />
182
284
  {showMobile && (
183
- <Area
184
- dataKey="mobile"
185
- type="natural"
186
- fill="url(#fillMobile)"
187
- stroke="var(--color-mobile)"
188
- stackId="a"
189
- />
285
+ <>
286
+ <Area
287
+ dataKey="mobile"
288
+ type="monotone"
289
+ fill="url(#fillMobile)"
290
+ stroke="none"
291
+ />
292
+ <Line
293
+ dataKey="mobile"
294
+ type="monotone"
295
+ stroke="var(--color-mobile)"
296
+ strokeWidth={2}
297
+ dot={<CustomDot />}
298
+ activeDot={{ r: 6 }}
299
+ />
300
+ </>
190
301
  )}
191
302
  <Area
192
303
  dataKey="desktop"
193
- type="natural"
304
+ type="monotone"
194
305
  fill="url(#fillDesktop)"
306
+ stroke="none"
307
+ />
308
+ <Line
309
+ dataKey="desktop"
310
+ type="monotone"
195
311
  stroke="var(--color-desktop)"
196
- stackId="a"
312
+ strokeWidth={2}
313
+ dot={<CustomDot />}
314
+ activeDot={{ r: 6 }}
197
315
  />
198
- </AreaChart>
316
+ </ComposedChart>
199
317
  </ChartContainer>
200
318
  </CardContent>
201
319
  </Card>
@@ -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
+ }