@wealthx/shadcn 1.0.2 → 1.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 (36) hide show
  1. package/.turbo/turbo-build.log +154 -138
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +82 -0
  4. package/dist/chunk-3EQP72AW.mjs +58 -0
  5. package/dist/chunk-5JGQAAQV.mjs +212 -0
  6. package/dist/chunk-GLW2UO6O.mjs +212 -0
  7. package/dist/chunk-RN67642N.mjs +171 -0
  8. package/dist/chunk-UEL4RD5P.mjs +272 -0
  9. package/dist/chunk-YBXCIF5Q.mjs +198 -0
  10. package/dist/components/ui/cashflow-bar-chart.js +596 -0
  11. package/dist/components/ui/cashflow-bar-chart.mjs +16 -0
  12. package/dist/components/ui/combobox.js +261 -0
  13. package/dist/components/ui/combobox.mjs +28 -0
  14. package/dist/components/ui/data-table.mjs +3 -3
  15. package/dist/components/ui/expense-bar-chart.js +543 -0
  16. package/dist/components/ui/expense-bar-chart.mjs +16 -0
  17. package/dist/components/ui/field.mjs +2 -2
  18. package/dist/components/ui/income-bar-chart.js +543 -0
  19. package/dist/components/ui/income-bar-chart.mjs +16 -0
  20. package/dist/components/ui/transactions-income-expense-bar-chart.js +478 -0
  21. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +16 -0
  22. package/dist/index.js +1685 -725
  23. package/dist/index.mjs +152 -111
  24. package/dist/styles.css +1 -1
  25. package/package.json +30 -2
  26. package/src/components/index.tsx +56 -0
  27. package/src/components/ui/cashflow-bar-chart.tsx +336 -0
  28. package/src/components/ui/chart-shared.tsx +100 -0
  29. package/src/components/ui/combobox.tsx +217 -0
  30. package/src/components/ui/expense-bar-chart.tsx +278 -0
  31. package/src/components/ui/income-bar-chart.tsx +278 -0
  32. package/src/components/ui/transactions-income-expense-bar-chart.tsx +198 -0
  33. package/src/styles/styles-css.ts +1 -1
  34. package/tsup.config.ts +5 -0
  35. package/dist/{chunk-K76E2TQU.mjs → chunk-CJ46PDXE.mjs} +5 -5
  36. package/dist/{chunk-HUVTPUV2.mjs → chunk-NLLKTU4B.mjs} +3 -3
@@ -1,3 +1,32 @@
1
+ export { TransactionsIncomeExpenseBarChart } from "./ui/transactions-income-expense-bar-chart";
2
+ export type { TransactionsIncomeExpenseBarChartProps } from "./ui/transactions-income-expense-bar-chart";
3
+
4
+ export { CashflowBarChart } from "./ui/cashflow-bar-chart";
5
+ export type {
6
+ CashflowBarChartProps,
7
+ CashflowChartData,
8
+ CashflowDataPoint,
9
+ CashflowPeriod,
10
+ } from "./ui/cashflow-bar-chart";
11
+
12
+ export { ExpenseBarChart } from "./ui/expense-bar-chart";
13
+ export type {
14
+ ExpenseBarChartProps,
15
+ ExpenseBarChartData,
16
+ ExpenseDataset,
17
+ ExpensePeriod,
18
+ ExpenseGranularity,
19
+ } from "./ui/expense-bar-chart";
20
+
21
+ export { IncomeBarChart } from "./ui/income-bar-chart";
22
+ export type {
23
+ IncomeBarChartProps,
24
+ IncomeBarChartData,
25
+ IncomeDataset,
26
+ IncomePeriod,
27
+ IncomeGranularity,
28
+ } from "./ui/income-bar-chart";
29
+
1
30
  export {
2
31
  Accordion,
3
32
  AccordionItem,
@@ -98,6 +127,33 @@ export type { CheckboxProps, CheckboxCardProps } from "./ui/checkbox";
98
127
  export { Chip } from "./ui/chip";
99
128
  export type { ChipProps } from "./ui/chip";
100
129
 
130
+ export {
131
+ Combobox,
132
+ ComboboxTrigger,
133
+ ComboboxValue,
134
+ ComboboxInput,
135
+ ComboboxContent,
136
+ ComboboxList,
137
+ ComboboxItem,
138
+ ComboboxEmpty,
139
+ ComboboxGroup,
140
+ ComboboxGroupLabel,
141
+ ComboboxSeparator,
142
+ } from "./ui/combobox";
143
+ export type {
144
+ ComboboxProps,
145
+ ComboboxTriggerProps,
146
+ ComboboxValueProps,
147
+ ComboboxInputProps,
148
+ ComboboxContentProps,
149
+ ComboboxListProps,
150
+ ComboboxItemProps,
151
+ ComboboxEmptyProps,
152
+ ComboboxGroupProps,
153
+ ComboboxGroupLabelProps,
154
+ ComboboxSeparatorProps,
155
+ } from "./ui/combobox";
156
+
101
157
  export {
102
158
  DataTable,
103
159
  DataTableToolbar,
@@ -0,0 +1,336 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ BarElement,
7
+ Tooltip,
8
+ Legend,
9
+ type ChartOptions,
10
+ type ChartData,
11
+ } from "chart.js";
12
+ import { Chart } from "react-chartjs-2";
13
+ import { useThemeVars } from "@/lib/theme-provider";
14
+ import { Card, CardContent, CardHeader, CardTitle, CardAction } from "./card";
15
+ import { Empty, EmptyDescription } from "./empty";
16
+ import { Skeleton } from "./skeleton";
17
+ import { cn } from "@/lib/utils";
18
+ import {
19
+ hexToRgba,
20
+ FALLBACK_TICK,
21
+ formatTooltipDate,
22
+ ChartPeriodButton,
23
+ } from "./chart-shared";
24
+
25
+ ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface CashflowDataPoint {
32
+ /** ISO date string e.g. "2024-01-01" */
33
+ date: string;
34
+ income: number;
35
+ expenses: number;
36
+ /** Positive value — ignored when overspending > 0 */
37
+ surplus: number;
38
+ /** Positive value — ignored when surplus > 0 */
39
+ overspending: number;
40
+ }
41
+
42
+ export interface CashflowChartData {
43
+ months: string[];
44
+ data: CashflowDataPoint[];
45
+ }
46
+
47
+ export type CashflowPeriod = 3 | 6 | 12;
48
+
49
+ export interface CashflowBarChartProps {
50
+ /** Full dataset — sliced to the selected period */
51
+ cashflowData: CashflowChartData | null;
52
+ title?: string;
53
+ /** Show or hide the chart legend */
54
+ showLegend?: boolean;
55
+ /** Show or hide X axis labels */
56
+ showXAxis?: boolean;
57
+ /** Show or hide Y axis labels */
58
+ showYAxis?: boolean;
59
+ /** Legend placement relative to chart */
60
+ legendPosition?: "top" | "bottom";
61
+ /** Default period selector value */
62
+ defaultPeriod?: CashflowPeriod;
63
+ /** Chart canvas height in pixels */
64
+ height?: number;
65
+ /** Width of the card in pixels */
66
+ width?: number | string;
67
+ className?: string;
68
+ /** Show skeleton loading state instead of the chart */
69
+ isLoading?: boolean;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // HTML legend (outside canvas — full spacing control)
74
+ // ---------------------------------------------------------------------------
75
+
76
+ interface LegendItemProps {
77
+ label: string;
78
+ fillColor: string;
79
+ strokeColor: string;
80
+ strokeWidth?: number;
81
+ }
82
+
83
+ function LegendItem({ label, fillColor, strokeColor, strokeWidth = 1.5 }: LegendItemProps) {
84
+ return (
85
+ <div className="flex items-center gap-1.5">
86
+ <div
87
+ style={{
88
+ width: 10,
89
+ height: 10,
90
+ backgroundColor: fillColor,
91
+ border: strokeWidth > 0 ? `${strokeWidth}px solid ${strokeColor}` : "none",
92
+ flexShrink: 0,
93
+ }}
94
+ />
95
+ <span className="text-[11px] text-muted-foreground leading-none">{label}</span>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ function ChartLegend({ primary, secondary }: { primary: string; secondary: string }) {
101
+ return (
102
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
103
+ <LegendItem
104
+ label="Income"
105
+ fillColor={hexToRgba(primary, 0.2)}
106
+ strokeColor={primary}
107
+ />
108
+ <LegendItem
109
+ label="Expenses and Liabilities"
110
+ fillColor={hexToRgba(secondary, 0.2)}
111
+ strokeColor={secondary}
112
+ />
113
+ <LegendItem
114
+ label="Surplus Income"
115
+ fillColor={primary}
116
+ strokeColor={primary}
117
+ strokeWidth={0}
118
+ />
119
+ <LegendItem
120
+ label="Over Spending"
121
+ fillColor={secondary}
122
+ strokeColor={secondary}
123
+ strokeWidth={0}
124
+ />
125
+ </div>
126
+ );
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Constants
131
+ // ---------------------------------------------------------------------------
132
+
133
+ const PERIODS: CashflowPeriod[] = [3, 6, 12];
134
+
135
+ const FALLBACK_PRIMARY = "#33FF99";
136
+ const FALLBACK_SECONDARY = "#162029";
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Component
140
+ // ---------------------------------------------------------------------------
141
+
142
+ export function CashflowBarChart({
143
+ cashflowData,
144
+ title = "Cashflow",
145
+ showLegend = true,
146
+ showXAxis = true,
147
+ showYAxis = true,
148
+ legendPosition = "top",
149
+ defaultPeriod = 6,
150
+ height = 280,
151
+ width = "100%",
152
+ className,
153
+ isLoading = false,
154
+ }: CashflowBarChartProps) {
155
+ const [period, setPeriod] = useState<CashflowPeriod>(defaultPeriod);
156
+
157
+ // Read theme colors from ThemeProvider context.
158
+ // Falls back to WealthX defaults when no ThemeProvider is present.
159
+ const themeVars = useThemeVars();
160
+ const brandPrimary: string =
161
+ (themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY;
162
+ const brandSecondary: string =
163
+ (themeVars["--theme-secondary"] as string | undefined) || FALLBACK_SECONDARY;
164
+
165
+ const sliced = useMemo<CashflowChartData | null>(() => {
166
+ if (!cashflowData?.data?.length) return null;
167
+ const count = Math.min(period, cashflowData.data.length);
168
+ const start = cashflowData.data.length - count;
169
+ return {
170
+ months: cashflowData.months.slice(start),
171
+ data: cashflowData.data.slice(start),
172
+ };
173
+ }, [cashflowData, period]);
174
+
175
+ const chartData = useMemo<ChartData<"bar">>(() => {
176
+ if (!sliced) return { labels: [], datasets: [] };
177
+
178
+ return {
179
+ labels: sliced.months,
180
+ datasets: [
181
+ {
182
+ label: "Income",
183
+ data: sliced.data.map((d) => d.income),
184
+ backgroundColor: hexToRgba(brandPrimary, 0.2),
185
+ hoverBackgroundColor: hexToRgba(brandPrimary, 0.35),
186
+ borderColor: brandPrimary,
187
+ borderWidth: 1.5,
188
+ borderRadius: 0,
189
+ borderSkipped: false,
190
+ barPercentage: 0.75,
191
+ categoryPercentage: 0.7,
192
+ },
193
+ {
194
+ label: "Expenses and Liabilities",
195
+ data: sliced.data.map((d) => d.expenses),
196
+ backgroundColor: hexToRgba(brandSecondary, 0.2),
197
+ hoverBackgroundColor: hexToRgba(brandSecondary, 0.35),
198
+ borderColor: brandSecondary,
199
+ borderWidth: 1.5,
200
+ borderRadius: 0,
201
+ borderSkipped: false,
202
+ barPercentage: 0.75,
203
+ categoryPercentage: 0.7,
204
+ },
205
+ {
206
+ label: "_thirdBar",
207
+ data: sliced.data.map((d) =>
208
+ d.overspending > 0 ? d.overspending : d.surplus
209
+ ),
210
+ backgroundColor: sliced.data.map((d) =>
211
+ d.overspending > 0 ? brandSecondary : brandPrimary
212
+ ),
213
+ hoverBackgroundColor: sliced.data.map((d) =>
214
+ d.overspending > 0
215
+ ? hexToRgba(brandSecondary, 0.8)
216
+ : hexToRgba(brandPrimary, 0.8)
217
+ ),
218
+ borderWidth: 0,
219
+ borderRadius: 0,
220
+ borderSkipped: false,
221
+ barPercentage: 0.75,
222
+ categoryPercentage: 0.7,
223
+ },
224
+ ],
225
+ };
226
+ }, [sliced, brandPrimary, brandSecondary]);
227
+
228
+ const options = useMemo<ChartOptions<"bar">>(() => ({
229
+ responsive: true,
230
+ maintainAspectRatio: false,
231
+ animation: { duration: 800, easing: "easeOutQuart" },
232
+ layout: { padding: 0 },
233
+ plugins: {
234
+ legend: { display: false },
235
+ tooltip: {
236
+ mode: "index",
237
+ intersect: false,
238
+ padding: 12,
239
+ cornerRadius: 0,
240
+ titleFont: { size: 11, weight: "600" },
241
+ bodyFont: { size: 12, weight: "500" },
242
+ callbacks: {
243
+ title: (tooltipItems) => {
244
+ const idx = tooltipItems[0]?.dataIndex;
245
+ if (idx != null && sliced?.data[idx]?.date) {
246
+ return formatTooltipDate(sliced.data[idx].date, "monthly");
247
+ }
248
+ return tooltipItems[0]?.label ?? "";
249
+ },
250
+ label: (ctx) => {
251
+ const val = ctx.raw as number;
252
+ if (val === 0) return null;
253
+ if (ctx.datasetIndex === 2) {
254
+ const d = sliced?.data[ctx.dataIndex];
255
+ if (!d) return null;
256
+ const lbl = d.overspending > 0 ? "Over Spending" : "Surplus Income";
257
+ return ` ${lbl}: $${val.toLocaleString()}`;
258
+ }
259
+ return ` ${ctx.dataset.label}: $${val.toLocaleString()}`;
260
+ },
261
+ },
262
+ },
263
+ },
264
+ scales: {
265
+ x: {
266
+ display: showXAxis,
267
+ grid: { display: false },
268
+ border: { display: false },
269
+ ticks: { font: { size: 10 }, color: FALLBACK_TICK },
270
+ },
271
+ y: {
272
+ display: showYAxis,
273
+ grid: { display: false },
274
+ border: { display: false },
275
+ ticks: {
276
+ font: { size: 10 },
277
+ color: FALLBACK_TICK,
278
+ maxTicksLimit: 5,
279
+ padding: 8,
280
+ callback: (v) => `$${Number(v).toLocaleString()}`,
281
+ },
282
+ },
283
+ },
284
+ }), [showXAxis, showYAxis, sliced]);
285
+
286
+ return (
287
+ <Card
288
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
289
+ style={{ maxWidth: width }}
290
+ >
291
+ <CardHeader className="px-3 sm:px-6">
292
+ <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
293
+ <CardAction>
294
+ <div className="flex gap-0.5 sm:gap-1">
295
+ {PERIODS.map((p) => (
296
+ <ChartPeriodButton
297
+ key={p}
298
+ period={p}
299
+ active={period === p}
300
+ onClick={() => setPeriod(p)}
301
+ />
302
+ ))}
303
+ </div>
304
+ </CardAction>
305
+ </CardHeader>
306
+
307
+ <CardContent className="px-3 sm:px-6">
308
+ {isLoading ? (
309
+ <Skeleton style={{ height, width: "100%" }} />
310
+ ) : !sliced ? (
311
+ <Empty className="flex-none p-4" style={{ height }}>
312
+ <EmptyDescription>No data available</EmptyDescription>
313
+ </Empty>
314
+ ) : (
315
+ <div className="flex flex-col gap-2">
316
+ {showLegend && legendPosition === "top" && (
317
+ <ChartLegend primary={brandPrimary} secondary={brandSecondary} />
318
+ )}
319
+ <div style={{ height, width: "100%", position: "relative" }}>
320
+ <Chart
321
+ key={`${brandPrimary}__${brandSecondary}`}
322
+ type="bar"
323
+ data={chartData}
324
+ options={options}
325
+ aria-label={title}
326
+ />
327
+ </div>
328
+ {showLegend && legendPosition === "bottom" && (
329
+ <ChartLegend primary={brandPrimary} secondary={brandSecondary} />
330
+ )}
331
+ </div>
332
+ )}
333
+ </CardContent>
334
+ </Card>
335
+ );
336
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared utilities and sub-components for IncomeBarChart and ExpenseBarChart.
3
+ * Not part of the public package API — internal use only.
4
+ */
5
+ import React from "react";
6
+ import { Button } from "./button";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export type ChartPeriod = 1 | 3 | 6 | 12;
13
+ export type ChartGranularity = "monthly" | "daily";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Period / slice config
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * How many data points to slice per period × granularity combination.
21
+ * monthly: slice by calendar month count.
22
+ * daily: slice by approximate day count (1M=30d, 3M=90d, 6M=180d, 12M=365d).
23
+ */
24
+ export const CHART_SLICE_COUNT: Record<ChartGranularity, Record<ChartPeriod, number>> = {
25
+ monthly: { 1: 1, 3: 3, 6: 6, 12: 12 },
26
+ daily: { 1: 30, 3: 90, 6: 180, 12: 365 },
27
+ };
28
+
29
+ /** Period buttons shown per granularity. monthly hides 1M; daily shows all four. */
30
+ export const CHART_PERIODS: Record<ChartGranularity, ChartPeriod[]> = {
31
+ monthly: [3, 6, 12],
32
+ daily: [1, 3, 6, 12],
33
+ };
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Chart.js helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export function hexToRgba(hex: string, alpha: number): string {
40
+ const clean = hex.replace("#", "");
41
+ const full =
42
+ clean.length === 3
43
+ ? clean.split("").map((c) => c + c).join("")
44
+ : clean;
45
+ const r = parseInt(full.slice(0, 2), 16);
46
+ const g = parseInt(full.slice(2, 4), 16);
47
+ const b = parseInt(full.slice(4, 6), 16);
48
+ return `rgba(${r},${g},${b},${alpha})`;
49
+ }
50
+
51
+ /** Opacity steps to derive distinct shades from a single brand color (up to 6 datasets). */
52
+ export const DATASET_ALPHAS = [1, 0.72, 0.52, 0.36, 0.24, 0.15];
53
+
54
+ export const FALLBACK_TICK = "#9EAAB5";
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Tooltip date format
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Format an ISO date string for the chart tooltip title.
62
+ * Daily granularity includes the day; monthly shows only month + year.
63
+ */
64
+ export function formatTooltipDate(iso: string, granularity: ChartGranularity): string {
65
+ const d = new Date(iso);
66
+ return d.toLocaleDateString("en-US",
67
+ granularity === "daily"
68
+ ? { month: "short", day: "numeric", year: "numeric" }
69
+ : { month: "short", year: "numeric" }
70
+ );
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Sub-components
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export function ChartLegendItem({ label, color }: { label: string; color: string }) {
78
+ return (
79
+ <div className="flex items-center gap-1.5">
80
+ <div style={{ width: 10, height: 10, backgroundColor: color, flexShrink: 0 }} />
81
+ <span className="text-[11px] text-muted-foreground leading-none">{label}</span>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ export function ChartPeriodButton({
87
+ period,
88
+ active,
89
+ onClick,
90
+ }: {
91
+ period: ChartPeriod;
92
+ active: boolean;
93
+ onClick: () => void;
94
+ }) {
95
+ return (
96
+ <Button variant={active ? "default" : "outline"} size="xs" onClick={onClick}>
97
+ {period}M
98
+ </Button>
99
+ );
100
+ }