@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
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Combobox — WealthX Design System
3
+ * Base: @base-ui/react/combobox
4
+ * WealthX overrides: square corners (rounded-none), design token colors,
5
+ * consistent styling with Select component, built-in search filtering.
6
+ */
7
+ import * as React from "react"
8
+ import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react"
9
+ import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"
10
+
11
+ import { cn } from "@/lib/utils"
12
+
13
+ export type ComboboxProps<Value = string, Multiple extends boolean | undefined = false> =
14
+ React.ComponentProps<typeof ComboboxPrimitive.Root<Value, Multiple>>
15
+
16
+ function Combobox<Value, Multiple extends boolean | undefined = false>({
17
+ ...props
18
+ }: React.ComponentProps<typeof ComboboxPrimitive.Root<Value, Multiple>>) {
19
+ return <ComboboxPrimitive.Root data-slot="combobox" {...props} />
20
+ }
21
+
22
+ export type ComboboxTriggerProps = React.ComponentProps<typeof ComboboxPrimitive.Trigger> & {
23
+ size?: "sm" | "default"
24
+ }
25
+
26
+ function ComboboxTrigger({
27
+ className,
28
+ size = "default",
29
+ children,
30
+ ...props
31
+ }: ComboboxTriggerProps) {
32
+ return (
33
+ <ComboboxPrimitive.Trigger
34
+ data-slot="combobox-trigger"
35
+ data-size={size}
36
+ className={cn(
37
+ "flex w-fit items-center justify-between gap-2 border border-input bg-transparent px-3 py-2 text-body-small whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20 data-[popup-open]:border-primary data-[popup-open]:ring-[3px] data-[popup-open]:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=combobox-value]:line-clamp-1 *:data-[slot=combobox-value]:flex *:data-[slot=combobox-value]:items-center *:data-[slot=combobox-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <ComboboxPrimitive.Icon className="transition-transform duration-200 data-[popup-open]:rotate-180">
44
+ <ChevronDownIcon className="size-4 opacity-50" />
45
+ </ComboboxPrimitive.Icon>
46
+ </ComboboxPrimitive.Trigger>
47
+ )
48
+ }
49
+
50
+ export type ComboboxValueProps = React.ComponentProps<typeof ComboboxPrimitive.Value>
51
+
52
+ function ComboboxValue({
53
+ ...props
54
+ }: ComboboxValueProps) {
55
+ return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
56
+ }
57
+
58
+ export type ComboboxInputProps = React.ComponentProps<typeof ComboboxPrimitive.Input>
59
+
60
+ function ComboboxInput({
61
+ className,
62
+ ...props
63
+ }: ComboboxInputProps) {
64
+ return (
65
+ <div data-slot="combobox-input-wrapper" className="flex items-center gap-2 border-b border-border px-3">
66
+ <SearchIcon className="size-4 shrink-0 text-muted-foreground" />
67
+ <ComboboxPrimitive.Input
68
+ data-slot="combobox-input"
69
+ className={cn(
70
+ "h-9 w-full min-w-0 bg-transparent py-1 text-body-small outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export type ComboboxContentProps = React.ComponentProps<typeof ComboboxPrimitive.Popup>
80
+
81
+ function ComboboxContent({
82
+ className,
83
+ children,
84
+ ...props
85
+ }: ComboboxContentProps) {
86
+ return (
87
+ <ComboboxPrimitive.Portal>
88
+ <ComboboxPrimitive.Positioner sideOffset={4} align="start">
89
+ <ComboboxPrimitive.Popup
90
+ data-slot="combobox-content"
91
+ className={cn(
92
+ "relative z-50 max-h-[var(--available-height)] min-w-[8rem] overflow-hidden border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[ending-style]:animate-out data-[ending-style]:fade-out-0 data-[ending-style]:zoom-out-95 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95",
93
+ className
94
+ )}
95
+ {...props}
96
+ >
97
+ {children}
98
+ </ComboboxPrimitive.Popup>
99
+ </ComboboxPrimitive.Positioner>
100
+ </ComboboxPrimitive.Portal>
101
+ )
102
+ }
103
+
104
+ export type ComboboxListProps = React.ComponentProps<typeof ComboboxPrimitive.List>
105
+
106
+ function ComboboxList({
107
+ className,
108
+ ...props
109
+ }: ComboboxListProps) {
110
+ return (
111
+ <ComboboxPrimitive.List
112
+ data-slot="combobox-list"
113
+ className={cn(
114
+ "max-h-[min(var(--available-height),18rem)] overflow-y-auto p-1",
115
+ className
116
+ )}
117
+ {...props}
118
+ />
119
+ )
120
+ }
121
+
122
+ export type ComboboxItemProps = React.ComponentProps<typeof ComboboxPrimitive.Item>
123
+
124
+ function ComboboxItem({
125
+ className,
126
+ children,
127
+ ...props
128
+ }: ComboboxItemProps) {
129
+ return (
130
+ <ComboboxPrimitive.Item
131
+ data-slot="combobox-item"
132
+ className={cn(
133
+ "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-body-small outline-hidden select-none data-[highlighted]:bg-primary/5 data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
134
+ className
135
+ )}
136
+ {...props}
137
+ >
138
+ <span
139
+ data-slot="combobox-item-indicator"
140
+ className="absolute right-2 flex size-3.5 items-center justify-center"
141
+ >
142
+ <ComboboxPrimitive.ItemIndicator>
143
+ <CheckIcon className="size-4" />
144
+ </ComboboxPrimitive.ItemIndicator>
145
+ </span>
146
+ {children}
147
+ </ComboboxPrimitive.Item>
148
+ )
149
+ }
150
+
151
+ export type ComboboxEmptyProps = React.ComponentProps<typeof ComboboxPrimitive.Empty>
152
+
153
+ function ComboboxEmpty({
154
+ className,
155
+ ...props
156
+ }: ComboboxEmptyProps) {
157
+ return (
158
+ <ComboboxPrimitive.Empty
159
+ data-slot="combobox-empty"
160
+ className={`text-body-small ${cn("py-6 text-center text-muted-foreground empty:hidden", className)}`}
161
+ {...props}
162
+ />
163
+ )
164
+ }
165
+
166
+ export type ComboboxGroupProps = React.ComponentProps<typeof ComboboxPrimitive.Group>
167
+
168
+ function ComboboxGroup({
169
+ ...props
170
+ }: ComboboxGroupProps) {
171
+ return <ComboboxPrimitive.Group data-slot="combobox-group" {...props} />
172
+ }
173
+
174
+ export type ComboboxGroupLabelProps = React.ComponentProps<typeof ComboboxPrimitive.GroupLabel>
175
+
176
+ function ComboboxGroupLabel({
177
+ className,
178
+ ...props
179
+ }: ComboboxGroupLabelProps) {
180
+ return (
181
+ <ComboboxPrimitive.GroupLabel
182
+ data-slot="combobox-group-label"
183
+ className={`text-label-small ${cn("px-2 py-1.5 uppercase text-muted-foreground", className)}`}
184
+ {...props}
185
+ />
186
+ )
187
+ }
188
+
189
+ export type ComboboxSeparatorProps = React.ComponentProps<"div">
190
+
191
+ function ComboboxSeparator({
192
+ className,
193
+ ...props
194
+ }: ComboboxSeparatorProps) {
195
+ return (
196
+ <div
197
+ role="separator"
198
+ data-slot="combobox-separator"
199
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
200
+ {...props}
201
+ />
202
+ )
203
+ }
204
+
205
+ export {
206
+ Combobox,
207
+ ComboboxTrigger,
208
+ ComboboxValue,
209
+ ComboboxInput,
210
+ ComboboxContent,
211
+ ComboboxList,
212
+ ComboboxItem,
213
+ ComboboxEmpty,
214
+ ComboboxGroup,
215
+ ComboboxGroupLabel,
216
+ ComboboxSeparator,
217
+ }
@@ -0,0 +1,278 @@
1
+ import React, { useEffect, useMemo, useRef, 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
+ DATASET_ALPHAS,
21
+ FALLBACK_TICK,
22
+ CHART_SLICE_COUNT,
23
+ CHART_PERIODS,
24
+ formatTooltipDate,
25
+ ChartLegendItem,
26
+ ChartPeriodButton,
27
+ type ChartPeriod,
28
+ type ChartGranularity,
29
+ } from "./chart-shared";
30
+
31
+ ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Types
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export interface ExpenseDataset {
38
+ /** Expense category label, e.g. "Housing", "Food", "Transport" */
39
+ label: string;
40
+ /** One value per data point, aligned to the months array */
41
+ data: number[];
42
+ }
43
+
44
+ export interface ExpenseBarChartData {
45
+ /** Display labels, e.g. ["Jul", "Aug"] for monthly or ["Mar 8", "Mar 9"] for daily */
46
+ months: string[];
47
+ /** Optional ISO date strings per point — used for the tooltip title */
48
+ dates?: string[];
49
+ datasets: ExpenseDataset[];
50
+ }
51
+
52
+ export type ExpensePeriod = ChartPeriod;
53
+ export type ExpenseGranularity = ChartGranularity;
54
+
55
+ export interface ExpenseBarChartProps {
56
+ /** Full dataset — sliced to the selected period */
57
+ expenseData: ExpenseBarChartData | null;
58
+ title?: string;
59
+ /** Show or hide the chart legend */
60
+ showLegend?: boolean;
61
+ /** Show or hide X axis labels */
62
+ showXAxis?: boolean;
63
+ /** Show or hide Y axis labels */
64
+ showYAxis?: boolean;
65
+ /** Legend placement relative to chart */
66
+ legendPosition?: "top" | "bottom";
67
+ /** Default period selector value */
68
+ defaultPeriod?: ExpensePeriod;
69
+ /**
70
+ * Data granularity — controls available period buttons and slice counts.
71
+ * "monthly" (default): shows 3M / 6M / 12M, slices by month count.
72
+ * "daily": shows 1M / 3M / 6M / 12M, slices by day count (1M=30, 3M=90, etc.).
73
+ */
74
+ granularity?: ExpenseGranularity;
75
+ /** Chart canvas height in pixels */
76
+ height?: number;
77
+ /** Width of the card in pixels */
78
+ width?: number | string;
79
+ className?: string;
80
+ /** Show skeleton loading state instead of the chart */
81
+ isLoading?: boolean;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Constants
86
+ // ---------------------------------------------------------------------------
87
+
88
+ const FALLBACK_SECONDARY = "#162029";
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Component
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export function ExpenseBarChart({
95
+ expenseData,
96
+ title = "Expenses",
97
+ showLegend = true,
98
+ showXAxis = true,
99
+ showYAxis = true,
100
+ legendPosition = "top",
101
+ defaultPeriod = 6,
102
+ granularity = "monthly",
103
+ height = 280,
104
+ width = "100%",
105
+ className,
106
+ isLoading = false,
107
+ }: ExpenseBarChartProps) {
108
+ const periods = CHART_PERIODS[granularity];
109
+ const [period, setPeriod] = useState<ExpensePeriod>(defaultPeriod);
110
+
111
+ // Reset period when granularity changes, but not on initial mount
112
+ // (defaultPeriod handles the initial value).
113
+ const isFirstRender = useRef(true);
114
+ useEffect(() => {
115
+ if (isFirstRender.current) { isFirstRender.current = false; return; }
116
+ setPeriod(CHART_PERIODS[granularity][0]);
117
+ }, [granularity]);
118
+
119
+ const themeVars = useThemeVars();
120
+ const brandSecondary: string =
121
+ (themeVars["--theme-secondary"] as string | undefined) || FALLBACK_SECONDARY;
122
+
123
+ const sliced = useMemo<ExpenseBarChartData | null>(() => {
124
+ if (!expenseData?.months?.length || !expenseData.datasets.length) return null;
125
+ const count = Math.min(CHART_SLICE_COUNT[granularity][period], expenseData.months.length);
126
+ const start = expenseData.months.length - count;
127
+ return {
128
+ months: expenseData.months.slice(start),
129
+ dates: expenseData.dates?.slice(start),
130
+ datasets: expenseData.datasets.map((ds) => ({
131
+ ...ds,
132
+ data: ds.data.slice(start),
133
+ })),
134
+ };
135
+ }, [expenseData, period, granularity]);
136
+
137
+ const datasetColors = useMemo(
138
+ () => sliced?.datasets.map((_, i) =>
139
+ hexToRgba(brandSecondary, DATASET_ALPHAS[i % DATASET_ALPHAS.length])
140
+ ) ?? [],
141
+ [sliced, brandSecondary]
142
+ );
143
+
144
+ const chartData = useMemo<ChartData<"bar">>(() => {
145
+ if (!sliced) return { labels: [], datasets: [] };
146
+ return {
147
+ labels: sliced.months,
148
+ datasets: sliced.datasets.map((ds, i) => ({
149
+ label: ds.label,
150
+ data: ds.data,
151
+ backgroundColor: datasetColors[i],
152
+ hoverBackgroundColor: hexToRgba(
153
+ brandSecondary,
154
+ Math.min(DATASET_ALPHAS[i % DATASET_ALPHAS.length] + 0.15, 1)
155
+ ),
156
+ borderWidth: 0,
157
+ borderRadius: 0,
158
+ borderSkipped: false,
159
+ barPercentage: 0.75,
160
+ categoryPercentage: 0.7,
161
+ stack: "expense",
162
+ })),
163
+ };
164
+ }, [sliced, datasetColors, brandSecondary]);
165
+
166
+ const options = useMemo<ChartOptions<"bar">>(() => ({
167
+ responsive: true,
168
+ maintainAspectRatio: false,
169
+ animation: { duration: 800, easing: "easeOutQuart" },
170
+ layout: { padding: 0 },
171
+ plugins: {
172
+ legend: { display: false },
173
+ tooltip: {
174
+ mode: "index",
175
+ intersect: false,
176
+ padding: 12,
177
+ cornerRadius: 0,
178
+ titleFont: { size: 11, weight: "600" },
179
+ bodyFont: { size: 12, weight: "500" },
180
+ callbacks: {
181
+ title: (tooltipItems) => {
182
+ const idx = tooltipItems[0]?.dataIndex;
183
+ if (idx != null && sliced?.dates?.[idx]) {
184
+ return formatTooltipDate(sliced.dates[idx], granularity);
185
+ }
186
+ return tooltipItems[0]?.label ?? "";
187
+ },
188
+ label: (ctx) => {
189
+ const val = ctx.raw as number;
190
+ if (val === 0) return null;
191
+ return ` ${ctx.dataset.label}: $${val.toLocaleString()}`;
192
+ },
193
+ },
194
+ },
195
+ },
196
+ scales: {
197
+ x: {
198
+ display: showXAxis,
199
+ stacked: true,
200
+ grid: { display: false },
201
+ border: { display: false },
202
+ ticks: { font: { size: 10 }, color: FALLBACK_TICK },
203
+ },
204
+ y: {
205
+ display: showYAxis,
206
+ stacked: true,
207
+ grid: { display: false },
208
+ border: { display: false },
209
+ ticks: {
210
+ font: { size: 10 },
211
+ color: FALLBACK_TICK,
212
+ maxTicksLimit: 5,
213
+ padding: 8,
214
+ callback: (v) => `$${Number(v).toLocaleString()}`,
215
+ },
216
+ },
217
+ },
218
+ }), [showXAxis, showYAxis, sliced, granularity]);
219
+
220
+ return (
221
+ <Card
222
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
223
+ style={{ maxWidth: width }}
224
+ >
225
+ <CardHeader className="px-3 sm:px-6">
226
+ <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
227
+ <CardAction>
228
+ <div className="flex gap-0.5 sm:gap-1">
229
+ {periods.map((p) => (
230
+ <ChartPeriodButton
231
+ key={p}
232
+ period={p}
233
+ active={period === p}
234
+ onClick={() => setPeriod(p)}
235
+ />
236
+ ))}
237
+ </div>
238
+ </CardAction>
239
+ </CardHeader>
240
+
241
+ <CardContent className="px-3 sm:px-6">
242
+ {isLoading ? (
243
+ <Skeleton style={{ height, width: "100%" }} />
244
+ ) : !sliced ? (
245
+ <Empty className="flex-none p-4" style={{ height }}>
246
+ <EmptyDescription>No data available</EmptyDescription>
247
+ </Empty>
248
+ ) : (
249
+ <div className="flex flex-col gap-2">
250
+ {showLegend && legendPosition === "top" && (
251
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
252
+ {sliced.datasets.map((ds, i) => (
253
+ <ChartLegendItem key={ds.label} label={ds.label} color={datasetColors[i]} />
254
+ ))}
255
+ </div>
256
+ )}
257
+ <div style={{ height, width: "100%", position: "relative" }}>
258
+ <Chart
259
+ key={brandSecondary}
260
+ type="bar"
261
+ data={chartData}
262
+ options={options}
263
+ aria-label={title}
264
+ />
265
+ </div>
266
+ {showLegend && legendPosition === "bottom" && (
267
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pt-2">
268
+ {sliced.datasets.map((ds, i) => (
269
+ <ChartLegendItem key={ds.label} label={ds.label} color={datasetColors[i]} />
270
+ ))}
271
+ </div>
272
+ )}
273
+ </div>
274
+ )}
275
+ </CardContent>
276
+ </Card>
277
+ );
278
+ }