@wealthx/shadcn 1.0.2 → 1.2.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 (300) hide show
  1. package/.turbo/turbo-build.log +235 -138
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +82 -0
  4. package/dist/{chunk-6OJF6XRN.mjs → chunk-24FUO7TD.mjs} +4 -8
  5. package/dist/{chunk-4AJ5HWHD.mjs → chunk-2I5S2AMY.mjs} +3 -3
  6. package/dist/chunk-2SF672SZ.mjs +161 -0
  7. package/dist/{chunk-GPRJQ24C.mjs → chunk-34NWQURD.mjs} +2 -2
  8. package/dist/{chunk-MQ72DIBH.mjs → chunk-3GF7OVTP.mjs} +14 -5
  9. package/dist/chunk-3WMX6KWS.mjs +245 -0
  10. package/dist/{chunk-PMKODV6M.mjs → chunk-462HMNO4.mjs} +6 -10
  11. package/dist/chunk-4CX4SBRO.mjs +153 -0
  12. package/dist/chunk-4MN6UQHG.mjs +443 -0
  13. package/dist/chunk-5QQVZTVZ.mjs +233 -0
  14. package/dist/{chunk-BGP2N52Z.mjs → chunk-66MI7Q4B.mjs} +5 -5
  15. package/dist/chunk-6FCGKSZX.mjs +268 -0
  16. package/dist/{chunk-CGOKTPXU.mjs → chunk-6JQFUE5I.mjs} +20 -23
  17. package/dist/{chunk-Z3MK2KKZ.mjs → chunk-7DHU4VGG.mjs} +7 -3
  18. package/dist/{chunk-VZ2NR7L3.mjs → chunk-7PYJD5JI.mjs} +35 -27
  19. package/dist/{chunk-JU2RUWHF.mjs → chunk-7XJHLGUV.mjs} +1 -1
  20. package/dist/{chunk-BMFN37JH.mjs → chunk-7YAU5CY6.mjs} +1 -1
  21. package/dist/chunk-A56YQQHG.mjs +402 -0
  22. package/dist/chunk-AH52LG6N.mjs +315 -0
  23. package/dist/{chunk-SLWCCURD.mjs → chunk-CLIN5525.mjs} +8 -4
  24. package/dist/{chunk-3VQNJ235.mjs → chunk-CSDO6VBW.mjs} +7 -0
  25. package/dist/chunk-D4ILTPOG.mjs +293 -0
  26. package/dist/{chunk-HS7TFG7V.mjs → chunk-D6ID6M4V.mjs} +1 -1
  27. package/dist/chunk-DOH3EHX7.mjs +378 -0
  28. package/dist/{chunk-MJIEMGRD.mjs → chunk-EFRENWEJ.mjs} +9 -17
  29. package/dist/chunk-ERGGHC2V.mjs +185 -0
  30. package/dist/{chunk-OXQQNQZI.mjs → chunk-FEZKMUCF.mjs} +10 -1
  31. package/dist/{chunk-55CEW76V.mjs → chunk-FH6QVUVZ.mjs} +1 -1
  32. package/dist/chunk-FMAXJ2SI.mjs +71 -0
  33. package/dist/chunk-FZIXGLMV.mjs +173 -0
  34. package/dist/{chunk-DS2AMHN2.mjs → chunk-GYMYRIZP.mjs} +2 -2
  35. package/dist/{chunk-KQDD5MU3.mjs → chunk-H45TKD34.mjs} +5 -5
  36. package/dist/{chunk-BBJBJSXQ.mjs → chunk-J5UICVJS.mjs} +1 -1
  37. package/dist/{chunk-RL772EH7.mjs → chunk-JHJHG4GO.mjs} +4 -12
  38. package/dist/chunk-KMCGSZTX.mjs +177 -0
  39. package/dist/{chunk-FHNT55I5.mjs → chunk-KUDCQ4FI.mjs} +4 -4
  40. package/dist/chunk-LE6YFY6D.mjs +209 -0
  41. package/dist/{chunk-HUVTPUV2.mjs → chunk-LLVQKSU3.mjs} +23 -19
  42. package/dist/{chunk-KKHTJNMM.mjs → chunk-MARPPFOJ.mjs} +8 -4
  43. package/dist/{chunk-6AFMNC42.mjs → chunk-N2PT566P.mjs} +15 -11
  44. package/dist/chunk-NLCKVHWB.mjs +161 -0
  45. package/dist/{chunk-YN5SYTOO.mjs → chunk-NQPOYKAQ.mjs} +9 -5
  46. package/dist/{chunk-ZZV5JVNW.mjs → chunk-NSLMILBT.mjs} +3 -7
  47. package/dist/chunk-NXA3CZ7A.mjs +248 -0
  48. package/dist/chunk-OGOYQ7BG.mjs +150 -0
  49. package/dist/{chunk-3NQGYJEZ.mjs → chunk-P6AM5V7O.mjs} +10 -18
  50. package/dist/{chunk-CZ3BW5GL.mjs → chunk-P76HMUI6.mjs} +5 -11
  51. package/dist/chunk-PCPLO5HT.mjs +671 -0
  52. package/dist/chunk-PG6K5XEC.mjs +475 -0
  53. package/dist/chunk-PJHPSRYD.mjs +234 -0
  54. package/dist/{chunk-DDPA2XXS.mjs → chunk-PMB3A7V3.mjs} +2 -2
  55. package/dist/chunk-PR6V5XKM.mjs +209 -0
  56. package/dist/{chunk-46OFHMQA.mjs → chunk-Q76O3RIQ.mjs} +10 -6
  57. package/dist/chunk-QVKWW6KE.mjs +272 -0
  58. package/dist/chunk-RGU7HOEC.mjs +140 -0
  59. package/dist/{chunk-JF4PHPD5.mjs → chunk-RGVKLTLH.mjs} +4 -4
  60. package/dist/{chunk-VG6UF6UT.mjs → chunk-RP3SQYA3.mjs} +2 -2
  61. package/dist/chunk-RRBS6D63.mjs +163 -0
  62. package/dist/chunk-SMQ3DG25.mjs +285 -0
  63. package/dist/chunk-SPJ5KXW7.mjs +199 -0
  64. package/dist/chunk-SYOD63OZ.mjs +225 -0
  65. package/dist/chunk-UFYSFDER.mjs +42 -0
  66. package/dist/chunk-VACKZOMY.mjs +190 -0
  67. package/dist/chunk-VLQZANBF.mjs +42 -0
  68. package/dist/chunk-WA6O6EUR.mjs +1885 -0
  69. package/dist/{chunk-E3K6O4FZ.mjs → chunk-WAZD7NFU.mjs} +5 -2
  70. package/dist/chunk-WG6JGJXB.mjs +165 -0
  71. package/dist/{chunk-I64K754C.mjs → chunk-WNGWBVLV.mjs} +2 -2
  72. package/dist/{chunk-3U7SD3MS.mjs → chunk-WOEHFRGB.mjs} +3 -3
  73. package/dist/{chunk-DKZRJOMF.mjs → chunk-XIRTEFKH.mjs} +12 -12
  74. package/dist/chunk-Y6DWJSKZ.mjs +79 -0
  75. package/dist/chunk-YKPROFLB.mjs +161 -0
  76. package/dist/{chunk-K76E2TQU.mjs → chunk-ZRO5JO3H.mjs} +107 -67
  77. package/dist/{chunk-VYMHBV6D.mjs → chunk-ZU4NV6RG.mjs} +5 -3
  78. package/dist/components/ui/accordion.js +40 -4
  79. package/dist/components/ui/accordion.mjs +2 -2
  80. package/dist/components/ui/add-column-modal.js +789 -0
  81. package/dist/components/ui/add-column-modal.mjs +17 -0
  82. package/dist/components/ui/add-lead-modal.js +647 -0
  83. package/dist/components/ui/add-lead-modal.mjs +16 -0
  84. package/dist/components/ui/ai-assistant-drawer.js +686 -0
  85. package/dist/components/ui/ai-assistant-drawer.mjs +16 -0
  86. package/dist/components/ui/alert-dialog.js +37 -5
  87. package/dist/components/ui/alert-dialog.mjs +4 -4
  88. package/dist/components/ui/alert.js +37 -11
  89. package/dist/components/ui/alert.mjs +2 -2
  90. package/dist/components/ui/avatar.js +36 -8
  91. package/dist/components/ui/avatar.mjs +2 -2
  92. package/dist/components/ui/backoffice-alert-history-chart.js +624 -0
  93. package/dist/components/ui/backoffice-alert-history-chart.mjs +16 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.js +687 -0
  95. package/dist/components/ui/backoffice-contact-history-chart.mjs +16 -0
  96. package/dist/components/ui/badge.js +37 -2
  97. package/dist/components/ui/badge.mjs +2 -2
  98. package/dist/components/ui/borrowing-capacity-line-chart.js +639 -0
  99. package/dist/components/ui/borrowing-capacity-line-chart.mjs +16 -0
  100. package/dist/components/ui/button.js +35 -3
  101. package/dist/components/ui/button.mjs +2 -2
  102. package/dist/components/ui/calendar.js +43 -19
  103. package/dist/components/ui/calendar.mjs +3 -3
  104. package/dist/components/ui/card.js +40 -4
  105. package/dist/components/ui/card.mjs +2 -2
  106. package/dist/components/ui/cash-balance-line-chart.js +627 -0
  107. package/dist/components/ui/cash-balance-line-chart.mjs +16 -0
  108. package/dist/components/ui/cashflow-bar-chart.js +650 -0
  109. package/dist/components/ui/cashflow-bar-chart.mjs +16 -0
  110. package/dist/components/ui/checkbox.js +36 -5
  111. package/dist/components/ui/checkbox.mjs +2 -3
  112. package/dist/components/ui/chip.js +37 -2
  113. package/dist/components/ui/chip.mjs +3 -3
  114. package/dist/components/ui/combobox.js +280 -0
  115. package/dist/components/ui/combobox.mjs +28 -0
  116. package/dist/components/ui/data-table.js +160 -88
  117. package/dist/components/ui/data-table.mjs +10 -11
  118. package/dist/components/ui/date-picker.js +44 -20
  119. package/dist/components/ui/date-picker.mjs +6 -7
  120. package/dist/components/ui/dialog.js +44 -12
  121. package/dist/components/ui/dialog.mjs +4 -4
  122. package/dist/components/ui/drawer.js +46 -10
  123. package/dist/components/ui/drawer.mjs +3 -3
  124. package/dist/components/ui/dropdown-menu.js +40 -16
  125. package/dist/components/ui/dropdown-menu.mjs +3 -3
  126. package/dist/components/ui/empty.js +41 -5
  127. package/dist/components/ui/empty.mjs +2 -2
  128. package/dist/components/ui/expense-bar-chart.js +642 -0
  129. package/dist/components/ui/expense-bar-chart.mjs +16 -0
  130. package/dist/components/ui/field.js +53 -21
  131. package/dist/components/ui/field.mjs +4 -4
  132. package/dist/components/ui/financial-cards.js +1002 -0
  133. package/dist/components/ui/financial-cards.mjs +24 -0
  134. package/dist/components/ui/financial-drawers.js +637 -0
  135. package/dist/components/ui/financial-drawers.mjs +17 -0
  136. package/dist/components/ui/financial-primitives.js +218 -0
  137. package/dist/components/ui/financial-primitives.mjs +22 -0
  138. package/dist/components/ui/financial-sections.js +1422 -0
  139. package/dist/components/ui/financial-sections.mjs +30 -0
  140. package/dist/components/ui/form-primitives.js +682 -0
  141. package/dist/components/ui/form-primitives.mjs +19 -0
  142. package/dist/components/ui/income-bar-chart.js +641 -0
  143. package/dist/components/ui/income-bar-chart.mjs +16 -0
  144. package/dist/components/ui/input-group.js +43 -7
  145. package/dist/components/ui/input-group.mjs +5 -5
  146. package/dist/components/ui/input-otp.js +39 -3
  147. package/dist/components/ui/input-otp.mjs +2 -2
  148. package/dist/components/ui/input.js +34 -2
  149. package/dist/components/ui/input.mjs +2 -2
  150. package/dist/components/ui/kanban-column.js +1143 -0
  151. package/dist/components/ui/kanban-column.mjs +20 -0
  152. package/dist/components/ui/label.js +35 -7
  153. package/dist/components/ui/label.mjs +2 -2
  154. package/dist/components/ui/opportunity-card.js +960 -0
  155. package/dist/components/ui/opportunity-card.mjs +20 -0
  156. package/dist/components/ui/opportunity-edit-modals.js +3360 -0
  157. package/dist/components/ui/opportunity-edit-modals.mjs +37 -0
  158. package/dist/components/ui/opportunity-summary-tab.js +4365 -0
  159. package/dist/components/ui/opportunity-summary-tab.mjs +34 -0
  160. package/dist/components/ui/pagination.js +35 -3
  161. package/dist/components/ui/pagination.mjs +3 -3
  162. package/dist/components/ui/pipeline-alerts.js +103 -0
  163. package/dist/components/ui/pipeline-alerts.mjs +8 -0
  164. package/dist/components/ui/pipeline-board.js +1408 -0
  165. package/dist/components/ui/pipeline-board.mjs +24 -0
  166. package/dist/components/ui/pipeline-chart.js +216 -0
  167. package/dist/components/ui/pipeline-chart.mjs +10 -0
  168. package/dist/components/ui/pipeline-dialogs.js +1183 -0
  169. package/dist/components/ui/pipeline-dialogs.mjs +23 -0
  170. package/dist/components/ui/pipeline-primitives.js +300 -0
  171. package/dist/components/ui/pipeline-primitives.mjs +11 -0
  172. package/dist/components/ui/popover.js +45 -4
  173. package/dist/components/ui/popover.mjs +3 -3
  174. package/dist/components/ui/progress.js +33 -1
  175. package/dist/components/ui/progress.mjs +2 -2
  176. package/dist/components/ui/property-cashflow-doughnut-chart.js +523 -0
  177. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +16 -0
  178. package/dist/components/ui/property-debt-equity-doughnut-chart.js +521 -0
  179. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +16 -0
  180. package/dist/components/ui/property-mobile-estimate-line-chart.js +682 -0
  181. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +16 -0
  182. package/dist/components/ui/radio-group.js +33 -1
  183. package/dist/components/ui/radio-group.mjs +2 -2
  184. package/dist/components/ui/select.js +66 -26
  185. package/dist/components/ui/select.mjs +3 -3
  186. package/dist/components/ui/separator.js +33 -1
  187. package/dist/components/ui/separator.mjs +2 -2
  188. package/dist/components/ui/sheet.js +37 -9
  189. package/dist/components/ui/sheet.mjs +3 -3
  190. package/dist/components/ui/skeleton.js +33 -1
  191. package/dist/components/ui/skeleton.mjs +2 -2
  192. package/dist/components/ui/slider.js +86 -102
  193. package/dist/components/ui/slider.mjs +2 -2
  194. package/dist/components/ui/spinner.js +33 -1
  195. package/dist/components/ui/spinner.mjs +2 -2
  196. package/dist/components/ui/stage-timeline.js +579 -0
  197. package/dist/components/ui/stage-timeline.mjs +15 -0
  198. package/dist/components/ui/switch.js +37 -4
  199. package/dist/components/ui/switch.mjs +2 -3
  200. package/dist/components/ui/table.js +37 -5
  201. package/dist/components/ui/table.mjs +2 -2
  202. package/dist/components/ui/tabs.js +36 -12
  203. package/dist/components/ui/tabs.mjs +2 -2
  204. package/dist/components/ui/textarea.js +34 -2
  205. package/dist/components/ui/textarea.mjs +2 -2
  206. package/dist/components/ui/toggle-group.js +35 -4
  207. package/dist/components/ui/toggle-group.mjs +3 -4
  208. package/dist/components/ui/toggle.js +35 -4
  209. package/dist/components/ui/toggle.mjs +2 -3
  210. package/dist/components/ui/tooltip.js +51 -22
  211. package/dist/components/ui/tooltip.mjs +3 -3
  212. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +528 -0
  213. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +16 -0
  214. package/dist/components/ui/transactions-income-expense-bar-chart.js +516 -0
  215. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +16 -0
  216. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +528 -0
  217. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +16 -0
  218. package/dist/index.js +11613 -2868
  219. package/dist/index.mjs +377 -164
  220. package/dist/lib/theme-provider.js +10 -1
  221. package/dist/lib/theme-provider.mjs +1 -1
  222. package/dist/lib/typography.js +8 -0
  223. package/dist/lib/typography.mjs +3 -1
  224. package/dist/lib/utils.js +33 -1
  225. package/dist/lib/utils.mjs +1 -1
  226. package/dist/styles.css +1 -1
  227. package/package.json +169 -6
  228. package/src/components/index.tsx +323 -13
  229. package/src/components/ui/accordion.tsx +6 -3
  230. package/src/components/ui/add-column-modal.tsx +339 -0
  231. package/src/components/ui/add-lead-modal.tsx +290 -0
  232. package/src/components/ui/ai-assistant-drawer.tsx +408 -0
  233. package/src/components/ui/alert-dialog.tsx +80 -54
  234. package/src/components/ui/alert.tsx +28 -28
  235. package/src/components/ui/avatar.tsx +30 -29
  236. package/src/components/ui/backoffice-alert-history-chart.tsx +260 -0
  237. package/src/components/ui/backoffice-contact-history-chart.tsx +325 -0
  238. package/src/components/ui/badge.tsx +17 -15
  239. package/src/components/ui/borrowing-capacity-line-chart.tsx +357 -0
  240. package/src/components/ui/button.tsx +30 -27
  241. package/src/components/ui/calendar.tsx +53 -67
  242. package/src/components/ui/card.tsx +27 -24
  243. package/src/components/ui/cash-balance-line-chart.tsx +302 -0
  244. package/src/components/ui/cashflow-bar-chart.tsx +363 -0
  245. package/src/components/ui/chart-shared.tsx +261 -0
  246. package/src/components/ui/checkbox.tsx +30 -26
  247. package/src/components/ui/combobox.tsx +223 -0
  248. package/src/components/ui/data-table.tsx +160 -99
  249. package/src/components/ui/date-picker.tsx +0 -2
  250. package/src/components/ui/dialog.tsx +70 -60
  251. package/src/components/ui/drawer.tsx +57 -48
  252. package/src/components/ui/dropdown-menu.tsx +90 -82
  253. package/src/components/ui/empty.tsx +31 -27
  254. package/src/components/ui/expense-bar-chart.tsx +296 -0
  255. package/src/components/ui/field.tsx +70 -62
  256. package/src/components/ui/financial-cards.tsx +830 -0
  257. package/src/components/ui/financial-drawers.tsx +339 -0
  258. package/src/components/ui/financial-primitives.tsx +331 -0
  259. package/src/components/ui/financial-sections.tsx +672 -0
  260. package/src/components/ui/form-primitives.tsx +536 -0
  261. package/src/components/ui/income-bar-chart.tsx +297 -0
  262. package/src/components/ui/input-group.tsx +41 -34
  263. package/src/components/ui/input-otp.tsx +29 -24
  264. package/src/components/ui/input.tsx +8 -8
  265. package/src/components/ui/kanban-column.tsx +333 -0
  266. package/src/components/ui/label.tsx +9 -12
  267. package/src/components/ui/opportunity-card.tsx +616 -0
  268. package/src/components/ui/opportunity-edit-modals.tsx +2528 -0
  269. package/src/components/ui/opportunity-summary-tab.tsx +579 -0
  270. package/src/components/ui/pipeline-alerts.tsx +74 -0
  271. package/src/components/ui/pipeline-board.tsx +268 -0
  272. package/src/components/ui/pipeline-chart.tsx +173 -0
  273. package/src/components/ui/pipeline-dialogs.tsx +303 -0
  274. package/src/components/ui/pipeline-primitives.tsx +108 -0
  275. package/src/components/ui/popover.tsx +41 -36
  276. package/src/components/ui/property-cashflow-doughnut-chart.tsx +188 -0
  277. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +185 -0
  278. package/src/components/ui/property-mobile-estimate-line-chart.tsx +393 -0
  279. package/src/components/ui/select.tsx +65 -52
  280. package/src/components/ui/sheet.tsx +55 -52
  281. package/src/components/ui/slider.tsx +54 -77
  282. package/src/components/ui/stage-timeline.tsx +205 -0
  283. package/src/components/ui/switch.tsx +42 -29
  284. package/src/components/ui/table.tsx +28 -28
  285. package/src/components/ui/tabs.tsx +22 -28
  286. package/src/components/ui/textarea.tsx +8 -8
  287. package/src/components/ui/toggle-group.tsx +0 -2
  288. package/src/components/ui/toggle.tsx +13 -15
  289. package/src/components/ui/tooltip.tsx +30 -28
  290. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +191 -0
  291. package/src/components/ui/transactions-income-expense-bar-chart.tsx +205 -0
  292. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +191 -0
  293. package/src/lib/theme-provider.tsx +10 -0
  294. package/src/lib/typography.ts +9 -0
  295. package/src/lib/utils.ts +41 -3
  296. package/src/styles/globals.css +371 -124
  297. package/src/styles/styles-css.ts +1 -1
  298. package/tsup.config.ts +32 -0
  299. package/dist/chunk-K74JRTJR.mjs +0 -105
  300. package/dist/chunk-V7CNWJT3.mjs +0 -10
@@ -0,0 +1,296 @@
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
+ FALLBACK_SECONDARY,
23
+ CHART_SLICE_COUNT,
24
+ CHART_PERIODS,
25
+ formatTooltipDate,
26
+ ChartLegendItem,
27
+ ChartPeriodButton,
28
+ type ChartPeriod,
29
+ type ChartGranularity,
30
+ } from "./chart-shared";
31
+
32
+ ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Types
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export interface ExpenseDataset {
39
+ /** Expense category label, e.g. "Housing", "Food", "Transport" */
40
+ label: string;
41
+ /** One value per data point, aligned to the months array */
42
+ data: number[];
43
+ }
44
+
45
+ export interface ExpenseBarChartData {
46
+ /** Display labels, e.g. ["Jul", "Aug"] for monthly or ["Mar 8", "Mar 9"] for daily */
47
+ months: string[];
48
+ /** Optional ISO date strings per point — used for the tooltip title */
49
+ dates?: string[];
50
+ datasets: ExpenseDataset[];
51
+ }
52
+
53
+ export type ExpensePeriod = ChartPeriod;
54
+ export type ExpenseGranularity = ChartGranularity;
55
+
56
+ export interface ExpenseBarChartProps {
57
+ /** Full dataset — sliced to the selected period */
58
+ expenseData: ExpenseBarChartData | null;
59
+ title?: string;
60
+ /** Show or hide the chart legend */
61
+ showLegend?: boolean;
62
+ /** Show or hide X axis labels */
63
+ showXAxis?: boolean;
64
+ /** Show or hide Y axis labels */
65
+ showYAxis?: boolean;
66
+ /** Legend placement relative to chart */
67
+ legendPosition?: "top" | "bottom";
68
+ /** Default period selector value */
69
+ defaultPeriod?: ExpensePeriod;
70
+ /**
71
+ * Data granularity — controls available period buttons and slice counts.
72
+ * "monthly" (default): shows 3M / 6M / 12M, slices by month count.
73
+ * "daily": shows 1M / 3M / 6M / 12M, slices by day count (1M=30, 3M=90, etc.).
74
+ */
75
+ granularity?: ExpenseGranularity;
76
+ /** Chart canvas height in pixels */
77
+ height?: number;
78
+ /** Width of the card in pixels */
79
+ width?: number | string;
80
+ className?: string;
81
+ /** Show skeleton loading state instead of the chart */
82
+ isLoading?: boolean;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // ---------------------------------------------------------------------------
87
+ // Component
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export function ExpenseBarChart({
91
+ expenseData,
92
+ title = "Expenses",
93
+ showLegend = true,
94
+ showXAxis = true,
95
+ showYAxis = true,
96
+ legendPosition = "top",
97
+ defaultPeriod = 6,
98
+ granularity = "monthly",
99
+ height = 280,
100
+ width = "100%",
101
+ className,
102
+ isLoading = false,
103
+ }: ExpenseBarChartProps) {
104
+ const periods = CHART_PERIODS[granularity];
105
+ const [period, setPeriod] = useState<ExpensePeriod>(defaultPeriod);
106
+
107
+ // Reset period when granularity changes, but not on initial mount
108
+ // (defaultPeriod handles the initial value).
109
+ const isFirstRender = useRef(true);
110
+ useEffect(() => {
111
+ if (isFirstRender.current) {
112
+ isFirstRender.current = false;
113
+ return;
114
+ }
115
+ setPeriod(CHART_PERIODS[granularity][0]);
116
+ }, [granularity]);
117
+
118
+ const themeVars = useThemeVars();
119
+ const brandSecondary: string =
120
+ (themeVars["--theme-secondary"] as string | undefined) ||
121
+ FALLBACK_SECONDARY;
122
+ const fontFamily: string =
123
+ (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif";
124
+
125
+ const sliced = useMemo<ExpenseBarChartData | null>(() => {
126
+ if (!expenseData?.months?.length || !expenseData.datasets.length)
127
+ return null;
128
+ const count = Math.min(
129
+ CHART_SLICE_COUNT[granularity][period],
130
+ expenseData.months.length,
131
+ );
132
+ const start = expenseData.months.length - count;
133
+ return {
134
+ months: expenseData.months.slice(start),
135
+ dates: expenseData.dates?.slice(start),
136
+ datasets: expenseData.datasets.map((ds) => ({
137
+ ...ds,
138
+ data: ds.data.slice(start),
139
+ })),
140
+ };
141
+ }, [expenseData, period, granularity]);
142
+
143
+ const datasetColors = useMemo(
144
+ () =>
145
+ sliced?.datasets.map((_, i) =>
146
+ hexToRgba(brandSecondary, DATASET_ALPHAS[i % DATASET_ALPHAS.length]),
147
+ ) ?? [],
148
+ [sliced, brandSecondary],
149
+ );
150
+
151
+ const chartData = useMemo<ChartData<"bar">>(() => {
152
+ if (!sliced) return { labels: [], datasets: [] };
153
+ return {
154
+ labels: sliced.months,
155
+ datasets: sliced.datasets.map((ds, i) => ({
156
+ label: ds.label,
157
+ data: ds.data,
158
+ backgroundColor: datasetColors[i],
159
+ hoverBackgroundColor: hexToRgba(
160
+ brandSecondary,
161
+ Math.min(DATASET_ALPHAS[i % DATASET_ALPHAS.length] + 0.15, 1),
162
+ ),
163
+ borderWidth: 0,
164
+ borderRadius: 0,
165
+ borderSkipped: false,
166
+ barPercentage: 0.75,
167
+ categoryPercentage: 0.7,
168
+ stack: "expense",
169
+ })),
170
+ };
171
+ }, [sliced, datasetColors, brandSecondary]);
172
+
173
+ const options = useMemo<ChartOptions<"bar">>(
174
+ () => ({
175
+ responsive: true,
176
+ maintainAspectRatio: false,
177
+ animation: { duration: 800, easing: "easeOutQuart" },
178
+ layout: { padding: 0 },
179
+ plugins: {
180
+ legend: { display: false },
181
+ tooltip: {
182
+ mode: "index",
183
+ intersect: false,
184
+ padding: 12,
185
+ cornerRadius: 0,
186
+ titleFont: { size: 11, weight: 600 },
187
+ bodyFont: { size: 12, weight: 500 },
188
+ callbacks: {
189
+ title: (tooltipItems) => {
190
+ const idx = tooltipItems[0]?.dataIndex;
191
+ if (idx != null && sliced?.dates?.[idx]) {
192
+ return formatTooltipDate(sliced.dates[idx], granularity);
193
+ }
194
+ return tooltipItems[0]?.label ?? "";
195
+ },
196
+ label: (ctx) => {
197
+ const val = ctx.raw as number;
198
+ if (val === 0) return;
199
+ return ` ${ctx.dataset.label}: $${val.toLocaleString()}`;
200
+ },
201
+ },
202
+ },
203
+ },
204
+ scales: {
205
+ x: {
206
+ display: showXAxis,
207
+ stacked: true,
208
+ grid: { display: false },
209
+ border: { display: false },
210
+ ticks: { font: { size: 10 }, color: FALLBACK_TICK },
211
+ },
212
+ y: {
213
+ display: showYAxis,
214
+ stacked: true,
215
+ grid: { display: false },
216
+ border: { display: false },
217
+ ticks: {
218
+ font: { size: 10 },
219
+ color: FALLBACK_TICK,
220
+ maxTicksLimit: 5,
221
+ padding: 8,
222
+ callback: (v) => `$${Number(v).toLocaleString()}`,
223
+ },
224
+ },
225
+ },
226
+ }),
227
+ [showXAxis, showYAxis, sliced, granularity],
228
+ );
229
+
230
+ return (
231
+ <Card
232
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
233
+ style={{ maxWidth: width, fontFamily }}
234
+ >
235
+ <CardHeader className="px-3 sm:px-6">
236
+ <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
237
+ <CardAction>
238
+ <div className="flex gap-0.5 sm:gap-1">
239
+ {periods.map((p) => (
240
+ <ChartPeriodButton
241
+ key={p}
242
+ period={p}
243
+ active={period === p}
244
+ onClick={() => setPeriod(p)}
245
+ />
246
+ ))}
247
+ </div>
248
+ </CardAction>
249
+ </CardHeader>
250
+
251
+ <CardContent className="px-3 sm:px-6">
252
+ {isLoading ? (
253
+ <Skeleton style={{ height, width: "100%" }} />
254
+ ) : !sliced ? (
255
+ <Empty className="flex-none p-4" style={{ height }}>
256
+ <EmptyDescription>No data available</EmptyDescription>
257
+ </Empty>
258
+ ) : (
259
+ <div className="flex flex-col gap-2">
260
+ {showLegend && legendPosition === "top" && (
261
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
262
+ {sliced.datasets.map((ds, i) => (
263
+ <ChartLegendItem
264
+ key={ds.label}
265
+ label={ds.label}
266
+ color={datasetColors[i]}
267
+ />
268
+ ))}
269
+ </div>
270
+ )}
271
+ <div style={{ height, width: "100%", position: "relative" }}>
272
+ <Chart
273
+ key={brandSecondary}
274
+ type="bar"
275
+ data={chartData}
276
+ options={options}
277
+ aria-label={title}
278
+ />
279
+ </div>
280
+ {showLegend && legendPosition === "bottom" && (
281
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pt-2">
282
+ {sliced.datasets.map((ds, i) => (
283
+ <ChartLegendItem
284
+ key={ds.label}
285
+ label={ds.label}
286
+ color={datasetColors[i]}
287
+ />
288
+ ))}
289
+ </div>
290
+ )}
291
+ </div>
292
+ )}
293
+ </CardContent>
294
+ </Card>
295
+ );
296
+ }
@@ -1,8 +1,8 @@
1
- import { type ReactElement, useMemo } from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import { cn } from "@/lib/utils"
4
- import { Label } from "@/components/ui/label"
5
- import { Separator } from "@/components/ui/separator"
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Separator } from "@/components/ui/separator";
6
6
 
7
7
  /**
8
8
  * Field — shadcn/WealthX
@@ -14,7 +14,7 @@ import { Separator } from "@/components/ui/separator"
14
14
  * - FieldError.uniqueErrors: Array.from() instead of spread on Map iterator (es5 compat)
15
15
  */
16
16
 
17
- export type FieldSetProps = React.ComponentProps<"fieldset">
17
+ export type FieldSetProps = React.ComponentProps<"fieldset">;
18
18
 
19
19
  function FieldSet({ className, ...props }: FieldSetProps): ReactElement {
20
20
  return (
@@ -22,15 +22,17 @@ function FieldSet({ className, ...props }: FieldSetProps): ReactElement {
22
22
  className={cn(
23
23
  "flex flex-col gap-6",
24
24
  "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
25
- className
25
+ className,
26
26
  )}
27
27
  data-slot="field-set"
28
28
  {...props}
29
29
  />
30
- )
30
+ );
31
31
  }
32
32
 
33
- export type FieldLegendProps = React.ComponentProps<"legend"> & { variant?: "legend" | "label" }
33
+ export type FieldLegendProps = React.ComponentProps<"legend"> & {
34
+ variant?: "legend" | "label";
35
+ };
34
36
 
35
37
  function FieldLegend({
36
38
  className,
@@ -40,31 +42,31 @@ function FieldLegend({
40
42
  return (
41
43
  <legend
42
44
  className={cn(
43
- "mb-3 font-medium",
44
- "data-[variant=legend]:text-base",
45
- "data-[variant=label]:text-sm",
46
- className
45
+ "mb-3",
46
+ "data-[variant=legend]:text-label-large",
47
+ "data-[variant=label]:text-label-medium",
48
+ className,
47
49
  )}
48
50
  data-slot="field-legend"
49
51
  data-variant={variant}
50
52
  {...props}
51
53
  />
52
- )
54
+ );
53
55
  }
54
56
 
55
- export type FieldGroupProps = React.ComponentProps<"div">
57
+ export type FieldGroupProps = React.ComponentProps<"div">;
56
58
 
57
59
  function FieldGroup({ className, ...props }: FieldGroupProps): ReactElement {
58
60
  return (
59
61
  <div
60
62
  className={cn(
61
63
  "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
62
- className
64
+ className,
63
65
  )}
64
66
  data-slot="field-group"
65
67
  {...props}
66
68
  />
67
- )
69
+ );
68
70
  }
69
71
 
70
72
  const fieldVariants = cva(
@@ -88,10 +90,11 @@ const fieldVariants = cva(
88
90
  defaultVariants: {
89
91
  orientation: "vertical",
90
92
  },
91
- }
92
- )
93
+ },
94
+ );
93
95
 
94
- export type FieldProps = React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>
96
+ export type FieldProps = React.ComponentProps<"div"> &
97
+ VariantProps<typeof fieldVariants>;
95
98
 
96
99
  function Field({
97
100
  className,
@@ -106,79 +109,82 @@ function Field({
106
109
  role="group"
107
110
  {...props}
108
111
  />
109
- )
112
+ );
110
113
  }
111
114
 
112
- export type FieldContentProps = React.ComponentProps<"div">
115
+ export type FieldContentProps = React.ComponentProps<"div">;
113
116
 
114
- function FieldContent({ className, ...props }: FieldContentProps): ReactElement {
117
+ function FieldContent({
118
+ className,
119
+ ...props
120
+ }: FieldContentProps): ReactElement {
115
121
  return (
116
122
  <div
117
123
  className={cn(
118
124
  "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
119
- className
125
+ className,
120
126
  )}
121
127
  data-slot="field-content"
122
128
  {...props}
123
129
  />
124
- )
130
+ );
125
131
  }
126
132
 
127
- export type FieldLabelProps = React.ComponentProps<typeof Label>
133
+ export type FieldLabelProps = React.ComponentProps<typeof Label>;
128
134
 
129
- function FieldLabel({
130
- className,
131
- ...props
132
- }: FieldLabelProps): ReactElement {
135
+ function FieldLabel({ className, ...props }: FieldLabelProps): ReactElement {
133
136
  return (
134
137
  <Label
135
138
  className={cn(
136
139
  "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
137
140
  "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
138
141
  "has-data-checked:border-primary has-data-checked:bg-primary/5 dark:has-data-checked:bg-primary/10",
139
- className
142
+ className,
140
143
  )}
141
144
  data-slot="field-label"
142
145
  {...props}
143
146
  />
144
- )
147
+ );
145
148
  }
146
149
 
147
- export type FieldTitleProps = React.ComponentProps<"div">
150
+ export type FieldTitleProps = React.ComponentProps<"div">;
148
151
 
149
152
  function FieldTitle({ className, ...props }: FieldTitleProps): ReactElement {
150
153
  return (
151
154
  <div
152
155
  className={cn(
153
- "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
154
- className
156
+ "flex w-fit items-center gap-2 text-label-medium leading-snug group-data-[disabled=true]/field:opacity-50",
157
+ className,
155
158
  )}
156
159
  data-slot="field-label"
157
160
  {...props}
158
161
  />
159
- )
162
+ );
160
163
  }
161
164
 
162
- export type FieldDescriptionProps = React.ComponentProps<"p">
165
+ export type FieldDescriptionProps = React.ComponentProps<"p">;
163
166
 
164
- function FieldDescription({ className, ...props }: FieldDescriptionProps): ReactElement {
167
+ function FieldDescription({
168
+ className,
169
+ ...props
170
+ }: FieldDescriptionProps): ReactElement {
165
171
  return (
166
172
  <p
167
173
  className={cn(
168
- "text-xs leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
174
+ "text-caption leading-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
169
175
  "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
170
176
  "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
171
- className
177
+ className,
172
178
  )}
173
179
  data-slot="field-description"
174
180
  {...props}
175
181
  />
176
- )
182
+ );
177
183
  }
178
184
 
179
185
  export type FieldSeparatorProps = React.ComponentProps<"div"> & {
180
- children?: React.ReactNode
181
- }
186
+ children?: React.ReactNode;
187
+ };
182
188
 
183
189
  function FieldSeparator({
184
190
  children,
@@ -188,27 +194,29 @@ function FieldSeparator({
188
194
  return (
189
195
  <div
190
196
  className={cn(
191
- "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
192
- className
197
+ "relative -my-2 h-5 text-body-small group-data-[variant=outline]/field-group:-mb-2",
198
+ className,
193
199
  )}
194
200
  data-content={Boolean(children)}
195
201
  data-slot="field-separator"
196
202
  {...props}
197
203
  >
198
204
  <Separator className="absolute inset-0 top-1/2" />
199
- {children ? <span
205
+ {children ? (
206
+ <span
200
207
  className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
201
208
  data-slot="field-separator-content"
202
209
  >
203
210
  {children}
204
- </span> : null}
211
+ </span>
212
+ ) : null}
205
213
  </div>
206
- )
214
+ );
207
215
  }
208
216
 
209
217
  export type FieldErrorProps = React.ComponentProps<"div"> & {
210
- errors?: ({ message?: string } | undefined)[]
211
- }
218
+ errors?: ({ message?: string } | undefined)[];
219
+ };
212
220
 
213
221
  function FieldError({
214
222
  className,
@@ -218,45 +226,45 @@ function FieldError({
218
226
  }: FieldErrorProps): ReactElement | null {
219
227
  const content = useMemo(() => {
220
228
  if (children) {
221
- return children
229
+ return children;
222
230
  }
223
231
 
224
232
  if (!errors?.length) {
225
- return null
233
+ return null;
226
234
  }
227
235
 
228
236
  const uniqueErrors = Array.from(
229
- new Map(errors.map((error) => [error?.message, error])).values()
230
- )
237
+ new Map(errors.map((error) => [error?.message, error])).values(),
238
+ );
231
239
 
232
240
  if (uniqueErrors.length === 1) {
233
- return uniqueErrors[0]?.message
241
+ return uniqueErrors[0]?.message;
234
242
  }
235
243
 
236
244
  return (
237
245
  <ul className="ml-4 flex list-disc flex-col gap-1">
238
246
  {uniqueErrors.map(
239
247
  (error) =>
240
- error?.message && <li key={error.message}>{error.message}</li>
248
+ error?.message && <li key={error.message}>{error.message}</li>,
241
249
  )}
242
250
  </ul>
243
- )
244
- }, [children, errors])
251
+ );
252
+ }, [children, errors]);
245
253
 
246
254
  if (!content) {
247
- return null
255
+ return null;
248
256
  }
249
257
 
250
258
  return (
251
259
  <div
252
- className={cn("text-xs font-normal text-destructive", className)}
260
+ className={cn("text-caption text-destructive", className)}
253
261
  data-slot="field-error"
254
262
  role="alert"
255
263
  {...props}
256
264
  >
257
265
  {content}
258
266
  </div>
259
- )
267
+ );
260
268
  }
261
269
 
262
270
  export {
@@ -270,4 +278,4 @@ export {
270
278
  FieldSet,
271
279
  FieldContent,
272
280
  FieldTitle,
273
- }
281
+ };