@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,536 @@
1
+ import * as React from "react";
2
+ import { Check, Search } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Label } from "@/components/ui/label";
6
+ import { Slider } from "@/components/ui/slider";
7
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
8
+
9
+ /**
10
+ * Form Primitives — WealthX DS (L3)
11
+ *
12
+ * Reusable input primitives for financial forms.
13
+ * Used inside Opportunity edit modals and loan application forms.
14
+ *
15
+ * Components:
16
+ * - CurrencyInputWithSlider — formatted currency text input + range slider
17
+ * - ConcernScale — 1–5 priority/importance selector
18
+ * - AddressAutocomplete — text input with filtered suggestion dropdown
19
+ * - OwnershipSplit — two-owner percentage split (sums to 100 %)
20
+ */
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // CurrencyInputWithSlider
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface CurrencyInputWithSliderProps {
27
+ /** Numeric value in dollars */
28
+ value?: number;
29
+ /** Default value when uncontrolled */
30
+ defaultValue?: number;
31
+ min?: number;
32
+ max?: number;
33
+ step?: number;
34
+ disabled?: boolean;
35
+ /** Called with the updated numeric value */
36
+ onValueChange?: (value: number) => void;
37
+ className?: string;
38
+ }
39
+
40
+ function parseCurrencyToNumber(raw: string): number {
41
+ const cleaned = raw.replace(/[^0-9.]/g, "");
42
+ const n = parseFloat(cleaned);
43
+ return isNaN(n) ? 0 : n;
44
+ }
45
+
46
+ function formatNumberToCurrency(n: number): string {
47
+ return "$" + Math.round(n).toLocaleString("en-AU");
48
+ }
49
+
50
+ /**
51
+ * Currency text input paired with a range slider.
52
+ * The two controls stay in sync — editing the input moves the slider and vice versa.
53
+ *
54
+ * ```tsx
55
+ * <CurrencyInputWithSlider
56
+ * value={loanAmount}
57
+ * min={100_000}
58
+ * max={3_000_000}
59
+ * step={10_000}
60
+ * onValueChange={setLoanAmount}
61
+ * />
62
+ * ```
63
+ */
64
+ export function CurrencyInputWithSlider({
65
+ value: controlledValue,
66
+ defaultValue = 0,
67
+ min = 0,
68
+ max = 5_000_000,
69
+ step = 10_000,
70
+ disabled = false,
71
+ onValueChange,
72
+ className,
73
+ }: CurrencyInputWithSliderProps) {
74
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
75
+ const numericValue =
76
+ controlledValue !== undefined ? controlledValue : internalValue;
77
+
78
+ const [inputText, setInputText] = React.useState(
79
+ formatNumberToCurrency(numericValue),
80
+ );
81
+ const [isFocused, setIsFocused] = React.useState(false);
82
+
83
+ // Sync inputText when controlled value changes externally
84
+ React.useEffect(() => {
85
+ if (!isFocused) {
86
+ setInputText(formatNumberToCurrency(numericValue));
87
+ }
88
+ }, [numericValue, isFocused]);
89
+
90
+ const commitValue = (n: number) => {
91
+ const clamped = Math.max(min, Math.min(max, n));
92
+ if (controlledValue === undefined) setInternalValue(clamped);
93
+ onValueChange?.(clamped);
94
+ setInputText(formatNumberToCurrency(clamped));
95
+ };
96
+
97
+ return (
98
+ <div className={cn("flex flex-col gap-3", className)}>
99
+ {/* Text input */}
100
+ <Input
101
+ type="text"
102
+ value={isFocused ? inputText : formatNumberToCurrency(numericValue)}
103
+ disabled={disabled}
104
+ onFocus={() => {
105
+ setIsFocused(true);
106
+ setInputText(formatNumberToCurrency(numericValue));
107
+ }}
108
+ onChange={(e) => setInputText(e.target.value)}
109
+ onBlur={() => {
110
+ setIsFocused(false);
111
+ commitValue(parseCurrencyToNumber(inputText));
112
+ }}
113
+ onKeyDown={(e) => {
114
+ if (e.key === "Enter") {
115
+ (e.target as HTMLInputElement).blur();
116
+ }
117
+ }}
118
+ className="tabular-nums"
119
+ aria-label="Currency amount"
120
+ />
121
+
122
+ {/* Slider */}
123
+ <Slider
124
+ value={numericValue}
125
+ min={min}
126
+ max={max}
127
+ step={step}
128
+ disabled={disabled}
129
+ onValueChange={(n) => {
130
+ if (controlledValue === undefined) setInternalValue(n);
131
+ onValueChange?.(n);
132
+ }}
133
+ />
134
+ </div>
135
+ );
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // ConcernScale
140
+ // ---------------------------------------------------------------------------
141
+
142
+ const CONCERN_LABELS: Record<number, string> = {
143
+ 1: "Not important",
144
+ 2: "Slightly important",
145
+ 3: "Moderately important",
146
+ 4: "Very important",
147
+ 5: "Critical",
148
+ };
149
+
150
+ export interface ConcernScaleProps {
151
+ value?: number;
152
+ defaultValue?: number;
153
+ disabled?: boolean;
154
+ /** Number of steps (default 5) */
155
+ steps?: number;
156
+ lowLabel?: string;
157
+ highLabel?: string;
158
+ onValueChange?: (value: number) => void;
159
+ className?: string;
160
+ }
161
+
162
+ /**
163
+ * 1–N priority / importance selector using DS ToggleGroup buttons.
164
+ * One button is active at a time; selecting the same value again clears to 0.
165
+ *
166
+ * ```tsx
167
+ * <ConcernScale value={priority} onValueChange={setPriority} />
168
+ * ```
169
+ */
170
+ export function ConcernScale({
171
+ value: controlledValue,
172
+ defaultValue = 0,
173
+ disabled = false,
174
+ steps = 5,
175
+ lowLabel = "Not important",
176
+ highLabel = "Critical",
177
+ onValueChange,
178
+ className,
179
+ }: ConcernScaleProps) {
180
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
181
+ const selected =
182
+ controlledValue !== undefined ? controlledValue : internalValue;
183
+
184
+ const handleValueChange = (val: string) => {
185
+ const n = val ? Number(val) : 0;
186
+ if (controlledValue === undefined) setInternalValue(n);
187
+ onValueChange?.(n);
188
+ };
189
+
190
+ return (
191
+ <div className={cn("flex flex-col gap-2", className)}>
192
+ <ToggleGroup
193
+ type="single"
194
+ value={selected > 0 ? String(selected) : ""}
195
+ onValueChange={handleValueChange}
196
+ variant="outline"
197
+ size="sm"
198
+ disabled={disabled}
199
+ className="w-full"
200
+ >
201
+ {Array.from({ length: steps }, (_, i) => i + 1).map((n) => (
202
+ <ToggleGroupItem
203
+ key={n}
204
+ value={String(n)}
205
+ aria-label={CONCERN_LABELS[n] ?? String(n)}
206
+ className="flex-1"
207
+ >
208
+ {n}
209
+ </ToggleGroupItem>
210
+ ))}
211
+ </ToggleGroup>
212
+
213
+ {/* Extreme labels */}
214
+ <div className="flex justify-between text-[10px] text-muted-foreground">
215
+ <span>{lowLabel}</span>
216
+ <span>{highLabel}</span>
217
+ </div>
218
+
219
+ {/* Selected label */}
220
+ {selected > 0 && (
221
+ <p className="text-xs text-foreground">
222
+ {CONCERN_LABELS[selected] ?? `Level ${selected}`}
223
+ </p>
224
+ )}
225
+ </div>
226
+ );
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // AddressAutocomplete
231
+ // ---------------------------------------------------------------------------
232
+
233
+ export interface AddressOption {
234
+ id: string;
235
+ label: string;
236
+ suburb?: string;
237
+ state?: string;
238
+ postcode?: string;
239
+ }
240
+
241
+ export interface AddressAutocompleteProps {
242
+ value?: string;
243
+ placeholder?: string;
244
+ suggestions?: AddressOption[];
245
+ disabled?: boolean;
246
+ onValueChange?: (value: string) => void;
247
+ onSelect?: (option: AddressOption) => void;
248
+ className?: string;
249
+ }
250
+
251
+ const DEFAULT_SUGGESTIONS: AddressOption[] = [
252
+ {
253
+ id: "1",
254
+ label: "12 Harbour View Terrace, Mosman NSW 2088",
255
+ suburb: "Mosman",
256
+ state: "NSW",
257
+ postcode: "2088",
258
+ },
259
+ {
260
+ id: "2",
261
+ label: "5 Coastal Road, Manly NSW 2095",
262
+ suburb: "Manly",
263
+ state: "NSW",
264
+ postcode: "2095",
265
+ },
266
+ {
267
+ id: "3",
268
+ label: "24 Collins Street, Melbourne VIC 3000",
269
+ suburb: "Melbourne",
270
+ state: "VIC",
271
+ postcode: "3000",
272
+ },
273
+ {
274
+ id: "4",
275
+ label: "88 Pacific Highway, St Leonards NSW 2065",
276
+ suburb: "St Leonards",
277
+ state: "NSW",
278
+ postcode: "2065",
279
+ },
280
+ {
281
+ id: "5",
282
+ label: "1 Queen Street, Brisbane QLD 4000",
283
+ suburb: "Brisbane",
284
+ state: "QLD",
285
+ postcode: "4000",
286
+ },
287
+ ];
288
+
289
+ /**
290
+ * Address text input with filtered suggestion dropdown.
291
+ * Filters the `suggestions` list on each keystroke (case-insensitive substring match).
292
+ * Pass `onSelect` to receive the full structured `AddressOption` on selection.
293
+ *
294
+ * In production, replace `suggestions` with results from a geocoding API.
295
+ *
296
+ * ```tsx
297
+ * <AddressAutocomplete
298
+ * value={address}
299
+ * onValueChange={setAddress}
300
+ * onSelect={(opt) => console.log(opt.postcode)}
301
+ * />
302
+ * ```
303
+ */
304
+ export function AddressAutocomplete({
305
+ value: controlledValue,
306
+ placeholder = "Start typing an address…",
307
+ suggestions = DEFAULT_SUGGESTIONS,
308
+ disabled = false,
309
+ onValueChange,
310
+ onSelect,
311
+ className,
312
+ }: AddressAutocompleteProps) {
313
+ const [internalValue, setInternalValue] = React.useState("");
314
+ const inputValue =
315
+ controlledValue !== undefined ? controlledValue : internalValue;
316
+
317
+ const [open, setOpen] = React.useState(false);
318
+ const [activeIndex, setActiveIndex] = React.useState(-1);
319
+ const containerRef = React.useRef<HTMLDivElement>(null);
320
+ const listRef = React.useRef<HTMLUListElement>(null);
321
+
322
+ const filtered = React.useMemo(() => {
323
+ if (!inputValue.trim()) return suggestions.slice(0, 5);
324
+ const q = inputValue.toLowerCase();
325
+ return suggestions
326
+ .filter((s) => s.label.toLowerCase().includes(q))
327
+ .slice(0, 5);
328
+ }, [inputValue, suggestions]);
329
+
330
+ const setValue = (v: string) => {
331
+ if (controlledValue === undefined) setInternalValue(v);
332
+ onValueChange?.(v);
333
+ };
334
+
335
+ const handleSelect = (opt: AddressOption) => {
336
+ setValue(opt.label);
337
+ onSelect?.(opt);
338
+ setOpen(false);
339
+ setActiveIndex(-1);
340
+ };
341
+
342
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
343
+ if (!open) return;
344
+ if (e.key === "ArrowDown") {
345
+ e.preventDefault();
346
+ setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));
347
+ } else if (e.key === "ArrowUp") {
348
+ e.preventDefault();
349
+ setActiveIndex((i) => Math.max(i - 1, -1));
350
+ } else if (e.key === "Enter" && activeIndex >= 0) {
351
+ e.preventDefault();
352
+ handleSelect(filtered[activeIndex]);
353
+ } else if (e.key === "Escape") {
354
+ setOpen(false);
355
+ }
356
+ };
357
+
358
+ // Close on outside click
359
+ React.useEffect(() => {
360
+ const handler = (e: MouseEvent) => {
361
+ if (
362
+ containerRef.current &&
363
+ !containerRef.current.contains(e.target as Node)
364
+ ) {
365
+ setOpen(false);
366
+ }
367
+ };
368
+ document.addEventListener("mousedown", handler);
369
+ return () => document.removeEventListener("mousedown", handler);
370
+ }, []);
371
+
372
+ return (
373
+ <div ref={containerRef} className={cn("relative", className)}>
374
+ <div className="relative">
375
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
376
+ <Input
377
+ type="text"
378
+ value={inputValue}
379
+ placeholder={placeholder}
380
+ disabled={disabled}
381
+ className="pl-8"
382
+ onChange={(e) => {
383
+ setValue(e.target.value);
384
+ setOpen(true);
385
+ setActiveIndex(-1);
386
+ }}
387
+ onFocus={() => setOpen(true)}
388
+ onKeyDown={handleKeyDown}
389
+ aria-autocomplete="list"
390
+ aria-expanded={open}
391
+ />
392
+ </div>
393
+
394
+ {open && filtered.length > 0 && (
395
+ <ul
396
+ ref={listRef}
397
+ role="listbox"
398
+ className={cn(
399
+ "absolute z-50 top-full left-0 right-0 mt-1",
400
+ "border border-border bg-popover shadow-md",
401
+ "max-h-48 overflow-y-auto py-1",
402
+ )}
403
+ >
404
+ {filtered.map((opt, idx) => (
405
+ <li
406
+ key={opt.id}
407
+ role="option"
408
+ aria-selected={idx === activeIndex}
409
+ onMouseEnter={() => setActiveIndex(idx)}
410
+ onMouseDown={(e) => {
411
+ e.preventDefault();
412
+ handleSelect(opt);
413
+ }}
414
+ className={cn(
415
+ "flex items-center gap-2 px-3 py-2 text-sm cursor-pointer",
416
+ idx === activeIndex
417
+ ? "bg-accent text-accent-foreground"
418
+ : "hover:bg-accent/50",
419
+ )}
420
+ >
421
+ <span className="flex-1 truncate">{opt.label}</span>
422
+ {idx === activeIndex && (
423
+ <Check className="h-3.5 w-3.5 shrink-0 text-primary" />
424
+ )}
425
+ </li>
426
+ ))}
427
+ </ul>
428
+ )}
429
+ </div>
430
+ );
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // OwnershipSplit
435
+ // ---------------------------------------------------------------------------
436
+
437
+ export interface OwnershipOwner {
438
+ id: string;
439
+ name: string;
440
+ /** Percentage 0–100 */
441
+ share: number;
442
+ }
443
+
444
+ export interface OwnershipSplitProps {
445
+ owners?: OwnershipOwner[];
446
+ disabled?: boolean;
447
+ onOwnersChange?: (owners: OwnershipOwner[]) => void;
448
+ className?: string;
449
+ }
450
+
451
+ const DEFAULT_OWNERS: OwnershipOwner[] = [
452
+ { id: "main", name: "Main Applicant", share: 50 },
453
+ { id: "co", name: "Co-Applicant", share: 50 },
454
+ ];
455
+
456
+ /**
457
+ * Two-owner (or N-owner) percentage split control.
458
+ * Adjusting one owner's slider redistributes the remainder proportionally.
459
+ * The sum always stays at 100 %.
460
+ *
461
+ * ```tsx
462
+ * <OwnershipSplit
463
+ * owners={[
464
+ * { id: "main", name: "James Harbour", share: 60 },
465
+ * { id: "co", name: "Sarah Harbour", share: 40 },
466
+ * ]}
467
+ * onOwnersChange={setOwners}
468
+ * />
469
+ * ```
470
+ */
471
+ export function OwnershipSplit({
472
+ owners: controlledOwners,
473
+ disabled = false,
474
+ onOwnersChange,
475
+ className,
476
+ }: OwnershipSplitProps) {
477
+ const [internalOwners, setInternalOwners] =
478
+ React.useState<OwnershipOwner[]>(DEFAULT_OWNERS);
479
+
480
+ const owners =
481
+ controlledOwners !== undefined ? controlledOwners : internalOwners;
482
+
483
+ const setOwners = (updated: OwnershipOwner[]) => {
484
+ if (controlledOwners === undefined) setInternalOwners(updated);
485
+ onOwnersChange?.(updated);
486
+ };
487
+
488
+ const handleSliderChange = (id: string, newShare: number) => {
489
+ if (owners.length !== 2) return; // Only 2-owner redistribution implemented
490
+ const clamped = Math.max(0, Math.min(100, Math.round(newShare)));
491
+ const other = owners.find((o) => o.id !== id);
492
+ if (!other) return;
493
+ setOwners(
494
+ owners.map((o) =>
495
+ o.id === id ? { ...o, share: clamped } : { ...o, share: 100 - clamped },
496
+ ),
497
+ );
498
+ };
499
+
500
+ const handleInputChange = (id: string, raw: string) => {
501
+ const n = parseInt(raw.replace(/[^0-9]/g, ""), 10);
502
+ if (isNaN(n)) return;
503
+ handleSliderChange(id, n);
504
+ };
505
+
506
+ return (
507
+ <div className={cn("flex gap-4", className)}>
508
+ {owners.map((owner) => {
509
+ const pct = Math.max(0, Math.min(100, owner.share));
510
+ return (
511
+ <div key={owner.id} className="flex flex-col gap-2 flex-1">
512
+ <div className="flex items-center justify-between">
513
+ <Label className="text-xs font-medium">{owner.name}</Label>
514
+ <Input
515
+ type="text"
516
+ value={`${pct}%`}
517
+ disabled={disabled}
518
+ className="w-14 h-7 text-xs text-right tabular-nums px-2"
519
+ onChange={(e) => handleInputChange(owner.id, e.target.value)}
520
+ onBlur={(e) => handleInputChange(owner.id, e.target.value)}
521
+ />
522
+ </div>
523
+ <Slider
524
+ value={pct}
525
+ min={0}
526
+ max={100}
527
+ step={1}
528
+ disabled={disabled}
529
+ onValueChange={(n) => handleSliderChange(owner.id, n)}
530
+ />
531
+ </div>
532
+ );
533
+ })}
534
+ </div>
535
+ );
536
+ }