@wealthx/shadcn 1.1.0 → 1.2.1

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