@apptimate/ui 1.2.0 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import {
5
+ AreaChart,
6
+ Area,
7
+ XAxis,
8
+ YAxis,
9
+ CartesianGrid,
10
+ Tooltip,
11
+ Legend,
12
+ ResponsiveContainer,
13
+ } from "recharts";
14
+ import {
15
+ CHART_COLORS,
16
+ CHART_GRADIENTS,
17
+ TOOLTIP_STYLE,
18
+ TOOLTIP_ITEM_STYLE,
19
+ AXIS_TICK_STYLE,
20
+ GRID_STROKE,
21
+ ANIMATION_DURATION,
22
+ ANIMATION_EASING,
23
+ } from "./palette";
24
+
25
+ export interface RAreaChartSeries {
26
+ dataKey: string;
27
+ name?: string;
28
+ color?: string;
29
+ strokeWidth?: number;
30
+ type?: "monotone" | "linear" | "step" | "basis";
31
+ stackId?: string;
32
+ fillOpacity?: number;
33
+ }
34
+
35
+ export interface RAreaChartProps {
36
+ data: Record<string, any>[];
37
+ series: RAreaChartSeries[];
38
+ xAxisKey: string;
39
+ height?: number;
40
+ colors?: string[];
41
+ showGrid?: boolean;
42
+ showTooltip?: boolean;
43
+ showLegend?: boolean;
44
+ legendPosition?: "top" | "bottom";
45
+ xAxisFormatter?: (value: any) => string;
46
+ yAxisFormatter?: (value: any) => string;
47
+ tooltipFormatter?: (value: any, name: string) => [string, string];
48
+ className?: string;
49
+ }
50
+
51
+ export function RAreaChart({
52
+ data,
53
+ series,
54
+ xAxisKey,
55
+ height = 350,
56
+ colors = [...CHART_COLORS],
57
+ showGrid = true,
58
+ showTooltip = true,
59
+ showLegend = true,
60
+ legendPosition = "bottom",
61
+ xAxisFormatter,
62
+ yAxisFormatter,
63
+ tooltipFormatter,
64
+ className = "",
65
+ }: RAreaChartProps) {
66
+ const gradientId = React.useId();
67
+
68
+ return (
69
+ <div className={`w-full ${className}`} style={{ height }}>
70
+ <ResponsiveContainer width="100%" height="100%">
71
+ <AreaChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 10 }}>
72
+ <defs>
73
+ {series.map((s, idx) => {
74
+ const gradientPair = CHART_GRADIENTS[idx % CHART_GRADIENTS.length];
75
+ const id = `area-gradient-${gradientId}-${idx}`;
76
+ return (
77
+ <linearGradient key={id} id={id} x1="0" y1="0" x2="0" y2="1">
78
+ <stop offset="0%" stopColor={s.color || gradientPair[0]} stopOpacity={0.4} />
79
+ <stop offset="50%" stopColor={s.color || gradientPair[1]} stopOpacity={0.1} />
80
+ <stop offset="100%" stopColor={s.color || gradientPair[1]} stopOpacity={0} />
81
+ </linearGradient>
82
+ );
83
+ })}
84
+ </defs>
85
+
86
+ {showGrid && (
87
+ <CartesianGrid stroke={GRID_STROKE} strokeDasharray="0" />
88
+ )}
89
+ <XAxis
90
+ dataKey={xAxisKey}
91
+ tickFormatter={xAxisFormatter}
92
+ tick={AXIS_TICK_STYLE}
93
+ tickLine={false}
94
+ axisLine={false}
95
+ />
96
+ <YAxis
97
+ tickFormatter={yAxisFormatter}
98
+ tick={AXIS_TICK_STYLE}
99
+ tickLine={false}
100
+ axisLine={false}
101
+ />
102
+ {showTooltip && (
103
+ <Tooltip
104
+ formatter={tooltipFormatter}
105
+ contentStyle={TOOLTIP_STYLE}
106
+ itemStyle={TOOLTIP_ITEM_STYLE}
107
+ animationDuration={300}
108
+ />
109
+ )}
110
+ {showLegend && (
111
+ <Legend
112
+ verticalAlign={legendPosition}
113
+ wrapperStyle={{ fontSize: "12px", fontWeight: 700, paddingTop: "8px" }}
114
+ iconType="circle"
115
+ iconSize={8}
116
+ />
117
+ )}
118
+ {series.map((s, idx) => (
119
+ <Area
120
+ key={s.dataKey}
121
+ type={s.type || "monotone"}
122
+ dataKey={s.dataKey}
123
+ name={s.name || s.dataKey}
124
+ stroke={s.color || colors[idx % colors.length]}
125
+ strokeWidth={s.strokeWidth || 2.5}
126
+ fill={`url(#area-gradient-${gradientId}-${idx})`}
127
+ fillOpacity={s.fillOpacity ?? 1}
128
+ stackId={s.stackId}
129
+ activeDot={{ r: 5, fill: "#fff", stroke: s.color || colors[idx % colors.length], strokeWidth: 2.5 }}
130
+ isAnimationActive={true}
131
+ animationDuration={ANIMATION_DURATION}
132
+ animationEasing={ANIMATION_EASING}
133
+ />
134
+ ))}
135
+ </AreaChart>
136
+ </ResponsiveContainer>
137
+ </div>
138
+ );
139
+ }
@@ -11,7 +11,16 @@ import {
11
11
  Legend,
12
12
  ResponsiveContainer,
13
13
  } from "recharts";
14
- import { CHART_COLORS } from "./palette";
14
+ import {
15
+ CHART_COLORS,
16
+ CHART_GRADIENTS,
17
+ TOOLTIP_STYLE,
18
+ TOOLTIP_ITEM_STYLE,
19
+ AXIS_TICK_STYLE,
20
+ GRID_STROKE,
21
+ ANIMATION_DURATION,
22
+ ANIMATION_EASING,
23
+ } from "./palette";
15
24
 
16
25
  export interface RBarChartSeries {
17
26
  dataKey: string;
@@ -36,6 +45,7 @@ export interface RBarChartProps {
36
45
  xAxisFormatter?: (value: any) => string;
37
46
  yAxisFormatter?: (value: any) => string;
38
47
  tooltipFormatter?: (value: any, name: string) => [string, string];
48
+ enableGradient?: boolean;
39
49
  className?: string;
40
50
  }
41
51
 
@@ -54,27 +64,46 @@ export function RBarChart({
54
64
  xAxisFormatter,
55
65
  yAxisFormatter,
56
66
  tooltipFormatter,
67
+ enableGradient = true,
57
68
  className = "",
58
69
  }: RBarChartProps) {
70
+ const gradientId = React.useId();
71
+
59
72
  return (
60
73
  <div className={`w-full ${className}`} style={{ height }}>
61
74
  <ResponsiveContainer width="100%" height="100%">
62
75
  <BarChart data={data} layout={layout} margin={{ top: 10, right: 10, left: 0, bottom: 10 }}>
76
+ {/* Gradient definitions */}
77
+ {enableGradient && (
78
+ <defs>
79
+ {series.map((s, idx) => {
80
+ const gradientPair = CHART_GRADIENTS[idx % CHART_GRADIENTS.length];
81
+ const id = `bar-gradient-${gradientId}-${idx}`;
82
+ return (
83
+ <linearGradient key={id} id={id} x1="0" y1="0" x2="0" y2="1">
84
+ <stop offset="0%" stopColor={s.color || gradientPair[0]} stopOpacity={1} />
85
+ <stop offset="100%" stopColor={s.color || gradientPair[1]} stopOpacity={0.7} />
86
+ </linearGradient>
87
+ );
88
+ })}
89
+ </defs>
90
+ )}
91
+
63
92
  {showGrid && (
64
- <CartesianGrid stroke="#f1f5f9" strokeDasharray="0" />
93
+ <CartesianGrid stroke={GRID_STROKE} strokeDasharray="0" />
65
94
  )}
66
95
  {layout === "horizontal" ? (
67
96
  <>
68
97
  <XAxis
69
98
  dataKey={xAxisKey}
70
99
  tickFormatter={xAxisFormatter}
71
- tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
100
+ tick={AXIS_TICK_STYLE}
72
101
  tickLine={false}
73
102
  axisLine={false}
74
103
  />
75
104
  <YAxis
76
105
  tickFormatter={yAxisFormatter}
77
- tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
106
+ tick={AXIS_TICK_STYLE}
78
107
  tickLine={false}
79
108
  axisLine={false}
80
109
  />
@@ -84,7 +113,7 @@ export function RBarChart({
84
113
  <XAxis
85
114
  type="number"
86
115
  tickFormatter={xAxisFormatter}
87
- tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
116
+ tick={AXIS_TICK_STYLE}
88
117
  tickLine={false}
89
118
  axisLine={false}
90
119
  />
@@ -92,7 +121,7 @@ export function RBarChart({
92
121
  type="category"
93
122
  dataKey={xAxisKey}
94
123
  tickFormatter={yAxisFormatter}
95
- tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
124
+ tick={AXIS_TICK_STYLE}
96
125
  tickLine={false}
97
126
  axisLine={false}
98
127
  />
@@ -101,26 +130,22 @@ export function RBarChart({
101
130
  {showTooltip && (
102
131
  <Tooltip
103
132
  formatter={tooltipFormatter}
104
- contentStyle={{
105
- backgroundColor: "#1e293b",
106
- color: "#fff",
107
- borderRadius: "8px",
108
- border: "none",
109
- }}
110
- itemStyle={{ color: "#fff" }}
111
- cursor={{ fill: "transparent" }}
112
- animationDuration={1000}
133
+ contentStyle={TOOLTIP_STYLE}
134
+ itemStyle={TOOLTIP_ITEM_STYLE}
135
+ cursor={{ fill: "rgba(99,102,241,0.06)" }}
136
+ animationDuration={300}
113
137
  />
114
138
  )}
115
139
  {showLegend && (
116
140
  <Legend
117
141
  verticalAlign={legendPosition}
118
- wrapperStyle={{ fontSize: "12px", fontWeight: 700 }}
142
+ wrapperStyle={{ fontSize: "12px", fontWeight: 700, paddingTop: "8px" }}
119
143
  iconType="circle"
144
+ iconSize={8}
120
145
  />
121
146
  )}
122
147
  {series.map((s, idx) => {
123
- const radius = s.radius !== undefined ? s.radius : 4;
148
+ const radius = s.radius !== undefined ? s.radius : 6;
124
149
  const radiusArray =
125
150
  layout === "vertical" ? [0, radius, radius, 0] : [radius, radius, 0, 0];
126
151
 
@@ -129,12 +154,13 @@ export function RBarChart({
129
154
  key={s.dataKey}
130
155
  dataKey={s.dataKey}
131
156
  name={s.name || s.dataKey}
132
- fill={s.color || colors[idx % colors.length]}
157
+ fill={enableGradient ? `url(#bar-gradient-${gradientId}-${idx})` : (s.color || colors[idx % colors.length])}
133
158
  stackId={s.stackId}
134
159
  barSize={barSize}
135
160
  radius={radiusArray as any}
136
161
  isAnimationActive={true}
137
- animationDuration={1000}
162
+ animationDuration={ANIMATION_DURATION}
163
+ animationEasing={ANIMATION_EASING}
138
164
  />
139
165
  );
140
166
  })}
@@ -11,7 +11,16 @@ import {
11
11
  Legend,
12
12
  ResponsiveContainer,
13
13
  } from "recharts";
14
- import { CHART_COLORS } from "./palette";
14
+ import {
15
+ CHART_COLORS,
16
+ CHART_GRADIENTS,
17
+ TOOLTIP_STYLE,
18
+ TOOLTIP_ITEM_STYLE,
19
+ AXIS_TICK_STYLE,
20
+ GRID_STROKE,
21
+ ANIMATION_DURATION,
22
+ ANIMATION_EASING,
23
+ } from "./palette";
15
24
 
16
25
  export interface RLineChartSeries {
17
26
  dataKey: string;
@@ -36,9 +45,24 @@ export interface RLineChartProps {
36
45
  xAxisFormatter?: (value: any) => string;
37
46
  yAxisFormatter?: (value: any) => string;
38
47
  tooltipFormatter?: (value: any, name: string) => [string, string];
48
+ enableGradient?: boolean;
39
49
  className?: string;
40
50
  }
41
51
 
52
+ // Pulsing active dot component
53
+ function PulsingActiveDot(props: any) {
54
+ const { cx, cy, fill } = props;
55
+ return (
56
+ <g>
57
+ <circle cx={cx} cy={cy} r={8} fill={fill} opacity={0.15}>
58
+ <animate attributeName="r" values="8;14;8" dur="2s" repeatCount="indefinite" />
59
+ <animate attributeName="opacity" values="0.15;0.05;0.15" dur="2s" repeatCount="indefinite" />
60
+ </circle>
61
+ <circle cx={cx} cy={cy} r={5} fill="#fff" stroke={fill} strokeWidth={2.5} />
62
+ </g>
63
+ );
64
+ }
65
+
42
66
  export function RLineChart({
43
67
  data,
44
68
  series,
@@ -52,46 +76,61 @@ export function RLineChart({
52
76
  xAxisFormatter,
53
77
  yAxisFormatter,
54
78
  tooltipFormatter,
79
+ enableGradient = true,
55
80
  className = "",
56
81
  }: RLineChartProps) {
82
+ const gradientId = React.useId();
83
+
57
84
  return (
58
85
  <div className={`w-full ${className}`} style={{ height }}>
59
86
  <ResponsiveContainer width="100%" height="100%">
60
87
  <LineChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 10 }}>
88
+ {/* Gradient definitions for area fill beneath lines */}
89
+ {enableGradient && (
90
+ <defs>
91
+ {series.map((s, idx) => {
92
+ const gradientPair = CHART_GRADIENTS[idx % CHART_GRADIENTS.length];
93
+ const id = `line-gradient-${gradientId}-${idx}`;
94
+ return (
95
+ <linearGradient key={id} id={id} x1="0" y1="0" x2="0" y2="1">
96
+ <stop offset="0%" stopColor={s.color || gradientPair[0]} stopOpacity={0.3} />
97
+ <stop offset="100%" stopColor={s.color || gradientPair[1]} stopOpacity={0} />
98
+ </linearGradient>
99
+ );
100
+ })}
101
+ </defs>
102
+ )}
103
+
61
104
  {showGrid && (
62
- <CartesianGrid stroke="#f1f5f9" strokeDasharray="0" />
105
+ <CartesianGrid stroke={GRID_STROKE} strokeDasharray="0" />
63
106
  )}
64
107
  <XAxis
65
108
  dataKey={xAxisKey}
66
109
  tickFormatter={xAxisFormatter}
67
- tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
110
+ tick={AXIS_TICK_STYLE}
68
111
  tickLine={false}
69
112
  axisLine={false}
70
113
  />
71
114
  <YAxis
72
115
  tickFormatter={yAxisFormatter}
73
- tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
116
+ tick={AXIS_TICK_STYLE}
74
117
  tickLine={false}
75
118
  axisLine={false}
76
119
  />
77
120
  {showTooltip && (
78
121
  <Tooltip
79
122
  formatter={tooltipFormatter}
80
- contentStyle={{
81
- backgroundColor: "#1e293b",
82
- color: "#fff",
83
- borderRadius: "8px",
84
- border: "none",
85
- }}
86
- itemStyle={{ color: "#fff" }}
87
- animationDuration={1000}
123
+ contentStyle={TOOLTIP_STYLE}
124
+ itemStyle={TOOLTIP_ITEM_STYLE}
125
+ animationDuration={300}
88
126
  />
89
127
  )}
90
128
  {showLegend && (
91
129
  <Legend
92
130
  verticalAlign={legendPosition}
93
- wrapperStyle={{ fontSize: "12px", fontWeight: 700 }}
131
+ wrapperStyle={{ fontSize: "12px", fontWeight: 700, paddingTop: "8px" }}
94
132
  iconType="circle"
133
+ iconSize={8}
95
134
  />
96
135
  )}
97
136
  {series.map((s, idx) => (
@@ -101,12 +140,13 @@ export function RLineChart({
101
140
  dataKey={s.dataKey}
102
141
  name={s.name || s.dataKey}
103
142
  stroke={s.color || colors[idx % colors.length]}
104
- strokeWidth={s.strokeWidth || 2}
143
+ strokeWidth={s.strokeWidth || 2.5}
105
144
  strokeDasharray={s.dashed ? "5 5" : undefined}
106
- dot={s.showDot ? { r: 4 } : false}
107
- activeDot={{ r: 6 }}
145
+ dot={s.showDot ? { r: 3, fill: "#fff", stroke: s.color || colors[idx % colors.length], strokeWidth: 2 } : false}
146
+ activeDot={<PulsingActiveDot fill={s.color || colors[idx % colors.length]} />}
108
147
  isAnimationActive={true}
109
- animationDuration={1000}
148
+ animationDuration={ANIMATION_DURATION}
149
+ animationEasing={ANIMATION_EASING}
110
150
  />
111
151
  ))}
112
152
  </LineChart>
@@ -0,0 +1,215 @@
1
+ "use client";
2
+
3
+ import React, { useState, useCallback } from "react";
4
+ import {
5
+ PieChart,
6
+ Pie,
7
+ Cell,
8
+ Tooltip,
9
+ Legend,
10
+ ResponsiveContainer,
11
+ Sector,
12
+ } from "recharts";
13
+ import {
14
+ CHART_COLORS,
15
+ CHART_GRADIENTS,
16
+ TOOLTIP_STYLE,
17
+ TOOLTIP_ITEM_STYLE,
18
+ ANIMATION_DURATION,
19
+ ANIMATION_EASING,
20
+ } from "./palette";
21
+
22
+ export interface RPieChartProps {
23
+ data: Record<string, any>[];
24
+ dataKey: string;
25
+ nameKey: string;
26
+ height?: number;
27
+ colors?: string[];
28
+ innerRadius?: number | string;
29
+ outerRadius?: number | string;
30
+ showTooltip?: boolean;
31
+ showLegend?: boolean;
32
+ legendPosition?: "top" | "bottom" | "right";
33
+ showLabels?: boolean;
34
+ labelType?: "percentage" | "value" | "name";
35
+ centerLabel?: string;
36
+ centerValue?: string | number;
37
+ enableGradient?: boolean;
38
+ tooltipFormatter?: (value: any, name: string) => [string, string];
39
+ className?: string;
40
+ }
41
+
42
+ // Custom active shape for hover-expand effect
43
+ function renderActiveShape(props: any) {
44
+ const {
45
+ cx, cy, innerRadius, outerRadius, startAngle, endAngle,
46
+ fill, payload, percent, value, nameKey, dataKey,
47
+ } = props;
48
+
49
+ return (
50
+ <g>
51
+ <Sector
52
+ cx={cx}
53
+ cy={cy}
54
+ innerRadius={innerRadius - 2}
55
+ outerRadius={outerRadius + 8}
56
+ startAngle={startAngle}
57
+ endAngle={endAngle}
58
+ fill={fill}
59
+ opacity={1}
60
+ style={{ filter: "drop-shadow(0 4px 12px rgba(0,0,0,0.15))", transition: "all 0.3s ease" }}
61
+ />
62
+ <Sector
63
+ cx={cx}
64
+ cy={cy}
65
+ innerRadius={innerRadius}
66
+ outerRadius={outerRadius}
67
+ startAngle={startAngle}
68
+ endAngle={endAngle}
69
+ fill={fill}
70
+ />
71
+ </g>
72
+ );
73
+ }
74
+
75
+ // Percentage label renderer
76
+ function renderLabel(props: any) {
77
+ const { cx, cy, midAngle, innerRadius, outerRadius, percent, name, labelType, value } = props;
78
+ const RADIAN = Math.PI / 180;
79
+ const radius = outerRadius + 20;
80
+ const x = cx + radius * Math.cos(-midAngle * RADIAN);
81
+ const y = cy + radius * Math.sin(-midAngle * RADIAN);
82
+
83
+ if (percent < 0.04) return null; // Hide labels for tiny slices
84
+
85
+ let displayText = `${(percent * 100).toFixed(0)}%`;
86
+ if (labelType === "value") displayText = `${value}`;
87
+ if (labelType === "name") displayText = name;
88
+
89
+ return (
90
+ <text
91
+ x={x}
92
+ y={y}
93
+ fill="#64748b"
94
+ textAnchor={x > cx ? "start" : "end"}
95
+ dominantBaseline="central"
96
+ fontSize={11}
97
+ fontWeight={600}
98
+ >
99
+ {displayText}
100
+ </text>
101
+ );
102
+ }
103
+
104
+ export function RPieChart({
105
+ data,
106
+ dataKey,
107
+ nameKey,
108
+ height = 350,
109
+ colors = [...CHART_COLORS],
110
+ innerRadius = "55%",
111
+ outerRadius = "80%",
112
+ showTooltip = true,
113
+ showLegend = true,
114
+ legendPosition = "bottom",
115
+ showLabels = false,
116
+ labelType = "percentage",
117
+ centerLabel,
118
+ centerValue,
119
+ enableGradient = true,
120
+ tooltipFormatter,
121
+ className = "",
122
+ }: RPieChartProps) {
123
+ const [activeIndex, setActiveIndex] = useState<number | undefined>(undefined);
124
+ const gradientId = React.useId();
125
+
126
+ const onMouseEnter = useCallback((_: any, index: number) => {
127
+ setActiveIndex(index);
128
+ }, []);
129
+
130
+ const onMouseLeave = useCallback(() => {
131
+ setActiveIndex(undefined);
132
+ }, []);
133
+
134
+ return (
135
+ <div className={`w-full relative ${className}`} style={{ height }}>
136
+ <ResponsiveContainer width="100%" height="100%">
137
+ <PieChart>
138
+ {enableGradient && (
139
+ <defs>
140
+ {data.map((_, idx) => {
141
+ const gradientPair = CHART_GRADIENTS[idx % CHART_GRADIENTS.length];
142
+ const id = `pie-gradient-${gradientId}-${idx}`;
143
+ return (
144
+ <linearGradient key={id} id={id} x1="0" y1="0" x2="1" y2="1">
145
+ <stop offset="0%" stopColor={gradientPair[0]} stopOpacity={1} />
146
+ <stop offset="100%" stopColor={gradientPair[1]} stopOpacity={0.85} />
147
+ </linearGradient>
148
+ );
149
+ })}
150
+ </defs>
151
+ )}
152
+ <Pie
153
+ data={data}
154
+ dataKey={dataKey}
155
+ nameKey={nameKey}
156
+ cx="50%"
157
+ cy="50%"
158
+ innerRadius={innerRadius}
159
+ outerRadius={outerRadius}
160
+ paddingAngle={2}
161
+ activeIndex={activeIndex}
162
+ activeShape={renderActiveShape}
163
+ onMouseEnter={onMouseEnter}
164
+ onMouseLeave={onMouseLeave}
165
+ isAnimationActive={true}
166
+ animationDuration={ANIMATION_DURATION}
167
+ animationEasing={ANIMATION_EASING}
168
+ label={showLabels ? (props: any) => renderLabel({ ...props, labelType }) : undefined}
169
+ >
170
+ {data.map((_, idx) => (
171
+ <Cell
172
+ key={`cell-${idx}`}
173
+ fill={enableGradient ? `url(#pie-gradient-${gradientId}-${idx})` : colors[idx % colors.length]}
174
+ stroke="none"
175
+ />
176
+ ))}
177
+ </Pie>
178
+ {showTooltip && (
179
+ <Tooltip
180
+ formatter={tooltipFormatter}
181
+ contentStyle={TOOLTIP_STYLE}
182
+ itemStyle={TOOLTIP_ITEM_STYLE}
183
+ animationDuration={300}
184
+ />
185
+ )}
186
+ {showLegend && (
187
+ <Legend
188
+ verticalAlign={legendPosition === "right" ? "middle" : legendPosition}
189
+ align={legendPosition === "right" ? "right" : "center"}
190
+ layout={legendPosition === "right" ? "vertical" : "horizontal"}
191
+ wrapperStyle={{ fontSize: "12px", fontWeight: 700, paddingTop: legendPosition === "bottom" ? "8px" : "0" }}
192
+ iconType="circle"
193
+ iconSize={8}
194
+ />
195
+ )}
196
+ </PieChart>
197
+ </ResponsiveContainer>
198
+
199
+ {/* Center label for donut charts */}
200
+ {(centerLabel || centerValue) && (
201
+ <div
202
+ className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none"
203
+ style={{ paddingBottom: showLegend && legendPosition === "bottom" ? "40px" : "0" }}
204
+ >
205
+ {centerValue && (
206
+ <span className="text-2xl font-bold text-[#2D3142] tracking-tight">{centerValue}</span>
207
+ )}
208
+ {centerLabel && (
209
+ <span className="text-xs font-semibold text-gray-400 uppercase tracking-wider">{centerLabel}</span>
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+ }
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState, useRef } from "react";
4
+ import { TrendingUp, TrendingDown, Minus } from "lucide-react";
5
+ import { SEMANTIC_COLORS } from "./palette";
6
+
7
+ export interface RStatCardProps {
8
+ label: string;
9
+ value: number | string;
10
+ prefix?: string;
11
+ suffix?: string;
12
+ decimals?: number;
13
+ trend?: "up" | "down" | "neutral";
14
+ trendValue?: string;
15
+ subtitle?: string;
16
+ icon?: React.ReactNode;
17
+ color?: string;
18
+ animate?: boolean;
19
+ className?: string;
20
+ }
21
+
22
+ // Animated count-up hook
23
+ function useCountUp(target: number, duration: number = 1000, enabled: boolean = true) {
24
+ const [current, setCurrent] = useState(0);
25
+ const frameRef = useRef<number | null>(null);
26
+
27
+ useEffect(() => {
28
+ if (!enabled || typeof target !== "number" || isNaN(target)) {
29
+ setCurrent(target);
30
+ return;
31
+ }
32
+
33
+ const startTime = performance.now();
34
+ const startValue = 0;
35
+
36
+ function tick(now: number) {
37
+ const elapsed = now - startTime;
38
+ const progress = Math.min(elapsed / duration, 1);
39
+ // Ease-out cubic
40
+ const eased = 1 - Math.pow(1 - progress, 3);
41
+ setCurrent(startValue + (target - startValue) * eased);
42
+
43
+ if (progress < 1) {
44
+ frameRef.current = requestAnimationFrame(tick);
45
+ }
46
+ }
47
+
48
+ frameRef.current = requestAnimationFrame(tick);
49
+ return () => {
50
+ if (frameRef.current) cancelAnimationFrame(frameRef.current);
51
+ };
52
+ }, [target, duration, enabled]);
53
+
54
+ return current;
55
+ }
56
+
57
+ function TrendIndicator({ trend, trendValue }: { trend: "up" | "down" | "neutral"; trendValue?: string }) {
58
+ const config = {
59
+ up: {
60
+ Icon: TrendingUp,
61
+ color: SEMANTIC_COLORS.success,
62
+ bg: "rgba(34,197,94,0.1)",
63
+ },
64
+ down: {
65
+ Icon: TrendingDown,
66
+ color: SEMANTIC_COLORS.danger,
67
+ bg: "rgba(239,68,68,0.1)",
68
+ },
69
+ neutral: {
70
+ Icon: Minus,
71
+ color: SEMANTIC_COLORS.neutral,
72
+ bg: "rgba(100,116,139,0.1)",
73
+ },
74
+ };
75
+
76
+ const { Icon, color, bg } = config[trend];
77
+
78
+ return (
79
+ <div
80
+ className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-bold"
81
+ style={{ backgroundColor: bg, color }}
82
+ >
83
+ <Icon size={12} strokeWidth={2.5} />
84
+ {trendValue && <span>{trendValue}</span>}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export function RStatCard({
90
+ label,
91
+ value,
92
+ prefix = "",
93
+ suffix = "",
94
+ decimals = 0,
95
+ trend,
96
+ trendValue,
97
+ subtitle,
98
+ icon,
99
+ color,
100
+ animate = true,
101
+ className = "",
102
+ }: RStatCardProps) {
103
+ const isNumeric = typeof value === "number" && !isNaN(value);
104
+ const animatedValue = useCountUp(isNumeric ? value : 0, 1200, animate && isNumeric);
105
+
106
+ const displayValue = isNumeric
107
+ ? `${prefix}${animatedValue.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}${suffix}`
108
+ : `${prefix}${value}${suffix}`;
109
+
110
+ return (
111
+ <div className={`flex flex-col gap-2 p-4 rounded-xl bg-white border border-gray-100 hover:border-gray-200 transition-all duration-200 hover:shadow-sm ${className}`}>
112
+ <div className="flex items-center justify-between">
113
+ <span className="text-xs font-semibold text-gray-400 uppercase tracking-wider">{label}</span>
114
+ {icon && (
115
+ <div
116
+ className="w-8 h-8 rounded-lg flex items-center justify-center"
117
+ style={{ backgroundColor: color ? `${color}15` : "rgba(99,102,241,0.08)" }}
118
+ >
119
+ {React.isValidElement(icon)
120
+ ? React.cloneElement(icon as React.ReactElement, { size: 16, style: { color: color || "#6366f1" } } as any)
121
+ : icon}
122
+ </div>
123
+ )}
124
+ </div>
125
+
126
+ <div className="flex items-end gap-2">
127
+ <span className="text-2xl font-bold text-[#2D3142] tracking-tight leading-none">
128
+ {displayValue}
129
+ </span>
130
+ {trend && <TrendIndicator trend={trend} trendValue={trendValue} />}
131
+ </div>
132
+
133
+ {subtitle && (
134
+ <span className="text-[11px] font-medium text-gray-400 leading-tight">{subtitle}</span>
135
+ )}
136
+ </div>
137
+ );
138
+ }
@@ -1,3 +1,7 @@
1
1
  export * from "./ApexCharts";
2
2
  export * from "./RLineChart";
3
3
  export * from "./RBarChart";
4
+ export * from "./RPieChart";
5
+ export * from "./RAreaChart";
6
+ export * from "./RStatCard";
7
+ export * from "./palette";
@@ -1,3 +1,7 @@
1
+ // ── Chart Color Palette ──────────────────────────────────────────────────────
2
+ // 16 curated colors with gradient pairs for premium chart aesthetics.
3
+ // Organized into primary rotation + semantic sets.
4
+
1
5
  export const CHART_COLORS = [
2
6
  '#6366f1', // Indigo
3
7
  '#22c55e', // Green
@@ -7,4 +11,74 @@ export const CHART_COLORS = [
7
11
  '#8b5cf6', // Violet
8
12
  '#ec4899', // Pink
9
13
  '#14b8a6', // Teal
14
+ '#f97316', // Orange
15
+ '#3b82f6', // Blue
16
+ '#a855f7', // Purple
17
+ '#10b981', // Emerald
18
+ '#e11d48', // Rose
19
+ '#0ea5e9', // Sky
20
+ '#84cc16', // Lime
21
+ '#d946ef', // Fuchsia
10
22
  ] as const;
23
+
24
+ // Gradient pairs: [from, to] for gradient fills on charts
25
+ export const CHART_GRADIENTS: [string, string][] = [
26
+ ['#6366f1', '#818cf8'], // Indigo
27
+ ['#22c55e', '#4ade80'], // Green
28
+ ['#f59e0b', '#fbbf24'], // Amber
29
+ ['#ef4444', '#f87171'], // Red
30
+ ['#06b6d4', '#22d3ee'], // Cyan
31
+ ['#8b5cf6', '#a78bfa'], // Violet
32
+ ['#ec4899', '#f472b6'], // Pink
33
+ ['#14b8a6', '#2dd4bf'], // Teal
34
+ ['#f97316', '#fb923c'], // Orange
35
+ ['#3b82f6', '#60a5fa'], // Blue
36
+ ['#a855f7', '#c084fc'], // Purple
37
+ ['#10b981', '#34d399'], // Emerald
38
+ ['#e11d48', '#fb7185'], // Rose
39
+ ['#0ea5e9', '#38bdf8'], // Sky
40
+ ['#84cc16', '#a3e635'], // Lime
41
+ ['#d946ef', '#e879f9'], // Fuchsia
42
+ ];
43
+
44
+ // Semantic color sets for specific use-cases
45
+ export const SEMANTIC_COLORS = {
46
+ success: '#22c55e',
47
+ successLight: '#4ade80',
48
+ warning: '#f59e0b',
49
+ warningLight: '#fbbf24',
50
+ danger: '#ef4444',
51
+ dangerLight: '#f87171',
52
+ info: '#3b82f6',
53
+ infoLight: '#60a5fa',
54
+ neutral: '#64748b',
55
+ neutralLight: '#94a3b8',
56
+ } as const;
57
+
58
+ // Tooltip styling constants (shared across all chart components)
59
+ export const TOOLTIP_STYLE = {
60
+ backgroundColor: '#1e293b',
61
+ color: '#fff',
62
+ borderRadius: '10px',
63
+ border: 'none',
64
+ boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
65
+ padding: '10px 14px',
66
+ fontSize: '12px',
67
+ fontWeight: 600,
68
+ } as const;
69
+
70
+ export const TOOLTIP_ITEM_STYLE = {
71
+ color: '#cbd5e1',
72
+ fontSize: '12px',
73
+ fontWeight: 500,
74
+ } as const;
75
+
76
+ export const AXIS_TICK_STYLE = {
77
+ fontSize: 11,
78
+ fontWeight: 600,
79
+ fill: '#94a3b8',
80
+ } as const;
81
+
82
+ export const GRID_STROKE = '#f1f5f9';
83
+ export const ANIMATION_DURATION = 1000;
84
+ export const ANIMATION_EASING = 'ease-out' as const;