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.
Files changed (31) 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 +108 -66
  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 +22 -9
  20. package/components/features/EvalDashboardFeature/useEvalDashboardFeature.d.ts +9 -0
  21. package/components/index.ts +2 -2
  22. package/dist/index.cjs +147 -86
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.css +3 -0
  25. package/dist/index.d.ts +87 -66
  26. package/dist/index.js +147 -86
  27. package/dist/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/components/composites/ThemeSelector/ThemeSelector.stories.tsx +0 -48
  30. package/components/composites/ThemeSelector/index.ts +0 -2
  31. 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,6 +1,5 @@
1
1
  import * as React from "react"
2
- import { Area, Line, ComposedChart, CartesianGrid, XAxis } from "recharts"
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 [timeRange, setTimeRange] = React.useState<DashboardChartTimeRange>(timeRanges[0]?.value)
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
- setTimeRange(range)
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={(props: TooltipContentProps) => {
185
- const { active, payload, content: _content, label: _label, ...rest } = props;
186
- if (!active || !payload?.length) return null;
187
-
188
- const uniquePayload = payload.filter(
189
- (item, index, self) =>
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={false}
256
- activeDot={{ r: 4 }}
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={false}
272
- activeDot={{ r: 4 }}
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
- * ThemeSelector Composite
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
- * <ThemeSelector themes={themes} value="default" onValueChange={setTheme} />
23
+ * <DefaultSwitcher themes={themes} value="default" onValueChange={setTheme} />
24
24
  * ```
25
25
  */
26
26
 
27
- export interface Theme {
27
+ export interface DefaultSwitcherItem {
28
28
  label: string
29
29
  value: string
30
30
  }
31
31
 
32
- export interface ThemeSelectorProps {
32
+ export interface DefaultSwitcherProps {
33
33
  /**
34
34
  * Available theme options
35
35
  */
36
- themes: Theme[]
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
- * ThemeSelector component
56
+ * DefaultSwitcher component
57
57
  *
58
58
  * Provides a select dropdown to choose from multiple theme variants.
59
59
  */
60
- export const ThemeSelector = React.memo<ThemeSelectorProps>(
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
- ThemeSelector.displayName = "ThemeSelector"
79
+ DefaultSwitcher.displayName = "DefaultSwitcher"
@@ -0,0 +1,2 @@
1
+ export { DefaultSwitcher } from './DefaultSwitcher'
2
+ export type { DefaultSwitcherProps, DefaultSwitcherItem } from './DefaultSwitcher'