@the21og/a16z 0.0.1 → 0.0.2
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/components/ui/chart.tsx +219 -0
- package/index.css +2 -0
- package/index.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Lib - Chart System
|
|
3
|
+
*
|
|
4
|
+
* @module Chart
|
|
5
|
+
* @description A flexible wrapper around Recharts designed for the Red/Neutral theme context.
|
|
6
|
+
* Focuses on accessible color mapping and responsive container handling.
|
|
7
|
+
* @version 1.0.0 - Generated for user request: "create a chart component"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { ResponsiveContainer, Tooltip as RechartsTooltip, Legend as RechartsLegend } from "recharts";
|
|
12
|
+
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
// Types & Context
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type ChartConfig = Record<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
label?: React.ReactNode;
|
|
21
|
+
icon?: React.ComponentType;
|
|
22
|
+
color?: string;
|
|
23
|
+
theme?: Record<"light" | "dark", string>;
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
type ChartContextProps = {
|
|
28
|
+
config: ChartConfig;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
|
32
|
+
|
|
33
|
+
const useChart = () => {
|
|
34
|
+
const context = React.useContext(ChartContext);
|
|
35
|
+
if (!context) {
|
|
36
|
+
throw new Error("useChart must be used within a ChartContainer");
|
|
37
|
+
}
|
|
38
|
+
return context;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// -----------------------------------------------------------------------------
|
|
42
|
+
// Components
|
|
43
|
+
// -----------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrapper that injects CSS variables for chart colors based on the current theme configuration.
|
|
47
|
+
*/
|
|
48
|
+
export const ChartContainer = React.forwardRef<
|
|
49
|
+
HTMLDivElement,
|
|
50
|
+
React.ComponentProps<"div"> & {
|
|
51
|
+
config: ChartConfig;
|
|
52
|
+
children: React.ReactComponentElement<any, any>;
|
|
53
|
+
}
|
|
54
|
+
>(({ id, className, children, config, ...props }, ref) => {
|
|
55
|
+
const uniqueId = React.useId();
|
|
56
|
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<ChartContext.Provider value={{ config }}>
|
|
60
|
+
<div
|
|
61
|
+
ref={ref}
|
|
62
|
+
data-chart={chartId}
|
|
63
|
+
id={chartId}
|
|
64
|
+
className={`flex aspect-video justify-center text-xs ${className || ""}`}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
<ChartStyle id={chartId} config={config} />
|
|
68
|
+
<ResponsiveContainer>
|
|
69
|
+
{children}
|
|
70
|
+
</ResponsiveContainer>
|
|
71
|
+
</div>
|
|
72
|
+
</ChartContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
ChartContainer.displayName = "ChartContainer";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generates a style tag to map internal chart colors to CSS variables.
|
|
79
|
+
*/
|
|
80
|
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
81
|
+
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
|
|
82
|
+
if (!colorConfig.length) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<style dangerouslySetInnerHTML={{ __html:
|
|
86
|
+
`[data-chart=${id}] {
|
|
87
|
+
${colorConfig.map(([key, item]) => {
|
|
88
|
+
const color = item.theme?.light || item.color;
|
|
89
|
+
return color ? `--color-${key}: ${color};` : "";
|
|
90
|
+
}).join("\n")}
|
|
91
|
+
}
|
|
92
|
+
.dark [data-chart=${id}] {
|
|
93
|
+
${colorConfig.map(([key, item]) => {
|
|
94
|
+
const color = item.theme?.dark || item.color;
|
|
95
|
+
return color ? `--color-${key}: ${color};` : "";
|
|
96
|
+
}).join("\n")}
|
|
97
|
+
}`
|
|
98
|
+
}} />
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Custom Tooltip implementation ensuring consistent Red/Neutral styling.
|
|
104
|
+
*/
|
|
105
|
+
export const ChartTooltip = RechartsTooltip;
|
|
106
|
+
|
|
107
|
+
export const ChartTooltipContent = React.forwardRef<
|
|
108
|
+
HTMLDivElement,
|
|
109
|
+
React.ComponentProps<"div"> & {
|
|
110
|
+
active?: boolean;
|
|
111
|
+
payload?: any[];
|
|
112
|
+
indicator?: "line" | "dot" | "dashed";
|
|
113
|
+
hideLabel?: boolean;
|
|
114
|
+
label?: string;
|
|
115
|
+
labelFormatter?: (value: any, payload: any[]) => React.ReactNode;
|
|
116
|
+
formatter?: (value: any, name: string, item: any, index: number, payload: any) => React.ReactNode;
|
|
117
|
+
color?: string;
|
|
118
|
+
nameKey?: string;
|
|
119
|
+
labelKey?: string;
|
|
120
|
+
}
|
|
121
|
+
>(({
|
|
122
|
+
active,
|
|
123
|
+
payload,
|
|
124
|
+
indicator = "dot",
|
|
125
|
+
hideLabel = false,
|
|
126
|
+
label,
|
|
127
|
+
labelFormatter,
|
|
128
|
+
className,
|
|
129
|
+
formatter,
|
|
130
|
+
color,
|
|
131
|
+
nameKey,
|
|
132
|
+
labelKey,
|
|
133
|
+
}, ref) => {
|
|
134
|
+
const { config } = useChart();
|
|
135
|
+
|
|
136
|
+
if (!active || !payload?.length) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
ref={ref}
|
|
143
|
+
className={`grid min-w-[8rem] items-start gap-1.5 rounded-xl border border-neutral-200 bg-white px-2.5 py-1.5 text-xs shadow-sm dark:border-neutral-800 dark:bg-neutral-950 ${className || ""}`}
|
|
144
|
+
>
|
|
145
|
+
{!hideLabel && (
|
|
146
|
+
<div className="mb-1 font-medium text-neutral-500 dark:text-neutral-400">
|
|
147
|
+
{labelFormatter ? labelFormatter(label, payload) : label}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
<div className="grid gap-1.5">
|
|
151
|
+
{payload.map((item, index) => {
|
|
152
|
+
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
|
153
|
+
const itemConfig = config[key] || config[item.name] || {};
|
|
154
|
+
|
|
155
|
+
const indicatorColor = color || item.payload.fill || item.color;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
key={item.dataKey + index}
|
|
160
|
+
className="flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-neutral-500 dark:[&>svg]:text-neutral-400"
|
|
161
|
+
>
|
|
162
|
+
{indicator === "dot" && (
|
|
163
|
+
<span
|
|
164
|
+
className="h-2.5 w-2.5 shrink-0 rounded-[2px] bg-[--color-bg]"
|
|
165
|
+
style={{ "--color-bg": indicatorColor } as React.CSSProperties}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
<div className="flex flex-1 justify-between leading-none gap-4">
|
|
169
|
+
<span className="text-neutral-500 dark:text-neutral-400">
|
|
170
|
+
{itemConfig?.label || item.name}
|
|
171
|
+
</span>
|
|
172
|
+
<span className="font-mono font-medium tabular-nums text-neutral-900 dark:text-neutral-50">
|
|
173
|
+
{item.value?.toLocaleString()}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
ChartTooltipContent.displayName = "ChartTooltipContent";
|
|
184
|
+
|
|
185
|
+
export const ChartLegend = RechartsLegend;
|
|
186
|
+
|
|
187
|
+
export const ChartLegendContent = React.forwardRef<
|
|
188
|
+
HTMLDivElement,
|
|
189
|
+
React.ComponentProps<"div"> & {
|
|
190
|
+
payload?: any[];
|
|
191
|
+
verticalAlign?: "top" | "bottom";
|
|
192
|
+
}
|
|
193
|
+
>(({ payload, verticalAlign = "bottom", className }, ref) => {
|
|
194
|
+
const { config } = useChart();
|
|
195
|
+
|
|
196
|
+
if (!payload?.length) return null;
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div
|
|
200
|
+
ref={ref}
|
|
201
|
+
className={`flex flex-wrap items-center justify-center gap-4 pt-2 ${verticalAlign === "top" ? "pb-2" : "pt-2"} ${className || ""}`}
|
|
202
|
+
>
|
|
203
|
+
{payload.map((item) => {
|
|
204
|
+
const key = `${item.dataKey || "value"}`;
|
|
205
|
+
const itemConfig = config[key] || {};
|
|
206
|
+
return (
|
|
207
|
+
<div key={item.value} className="flex items-center gap-1.5 text-xs text-neutral-600 dark:text-neutral-400">
|
|
208
|
+
<div
|
|
209
|
+
className="h-2 w-2 rounded-full"
|
|
210
|
+
style={{ backgroundColor: item.color }}
|
|
211
|
+
/>
|
|
212
|
+
<span>{itemConfig?.label || item.value}</span>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
ChartLegendContent.displayName = "ChartLegendContent";
|
package/index.css
CHANGED
package/index.ts
CHANGED