beads-kanban-ui 0.1.0

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 (154) hide show
  1. package/.designs/beads-kanban-ui-bj0.md +73 -0
  2. package/.designs/beads-kanban-ui-qxq.md +144 -0
  3. package/.designs/epic-support.md +282 -0
  4. package/.env.local.example +2 -0
  5. package/.eslintrc.json +3 -0
  6. package/.gitattributes +3 -0
  7. package/.github/workflows/release.yml +123 -0
  8. package/.history/README_20260121193710.md +227 -0
  9. package/.history/README_20260121193918.md +227 -0
  10. package/.history/README_20260121193921.md +227 -0
  11. package/.history/README_20260121193933.md +227 -0
  12. package/.history/README_20260121193934.md +227 -0
  13. package/.history/README_20260121193944.md +227 -0
  14. package/.history/README_20260121193953.md +227 -0
  15. package/.history/src/app/page_20260121133429.tsx +134 -0
  16. package/.history/src/app/page_20260121133928.tsx +134 -0
  17. package/.history/src/app/page_20260121144850.tsx +138 -0
  18. package/.history/src/app/page_20260121144854.tsx +138 -0
  19. package/.history/src/app/page_20260121144858.tsx +138 -0
  20. package/.history/src/app/page_20260121144902.tsx +138 -0
  21. package/.history/src/app/page_20260121144906.tsx +138 -0
  22. package/.history/src/app/page_20260121144911.tsx +138 -0
  23. package/.history/src/app/page_20260121144928.tsx +138 -0
  24. package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
  25. package/.playwright-mcp/beams-test.png +0 -0
  26. package/.playwright-mcp/card-verification.png +0 -0
  27. package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
  28. package/.playwright-mcp/dialog-width-test.png +0 -0
  29. package/.playwright-mcp/homepage.png +0 -0
  30. package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
  31. package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
  32. package/.playwright-mcp/morphing-dialog-open.png +0 -0
  33. package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
  34. package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
  35. package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
  36. package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
  37. package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
  38. package/.playwright-mcp/screenshot-after-click.png +0 -0
  39. package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
  40. package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
  41. package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
  42. package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
  43. package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
  44. package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
  45. package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
  46. package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
  47. package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
  48. package/README.md +243 -0
  49. package/Screenshots/bead-detail.png +0 -0
  50. package/Screenshots/dashboard.png +0 -0
  51. package/Screenshots/kanban-board.png +0 -0
  52. package/components.json +27 -0
  53. package/logo/logo.svg +1 -0
  54. package/next.config.js +9 -0
  55. package/npm/README.md +37 -0
  56. package/npm/bin/cli.js +107 -0
  57. package/npm/package.json +20 -0
  58. package/npm/scripts/postinstall.js +132 -0
  59. package/package.json +62 -0
  60. package/postcss.config.js +6 -0
  61. package/public/logo.svg +1 -0
  62. package/restart.sh +5 -0
  63. package/server/Cargo.lock +1685 -0
  64. package/server/Cargo.toml +24 -0
  65. package/server/src/db.rs +570 -0
  66. package/server/src/main.rs +141 -0
  67. package/server/src/routes/beads.rs +413 -0
  68. package/server/src/routes/cli.rs +150 -0
  69. package/server/src/routes/fs.rs +360 -0
  70. package/server/src/routes/git.rs +169 -0
  71. package/server/src/routes/mod.rs +107 -0
  72. package/server/src/routes/projects.rs +177 -0
  73. package/server/src/routes/watch.rs +211 -0
  74. package/src/app/globals.css +101 -0
  75. package/src/app/layout.tsx +36 -0
  76. package/src/app/page.tsx +348 -0
  77. package/src/app/project/kanban-board.tsx +356 -0
  78. package/src/app/project/page.tsx +18 -0
  79. package/src/app/settings/page.tsx +224 -0
  80. package/src/components/Beams.css +5 -0
  81. package/src/components/Beams.jsx +307 -0
  82. package/src/components/Galaxy.css +5 -0
  83. package/src/components/Galaxy.jsx +333 -0
  84. package/src/components/activity-timeline.tsx +172 -0
  85. package/src/components/add-project-dialog.tsx +219 -0
  86. package/src/components/bead-card.tsx +196 -0
  87. package/src/components/bead-detail.tsx +306 -0
  88. package/src/components/color-picker.tsx +101 -0
  89. package/src/components/comment-input.tsx +155 -0
  90. package/src/components/comment-list.tsx +147 -0
  91. package/src/components/dependency-badge.tsx +106 -0
  92. package/src/components/design-doc-dialog.tsx +58 -0
  93. package/src/components/design-doc-preview.tsx +97 -0
  94. package/src/components/design-doc-viewer.tsx +199 -0
  95. package/src/components/editable-project-name.tsx +178 -0
  96. package/src/components/epic-card.tsx +263 -0
  97. package/src/components/folder-browser.tsx +273 -0
  98. package/src/components/footer.tsx +27 -0
  99. package/src/components/kanban/default.tsx +184 -0
  100. package/src/components/kanban-column.tsx +167 -0
  101. package/src/components/project-card.tsx +191 -0
  102. package/src/components/quick-filter-bar.tsx +279 -0
  103. package/src/components/scan-directory-dialog.tsx +368 -0
  104. package/src/components/status-donut.tsx +197 -0
  105. package/src/components/subtask-list.tsx +128 -0
  106. package/src/components/tag-picker.tsx +252 -0
  107. package/src/components/ui/.gitkeep +0 -0
  108. package/src/components/ui/alert-dialog.tsx +141 -0
  109. package/src/components/ui/avatar.tsx +67 -0
  110. package/src/components/ui/badge.tsx +230 -0
  111. package/src/components/ui/button.tsx +433 -0
  112. package/src/components/ui/card/index.tsx +24 -0
  113. package/src/components/ui/card/roiui-card.module.css +197 -0
  114. package/src/components/ui/card/roiui-card.tsx +154 -0
  115. package/src/components/ui/card/shadcn-card.tsx +76 -0
  116. package/src/components/ui/chart.tsx +369 -0
  117. package/src/components/ui/dialog.tsx +122 -0
  118. package/src/components/ui/dropdown-menu.tsx +201 -0
  119. package/src/components/ui/input.tsx +22 -0
  120. package/src/components/ui/kanban.tsx +522 -0
  121. package/src/components/ui/morphing-dialog.tsx +457 -0
  122. package/src/components/ui/popover.tsx +33 -0
  123. package/src/components/ui/progress.tsx +28 -0
  124. package/src/components/ui/scroll-area.tsx +48 -0
  125. package/src/components/ui/select.tsx +159 -0
  126. package/src/components/ui/separator.tsx +31 -0
  127. package/src/components/ui/sheet.tsx +142 -0
  128. package/src/components/ui/skeleton.tsx +15 -0
  129. package/src/components/ui/toast.tsx +129 -0
  130. package/src/components/ui/toaster.tsx +35 -0
  131. package/src/components/ui/tooltip.tsx +30 -0
  132. package/src/hooks/.gitkeep +0 -0
  133. package/src/hooks/use-bead-filters.ts +261 -0
  134. package/src/hooks/use-beads.ts +162 -0
  135. package/src/hooks/use-branch-statuses.ts +161 -0
  136. package/src/hooks/use-epics.ts +173 -0
  137. package/src/hooks/use-file-watcher.ts +111 -0
  138. package/src/hooks/use-keyboard-navigation.ts +282 -0
  139. package/src/hooks/use-project.ts +61 -0
  140. package/src/hooks/use-projects.ts +93 -0
  141. package/src/hooks/use-toast.ts +194 -0
  142. package/src/hooks/useClickOutside.tsx +26 -0
  143. package/src/lib/.gitkeep +0 -0
  144. package/src/lib/api.ts +186 -0
  145. package/src/lib/beads-parser.ts +252 -0
  146. package/src/lib/cli.ts +193 -0
  147. package/src/lib/db.ts +145 -0
  148. package/src/lib/design-doc.ts +74 -0
  149. package/src/lib/epic-parser.ts +242 -0
  150. package/src/lib/git.ts +102 -0
  151. package/src/lib/utils.ts +12 -0
  152. package/src/types/index.ts +107 -0
  153. package/tailwind.config.ts +85 -0
  154. package/tsconfig.json +26 -0
@@ -0,0 +1,154 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import Image from "next/image";
3
+ import { cn } from "@/lib/utils";
4
+ import styles from "./roiui-card.module.css";
5
+
6
+ const cardVariants = cva(styles.card, {
7
+ variants: {
8
+ variant: {
9
+ default: "",
10
+ lift: styles.cardLift,
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ variant: "default",
15
+ },
16
+ });
17
+
18
+ /**
19
+ * Card component for displaying content in a contained layout.
20
+ *
21
+ * @param variant - The visual style of the card
22
+ * - `"default"` - Standard card appearance
23
+ * - `"lift"` - Animated card with hover effects. On hover, the image scales up, content and footer animate with elevation effects
24
+ * @param className - Optional CSS class names
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * // Standard card
29
+ * <Card>
30
+ * <CardHeader>
31
+ * <CardTitle>Title</CardTitle>
32
+ * </CardHeader>
33
+ * <CardContent>Content goes here</CardContent>
34
+ * </Card>
35
+ *
36
+ * // Lift variant with hover animations
37
+ * <Card variant="lift">
38
+ * <CardImage src="/image.jpg" alt="Image" />
39
+ * <CardContent>
40
+ * <CardTitle>Title</CardTitle>
41
+ * </CardContent>
42
+ * <CardFooter>Footer content</CardFooter>
43
+ * </Card>
44
+ * ```
45
+ */
46
+ function Card({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof cardVariants>) {
47
+ return <div className={cn(cardVariants({ variant }), className)} data-slot="card" {...props} />;
48
+ }
49
+
50
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
51
+ return <div className={cn(styles.header, className)} data-slot="card-header" {...props} />;
52
+ }
53
+
54
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
55
+ return <div className={cn(styles.title, className)} data-slot="card-title" {...props} />;
56
+ }
57
+
58
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
59
+ return <div className={cn(styles.description, className)} data-slot="card-description" {...props} />;
60
+ }
61
+
62
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
63
+ return <div className={cn(styles.content, className)} data-slot="card-content" {...props} />;
64
+ }
65
+
66
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
67
+ return <div className={cn(styles.footer, className)} data-slot="card-footer" {...props} />;
68
+ }
69
+
70
+ /**
71
+ * CardImage component for displaying images within a Card. Uses Next Image.
72
+ *
73
+ * @param src - The image source URL
74
+ * @param alt - Alternative text for the image (required for accessibility)
75
+ * @param className - Optional CSS class names
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * <Card>
80
+ * <CardImage src="/scene.jpg" alt="Mountain landscape" />
81
+ * <CardContent>...</CardContent>
82
+ * </Card>
83
+ * ```
84
+ */
85
+ function CardImage({
86
+ className,
87
+ src,
88
+ alt,
89
+ ...props
90
+ }: {
91
+ className?: string;
92
+ src: string;
93
+ alt: string;
94
+ } & Omit<React.ComponentProps<typeof Image>, "src" | "alt" | "width" | "height" | "children">) {
95
+ return (
96
+ <Image
97
+ alt={alt}
98
+ className={cn(styles.image, className)}
99
+ data-slot="card-image"
100
+ height={300}
101
+ src={src}
102
+ width={300}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ /**
109
+ * CardImageContent component for overlaying content on top of a CardImage.
110
+ * Creates an absolutely positioned overlay with a gradient background for text readability.
111
+ *
112
+ * @param className - Optional CSS class names
113
+ * @param children - Content to display in the overlay (typically text or CTAs)
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * <Card>
118
+ * <CardImage src="/scene.jpg" alt="Mountain landscape" />
119
+ * <CardImageContent>
120
+ * <h2>Mountain Adventure</h2>
121
+ * <p>Explore the peaks</p>
122
+ * </CardImageContent>
123
+ * <CardContent>...</CardContent>
124
+ * </Card>
125
+ * ```
126
+ */
127
+ function CardImageContent({ className, ...props }: React.ComponentProps<"div">) {
128
+ return <div className={cn(styles.imageContent, className)} data-slot="card-image-content" {...props} />;
129
+ }
130
+
131
+ function CardIcon({ className, children, ...props }: React.ComponentProps<"div">) {
132
+ return (
133
+ <div className={cn(styles.icon, className)} data-slot="card-icon" {...props}>
134
+ {children}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
140
+ return <div className={cn(styles.action, className)} data-slot="card-action" {...props} />;
141
+ }
142
+
143
+ export {
144
+ Card,
145
+ CardAction,
146
+ CardContent,
147
+ CardDescription,
148
+ CardFooter,
149
+ CardHeader,
150
+ CardIcon,
151
+ CardImage,
152
+ CardImageContent,
153
+ CardTitle,
154
+ };
@@ -0,0 +1,76 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-xl border bg-card text-card-foreground shadow",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ ))
42
+ CardTitle.displayName = "CardTitle"
43
+
44
+ const CardDescription = React.forwardRef<
45
+ HTMLDivElement,
46
+ React.HTMLAttributes<HTMLDivElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <div
49
+ ref={ref}
50
+ className={cn("text-sm text-muted-foreground", className)}
51
+ {...props}
52
+ />
53
+ ))
54
+ CardDescription.displayName = "CardDescription"
55
+
56
+ const CardContent = React.forwardRef<
57
+ HTMLDivElement,
58
+ React.HTMLAttributes<HTMLDivElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
+ ))
62
+ CardContent.displayName = "CardContent"
63
+
64
+ const CardFooter = React.forwardRef<
65
+ HTMLDivElement,
66
+ React.HTMLAttributes<HTMLDivElement>
67
+ >(({ className, ...props }, ref) => (
68
+ <div
69
+ ref={ref}
70
+ className={cn("flex items-center p-6 pt-0", className)}
71
+ {...props}
72
+ />
73
+ ))
74
+ CardFooter.displayName = "CardFooter"
75
+
76
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,369 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RechartsPrimitive from "recharts"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode
14
+ icon?: React.ComponentType
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ )
19
+ }
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig
23
+ }
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext)
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />")
32
+ }
33
+
34
+ return context
35
+ }
36
+
37
+ const ChartContainer = React.forwardRef<
38
+ HTMLDivElement,
39
+ React.ComponentProps<"div"> & {
40
+ config: ChartConfig
41
+ children: React.ComponentProps<
42
+ typeof RechartsPrimitive.ResponsiveContainer
43
+ >["children"]
44
+ }
45
+ >(({ id, className, children, config, ...props }, ref) => {
46
+ const uniqueId = React.useId()
47
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
48
+
49
+ return (
50
+ <ChartContext.Provider value={{ config }}>
51
+ <div
52
+ data-chart={chartId}
53
+ ref={ref}
54
+ className={cn(
55
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
56
+ className
57
+ )}
58
+ {...props}
59
+ >
60
+ <ChartStyle id={chartId} config={config} />
61
+ <RechartsPrimitive.ResponsiveContainer>
62
+ {children}
63
+ </RechartsPrimitive.ResponsiveContainer>
64
+ </div>
65
+ </ChartContext.Provider>
66
+ )
67
+ })
68
+ ChartContainer.displayName = "Chart"
69
+
70
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71
+ const colorConfig = Object.entries(config).filter(
72
+ ([, config]) => config.theme || config.color
73
+ )
74
+
75
+ if (!colorConfig.length) {
76
+ return null
77
+ }
78
+
79
+ return (
80
+ <style
81
+ dangerouslySetInnerHTML={{
82
+ __html: Object.entries(THEMES)
83
+ .map(
84
+ ([theme, prefix]) => `
85
+ ${prefix} [data-chart=${id}] {
86
+ ${colorConfig
87
+ .map(([key, itemConfig]) => {
88
+ const color =
89
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
90
+ itemConfig.color
91
+ return color ? ` --color-${key}: ${color};` : null
92
+ })
93
+ .join("\n")}
94
+ }
95
+ `
96
+ )
97
+ .join("\n"),
98
+ }}
99
+ />
100
+ )
101
+ }
102
+
103
+ const ChartTooltip = RechartsPrimitive.Tooltip
104
+
105
+ const ChartTooltipContent = React.forwardRef<
106
+ HTMLDivElement,
107
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
108
+ React.ComponentProps<"div"> & {
109
+ hideLabel?: boolean
110
+ hideIndicator?: boolean
111
+ indicator?: "line" | "dot" | "dashed"
112
+ nameKey?: string
113
+ labelKey?: string
114
+ }
115
+ >(
116
+ (
117
+ {
118
+ active,
119
+ payload,
120
+ className,
121
+ indicator = "dot",
122
+ hideLabel = false,
123
+ hideIndicator = false,
124
+ label,
125
+ labelFormatter,
126
+ labelClassName,
127
+ formatter,
128
+ color,
129
+ nameKey,
130
+ labelKey,
131
+ },
132
+ ref
133
+ ) => {
134
+ const { config } = useChart()
135
+
136
+ const tooltipLabel = React.useMemo(() => {
137
+ if (hideLabel || !payload?.length) {
138
+ return null
139
+ }
140
+
141
+ const [item] = payload
142
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`
143
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
144
+ const value =
145
+ !labelKey && typeof label === "string"
146
+ ? config[label as keyof typeof config]?.label || label
147
+ : itemConfig?.label
148
+
149
+ if (labelFormatter) {
150
+ return (
151
+ <div className={cn("font-medium", labelClassName)}>
152
+ {labelFormatter(value, payload)}
153
+ </div>
154
+ )
155
+ }
156
+
157
+ if (!value) {
158
+ return null
159
+ }
160
+
161
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
162
+ }, [
163
+ label,
164
+ labelFormatter,
165
+ payload,
166
+ hideLabel,
167
+ labelClassName,
168
+ config,
169
+ labelKey,
170
+ ])
171
+
172
+ if (!active || !payload?.length) {
173
+ return null
174
+ }
175
+
176
+ const nestLabel = payload.length === 1 && indicator !== "dot"
177
+
178
+ return (
179
+ <div
180
+ ref={ref}
181
+ className={cn(
182
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
183
+ className
184
+ )}
185
+ >
186
+ {!nestLabel ? tooltipLabel : null}
187
+ <div className="grid gap-1.5">
188
+ {payload
189
+ .filter((item) => item.type !== "none")
190
+ .map((item, index) => {
191
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
192
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
193
+ const indicatorColor = color || item.payload.fill || item.color
194
+
195
+ return (
196
+ <div
197
+ key={item.dataKey}
198
+ className={cn(
199
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
200
+ indicator === "dot" && "items-center"
201
+ )}
202
+ >
203
+ {formatter && item?.value !== undefined && item.name ? (
204
+ formatter(item.value, item.name, item, index, item.payload)
205
+ ) : (
206
+ <>
207
+ {itemConfig?.icon ? (
208
+ <itemConfig.icon />
209
+ ) : (
210
+ !hideIndicator && (
211
+ <div
212
+ className={cn(
213
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
214
+ {
215
+ "h-2.5 w-2.5": indicator === "dot",
216
+ "w-1": indicator === "line",
217
+ "w-0 border-[1.5px] border-dashed bg-transparent":
218
+ indicator === "dashed",
219
+ "my-0.5": nestLabel && indicator === "dashed",
220
+ }
221
+ )}
222
+ style={
223
+ {
224
+ "--color-bg": indicatorColor,
225
+ "--color-border": indicatorColor,
226
+ } as React.CSSProperties
227
+ }
228
+ />
229
+ )
230
+ )}
231
+ <div
232
+ className={cn(
233
+ "flex flex-1 justify-between leading-none",
234
+ nestLabel ? "items-end" : "items-center"
235
+ )}
236
+ >
237
+ <div className="grid gap-1.5">
238
+ {nestLabel ? tooltipLabel : null}
239
+ <span className="text-muted-foreground">
240
+ {itemConfig?.label || item.name}
241
+ </span>
242
+ </div>
243
+ {item.value && (
244
+ <span className="font-mono font-medium tabular-nums text-foreground">
245
+ {item.value.toLocaleString()}
246
+ </span>
247
+ )}
248
+ </div>
249
+ </>
250
+ )}
251
+ </div>
252
+ )
253
+ })}
254
+ </div>
255
+ </div>
256
+ )
257
+ }
258
+ )
259
+ ChartTooltipContent.displayName = "ChartTooltip"
260
+
261
+ const ChartLegend = RechartsPrimitive.Legend
262
+
263
+ const ChartLegendContent = React.forwardRef<
264
+ HTMLDivElement,
265
+ React.ComponentProps<"div"> &
266
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
267
+ hideIcon?: boolean
268
+ nameKey?: string
269
+ }
270
+ >(
271
+ (
272
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
273
+ ref
274
+ ) => {
275
+ const { config } = useChart()
276
+
277
+ if (!payload?.length) {
278
+ return null
279
+ }
280
+
281
+ return (
282
+ <div
283
+ ref={ref}
284
+ className={cn(
285
+ "flex items-center justify-center gap-4",
286
+ verticalAlign === "top" ? "pb-3" : "pt-3",
287
+ className
288
+ )}
289
+ >
290
+ {payload
291
+ .filter((item) => item.type !== "none")
292
+ .map((item) => {
293
+ const key = `${nameKey || item.dataKey || "value"}`
294
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
295
+
296
+ return (
297
+ <div
298
+ key={item.value}
299
+ className={cn(
300
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
301
+ )}
302
+ >
303
+ {itemConfig?.icon && !hideIcon ? (
304
+ <itemConfig.icon />
305
+ ) : (
306
+ <div
307
+ className="h-2 w-2 shrink-0 rounded-[2px]"
308
+ style={{
309
+ backgroundColor: item.color,
310
+ }}
311
+ />
312
+ )}
313
+ {itemConfig?.label}
314
+ </div>
315
+ )
316
+ })}
317
+ </div>
318
+ )
319
+ }
320
+ )
321
+ ChartLegendContent.displayName = "ChartLegend"
322
+
323
+ // Helper to extract item config from a payload.
324
+ function getPayloadConfigFromPayload(
325
+ config: ChartConfig,
326
+ payload: unknown,
327
+ key: string
328
+ ) {
329
+ if (typeof payload !== "object" || payload === null) {
330
+ return undefined
331
+ }
332
+
333
+ const payloadPayload =
334
+ "payload" in payload &&
335
+ typeof payload.payload === "object" &&
336
+ payload.payload !== null
337
+ ? payload.payload
338
+ : undefined
339
+
340
+ let configLabelKey: string = key
341
+
342
+ if (
343
+ key in payload &&
344
+ typeof payload[key as keyof typeof payload] === "string"
345
+ ) {
346
+ configLabelKey = payload[key as keyof typeof payload] as string
347
+ } else if (
348
+ payloadPayload &&
349
+ key in payloadPayload &&
350
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
351
+ ) {
352
+ configLabelKey = payloadPayload[
353
+ key as keyof typeof payloadPayload
354
+ ] as string
355
+ }
356
+
357
+ return configLabelKey in config
358
+ ? config[configLabelKey]
359
+ : config[key as keyof typeof config]
360
+ }
361
+
362
+ export {
363
+ ChartContainer,
364
+ ChartTooltip,
365
+ ChartTooltipContent,
366
+ ChartLegend,
367
+ ChartLegendContent,
368
+ ChartStyle,
369
+ }