@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,393 @@
1
+ import React, { useMemo, useState } 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, CardAction } from "./card";
15
+ import { Empty, EmptyDescription } from "./empty";
16
+ import { Skeleton } from "./skeleton";
17
+ import { cn } from "@/lib/utils";
18
+ import {
19
+ FALLBACK_TICK,
20
+ FALLBACK_PRIMARY,
21
+ FALLBACK_SECONDARY,
22
+ FALLBACK_BG,
23
+ FALLBACK_NEUTRAL,
24
+ formatAbbrev,
25
+ formatTooltipDate,
26
+ formatMonthLabel,
27
+ ChartPeriodButton,
28
+ } from "./chart-shared";
29
+
30
+ ChartJS.register(
31
+ CategoryScale,
32
+ LinearScale,
33
+ LineElement,
34
+ PointElement,
35
+ Tooltip,
36
+ );
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Types
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export type PropertyPeriod = 1 | 3 | 5 | 10;
43
+
44
+ export interface PropertyEstimateDataPoint {
45
+ /** ISO date string e.g. "2024-01-01" */
46
+ date: string;
47
+ /** Property estimate value in dollars */
48
+ estimateValue: number;
49
+ /** Suburb average price in dollars */
50
+ suburbAverage: number;
51
+ /**
52
+ * Outstanding debt in dollars.
53
+ * Optional — when provided, a third "Debt" line is rendered.
54
+ * Used in the property estimate + debt combined view (backoffice / property detail).
55
+ */
56
+ debt?: number;
57
+ }
58
+
59
+ export interface PropertyMobileEstimateLineChartProps {
60
+ chartData?: PropertyEstimateDataPoint[] | null;
61
+ title?: string;
62
+ /** Show or hide the chart legend */
63
+ showLegend?: boolean;
64
+ /** Legend placement relative to the chart */
65
+ legendPosition?: "top" | "bottom";
66
+ /** Show or hide X axis labels */
67
+ showXAxis?: boolean;
68
+ /** Show or hide Y axis labels */
69
+ showYAxis?: boolean;
70
+ /** Default year period selector value */
71
+ defaultPeriod?: PropertyPeriod;
72
+ /**
73
+ * Show or hide the 1Y / 3Y / 5Y / 10Y period selector buttons.
74
+ * Set to false for dashboard/summary contexts where a fixed view is preferred.
75
+ * Defaults to true (mobile property detail use case).
76
+ */
77
+ showPeriodSelector?: boolean;
78
+ /** Chart canvas height in pixels */
79
+ height?: number;
80
+ /** Card max-width in pixels or CSS string */
81
+ width?: number | string;
82
+ className?: string;
83
+ /** Show skeleton loading state instead of the chart */
84
+ isLoading?: boolean;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Constants
89
+ // ---------------------------------------------------------------------------
90
+
91
+ const PROPERTY_PERIODS: PropertyPeriod[] = [1, 3, 5, 10];
92
+
93
+ /** Approx monthly data points per year period */
94
+ const PROPERTY_SLICE_COUNT: Record<PropertyPeriod, number> = {
95
+ 1: 12,
96
+ 3: 36,
97
+ 5: 60,
98
+ 10: 120,
99
+ };
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Legend
103
+ // ---------------------------------------------------------------------------
104
+
105
+ interface LegendItemProps {
106
+ label: string;
107
+ color: string;
108
+ lineWidth: number;
109
+ dashed?: boolean;
110
+ }
111
+
112
+ function LegendItem({
113
+ label,
114
+ color,
115
+ lineWidth,
116
+ dashed = false,
117
+ }: LegendItemProps) {
118
+ return (
119
+ <div className="flex items-center gap-1.5">
120
+ <div
121
+ style={{
122
+ width: 24,
123
+ height: lineWidth,
124
+ flexShrink: 0,
125
+ ...(dashed
126
+ ? {
127
+ backgroundImage: `repeating-linear-gradient(90deg, ${color} 0px, ${color} 5px, transparent 5px, transparent 9px)`,
128
+ }
129
+ : { backgroundColor: color }),
130
+ }}
131
+ />
132
+ <span className="text-[11px] text-muted-foreground leading-none">
133
+ {label}
134
+ </span>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function ChartLegend({
140
+ primaryColor,
141
+ secondaryColor,
142
+ showDebt,
143
+ }: {
144
+ primaryColor: string;
145
+ secondaryColor: string;
146
+ showDebt: boolean;
147
+ }) {
148
+ return (
149
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
150
+ <LegendItem label="Estimate Value" color={primaryColor} lineWidth={2.5} />
151
+ <LegendItem
152
+ label="Suburb Average"
153
+ color={FALLBACK_NEUTRAL}
154
+ lineWidth={1.5}
155
+ dashed={showDebt}
156
+ />
157
+ {showDebt && (
158
+ <LegendItem label="Debt" color={secondaryColor} lineWidth={2} />
159
+ )}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Component
166
+ // ---------------------------------------------------------------------------
167
+
168
+ export function PropertyMobileEstimateLineChart({
169
+ chartData,
170
+ title = "Property Estimate",
171
+ showLegend = true,
172
+ legendPosition = "top",
173
+ showXAxis = true,
174
+ showYAxis = false,
175
+ defaultPeriod = 1,
176
+ showPeriodSelector = true,
177
+ height = 200,
178
+ width = "100%",
179
+ className,
180
+ isLoading = false,
181
+ }: PropertyMobileEstimateLineChartProps) {
182
+ const [period, setPeriod] = useState<PropertyPeriod>(defaultPeriod);
183
+
184
+ const themeVars = useThemeVars();
185
+ const brandPrimary: string =
186
+ (themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY;
187
+ const brandSecondary: string =
188
+ (themeVars["--theme-secondary"] as string | undefined) ||
189
+ FALLBACK_SECONDARY;
190
+ const fontFamily: string =
191
+ (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif";
192
+
193
+ const sliced = useMemo<PropertyEstimateDataPoint[] | null>(() => {
194
+ if (!chartData?.length) return null;
195
+ const count = Math.min(PROPERTY_SLICE_COUNT[period], chartData.length);
196
+ const start = chartData.length - count;
197
+ return chartData.slice(start);
198
+ }, [chartData, period]);
199
+
200
+ /** True when at least one data point has a debt value */
201
+ const showDebt = useMemo(
202
+ () => !!sliced?.some((p) => p.debt != null && p.debt > 0),
203
+ [sliced],
204
+ );
205
+
206
+ const labels = useMemo(
207
+ () => sliced?.map((p) => formatMonthLabel(p.date)) ?? [],
208
+ [sliced],
209
+ );
210
+
211
+ const data = useMemo<ChartData<"line">>(() => {
212
+ if (!sliced) return { labels: [], datasets: [] };
213
+ return {
214
+ labels,
215
+ datasets: [
216
+ {
217
+ label: "Estimate Value",
218
+ data: sliced.map((p) => p.estimateValue),
219
+ borderColor: brandPrimary,
220
+ backgroundColor: "transparent",
221
+ borderWidth: 2.5,
222
+ tension: 0.4,
223
+ pointRadius: 0,
224
+ pointHoverRadius: 6,
225
+ pointHoverBackgroundColor: FALLBACK_BG,
226
+ pointHoverBorderColor: brandPrimary,
227
+ pointHoverBorderWidth: 3,
228
+ pointHitRadius: 10,
229
+ fill: false,
230
+ },
231
+ {
232
+ label: "Suburb Average",
233
+ data: sliced.map((p) => p.suburbAverage),
234
+ borderColor: FALLBACK_NEUTRAL,
235
+ backgroundColor: "transparent",
236
+ ...(showDebt ? { borderDash: [4, 4] } : {}),
237
+ borderWidth: 1.5,
238
+ tension: 0.4,
239
+ pointRadius: 0,
240
+ pointHoverRadius: 5,
241
+ pointHoverBackgroundColor: FALLBACK_BG,
242
+ pointHoverBorderColor: FALLBACK_NEUTRAL,
243
+ pointHoverBorderWidth: 2,
244
+ pointHitRadius: 10,
245
+ fill: false,
246
+ },
247
+ ...(showDebt
248
+ ? [
249
+ {
250
+ label: "Debt",
251
+ data: sliced.map((p) => p.debt ?? 0),
252
+ borderColor: brandSecondary,
253
+ backgroundColor: "transparent",
254
+ borderWidth: 2,
255
+ tension: 0.4,
256
+ pointRadius: 0,
257
+ pointHoverRadius: 5,
258
+ pointHoverBackgroundColor: FALLBACK_BG,
259
+ pointHoverBorderColor: brandSecondary,
260
+ pointHoverBorderWidth: 2,
261
+ pointHitRadius: 10,
262
+ fill: false,
263
+ },
264
+ ]
265
+ : []),
266
+ ],
267
+ };
268
+ }, [sliced, labels, brandPrimary, brandSecondary, showDebt]);
269
+
270
+ const options = useMemo<ChartOptions<"line">>(
271
+ () => ({
272
+ responsive: true,
273
+ maintainAspectRatio: false,
274
+ animation: { duration: 800, easing: "easeOutQuart" },
275
+ layout: { padding: 0 },
276
+ plugins: {
277
+ legend: { display: false },
278
+ tooltip: {
279
+ mode: "index",
280
+ intersect: false,
281
+ padding: 12,
282
+ cornerRadius: 0,
283
+ titleFont: { size: 11, weight: 600 },
284
+ bodyFont: { size: 12, weight: 500 },
285
+ callbacks: {
286
+ title: (tooltipItems) => {
287
+ const idx = tooltipItems[0]?.dataIndex;
288
+ if (idx != null && sliced?.[idx]?.date) {
289
+ return formatTooltipDate(sliced[idx].date, "monthly");
290
+ }
291
+ return tooltipItems[0]?.label ?? "";
292
+ },
293
+ label: (ctx) => {
294
+ const val = ctx.raw as number;
295
+ if (val === 0) return;
296
+ return ` ${ctx.dataset.label}: ${formatAbbrev(val)}`;
297
+ },
298
+ },
299
+ },
300
+ },
301
+ scales: {
302
+ x: {
303
+ display: showXAxis,
304
+ grid: { display: false },
305
+ border: { display: false },
306
+ ticks: {
307
+ maxRotation: 0,
308
+ minRotation: 0,
309
+ color: FALLBACK_TICK,
310
+ font: { size: 10 },
311
+ maxTicksLimit: 12,
312
+ },
313
+ },
314
+ y: {
315
+ display: showYAxis,
316
+ position: "left",
317
+ grid: { display: false },
318
+ border: { display: false },
319
+ ticks: {
320
+ padding: 8,
321
+ maxTicksLimit: 5,
322
+ color: FALLBACK_TICK,
323
+ font: { size: 10 },
324
+ callback: (v) => formatAbbrev(Number(v)),
325
+ },
326
+ },
327
+ },
328
+ }),
329
+ [showXAxis, showYAxis, sliced],
330
+ );
331
+
332
+ return (
333
+ <Card
334
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
335
+ style={{ maxWidth: width, fontFamily }}
336
+ >
337
+ <CardHeader className="px-3 sm:px-6">
338
+ <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
339
+ {showPeriodSelector && (
340
+ <CardAction>
341
+ <div className="flex gap-0.5 sm:gap-1">
342
+ {PROPERTY_PERIODS.map((p) => (
343
+ <ChartPeriodButton
344
+ key={p}
345
+ period={p}
346
+ active={period === p}
347
+ onClick={() => setPeriod(p)}
348
+ unit="Y"
349
+ />
350
+ ))}
351
+ </div>
352
+ </CardAction>
353
+ )}
354
+ </CardHeader>
355
+
356
+ <CardContent className="px-3 sm:px-6">
357
+ {isLoading ? (
358
+ <Skeleton style={{ height, width: "100%" }} />
359
+ ) : !sliced ? (
360
+ <Empty className="flex-none p-4" style={{ height }}>
361
+ <EmptyDescription>No data available</EmptyDescription>
362
+ </Empty>
363
+ ) : (
364
+ <div className="flex flex-col gap-2">
365
+ {showLegend && legendPosition === "top" && (
366
+ <ChartLegend
367
+ primaryColor={brandPrimary}
368
+ secondaryColor={brandSecondary}
369
+ showDebt={showDebt}
370
+ />
371
+ )}
372
+ <div style={{ height, width: "100%", position: "relative" }}>
373
+ <Chart
374
+ key={`${brandPrimary}__${brandSecondary}`}
375
+ type="line"
376
+ data={data}
377
+ options={options}
378
+ aria-label={title}
379
+ />
380
+ </div>
381
+ {showLegend && legendPosition === "bottom" && (
382
+ <ChartLegend
383
+ primaryColor={brandPrimary}
384
+ secondaryColor={brandSecondary}
385
+ showDebt={showDebt}
386
+ />
387
+ )}
388
+ </div>
389
+ )}
390
+ </CardContent>
391
+ </Card>
392
+ );
393
+ }
@@ -1,37 +1,37 @@
1
- import { type ReactElement } from "react"
2
- import * as React from "react"
3
- import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
4
- import { Select as SelectPrimitive } from "@base-ui/react/select"
5
- import { cn } from "@/lib/utils"
6
- import { useThemeVars } from "@/lib/theme-provider"
1
+ import { type ReactElement } from "react";
2
+ import * as React from "react";
3
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
4
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
5
+ import { cn } from "@/lib/utils";
6
+ import { useThemeVars } from "@/lib/theme-provider";
7
7
 
8
- export type SelectProps = React.ComponentProps<typeof SelectPrimitive.Root>
8
+ export type SelectProps = React.ComponentProps<typeof SelectPrimitive.Root>;
9
9
 
10
- function Select({
11
- ...props
12
- }: SelectProps): ReactElement {
13
- return <SelectPrimitive.Root data-slot="select" {...props} />
10
+ function Select({ ...props }: SelectProps): ReactElement {
11
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
14
12
  }
15
13
 
16
- export type SelectGroupProps = React.ComponentProps<typeof SelectPrimitive.Group>
14
+ export type SelectGroupProps = React.ComponentProps<
15
+ typeof SelectPrimitive.Group
16
+ >;
17
17
 
18
- function SelectGroup({
19
- ...props
20
- }: SelectGroupProps): ReactElement {
21
- return <SelectPrimitive.Group data-slot="select-group" {...props} />
18
+ function SelectGroup({ ...props }: SelectGroupProps): ReactElement {
19
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
22
20
  }
23
21
 
24
- export type SelectValueProps = React.ComponentProps<typeof SelectPrimitive.Value>
22
+ export type SelectValueProps = React.ComponentProps<
23
+ typeof SelectPrimitive.Value
24
+ >;
25
25
 
26
- function SelectValue({
27
- ...props
28
- }: SelectValueProps): ReactElement {
29
- return <SelectPrimitive.Value data-slot="select-value" {...props} />
26
+ function SelectValue({ ...props }: SelectValueProps): ReactElement {
27
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
30
28
  }
31
29
 
32
- export type SelectTriggerProps = React.ComponentProps<typeof SelectPrimitive.Trigger> & {
33
- size?: "sm" | "default"
34
- }
30
+ export type SelectTriggerProps = React.ComponentProps<
31
+ typeof SelectPrimitive.Trigger
32
+ > & {
33
+ size?: "sm" | "default";
34
+ };
35
35
 
36
36
  function SelectTrigger({
37
37
  className,
@@ -42,8 +42,8 @@ function SelectTrigger({
42
42
  return (
43
43
  <SelectPrimitive.Trigger
44
44
  className={cn(
45
- "flex w-fit items-center justify-between gap-2 border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20 data-popup-open:border-primary data-popup-open:ring-[3px] data-popup-open:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
46
- className
45
+ "flex w-fit items-center justify-between gap-2 border border-input bg-transparent px-3 py-2 text-body-medium whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20 data-popup-open:border-primary data-popup-open:ring-[3px] data-popup-open:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:font-normal data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
46
+ className,
47
47
  )}
48
48
  data-size={size}
49
49
  data-slot="select-trigger"
@@ -54,10 +54,12 @@ function SelectTrigger({
54
54
  <ChevronDownIcon className="size-4 opacity-50" />
55
55
  </SelectPrimitive.Icon>
56
56
  </SelectPrimitive.Trigger>
57
- )
57
+ );
58
58
  }
59
59
 
60
- export type SelectContentProps = React.ComponentProps<typeof SelectPrimitive.Popup>
60
+ export type SelectContentProps = React.ComponentProps<
61
+ typeof SelectPrimitive.Popup
62
+ >;
61
63
 
62
64
  function SelectContent({
63
65
  className,
@@ -68,11 +70,16 @@ function SelectContent({
68
70
  const themeVars = useThemeVars();
69
71
  return (
70
72
  <SelectPrimitive.Portal>
71
- <SelectPrimitive.Positioner align="start" alignItemWithTrigger={false} sideOffset={4}>
73
+ <SelectPrimitive.Positioner
74
+ className="z-[200]"
75
+ align="start"
76
+ alignItemWithTrigger={false}
77
+ sideOffset={4}
78
+ >
72
79
  <SelectPrimitive.Popup
73
80
  className={cn(
74
- "relative z-50 max-h-[var(--available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95 data-ending-style:fill-mode-forwards data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
75
- className
81
+ "relative max-h-[var(--available-height)] min-w-[var(--anchor-width,8rem)] overflow-x-hidden overflow-y-auto border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95 data-ending-style:fill-mode-forwards data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
82
+ className,
76
83
  )}
77
84
  data-slot="select-content"
78
85
  style={{ ...themeVars, ...style } as React.CSSProperties}
@@ -84,25 +91,27 @@ function SelectContent({
84
91
  </SelectPrimitive.Popup>
85
92
  </SelectPrimitive.Positioner>
86
93
  </SelectPrimitive.Portal>
87
- )
94
+ );
88
95
  }
89
96
 
90
- export type SelectLabelProps = React.ComponentProps<typeof SelectPrimitive.GroupLabel>
97
+ export type SelectLabelProps = React.ComponentProps<
98
+ typeof SelectPrimitive.GroupLabel
99
+ >;
91
100
 
92
- function SelectLabel({
93
- className,
94
- ...props
95
- }: SelectLabelProps): ReactElement {
101
+ function SelectLabel({ className, ...props }: SelectLabelProps): ReactElement {
96
102
  return (
97
103
  <SelectPrimitive.GroupLabel
98
- className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", className)}
104
+ className={cn(
105
+ "px-2 py-1.5 text-label-small text-muted-foreground",
106
+ className,
107
+ )}
99
108
  data-slot="select-label"
100
109
  {...props}
101
110
  />
102
- )
111
+ );
103
112
  }
104
113
 
105
- export type SelectItemProps = React.ComponentProps<typeof SelectPrimitive.Item>
114
+ export type SelectItemProps = React.ComponentProps<typeof SelectPrimitive.Item>;
106
115
 
107
116
  function SelectItem({
108
117
  className,
@@ -112,8 +121,8 @@ function SelectItem({
112
121
  return (
113
122
  <SelectPrimitive.Item
114
123
  className={cn(
115
- "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-highlighted:bg-primary/5 data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
116
- className
124
+ "relative flex w-full cursor-default items-center gap-2 py-1.5 pr-8 pl-2 text-body-small outline-hidden select-none data-highlighted:bg-primary/5 data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
125
+ className,
117
126
  )}
118
127
  data-slot="select-item"
119
128
  {...props}
@@ -128,10 +137,10 @@ function SelectItem({
128
137
  </span>
129
138
  <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
130
139
  </SelectPrimitive.Item>
131
- )
140
+ );
132
141
  }
133
142
 
134
- export type SelectSeparatorProps = React.ComponentProps<"div">
143
+ export type SelectSeparatorProps = React.ComponentProps<"div">;
135
144
 
136
145
  function SelectSeparator({
137
146
  className,
@@ -144,10 +153,12 @@ function SelectSeparator({
144
153
  role="separator"
145
154
  {...props}
146
155
  />
147
- )
156
+ );
148
157
  }
149
158
 
150
- export type SelectScrollUpButtonProps = React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>
159
+ export type SelectScrollUpButtonProps = React.ComponentProps<
160
+ typeof SelectPrimitive.ScrollUpArrow
161
+ >;
151
162
 
152
163
  function SelectScrollUpButton({
153
164
  className,
@@ -157,17 +168,19 @@ function SelectScrollUpButton({
157
168
  <SelectPrimitive.ScrollUpArrow
158
169
  className={cn(
159
170
  "flex cursor-default items-center justify-center py-1",
160
- className
171
+ className,
161
172
  )}
162
173
  data-slot="select-scroll-up-button"
163
174
  {...props}
164
175
  >
165
176
  <ChevronUpIcon className="size-4" />
166
177
  </SelectPrimitive.ScrollUpArrow>
167
- )
178
+ );
168
179
  }
169
180
 
170
- export type SelectScrollDownButtonProps = React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>
181
+ export type SelectScrollDownButtonProps = React.ComponentProps<
182
+ typeof SelectPrimitive.ScrollDownArrow
183
+ >;
171
184
 
172
185
  function SelectScrollDownButton({
173
186
  className,
@@ -177,14 +190,14 @@ function SelectScrollDownButton({
177
190
  <SelectPrimitive.ScrollDownArrow
178
191
  className={cn(
179
192
  "flex cursor-default items-center justify-center py-1",
180
- className
193
+ className,
181
194
  )}
182
195
  data-slot="select-scroll-down-button"
183
196
  {...props}
184
197
  >
185
198
  <ChevronDownIcon className="size-4" />
186
199
  </SelectPrimitive.ScrollDownArrow>
187
- )
200
+ );
188
201
  }
189
202
 
190
203
  export {
@@ -198,4 +211,4 @@ export {
198
211
  SelectSeparator,
199
212
  SelectTrigger,
200
213
  SelectValue,
201
- }
214
+ };