@hexclave/dashboard-ui-components 1.0.3 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/components/alert.js +4 -4
  2. package/dist/components/alert.js.map +1 -1
  3. package/dist/components/badge.js +1 -1
  4. package/dist/components/badge.js.map +1 -1
  5. package/dist/components/button.d.ts +4 -4
  6. package/dist/components/button.js +3 -3
  7. package/dist/components/button.js.map +1 -1
  8. package/dist/components/data-grid/data-grid-toolbar.js +5 -5
  9. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
  10. package/dist/components/data-grid/data-grid.d.ts.map +1 -1
  11. package/dist/components/data-grid/data-grid.js +11 -5
  12. package/dist/components/data-grid/data-grid.js.map +1 -1
  13. package/dist/components/data-grid/data-grid.test.js +34 -1
  14. package/dist/components/data-grid/data-grid.test.js.map +1 -1
  15. package/dist/components/dialog.d.ts +6 -2
  16. package/dist/components/dialog.d.ts.map +1 -1
  17. package/dist/components/dialog.js +14 -10
  18. package/dist/components/dialog.js.map +1 -1
  19. package/dist/components/input.js +12 -9
  20. package/dist/components/input.js.map +1 -1
  21. package/dist/components/pill-toggle.js +3 -3
  22. package/dist/components/pill-toggle.js.map +1 -1
  23. package/dist/components/tabs.js +2 -2
  24. package/dist/components/tabs.js.map +1 -1
  25. package/dist/dashboard-ui-components.global.js +15639 -17400
  26. package/dist/dashboard-ui-components.global.js.map +4 -4
  27. package/dist/esm/components/alert.js +4 -4
  28. package/dist/esm/components/alert.js.map +1 -1
  29. package/dist/esm/components/badge.js +1 -1
  30. package/dist/esm/components/badge.js.map +1 -1
  31. package/dist/esm/components/button.d.ts +4 -4
  32. package/dist/esm/components/button.js +3 -3
  33. package/dist/esm/components/button.js.map +1 -1
  34. package/dist/esm/components/data-grid/data-grid-toolbar.js +5 -5
  35. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
  36. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/dist/esm/components/data-grid/data-grid.js +11 -5
  38. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/dist/esm/components/data-grid/data-grid.test.js +34 -1
  40. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
  41. package/dist/esm/components/dialog.d.ts +6 -2
  42. package/dist/esm/components/dialog.d.ts.map +1 -1
  43. package/dist/esm/components/dialog.js +13 -11
  44. package/dist/esm/components/dialog.js.map +1 -1
  45. package/dist/esm/components/input.js +12 -9
  46. package/dist/esm/components/input.js.map +1 -1
  47. package/dist/esm/components/pill-toggle.js +4 -4
  48. package/dist/esm/components/pill-toggle.js.map +1 -1
  49. package/dist/esm/components/tabs.js +2 -2
  50. package/dist/esm/components/tabs.js.map +1 -1
  51. package/dist/esm/index.d.ts +2 -2
  52. package/dist/esm/index.js +2 -2
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +12 -0
  55. package/package.json +4 -3
  56. package/src/components/alert.tsx +120 -0
  57. package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
  58. package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
  59. package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
  60. package/src/components/analytics-chart/format.ts +101 -0
  61. package/src/components/analytics-chart/index.ts +75 -0
  62. package/src/components/analytics-chart/palette.ts +68 -0
  63. package/src/components/analytics-chart/render-data-series.tsx +169 -0
  64. package/src/components/analytics-chart/state.ts +165 -0
  65. package/src/components/analytics-chart/strings.ts +72 -0
  66. package/src/components/analytics-chart/types.ts +220 -0
  67. package/src/components/badge.tsx +108 -0
  68. package/src/components/button.tsx +104 -0
  69. package/src/components/card.tsx +263 -0
  70. package/src/components/chart-card.tsx +101 -0
  71. package/src/components/chart-container.tsx +155 -0
  72. package/src/components/chart-legend.tsx +65 -0
  73. package/src/components/chart-theme.tsx +52 -0
  74. package/src/components/chart-tooltip.tsx +165 -0
  75. package/src/components/cursor-blast-effect.tsx +334 -0
  76. package/src/components/data-grid/data-grid-sizing.ts +51 -0
  77. package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
  78. package/src/components/data-grid/data-grid.test.tsx +407 -0
  79. package/src/components/data-grid/data-grid.tsx +1231 -0
  80. package/src/components/data-grid/index.ts +64 -0
  81. package/src/components/data-grid/state.ts +235 -0
  82. package/src/components/data-grid/strings.ts +45 -0
  83. package/src/components/data-grid/types.ts +401 -0
  84. package/src/components/data-grid/use-data-source.ts +413 -0
  85. package/src/components/data-grid/use-url-state.test.tsx +88 -0
  86. package/src/components/data-grid/use-url-state.ts +298 -0
  87. package/src/components/dialog.tsx +218 -0
  88. package/src/components/edit-mode.tsx +17 -0
  89. package/src/components/empty-state.tsx +64 -0
  90. package/src/components/input.tsx +97 -0
  91. package/src/components/metric-card.tsx +142 -0
  92. package/src/components/pill-toggle.tsx +159 -0
  93. package/src/components/progress-bar.tsx +82 -0
  94. package/src/components/separator.tsx +36 -0
  95. package/src/components/skeleton.tsx +30 -0
  96. package/src/components/table.tsx +113 -0
  97. package/src/components/tabs.tsx +217 -0
  98. package/src/index.ts +71 -0
@@ -0,0 +1,263 @@
1
+ "use client";
2
+
3
+ import { Card } from "@hexclave/ui";
4
+ import { cn } from "@hexclave/ui";
5
+ import React from "react";
6
+
7
+ // ─── Card nesting context ────────────────────────────────────────────────────
8
+ // Components with a `glassmorphic` prop use this to auto-detect whether they
9
+ // sit inside a DesignCard. When they do, glassmorphic defaults to `true`;
10
+ // when they don't, it defaults to `false`.
11
+
12
+ const DesignCardNestingContext = React.createContext(false);
13
+
14
+ /**
15
+ * Returns `true` when the calling component is rendered inside a DesignCard.
16
+ * Useful for deriving a glassmorphic default.
17
+ */
18
+ export function useInsideDesignCard(): boolean {
19
+ return React.useContext(DesignCardNestingContext);
20
+ }
21
+
22
+ /**
23
+ * Resolve the effective glassmorphic value.
24
+ * - If the caller passed an explicit boolean → honour it.
25
+ * - Otherwise → fall back to whether we're inside a DesignCard.
26
+ */
27
+ export function useGlassmorphicDefault(explicit: boolean | undefined): boolean {
28
+ const insideCard = useInsideDesignCard();
29
+ return explicit ?? insideCard;
30
+ }
31
+
32
+ type DesignCardGradient = "blue" | "cyan" | "purple" | "green" | "orange" | "default";
33
+
34
+ const hoverTintClasses = new Map<DesignCardGradient, string>([
35
+ ["blue", "group-hover:bg-blue-500/[0.03]"],
36
+ ["purple", "group-hover:bg-purple-500/[0.03]"],
37
+ ["green", "group-hover:bg-emerald-500/[0.03]"],
38
+ ["orange", "group-hover:bg-orange-500/[0.03]"],
39
+ ["default", "group-hover:bg-slate-500/[0.02]"],
40
+ ["cyan", "group-hover:bg-cyan-500/[0.03]"],
41
+ ]);
42
+
43
+ const demoTintClasses = new Map<DesignCardGradient, string>([
44
+ ["blue", "group-hover/tint:bg-blue-500/[0.02]"],
45
+ ["purple", "group-hover/tint:bg-purple-500/[0.02]"],
46
+ ["green", "group-hover/tint:bg-emerald-500/[0.02]"],
47
+ ["orange", "group-hover/tint:bg-orange-500/[0.02]"],
48
+ ["default", "group-hover/tint:bg-slate-500/[0.015]"],
49
+ ["cyan", "group-hover/tint:bg-cyan-500/[0.02]"],
50
+ ]);
51
+
52
+ const bodyPaddingClass = "p-5";
53
+
54
+ // ─── Discriminated props ──────────────────────────────────────────────────
55
+ // - If title is given, icon is required.
56
+ // - The layout is derived automatically:
57
+ // title + subtitle → "header" (full header block with subtitle)
58
+ // title only → "compact" (slim bar with border-b)
59
+ // no title → "bodyOnly" (just the body)
60
+
61
+ type DesignCardBaseProps = {
62
+ glassmorphic?: boolean,
63
+ gradient?: DesignCardGradient,
64
+ contentClassName?: string,
65
+ } & Omit<React.ComponentProps<typeof Card>, "title">;
66
+
67
+ type WithTitleProps = {
68
+ title: React.ReactNode,
69
+ subtitle?: React.ReactNode,
70
+ icon: React.ElementType,
71
+ actions?: React.ReactNode,
72
+ };
73
+
74
+ type WithoutTitleProps = {
75
+ title?: never,
76
+ subtitle?: never,
77
+ icon?: never,
78
+ actions?: never,
79
+ };
80
+
81
+ export type DesignCardProps = DesignCardBaseProps & (WithTitleProps | WithoutTitleProps);
82
+
83
+ /**
84
+ * General-purpose card for grouping related content: section headers,
85
+ * description blocks, chart-less panels, etc. If the content is a
86
+ * big-number metric, use `DesignMetricCard` instead. If it contains a
87
+ * chart, wrap it in `DesignChartCard`.
88
+ *
89
+ * Two shapes, picked automatically by the props you pass:
90
+ *
91
+ * ```tsx
92
+ * // With a header (title requires icon):
93
+ * <DesignCard title="Recent activity" icon={ActivityIcon}>
94
+ * ...body...
95
+ * </DesignCard>
96
+ *
97
+ * // Body-only (no header):
98
+ * <DesignCard>
99
+ * <h2 className="text-2xl font-semibold">Dashboard</h2>
100
+ * <p className="text-muted-foreground">Overview of your user base</p>
101
+ * </DesignCard>
102
+ * ```
103
+ *
104
+ * Rules:
105
+ * - DO NOT add padding classes (`p-6`, `p-5`, etc.) to `className` — the
106
+ * component already has built-in padding and extra padding will look wrong.
107
+ * - If you set `title`, you MUST also set `icon`. The TS types enforce this,
108
+ * but writing `title` without `icon` will fail.
109
+ * - Body-only cards (no `title`) automatically go transparent in dark mode.
110
+ */
111
+ export function DesignCard({
112
+ title,
113
+ subtitle,
114
+ icon: Icon,
115
+ actions,
116
+ glassmorphic: glassmorphicProp,
117
+ gradient = "default",
118
+ children,
119
+ className,
120
+ contentClassName,
121
+ ...props
122
+ }: DesignCardProps) {
123
+ const glassmorphic = glassmorphicProp ?? true;
124
+ const hoverTintClass = hoverTintClasses.get(gradient) ?? "group-hover:bg-slate-500/[0.02]";
125
+ const hasContent = React.Children.count(children) > 0;
126
+
127
+ // Derive layout from which props were provided
128
+ const variant = title != null
129
+ ? (subtitle != null ? "header" : "compact")
130
+ : "bodyOnly";
131
+
132
+ return (
133
+ <DesignCardNestingContext.Provider value={true}>
134
+ <Card
135
+ className={cn(
136
+ "group relative rounded-2xl overflow-hidden",
137
+ glassmorphic && [
138
+ "bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl border-0 transition-all duration-150 hover:transition-none",
139
+ "ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1]",
140
+ "shadow-none",
141
+ ],
142
+ glassmorphic && variant === "bodyOnly" && "dark:bg-transparent dark:ring-0 dark:shadow-none",
143
+ className
144
+ )}
145
+ {...props}
146
+ >
147
+ {glassmorphic && (
148
+ <>
149
+ <div className={cn(
150
+ "absolute inset-0 bg-gradient-to-br from-foreground/[0.04] dark:from-foreground/[0.02] to-transparent pointer-events-none rounded-2xl",
151
+ variant === "bodyOnly" && "dark:hidden"
152
+ )} />
153
+ {variant !== "bodyOnly" && (
154
+ <div
155
+ className={cn(
156
+ "absolute inset-0 transition-colors duration-150 group-hover:transition-none pointer-events-none rounded-2xl",
157
+ hoverTintClass
158
+ )}
159
+ />
160
+ )}
161
+ </>
162
+ )}
163
+ <div className="relative">
164
+ {variant === "header" && (
165
+ <div className={bodyPaddingClass}>
166
+ <div className="flex items-start justify-between gap-4">
167
+ <div className="flex-1 min-w-0">
168
+ <div className="flex items-center gap-2">
169
+ {Icon && (
170
+ <div className="p-1.5 rounded-lg bg-foreground/[0.06] dark:bg-foreground/[0.04]">
171
+ <Icon className="h-3.5 w-3.5 text-foreground/70 dark:text-muted-foreground" />
172
+ </div>
173
+ )}
174
+ <span className="text-xs font-semibold text-foreground uppercase tracking-wider">
175
+ {title}
176
+ </span>
177
+ </div>
178
+ {subtitle && (
179
+ <p className="text-sm text-muted-foreground mt-1">
180
+ {subtitle}
181
+ </p>
182
+ )}
183
+ </div>
184
+ {actions && (
185
+ <div className="flex-shrink-0">
186
+ {actions}
187
+ </div>
188
+ )}
189
+ </div>
190
+ </div>
191
+ )}
192
+ {variant === "compact" && (
193
+ <div className="p-5 flex items-center justify-between gap-4 border-b border-black/[0.12] dark:border-white/[0.06]">
194
+ <div className="flex items-center gap-2 min-w-0">
195
+ {Icon && (
196
+ <div className="p-1.5 rounded-lg bg-foreground/[0.04]">
197
+ <Icon className="h-3.5 w-3.5 text-muted-foreground" />
198
+ </div>
199
+ )}
200
+ <span className="text-xs font-semibold text-foreground uppercase tracking-wider">
201
+ {title}
202
+ </span>
203
+ </div>
204
+ {actions && (
205
+ <div className="flex-shrink-0">
206
+ {actions}
207
+ </div>
208
+ )}
209
+ </div>
210
+ )}
211
+ {hasContent && (
212
+ <div
213
+ className={cn(
214
+ variant === "header" ? "border-t border-black/[0.12] dark:border-white/[0.06]" : "",
215
+ variant === "compact" ? "px-5 py-4" : "",
216
+ variant === "bodyOnly" || variant === "header" ? bodyPaddingClass : "",
217
+ contentClassName
218
+ )}
219
+ >
220
+ {children}
221
+ </div>
222
+ )}
223
+ </div>
224
+ </Card>
225
+ </DesignCardNestingContext.Provider>
226
+ );
227
+ }
228
+
229
+ export type DesignCardTintProps = {
230
+ gradient: DesignCardGradient,
231
+ } & React.ComponentProps<"div">
232
+
233
+ export function DesignCardTint({
234
+ gradient,
235
+ className,
236
+ children,
237
+ ...props
238
+ }: DesignCardTintProps) {
239
+ const tintClass = demoTintClasses.get(gradient) ?? "group-hover/tint:bg-slate-500/[0.015]";
240
+
241
+ return (
242
+ <div
243
+ className={cn(
244
+ "group/tint relative rounded-2xl bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl transition-all duration-150 hover:transition-none",
245
+ "ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1]",
246
+ "shadow-none overflow-hidden",
247
+ className
248
+ )}
249
+ {...props}
250
+ >
251
+ <div className="absolute inset-0 bg-gradient-to-br from-foreground/[0.04] dark:from-foreground/[0.02] to-transparent pointer-events-none rounded-2xl" />
252
+ <div
253
+ className={cn(
254
+ "absolute inset-0 transition-colors duration-150 group-hover/tint:transition-none pointer-events-none rounded-2xl",
255
+ tintClass
256
+ )}
257
+ />
258
+ <div className="relative">
259
+ {children}
260
+ </div>
261
+ </div>
262
+ );
263
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hexclave/ui";
4
+ import React from "react";
5
+
6
+ type DesignChartCardGradient = "blue" | "cyan" | "purple" | "green" | "orange" | "default";
7
+
8
+ const hoverTintClasses = new Map<DesignChartCardGradient, string>([
9
+ ["blue", "group-hover:bg-blue-500/[0.03]"],
10
+ ["cyan", "group-hover:bg-cyan-500/[0.03]"],
11
+ ["purple", "group-hover:bg-purple-500/[0.03]"],
12
+ ["green", "group-hover:bg-emerald-500/[0.03]"],
13
+ ["orange", "group-hover:bg-orange-500/[0.03]"],
14
+ ["default", "group-hover:bg-slate-500/[0.02]"],
15
+ ]);
16
+
17
+ export type DesignChartCardProps = {
18
+ gradient?: DesignChartCardGradient,
19
+ title?: React.ReactNode,
20
+ description?: React.ReactNode,
21
+ } & Omit<React.ComponentProps<"div">, "title">;
22
+
23
+ /**
24
+ * Card chrome (title + description + border) for a chart. Wrap every
25
+ * `AnalyticsChart` in this so the chart has context. Also used around raw
26
+ * Recharts components paired with `DesignChartContainer` for non-time-series
27
+ * fallbacks.
28
+ *
29
+ * ```tsx
30
+ * // Time-series chart (preferred):
31
+ * <DesignChartCard title="Signups" description="Last 30 days">
32
+ * <AnalyticsChart data={data} state={state} onChange={setState} />
33
+ * </DesignChartCard>
34
+ *
35
+ * // Non-time-series fallback (static ranking, distribution, etc.):
36
+ * <DesignChartCard title="Top referrers" description="This month">
37
+ * <DesignChartContainer config={chartConfig} maxHeight={300}>
38
+ * <Recharts.BarChart data={data}>
39
+ * <Recharts.CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
40
+ * <Recharts.XAxis dataKey="name" tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 12 }} />
41
+ * <Recharts.YAxis tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 12 }} />
42
+ * <Recharts.Tooltip content={<DesignChartTooltipContent />} />
43
+ * <Recharts.Bar dataKey="count" fill={getDesignChartColor(0)} radius={[4, 4, 0, 0]} />
44
+ * </Recharts.BarChart>
45
+ * </DesignChartContainer>
46
+ * </DesignChartCard>
47
+ * ```
48
+ *
49
+ * `chartConfig` for `DesignChartContainer` maps each `dataKey` to its label and color:
50
+ * `{ count: { label: "Count", color: getDesignChartColor(0) } }`.
51
+ */
52
+ export function DesignChartCard({
53
+ gradient = "default",
54
+ title,
55
+ description,
56
+ className,
57
+ children,
58
+ ...props
59
+ }: DesignChartCardProps) {
60
+ const hoverTintClass = hoverTintClasses.get(gradient) ?? "group-hover:bg-slate-500/[0.02]";
61
+
62
+ return (
63
+ <>
64
+ <style dangerouslySetInnerHTML={{ __html: `
65
+ .design-chart-card-tooltip-escape .recharts-tooltip-wrapper {
66
+ z-index: 9999 !important;
67
+ overflow: visible !important;
68
+ }
69
+ .design-chart-card-tooltip-escape .recharts-tooltip-wrapper > * {
70
+ overflow: visible !important;
71
+ }
72
+ ` }} />
73
+ <div
74
+ className={cn(
75
+ "group relative rounded-2xl bg-white/90 dark:bg-background/60 backdrop-blur-xl transition-all duration-150 hover:transition-none design-chart-card-tooltip-escape",
76
+ "ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1]",
77
+ "shadow-sm hover:shadow-md hover:z-10",
78
+ className
79
+ )}
80
+ {...props}
81
+ >
82
+ <div className="absolute inset-0 bg-gradient-to-br from-foreground/[0.04] dark:from-foreground/[0.02] to-transparent pointer-events-none rounded-2xl overflow-hidden" />
83
+ <div
84
+ className={cn(
85
+ "absolute inset-0 transition-colors duration-150 group-hover:transition-none pointer-events-none rounded-2xl overflow-hidden",
86
+ hoverTintClass
87
+ )}
88
+ />
89
+ <div className="relative h-full flex flex-col p-4">
90
+ {(title || description) && (
91
+ <div className="mb-3">
92
+ {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
93
+ {description && <p className="text-xs text-muted-foreground mt-0.5">{description}</p>}
94
+ </div>
95
+ )}
96
+ {children}
97
+ </div>
98
+ </div>
99
+ </>
100
+ );
101
+ }
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+ import { cn } from "@hexclave/ui";
6
+
7
+ const THEMES = { light: "", dark: ".dark" } as const;
8
+
9
+ export type DesignChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode,
12
+ icon?: React.ComponentType,
13
+ } & (
14
+ | { color?: string, theme?: never }
15
+ | { color?: never, theme: Record<keyof typeof THEMES, string> }
16
+ )
17
+ };
18
+
19
+ type ChartContextProps = {
20
+ config: DesignChartConfig,
21
+ };
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
24
+
25
+ export function useDesignChart() {
26
+ const context = React.useContext(ChartContext);
27
+ if (!context) {
28
+ throw new Error("useDesignChart must be used within a <DesignChartContainer />");
29
+ }
30
+ return context;
31
+ }
32
+
33
+ export function DesignChartStyle({ id, config }: { id: string, config: DesignChartConfig }) {
34
+ const colorConfig = Object.entries(config).filter(
35
+ ([_, cfg]) => cfg.theme || cfg.color
36
+ );
37
+
38
+ if (!colorConfig.length) {
39
+ return null;
40
+ }
41
+
42
+ return (
43
+ <style
44
+ dangerouslySetInnerHTML={{
45
+ __html: Object.entries(THEMES)
46
+ .map(
47
+ ([theme, prefix]) => `
48
+ ${prefix} [data-chart=${id}] {
49
+ ${colorConfig
50
+ .map(([key, itemConfig]) => {
51
+ const color =
52
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
53
+ itemConfig.color;
54
+ return color ? ` --color-${key}: ${color};` : null;
55
+ })
56
+ .join("\n")}
57
+ }
58
+ `
59
+ )
60
+ .join("\n"),
61
+ }}
62
+ />
63
+ );
64
+ }
65
+
66
+ export const DesignChartContainer = React.forwardRef<
67
+ HTMLDivElement,
68
+ React.ComponentProps<"div"> & {
69
+ maxHeight?: number,
70
+ config: DesignChartConfig,
71
+ children: React.ComponentProps<
72
+ typeof RechartsPrimitive.ResponsiveContainer
73
+ >["children"],
74
+ }
75
+ >(({ id, className, children, config, maxHeight, ...props }, ref) => {
76
+ const uniqueId = React.useId();
77
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
78
+
79
+ return (
80
+ <ChartContext.Provider value={{ config }}>
81
+ <div
82
+ data-chart={chartId}
83
+ ref={ref}
84
+ className={cn(
85
+ "flex aspect-video justify-center text-xs",
86
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
87
+ "[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-black/[0.06] dark:[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-white/[0.06]",
88
+ "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-black/[0.12] dark:[&_.recharts-curve.recharts-tooltip-cursor]:stroke-white/[0.12]",
89
+ "[&_.recharts-dot[stroke='#fff']]:stroke-transparent",
90
+ "[&_.recharts-layer]:outline-none",
91
+ "[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-black/[0.06] dark:[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-white/[0.06]",
92
+ "[&_.recharts-radial-bar-background-sector]:fill-black/[0.04] dark:[&_.recharts-radial-bar-background-sector]:fill-white/[0.04]",
93
+ "[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-black/[0.04] dark:[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-white/[0.04]",
94
+ "[&_.recharts-reference-line_[stroke='#ccc']]:stroke-black/[0.06] dark:[&_.recharts-reference-line_[stroke='#ccc']]:stroke-white/[0.06]",
95
+ "[&_.recharts-sector[stroke='#fff']]:stroke-transparent",
96
+ "[&_.recharts-sector]:outline-none",
97
+ "[&_.recharts-surface]:outline-none",
98
+ className
99
+ )}
100
+ {...props}
101
+ style={{
102
+ ...props.style,
103
+ maxHeight,
104
+ }}
105
+ >
106
+ <DesignChartStyle id={chartId} config={config} />
107
+ <RechartsPrimitive.ResponsiveContainer maxHeight={maxHeight}>
108
+ {children}
109
+ </RechartsPrimitive.ResponsiveContainer>
110
+ </div>
111
+ </ChartContext.Provider>
112
+ );
113
+ });
114
+ DesignChartContainer.displayName = "DesignChartContainer";
115
+
116
+ /**
117
+ * Helper to extract item config from a Recharts payload object.
118
+ */
119
+ export function getPayloadConfigFromPayload(
120
+ config: DesignChartConfig,
121
+ payload: unknown,
122
+ key: string,
123
+ ) {
124
+ if (typeof payload !== "object" || payload === null) {
125
+ return undefined;
126
+ }
127
+
128
+ const payloadPayload =
129
+ "payload" in payload &&
130
+ typeof payload.payload === "object" &&
131
+ payload.payload !== null
132
+ ? payload.payload
133
+ : undefined;
134
+
135
+ let configLabelKey: string = key;
136
+
137
+ if (
138
+ key in payload &&
139
+ typeof payload[key as keyof typeof payload] === "string"
140
+ ) {
141
+ configLabelKey = payload[key as keyof typeof payload] as string;
142
+ } else if (
143
+ payloadPayload &&
144
+ key in payloadPayload &&
145
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
146
+ ) {
147
+ configLabelKey = payloadPayload[
148
+ key as keyof typeof payloadPayload
149
+ ] as string;
150
+ }
151
+
152
+ return configLabelKey in config
153
+ ? config[configLabelKey]
154
+ : config[key as keyof typeof config];
155
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+ import { cn } from "@hexclave/ui";
6
+ import { useDesignChart, getPayloadConfigFromPayload } from "./chart-container";
7
+
8
+ export const DesignChartLegendContent = React.forwardRef<
9
+ HTMLDivElement,
10
+ React.ComponentProps<"div"> &
11
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
12
+ hideIcon?: boolean,
13
+ nameKey?: string,
14
+ }
15
+ >(
16
+ (
17
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
18
+ ref
19
+ ) => {
20
+ const { config } = useDesignChart();
21
+
22
+ if (!payload?.length) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <div
28
+ ref={ref}
29
+ className={cn(
30
+ "flex flex-wrap items-center justify-center gap-2",
31
+ verticalAlign === "top" ? "pb-3" : "pt-3",
32
+ className
33
+ )}
34
+ >
35
+ {payload.map((item) => {
36
+ const key = `${nameKey || item.dataKey || "value"}`;
37
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
38
+
39
+ return (
40
+ <div
41
+ key={item.value}
42
+ className={cn(
43
+ "flex items-center gap-1.5 rounded-full bg-foreground/[0.03] ring-1 ring-foreground/[0.06] px-3 py-1.5 text-xs",
44
+ "transition-colors duration-150 hover:transition-none hover:bg-foreground/[0.05]"
45
+ )}
46
+ >
47
+ {itemConfig?.icon && !hideIcon ? (
48
+ <itemConfig.icon />
49
+ ) : (
50
+ <span
51
+ className="h-2 w-2 shrink-0 rounded-full"
52
+ style={{ backgroundColor: item.color }}
53
+ />
54
+ )}
55
+ <span className="font-medium text-foreground">
56
+ {itemConfig?.label || item.value}
57
+ </span>
58
+ </div>
59
+ );
60
+ })}
61
+ </div>
62
+ );
63
+ }
64
+ );
65
+ DesignChartLegendContent.displayName = "DesignChartLegendContent";
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ export type DesignChartColorEntry = {
4
+ light: string,
5
+ dark: string,
6
+ };
7
+
8
+ /**
9
+ * Design-system-consistent chart colors that work in light and dark mode.
10
+ * Maps to the gradient system used across the dashboard design components.
11
+ */
12
+ export const DESIGN_CHART_COLORS: readonly DesignChartColorEntry[] = [
13
+ { light: "hsl(221, 83%, 53%)", dark: "hsl(217, 91%, 60%)" }, // blue
14
+ { light: "hsl(192, 91%, 36%)", dark: "hsl(188, 94%, 43%)" }, // cyan
15
+ { light: "hsl(271, 91%, 65%)", dark: "hsl(270, 95%, 75%)" }, // purple
16
+ { light: "hsl(160, 84%, 39%)", dark: "hsl(160, 84%, 45%)" }, // emerald/green
17
+ { light: "hsl(38, 92%, 50%)", dark: "hsl(38, 92%, 50%)" }, // amber/orange
18
+ { light: "hsl(0, 84%, 60%)", dark: "hsl(0, 84%, 65%)" }, // red
19
+ ] as const;
20
+
21
+ export type DesignChartColorName = "blue" | "cyan" | "purple" | "green" | "orange" | "red";
22
+
23
+ const colorNameIndexMap = new Map<DesignChartColorName, number>([
24
+ ["blue", 0],
25
+ ["cyan", 1],
26
+ ["purple", 2],
27
+ ["green", 3],
28
+ ["orange", 4],
29
+ ["red", 5],
30
+ ]);
31
+
32
+ /**
33
+ * Get a chart color by index (wraps around) or by name.
34
+ */
35
+ export function getDesignChartColor(
36
+ indexOrName: number | DesignChartColorName,
37
+ mode: "light" | "dark" = "dark",
38
+ ): string {
39
+ const index = typeof indexOrName === "string"
40
+ ? colorNameIndexMap.get(indexOrName) ?? 0
41
+ : indexOrName % DESIGN_CHART_COLORS.length;
42
+ return DESIGN_CHART_COLORS[index][mode];
43
+ }
44
+
45
+ /**
46
+ * Recharts-compatible grid/axis styling constants that match the design system.
47
+ */
48
+ export const DESIGN_CHART_GRID_COLOR = "hsl(0 0% 50% / 0.12)";
49
+ export const DESIGN_CHART_AXIS_TICK_STYLE = {
50
+ fill: "hsl(0 0% 50% / 0.5)",
51
+ fontSize: 11,
52
+ } as const;