@wealthx/shadcn 1.1.0 → 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 -154
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-6OJF6XRN.mjs → chunk-24FUO7TD.mjs} +4 -8
  4. package/dist/{chunk-4AJ5HWHD.mjs → chunk-2I5S2AMY.mjs} +3 -3
  5. package/dist/chunk-2SF672SZ.mjs +161 -0
  6. package/dist/{chunk-GPRJQ24C.mjs → chunk-34NWQURD.mjs} +2 -2
  7. package/dist/{chunk-MQ72DIBH.mjs → chunk-3GF7OVTP.mjs} +14 -5
  8. package/dist/chunk-3WMX6KWS.mjs +245 -0
  9. package/dist/{chunk-PMKODV6M.mjs → chunk-462HMNO4.mjs} +6 -10
  10. package/dist/chunk-4CX4SBRO.mjs +153 -0
  11. package/dist/chunk-4MN6UQHG.mjs +443 -0
  12. package/dist/{chunk-GLW2UO6O.mjs → chunk-5QQVZTVZ.mjs} +82 -61
  13. package/dist/{chunk-BGP2N52Z.mjs → chunk-66MI7Q4B.mjs} +5 -5
  14. package/dist/chunk-6FCGKSZX.mjs +268 -0
  15. package/dist/{chunk-CGOKTPXU.mjs → chunk-6JQFUE5I.mjs} +20 -23
  16. package/dist/{chunk-Z3MK2KKZ.mjs → chunk-7DHU4VGG.mjs} +7 -3
  17. package/dist/{chunk-VZ2NR7L3.mjs → chunk-7PYJD5JI.mjs} +35 -27
  18. package/dist/{chunk-JU2RUWHF.mjs → chunk-7XJHLGUV.mjs} +1 -1
  19. package/dist/{chunk-BMFN37JH.mjs → chunk-7YAU5CY6.mjs} +1 -1
  20. package/dist/chunk-A56YQQHG.mjs +402 -0
  21. package/dist/chunk-AH52LG6N.mjs +315 -0
  22. package/dist/{chunk-SLWCCURD.mjs → chunk-CLIN5525.mjs} +8 -4
  23. package/dist/{chunk-3VQNJ235.mjs → chunk-CSDO6VBW.mjs} +7 -0
  24. package/dist/chunk-D4ILTPOG.mjs +293 -0
  25. package/dist/{chunk-HS7TFG7V.mjs → chunk-D6ID6M4V.mjs} +1 -1
  26. package/dist/chunk-DOH3EHX7.mjs +378 -0
  27. package/dist/{chunk-MJIEMGRD.mjs → chunk-EFRENWEJ.mjs} +9 -17
  28. package/dist/{chunk-YBXCIF5Q.mjs → chunk-ERGGHC2V.mjs} +36 -49
  29. package/dist/{chunk-OXQQNQZI.mjs → chunk-FEZKMUCF.mjs} +10 -1
  30. package/dist/{chunk-55CEW76V.mjs → chunk-FH6QVUVZ.mjs} +1 -1
  31. package/dist/chunk-FMAXJ2SI.mjs +71 -0
  32. package/dist/chunk-FZIXGLMV.mjs +173 -0
  33. package/dist/{chunk-DS2AMHN2.mjs → chunk-GYMYRIZP.mjs} +2 -2
  34. package/dist/{chunk-KQDD5MU3.mjs → chunk-H45TKD34.mjs} +5 -5
  35. package/dist/{chunk-BBJBJSXQ.mjs → chunk-J5UICVJS.mjs} +1 -1
  36. package/dist/{chunk-RL772EH7.mjs → chunk-JHJHG4GO.mjs} +4 -12
  37. package/dist/{chunk-RN67642N.mjs → chunk-KMCGSZTX.mjs} +47 -41
  38. package/dist/{chunk-FHNT55I5.mjs → chunk-KUDCQ4FI.mjs} +4 -4
  39. package/dist/chunk-LE6YFY6D.mjs +209 -0
  40. package/dist/{chunk-NLLKTU4B.mjs → chunk-LLVQKSU3.mjs} +21 -17
  41. package/dist/{chunk-KKHTJNMM.mjs → chunk-MARPPFOJ.mjs} +8 -4
  42. package/dist/{chunk-6AFMNC42.mjs → chunk-N2PT566P.mjs} +15 -11
  43. package/dist/chunk-NLCKVHWB.mjs +161 -0
  44. package/dist/{chunk-YN5SYTOO.mjs → chunk-NQPOYKAQ.mjs} +9 -5
  45. package/dist/{chunk-ZZV5JVNW.mjs → chunk-NSLMILBT.mjs} +3 -7
  46. package/dist/chunk-NXA3CZ7A.mjs +248 -0
  47. package/dist/chunk-OGOYQ7BG.mjs +150 -0
  48. package/dist/{chunk-3NQGYJEZ.mjs → chunk-P6AM5V7O.mjs} +10 -18
  49. package/dist/{chunk-CZ3BW5GL.mjs → chunk-P76HMUI6.mjs} +5 -11
  50. package/dist/chunk-PCPLO5HT.mjs +671 -0
  51. package/dist/chunk-PG6K5XEC.mjs +475 -0
  52. package/dist/{chunk-5JGQAAQV.mjs → chunk-PJHPSRYD.mjs} +84 -62
  53. package/dist/{chunk-DDPA2XXS.mjs → chunk-PMB3A7V3.mjs} +2 -2
  54. package/dist/chunk-PR6V5XKM.mjs +209 -0
  55. package/dist/{chunk-46OFHMQA.mjs → chunk-Q76O3RIQ.mjs} +10 -6
  56. package/dist/chunk-QVKWW6KE.mjs +272 -0
  57. package/dist/chunk-RGU7HOEC.mjs +140 -0
  58. package/dist/{chunk-JF4PHPD5.mjs → chunk-RGVKLTLH.mjs} +4 -4
  59. package/dist/{chunk-VG6UF6UT.mjs → chunk-RP3SQYA3.mjs} +2 -2
  60. package/dist/chunk-RRBS6D63.mjs +163 -0
  61. package/dist/{chunk-UEL4RD5P.mjs → chunk-SMQ3DG25.mjs} +80 -67
  62. package/dist/chunk-SPJ5KXW7.mjs +199 -0
  63. package/dist/chunk-SYOD63OZ.mjs +225 -0
  64. package/dist/chunk-UFYSFDER.mjs +42 -0
  65. package/dist/chunk-VACKZOMY.mjs +190 -0
  66. package/dist/chunk-VLQZANBF.mjs +42 -0
  67. package/dist/chunk-WA6O6EUR.mjs +1885 -0
  68. package/dist/{chunk-E3K6O4FZ.mjs → chunk-WAZD7NFU.mjs} +5 -2
  69. package/dist/chunk-WG6JGJXB.mjs +165 -0
  70. package/dist/{chunk-I64K754C.mjs → chunk-WNGWBVLV.mjs} +2 -2
  71. package/dist/{chunk-3U7SD3MS.mjs → chunk-WOEHFRGB.mjs} +3 -3
  72. package/dist/{chunk-DKZRJOMF.mjs → chunk-XIRTEFKH.mjs} +12 -12
  73. package/dist/chunk-Y6DWJSKZ.mjs +79 -0
  74. package/dist/chunk-YKPROFLB.mjs +161 -0
  75. package/dist/{chunk-CJ46PDXE.mjs → chunk-ZRO5JO3H.mjs} +106 -66
  76. package/dist/{chunk-VYMHBV6D.mjs → chunk-ZU4NV6RG.mjs} +5 -3
  77. package/dist/components/ui/accordion.js +40 -4
  78. package/dist/components/ui/accordion.mjs +2 -2
  79. package/dist/components/ui/add-column-modal.js +789 -0
  80. package/dist/components/ui/add-column-modal.mjs +17 -0
  81. package/dist/components/ui/add-lead-modal.js +647 -0
  82. package/dist/components/ui/add-lead-modal.mjs +16 -0
  83. package/dist/components/ui/ai-assistant-drawer.js +686 -0
  84. package/dist/components/ui/ai-assistant-drawer.mjs +16 -0
  85. package/dist/components/ui/alert-dialog.js +37 -5
  86. package/dist/components/ui/alert-dialog.mjs +4 -4
  87. package/dist/components/ui/alert.js +37 -11
  88. package/dist/components/ui/alert.mjs +2 -2
  89. package/dist/components/ui/avatar.js +36 -8
  90. package/dist/components/ui/avatar.mjs +2 -2
  91. package/dist/components/ui/backoffice-alert-history-chart.js +624 -0
  92. package/dist/components/ui/backoffice-alert-history-chart.mjs +16 -0
  93. package/dist/components/ui/backoffice-contact-history-chart.js +687 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.mjs +16 -0
  95. package/dist/components/ui/badge.js +37 -2
  96. package/dist/components/ui/badge.mjs +2 -2
  97. package/dist/components/ui/borrowing-capacity-line-chart.js +639 -0
  98. package/dist/components/ui/borrowing-capacity-line-chart.mjs +16 -0
  99. package/dist/components/ui/button.js +35 -3
  100. package/dist/components/ui/button.mjs +2 -2
  101. package/dist/components/ui/calendar.js +43 -19
  102. package/dist/components/ui/calendar.mjs +3 -3
  103. package/dist/components/ui/card.js +40 -4
  104. package/dist/components/ui/card.mjs +2 -2
  105. package/dist/components/ui/cash-balance-line-chart.js +627 -0
  106. package/dist/components/ui/cash-balance-line-chart.mjs +16 -0
  107. package/dist/components/ui/cashflow-bar-chart.js +123 -69
  108. package/dist/components/ui/cashflow-bar-chart.mjs +8 -8
  109. package/dist/components/ui/checkbox.js +36 -5
  110. package/dist/components/ui/checkbox.mjs +2 -3
  111. package/dist/components/ui/chip.js +37 -2
  112. package/dist/components/ui/chip.mjs +3 -3
  113. package/dist/components/ui/combobox.js +68 -49
  114. package/dist/components/ui/combobox.mjs +2 -2
  115. package/dist/components/ui/data-table.js +160 -88
  116. package/dist/components/ui/data-table.mjs +10 -11
  117. package/dist/components/ui/date-picker.js +44 -20
  118. package/dist/components/ui/date-picker.mjs +6 -7
  119. package/dist/components/ui/dialog.js +44 -12
  120. package/dist/components/ui/dialog.mjs +4 -4
  121. package/dist/components/ui/drawer.js +46 -10
  122. package/dist/components/ui/drawer.mjs +3 -3
  123. package/dist/components/ui/dropdown-menu.js +40 -16
  124. package/dist/components/ui/dropdown-menu.mjs +3 -3
  125. package/dist/components/ui/empty.js +41 -5
  126. package/dist/components/ui/empty.mjs +2 -2
  127. package/dist/components/ui/expense-bar-chart.js +165 -66
  128. package/dist/components/ui/expense-bar-chart.mjs +8 -8
  129. package/dist/components/ui/field.js +53 -21
  130. package/dist/components/ui/field.mjs +4 -4
  131. package/dist/components/ui/financial-cards.js +1002 -0
  132. package/dist/components/ui/financial-cards.mjs +24 -0
  133. package/dist/components/ui/financial-drawers.js +637 -0
  134. package/dist/components/ui/financial-drawers.mjs +17 -0
  135. package/dist/components/ui/financial-primitives.js +218 -0
  136. package/dist/components/ui/financial-primitives.mjs +22 -0
  137. package/dist/components/ui/financial-sections.js +1422 -0
  138. package/dist/components/ui/financial-sections.mjs +30 -0
  139. package/dist/components/ui/form-primitives.js +682 -0
  140. package/dist/components/ui/form-primitives.mjs +19 -0
  141. package/dist/components/ui/income-bar-chart.js +163 -65
  142. package/dist/components/ui/income-bar-chart.mjs +8 -8
  143. package/dist/components/ui/input-group.js +43 -7
  144. package/dist/components/ui/input-group.mjs +5 -5
  145. package/dist/components/ui/input-otp.js +39 -3
  146. package/dist/components/ui/input-otp.mjs +2 -2
  147. package/dist/components/ui/input.js +34 -2
  148. package/dist/components/ui/input.mjs +2 -2
  149. package/dist/components/ui/kanban-column.js +1143 -0
  150. package/dist/components/ui/kanban-column.mjs +20 -0
  151. package/dist/components/ui/label.js +35 -7
  152. package/dist/components/ui/label.mjs +2 -2
  153. package/dist/components/ui/opportunity-card.js +960 -0
  154. package/dist/components/ui/opportunity-card.mjs +20 -0
  155. package/dist/components/ui/opportunity-edit-modals.js +3360 -0
  156. package/dist/components/ui/opportunity-edit-modals.mjs +37 -0
  157. package/dist/components/ui/opportunity-summary-tab.js +4365 -0
  158. package/dist/components/ui/opportunity-summary-tab.mjs +34 -0
  159. package/dist/components/ui/pagination.js +35 -3
  160. package/dist/components/ui/pagination.mjs +3 -3
  161. package/dist/components/ui/pipeline-alerts.js +103 -0
  162. package/dist/components/ui/pipeline-alerts.mjs +8 -0
  163. package/dist/components/ui/pipeline-board.js +1408 -0
  164. package/dist/components/ui/pipeline-board.mjs +24 -0
  165. package/dist/components/ui/pipeline-chart.js +216 -0
  166. package/dist/components/ui/pipeline-chart.mjs +10 -0
  167. package/dist/components/ui/pipeline-dialogs.js +1183 -0
  168. package/dist/components/ui/pipeline-dialogs.mjs +23 -0
  169. package/dist/components/ui/pipeline-primitives.js +300 -0
  170. package/dist/components/ui/pipeline-primitives.mjs +11 -0
  171. package/dist/components/ui/popover.js +45 -4
  172. package/dist/components/ui/popover.mjs +3 -3
  173. package/dist/components/ui/progress.js +33 -1
  174. package/dist/components/ui/progress.mjs +2 -2
  175. package/dist/components/ui/property-cashflow-doughnut-chart.js +523 -0
  176. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +16 -0
  177. package/dist/components/ui/property-debt-equity-doughnut-chart.js +521 -0
  178. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +16 -0
  179. package/dist/components/ui/property-mobile-estimate-line-chart.js +682 -0
  180. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +16 -0
  181. package/dist/components/ui/radio-group.js +33 -1
  182. package/dist/components/ui/radio-group.mjs +2 -2
  183. package/dist/components/ui/select.js +66 -26
  184. package/dist/components/ui/select.mjs +3 -3
  185. package/dist/components/ui/separator.js +33 -1
  186. package/dist/components/ui/separator.mjs +2 -2
  187. package/dist/components/ui/sheet.js +37 -9
  188. package/dist/components/ui/sheet.mjs +3 -3
  189. package/dist/components/ui/skeleton.js +33 -1
  190. package/dist/components/ui/skeleton.mjs +2 -2
  191. package/dist/components/ui/slider.js +86 -102
  192. package/dist/components/ui/slider.mjs +2 -2
  193. package/dist/components/ui/spinner.js +33 -1
  194. package/dist/components/ui/spinner.mjs +2 -2
  195. package/dist/components/ui/stage-timeline.js +579 -0
  196. package/dist/components/ui/stage-timeline.mjs +15 -0
  197. package/dist/components/ui/switch.js +37 -4
  198. package/dist/components/ui/switch.mjs +2 -3
  199. package/dist/components/ui/table.js +37 -5
  200. package/dist/components/ui/table.mjs +2 -2
  201. package/dist/components/ui/tabs.js +36 -12
  202. package/dist/components/ui/tabs.mjs +2 -2
  203. package/dist/components/ui/textarea.js +34 -2
  204. package/dist/components/ui/textarea.mjs +2 -2
  205. package/dist/components/ui/toggle-group.js +35 -4
  206. package/dist/components/ui/toggle-group.mjs +3 -4
  207. package/dist/components/ui/toggle.js +35 -4
  208. package/dist/components/ui/toggle.mjs +2 -3
  209. package/dist/components/ui/tooltip.js +51 -22
  210. package/dist/components/ui/tooltip.mjs +3 -3
  211. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +528 -0
  212. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +16 -0
  213. package/dist/components/ui/transactions-income-expense-bar-chart.js +76 -38
  214. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +8 -8
  215. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +528 -0
  216. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +16 -0
  217. package/dist/index.js +11616 -3831
  218. package/dist/index.mjs +333 -161
  219. package/dist/lib/theme-provider.js +10 -1
  220. package/dist/lib/theme-provider.mjs +1 -1
  221. package/dist/lib/typography.js +8 -0
  222. package/dist/lib/typography.mjs +3 -1
  223. package/dist/lib/utils.js +33 -1
  224. package/dist/lib/utils.mjs +1 -1
  225. package/dist/styles.css +1 -1
  226. package/package.json +140 -5
  227. package/src/components/index.tsx +296 -42
  228. package/src/components/ui/accordion.tsx +6 -3
  229. package/src/components/ui/add-column-modal.tsx +339 -0
  230. package/src/components/ui/add-lead-modal.tsx +290 -0
  231. package/src/components/ui/ai-assistant-drawer.tsx +408 -0
  232. package/src/components/ui/alert-dialog.tsx +80 -54
  233. package/src/components/ui/alert.tsx +28 -28
  234. package/src/components/ui/avatar.tsx +30 -29
  235. package/src/components/ui/backoffice-alert-history-chart.tsx +260 -0
  236. package/src/components/ui/backoffice-contact-history-chart.tsx +325 -0
  237. package/src/components/ui/badge.tsx +17 -15
  238. package/src/components/ui/borrowing-capacity-line-chart.tsx +357 -0
  239. package/src/components/ui/button.tsx +30 -27
  240. package/src/components/ui/calendar.tsx +53 -67
  241. package/src/components/ui/card.tsx +27 -24
  242. package/src/components/ui/cash-balance-line-chart.tsx +302 -0
  243. package/src/components/ui/cashflow-bar-chart.tsx +104 -77
  244. package/src/components/ui/chart-shared.tsx +176 -15
  245. package/src/components/ui/checkbox.tsx +30 -26
  246. package/src/components/ui/combobox.tsx +78 -72
  247. package/src/components/ui/data-table.tsx +160 -99
  248. package/src/components/ui/date-picker.tsx +0 -2
  249. package/src/components/ui/dialog.tsx +70 -60
  250. package/src/components/ui/drawer.tsx +57 -48
  251. package/src/components/ui/dropdown-menu.tsx +90 -82
  252. package/src/components/ui/empty.tsx +31 -27
  253. package/src/components/ui/expense-bar-chart.tsx +83 -65
  254. package/src/components/ui/field.tsx +70 -62
  255. package/src/components/ui/financial-cards.tsx +830 -0
  256. package/src/components/ui/financial-drawers.tsx +339 -0
  257. package/src/components/ui/financial-primitives.tsx +331 -0
  258. package/src/components/ui/financial-sections.tsx +672 -0
  259. package/src/components/ui/form-primitives.tsx +536 -0
  260. package/src/components/ui/income-bar-chart.tsx +79 -60
  261. package/src/components/ui/input-group.tsx +41 -34
  262. package/src/components/ui/input-otp.tsx +29 -24
  263. package/src/components/ui/input.tsx +8 -8
  264. package/src/components/ui/kanban-column.tsx +333 -0
  265. package/src/components/ui/label.tsx +9 -12
  266. package/src/components/ui/opportunity-card.tsx +616 -0
  267. package/src/components/ui/opportunity-edit-modals.tsx +2528 -0
  268. package/src/components/ui/opportunity-summary-tab.tsx +579 -0
  269. package/src/components/ui/pipeline-alerts.tsx +74 -0
  270. package/src/components/ui/pipeline-board.tsx +268 -0
  271. package/src/components/ui/pipeline-chart.tsx +173 -0
  272. package/src/components/ui/pipeline-dialogs.tsx +303 -0
  273. package/src/components/ui/pipeline-primitives.tsx +108 -0
  274. package/src/components/ui/popover.tsx +41 -36
  275. package/src/components/ui/property-cashflow-doughnut-chart.tsx +188 -0
  276. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +185 -0
  277. package/src/components/ui/property-mobile-estimate-line-chart.tsx +393 -0
  278. package/src/components/ui/select.tsx +65 -52
  279. package/src/components/ui/sheet.tsx +55 -52
  280. package/src/components/ui/slider.tsx +54 -77
  281. package/src/components/ui/stage-timeline.tsx +205 -0
  282. package/src/components/ui/switch.tsx +42 -29
  283. package/src/components/ui/table.tsx +28 -28
  284. package/src/components/ui/tabs.tsx +22 -28
  285. package/src/components/ui/textarea.tsx +8 -8
  286. package/src/components/ui/toggle-group.tsx +0 -2
  287. package/src/components/ui/toggle.tsx +13 -15
  288. package/src/components/ui/tooltip.tsx +30 -28
  289. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +191 -0
  290. package/src/components/ui/transactions-income-expense-bar-chart.tsx +45 -38
  291. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +191 -0
  292. package/src/lib/theme-provider.tsx +10 -0
  293. package/src/lib/typography.ts +9 -0
  294. package/src/lib/utils.ts +41 -3
  295. package/src/styles/globals.css +371 -124
  296. package/src/styles/styles-css.ts +1 -1
  297. package/tsup.config.ts +27 -0
  298. package/dist/chunk-3EQP72AW.mjs +0 -58
  299. package/dist/chunk-K74JRTJR.mjs +0 -105
  300. package/dist/chunk-V7CNWJT3.mjs +0 -10
@@ -0,0 +1,302 @@
1
+ import React, { useMemo } from "react";
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ LineElement,
7
+ PointElement,
8
+ Tooltip,
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 } from "./card";
15
+ import { Empty, EmptyDescription } from "./empty";
16
+ import { Spinner } from "./spinner";
17
+ import { cn } from "@/lib/utils";
18
+ import {
19
+ FALLBACK_TICK,
20
+ FALLBACK_PRIMARY,
21
+ FALLBACK_BG,
22
+ ChartLegendItem,
23
+ } from "./chart-shared";
24
+
25
+ ChartJS.register(
26
+ CategoryScale,
27
+ LinearScale,
28
+ LineElement,
29
+ PointElement,
30
+ Tooltip,
31
+ );
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Abbreviate a dollar value: $1.2K, $4.5M, $1.1B */
38
+ function formatAbbrev(value: number): string {
39
+ const abs = Math.abs(value);
40
+ const sign = value < 0 ? "-" : "";
41
+ if (abs >= 1_000_000_000)
42
+ return `${sign}$${(abs / 1_000_000_000).toFixed(1)}B`;
43
+ if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(1)}M`;
44
+ if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(1)}K`;
45
+ return `${sign}$${abs.toFixed(0)}`;
46
+ }
47
+
48
+ /** Format a balance value for prominent display: $12,345.67 */
49
+ function formatBalanceFull(value: number): string {
50
+ const abs = Math.abs(value);
51
+ const sign = value < 0 ? "-" : "";
52
+ return `${sign}$${abs.toLocaleString(undefined, {
53
+ minimumFractionDigits: 2,
54
+ maximumFractionDigits: 2,
55
+ })}`;
56
+ }
57
+
58
+ /** Format ISO date string to "MMM dd" label e.g. "Jan 15" */
59
+ function formatDateLabel(iso: string): string {
60
+ const d = new Date(iso);
61
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
62
+ }
63
+
64
+ /** Format ISO date string for tooltip e.g. "Jan 15, 2024" */
65
+ function formatDateTooltip(iso: string): string {
66
+ const d = new Date(iso);
67
+ return d.toLocaleDateString("en-US", {
68
+ month: "short",
69
+ day: "numeric",
70
+ year: "numeric",
71
+ });
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Types
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export interface CashBalanceDataPoint {
79
+ /** ISO date string e.g. "2024-01-15" */
80
+ x: string;
81
+ y: number;
82
+ }
83
+
84
+ export interface CashBalanceLineChartProps {
85
+ chartData?: CashBalanceDataPoint[] | null;
86
+ title?: string;
87
+ /** Chart canvas height in pixels */
88
+ height?: number;
89
+ /** Width of the card */
90
+ width?: number | string;
91
+ className?: string;
92
+ /** Show skeleton loading state instead of the chart */
93
+ isLoading?: boolean;
94
+ /** Show or hide X axis labels */
95
+ showXAxis?: boolean;
96
+ /** Show or hide Y axis labels */
97
+ showYAxis?: boolean;
98
+ /** Show the latest balance value prominently below the title */
99
+ showBalanceValue?: boolean;
100
+ /** Show or hide the chart legend */
101
+ showLegend?: boolean;
102
+ /** Legend placement relative to the chart */
103
+ legendPosition?: "top" | "bottom";
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Component
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function CashBalanceLineChart({
111
+ chartData,
112
+ title = "Cash Balance",
113
+ height = 200,
114
+ width = "100%",
115
+ className,
116
+ isLoading = false,
117
+ showXAxis = true,
118
+ showYAxis = true,
119
+ showBalanceValue = false,
120
+ showLegend = false,
121
+ legendPosition = "bottom",
122
+ }: CashBalanceLineChartProps) {
123
+ const themeVars = useThemeVars();
124
+ const brandPrimary =
125
+ (themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY;
126
+ const fontFamily =
127
+ (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif";
128
+
129
+ const hasData = Array.isArray(chartData) && chartData.length > 0;
130
+
131
+ // Indices to show on x-axis (max ~6 ticks)
132
+ const tickIndices = useMemo<Set<number>>(() => {
133
+ if (!hasData) return new Set();
134
+ const count = chartData!.length;
135
+ const step = Math.max(1, Math.floor(count / 6));
136
+ const indices = new Set<number>();
137
+ for (let i = 0; i < count; i += step) indices.add(i);
138
+ indices.add(count - 1);
139
+ return indices;
140
+ }, [hasData, chartData]);
141
+
142
+ const data = useMemo<ChartData<"line">>(() => {
143
+ if (!hasData) return { labels: [], datasets: [] };
144
+ return {
145
+ labels: chartData!.map((p) => p.x), // raw ISO — used for tooltip + tick lookup
146
+ datasets: [
147
+ {
148
+ label: title,
149
+ data: chartData!.map((p) => p.y),
150
+ fill: false,
151
+ borderColor: brandPrimary,
152
+ backgroundColor: "transparent",
153
+ borderWidth: 2.5,
154
+ tension: 0.4,
155
+ pointRadius: 0,
156
+ pointHoverRadius: 6,
157
+ pointHoverBackgroundColor: FALLBACK_BG,
158
+ pointHoverBorderColor: brandPrimary,
159
+ pointHoverBorderWidth: 3,
160
+ pointHitRadius: 10,
161
+ },
162
+ ],
163
+ };
164
+ }, [hasData, chartData, brandPrimary, title]);
165
+
166
+ const options = useMemo<ChartOptions<"line">>(
167
+ () => ({
168
+ responsive: true,
169
+ maintainAspectRatio: false,
170
+ animation: { duration: 1200, easing: "easeOutQuart" },
171
+ plugins: {
172
+ legend: { display: false },
173
+ tooltip: {
174
+ enabled: true,
175
+ mode: "index",
176
+ intersect: false,
177
+ displayColors: false,
178
+ padding: 12,
179
+ cornerRadius: 0,
180
+ titleFont: { size: 11, weight: "600", family: fontFamily },
181
+ bodyFont: { size: 13, weight: "700", family: fontFamily },
182
+ callbacks: {
183
+ title: (items) => {
184
+ const iso = items[0]?.label;
185
+ return iso ? formatDateTooltip(iso) : "";
186
+ },
187
+ label: (ctx) => {
188
+ const val = ctx.parsed.y;
189
+ const formatted = Math.abs(val).toLocaleString(undefined, {
190
+ minimumFractionDigits: 2,
191
+ maximumFractionDigits: 2,
192
+ });
193
+ return `$ ${formatted}`;
194
+ },
195
+ },
196
+ },
197
+ },
198
+ interaction: { mode: "index", intersect: false },
199
+ scales: {
200
+ x: {
201
+ type: "category",
202
+ display: showXAxis,
203
+ grid: { display: false },
204
+ border: { display: false },
205
+ ticks: {
206
+ autoSkip: false,
207
+ maxRotation: 0,
208
+ minRotation: 0,
209
+ color: FALLBACK_TICK,
210
+ font: { size: 10, family: fontFamily },
211
+ callback: function (_, index) {
212
+ if (!tickIndices.has(index) || !chartData) return "";
213
+ return formatDateLabel(chartData[index].x);
214
+ },
215
+ },
216
+ },
217
+ y: {
218
+ display: showYAxis,
219
+ position: "left",
220
+ grid: { display: false },
221
+ border: { display: false },
222
+ ticks: {
223
+ beginAtZero: false,
224
+ maxTicksLimit: 5,
225
+ padding: 8,
226
+ color: FALLBACK_TICK,
227
+ font: { size: 10, family: fontFamily },
228
+ callback: (v) => formatAbbrev(Number(v)),
229
+ },
230
+ },
231
+ },
232
+ }),
233
+ [tickIndices, chartData, showXAxis, showYAxis, fontFamily],
234
+ );
235
+
236
+ const latestValue = hasData ? chartData![chartData!.length - 1].y : null;
237
+
238
+ return (
239
+ <Card
240
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
241
+ style={{ maxWidth: width, fontFamily }}
242
+ >
243
+ <CardHeader className="px-3 sm:px-6">
244
+ <div className="flex flex-col gap-0.5">
245
+ <CardTitle className="text-sm sm:text-base uppercase tracking-wider">
246
+ {title}
247
+ </CardTitle>
248
+ {showBalanceValue && latestValue !== null && (
249
+ <p className="text-2xl font-bold tabular-nums leading-tight">
250
+ {formatBalanceFull(latestValue)}
251
+ </p>
252
+ )}
253
+ </div>
254
+ </CardHeader>
255
+
256
+ <CardContent className="px-3 sm:px-6">
257
+ {isLoading ? (
258
+ <div
259
+ className="flex items-center justify-center text-muted-foreground"
260
+ style={{ height, width: "100%" }}
261
+ >
262
+ <Spinner size="lg" />
263
+ </div>
264
+ ) : !hasData ? (
265
+ <Empty className="flex-none p-4" style={{ height }}>
266
+ <EmptyDescription>No data available</EmptyDescription>
267
+ </Empty>
268
+ ) : (
269
+ <div className="flex flex-col gap-2">
270
+ {showLegend && legendPosition === "top" && (
271
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
272
+ <ChartLegendItem
273
+ label={title}
274
+ color={brandPrimary}
275
+ lineStyle="solid"
276
+ />
277
+ </div>
278
+ )}
279
+ <div style={{ height, width: "100%", position: "relative" }}>
280
+ <Chart
281
+ key={brandPrimary}
282
+ type="line"
283
+ data={data}
284
+ options={options}
285
+ aria-label={title}
286
+ />
287
+ </div>
288
+ {showLegend && legendPosition === "bottom" && (
289
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
290
+ <ChartLegendItem
291
+ label={title}
292
+ color={brandPrimary}
293
+ lineStyle="solid"
294
+ />
295
+ </div>
296
+ )}
297
+ </div>
298
+ )}
299
+ </CardContent>
300
+ </Card>
301
+ );
302
+ }
@@ -18,6 +18,8 @@ import { cn } from "@/lib/utils";
18
18
  import {
19
19
  hexToRgba,
20
20
  FALLBACK_TICK,
21
+ FALLBACK_PRIMARY,
22
+ FALLBACK_SECONDARY,
21
23
  formatTooltipDate,
22
24
  ChartPeriodButton,
23
25
  } from "./chart-shared";
@@ -60,6 +62,8 @@ export interface CashflowBarChartProps {
60
62
  legendPosition?: "top" | "bottom";
61
63
  /** Default period selector value */
62
64
  defaultPeriod?: CashflowPeriod;
65
+ /** Show or hide the period selector buttons (3M / 6M / 12M) */
66
+ showPeriodSelector?: boolean;
63
67
  /** Chart canvas height in pixels */
64
68
  height?: number;
65
69
  /** Width of the card in pixels */
@@ -80,7 +84,12 @@ interface LegendItemProps {
80
84
  strokeWidth?: number;
81
85
  }
82
86
 
83
- function LegendItem({ label, fillColor, strokeColor, strokeWidth = 1.5 }: LegendItemProps) {
87
+ function LegendItem({
88
+ label,
89
+ fillColor,
90
+ strokeColor,
91
+ strokeWidth = 1.5,
92
+ }: LegendItemProps) {
84
93
  return (
85
94
  <div className="flex items-center gap-1.5">
86
95
  <div
@@ -88,16 +97,25 @@ function LegendItem({ label, fillColor, strokeColor, strokeWidth = 1.5 }: Legend
88
97
  width: 10,
89
98
  height: 10,
90
99
  backgroundColor: fillColor,
91
- border: strokeWidth > 0 ? `${strokeWidth}px solid ${strokeColor}` : "none",
100
+ border:
101
+ strokeWidth > 0 ? `${strokeWidth}px solid ${strokeColor}` : "none",
92
102
  flexShrink: 0,
93
103
  }}
94
104
  />
95
- <span className="text-[11px] text-muted-foreground leading-none">{label}</span>
105
+ <span className="text-[11px] text-muted-foreground leading-none">
106
+ {label}
107
+ </span>
96
108
  </div>
97
109
  );
98
110
  }
99
111
 
100
- function ChartLegend({ primary, secondary }: { primary: string; secondary: string }) {
112
+ function ChartLegend({
113
+ primary,
114
+ secondary,
115
+ }: {
116
+ primary: string;
117
+ secondary: string;
118
+ }) {
101
119
  return (
102
120
  <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
103
121
  <LegendItem
@@ -132,9 +150,6 @@ function ChartLegend({ primary, secondary }: { primary: string; secondary: strin
132
150
 
133
151
  const PERIODS: CashflowPeriod[] = [3, 6, 12];
134
152
 
135
- const FALLBACK_PRIMARY = "#33FF99";
136
- const FALLBACK_SECONDARY = "#162029";
137
-
138
153
  // ---------------------------------------------------------------------------
139
154
  // Component
140
155
  // ---------------------------------------------------------------------------
@@ -147,6 +162,7 @@ export function CashflowBarChart({
147
162
  showYAxis = true,
148
163
  legendPosition = "top",
149
164
  defaultPeriod = 6,
165
+ showPeriodSelector = true,
150
166
  height = 280,
151
167
  width = "100%",
152
168
  className,
@@ -160,7 +176,10 @@ export function CashflowBarChart({
160
176
  const brandPrimary: string =
161
177
  (themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY;
162
178
  const brandSecondary: string =
163
- (themeVars["--theme-secondary"] as string | undefined) || FALLBACK_SECONDARY;
179
+ (themeVars["--theme-secondary"] as string | undefined) ||
180
+ FALLBACK_SECONDARY;
181
+ const fontFamily: string =
182
+ (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif";
164
183
 
165
184
  const sliced = useMemo<CashflowChartData | null>(() => {
166
185
  if (!cashflowData?.data?.length) return null;
@@ -205,15 +224,15 @@ export function CashflowBarChart({
205
224
  {
206
225
  label: "_thirdBar",
207
226
  data: sliced.data.map((d) =>
208
- d.overspending > 0 ? d.overspending : d.surplus
227
+ d.overspending > 0 ? d.overspending : d.surplus,
209
228
  ),
210
229
  backgroundColor: sliced.data.map((d) =>
211
- d.overspending > 0 ? brandSecondary : brandPrimary
230
+ d.overspending > 0 ? brandSecondary : brandPrimary,
212
231
  ),
213
232
  hoverBackgroundColor: sliced.data.map((d) =>
214
233
  d.overspending > 0
215
234
  ? hexToRgba(brandSecondary, 0.8)
216
- : hexToRgba(brandPrimary, 0.8)
235
+ : hexToRgba(brandPrimary, 0.8),
217
236
  ),
218
237
  borderWidth: 0,
219
238
  borderRadius: 0,
@@ -225,83 +244,91 @@ export function CashflowBarChart({
225
244
  };
226
245
  }, [sliced, brandPrimary, brandSecondary]);
227
246
 
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()}`;
247
+ const options = useMemo<ChartOptions<"bar">>(
248
+ () => ({
249
+ responsive: true,
250
+ maintainAspectRatio: false,
251
+ animation: { duration: 800, easing: "easeOutQuart" },
252
+ layout: { padding: 0 },
253
+ plugins: {
254
+ legend: { display: false },
255
+ tooltip: {
256
+ mode: "index",
257
+ intersect: false,
258
+ padding: 12,
259
+ cornerRadius: 0,
260
+ titleFont: { size: 11, weight: 600 },
261
+ bodyFont: { size: 12, weight: 500 },
262
+ callbacks: {
263
+ title: (tooltipItems) => {
264
+ const idx = tooltipItems[0]?.dataIndex;
265
+ if (idx != null && sliced?.data[idx]?.date) {
266
+ return formatTooltipDate(sliced.data[idx].date, "monthly");
267
+ }
268
+ return tooltipItems[0]?.label ?? "";
269
+ },
270
+ label: (ctx) => {
271
+ const val = ctx.raw as number;
272
+ if (val === 0) return;
273
+ if (ctx.datasetIndex === 2) {
274
+ const d = sliced?.data[ctx.dataIndex];
275
+ if (!d) return;
276
+ const lbl =
277
+ d.overspending > 0 ? "Over Spending" : "Surplus Income";
278
+ return ` ${lbl}: $${val.toLocaleString()}`;
279
+ }
280
+ return ` ${ctx.dataset.label}: $${val.toLocaleString()}`;
281
+ },
260
282
  },
261
283
  },
262
284
  },
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()}`,
285
+ scales: {
286
+ x: {
287
+ display: showXAxis,
288
+ grid: { display: false },
289
+ border: { display: false },
290
+ ticks: { font: { size: 10 }, color: FALLBACK_TICK },
291
+ },
292
+ y: {
293
+ display: showYAxis,
294
+ grid: { display: false },
295
+ border: { display: false },
296
+ ticks: {
297
+ font: { size: 10 },
298
+ color: FALLBACK_TICK,
299
+ maxTicksLimit: 5,
300
+ padding: 8,
301
+ callback: (v) => `$${Number(v).toLocaleString()}`,
302
+ },
281
303
  },
282
304
  },
283
- },
284
- }), [showXAxis, showYAxis, sliced]);
305
+ }),
306
+ [showXAxis, showYAxis, sliced],
307
+ );
285
308
 
286
309
  return (
287
310
  <Card
288
311
  className={cn("w-full py-4 sm:py-6 gap-2", className)}
289
- style={{ maxWidth: width }}
312
+ style={{ maxWidth: width, fontFamily }}
290
313
  >
291
314
  <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>
315
+ <CardTitle className="text-sm sm:text-base uppercase tracking-wider">
316
+ {title}
317
+ </CardTitle>
318
+ {showPeriodSelector && (
319
+ <CardAction>
320
+ <div className="flex gap-0.5 sm:gap-1">
321
+ {PERIODS.map((p) => (
322
+ <ChartPeriodButton
323
+ key={p}
324
+ period={p}
325
+ active={period === p}
326
+ onClick={() => setPeriod(p)}
327
+ />
328
+ ))}
329
+ </div>
330
+ </CardAction>
331
+ )}
305
332
  </CardHeader>
306
333
 
307
334
  <CardContent className="px-3 sm:px-6">