@wealthx/shadcn 1.3.2 → 1.3.4

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 (313) hide show
  1. package/.turbo/turbo-build.log +227 -191
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-2UM72RJ7.mjs → chunk-2D3HQPFN.mjs} +12 -10
  4. package/dist/chunk-2EM2FRU6.mjs +613 -0
  5. package/dist/{chunk-FH6QVUVZ.mjs → chunk-2GIYVERS.mjs} +2 -2
  6. package/dist/chunk-2P7HP7LR.mjs +68 -0
  7. package/dist/{chunk-HISNT2MG.mjs → chunk-37AE3OM5.mjs} +5 -5
  8. package/dist/{chunk-HBZLGDIN.mjs → chunk-3ERBUVHC.mjs} +169 -110
  9. package/dist/{chunk-C7CQJNMR.mjs → chunk-3VDET466.mjs} +2 -2
  10. package/dist/{chunk-462HMNO4.mjs → chunk-4MM7LHM5.mjs} +2 -2
  11. package/dist/{chunk-QMY3AZJH.mjs → chunk-4Z66LMIQ.mjs} +2 -2
  12. package/dist/{chunk-U5X52X37.mjs → chunk-57ZXILTS.mjs} +6 -6
  13. package/dist/{chunk-2A5RRQGG.mjs → chunk-5NF6T2RS.mjs} +11 -20
  14. package/dist/{chunk-3OYFOX3X.mjs → chunk-5VOTTIXF.mjs} +2 -2
  15. package/dist/{chunk-LBMRIB3G.mjs → chunk-6AJUS7VX.mjs} +1 -1
  16. package/dist/{chunk-OODBHKG7.mjs → chunk-6HIOM2HL.mjs} +7 -4
  17. package/dist/{chunk-BDYZCBRT.mjs → chunk-6QAFGZC2.mjs} +2 -2
  18. package/dist/{chunk-U4NDAF2P.mjs → chunk-6TX73WG7.mjs} +1 -1
  19. package/dist/{chunk-GD4BJDJR.mjs → chunk-7BTFGCFC.mjs} +4 -4
  20. package/dist/{chunk-FAKPBKLT.mjs → chunk-7GWRPXHD.mjs} +4 -4
  21. package/dist/{chunk-NMOI6CQD.mjs → chunk-7YI3HEBH.mjs} +5 -5
  22. package/dist/{chunk-T4BJLT57.mjs → chunk-AE7MASLF.mjs} +5 -5
  23. package/dist/{chunk-VLQZANBF.mjs → chunk-AFML43VJ.mjs} +6 -1
  24. package/dist/chunk-BBXSNDS3.mjs +260 -0
  25. package/dist/chunk-BOW7U26Y.mjs +203 -0
  26. package/dist/{chunk-34NWQURD.mjs → chunk-BS75ICOO.mjs} +2 -2
  27. package/dist/chunk-D2NSIIXG.mjs +394 -0
  28. package/dist/{chunk-3GF7OVTP.mjs → chunk-DGNHGNYH.mjs} +2 -2
  29. package/dist/{chunk-VLARHE5V.mjs → chunk-DMXYRCHM.mjs} +6 -6
  30. package/dist/{chunk-OGOYQ7BG.mjs → chunk-DQB4EPIS.mjs} +1 -1
  31. package/dist/{chunk-MIZQHHUO.mjs → chunk-FL6DZFJK.mjs} +106 -38
  32. package/dist/{chunk-I3RZS7V2.mjs → chunk-FLL633WS.mjs} +19 -33
  33. package/dist/{chunk-PBL4OQV2.mjs → chunk-FTPBQVQ6.mjs} +4 -4
  34. package/dist/chunk-FYPSTTEJ.mjs +169 -0
  35. package/dist/{chunk-6O6KD7CE.mjs → chunk-G27TSQLQ.mjs} +6 -6
  36. package/dist/{chunk-66MI7Q4B.mjs → chunk-GT3RU6GA.mjs} +2 -2
  37. package/dist/{chunk-D6ID6M4V.mjs → chunk-GTAVSBDO.mjs} +2 -2
  38. package/dist/{chunk-24FUO7TD.mjs → chunk-H6NQTIF4.mjs} +2 -2
  39. package/dist/{chunk-7DHU4VGG.mjs → chunk-HK4HUQTV.mjs} +2 -2
  40. package/dist/chunk-I4KVSZCH.mjs +101 -0
  41. package/dist/{chunk-RGVKLTLH.mjs → chunk-IKXYTCSB.mjs} +2 -2
  42. package/dist/{chunk-Y6DWJSKZ.mjs → chunk-ISUA7DSB.mjs} +1 -1
  43. package/dist/{chunk-J5UICVJS.mjs → chunk-JPGL36WQ.mjs} +2 -2
  44. package/dist/{chunk-7XJHLGUV.mjs → chunk-JTK6VJXY.mjs} +2 -2
  45. package/dist/{chunk-7YAU5CY6.mjs → chunk-JVMXMFBB.mjs} +2 -2
  46. package/dist/{chunk-IAE3F7DR.mjs → chunk-JZY6TNIS.mjs} +21 -21
  47. package/dist/{chunk-K5A5L6T2.mjs → chunk-K4KOD3KR.mjs} +12 -12
  48. package/dist/{chunk-MBON7YRJ.mjs → chunk-K5QV4TT6.mjs} +3 -3
  49. package/dist/{chunk-IHMFS7NZ.mjs → chunk-K5VHK7CM.mjs} +21 -21
  50. package/dist/{chunk-RJI6GKVF.mjs → chunk-KCWNDYPZ.mjs} +5 -5
  51. package/dist/{chunk-UFYSFDER.mjs → chunk-KFH36NKF.mjs} +1 -1
  52. package/dist/{chunk-EBXQWIYG.mjs → chunk-KLTACJ2G.mjs} +5 -5
  53. package/dist/{chunk-3TTACBDP.mjs → chunk-KWD6GANL.mjs} +4 -4
  54. package/dist/{chunk-IOJRDS6V.mjs → chunk-L4NSRQ3T.mjs} +218 -147
  55. package/dist/{chunk-GYMYRIZP.mjs → chunk-LBTHZSBT.mjs} +2 -2
  56. package/dist/{chunk-AMQZRHEZ.mjs → chunk-LQULK2E3.mjs} +5 -5
  57. package/dist/{chunk-YJG55G2H.mjs → chunk-LR6LHDP3.mjs} +5 -5
  58. package/dist/{chunk-7PV3IWCN.mjs → chunk-M4VYX2PV.mjs} +19 -1
  59. package/dist/{chunk-P76HMUI6.mjs → chunk-MDUKXXIL.mjs} +2 -2
  60. package/dist/{chunk-LV35NGVG.mjs → chunk-N6Q5IPKT.mjs} +9 -9
  61. package/dist/{chunk-DOEO3CDL.mjs → chunk-NB3ZL36B.mjs} +1 -1
  62. package/dist/{chunk-XREGSKX3.mjs → chunk-NOOEKOWY.mjs} +5 -5
  63. package/dist/{chunk-NL3ZO62D.mjs → chunk-NT4FX27K.mjs} +1 -1
  64. package/dist/{chunk-QZ4RE6NA.mjs → chunk-NTYQWVLI.mjs} +6 -6
  65. package/dist/{chunk-ERGGHC2V.mjs → chunk-OEOOYMC2.mjs} +2 -2
  66. package/dist/{chunk-4GAWMKMI.mjs → chunk-OIKBW2QD.mjs} +291 -54
  67. package/dist/{chunk-DUJTAXMH.mjs → chunk-OKTJFDPN.mjs} +6 -6
  68. package/dist/chunk-OLKMCXAR.mjs +1219 -0
  69. package/dist/{chunk-EI5F6FMT.mjs → chunk-OWFQSXVD.mjs} +3 -3
  70. package/dist/{chunk-6DZEXFNB.mjs → chunk-P2N2PEFY.mjs} +3 -3
  71. package/dist/{chunk-NSLMILBT.mjs → chunk-P7CEBZM6.mjs} +2 -2
  72. package/dist/{chunk-7S5AESZO.mjs → chunk-PNRUH7JY.mjs} +6 -6
  73. package/dist/{chunk-ZU4NV6RG.mjs → chunk-PNSYFE3K.mjs} +2 -2
  74. package/dist/{chunk-JKGDCQTZ.mjs → chunk-QTRSCVQ3.mjs} +5 -5
  75. package/dist/{chunk-ABFDMHOR.mjs → chunk-QX7IFQSF.mjs} +5 -5
  76. package/dist/{chunk-CFMQP5QS.mjs → chunk-QXKGOMUX.mjs} +6 -6
  77. package/dist/{chunk-NQPOYKAQ.mjs → chunk-R2ON6CAN.mjs} +2 -2
  78. package/dist/{chunk-DBHJ5KC3.mjs → chunk-R4HCRDU5.mjs} +1 -1
  79. package/dist/{chunk-EWRB4PAD.mjs → chunk-RCAOCHWA.mjs} +14 -14
  80. package/dist/{chunk-EFRENWEJ.mjs → chunk-RSUIPKGX.mjs} +2 -2
  81. package/dist/{chunk-DGHAXJBN.mjs → chunk-S2FKV4M5.mjs} +5 -5
  82. package/dist/{chunk-RGU7HOEC.mjs → chunk-SET2ANTY.mjs} +5 -7
  83. package/dist/chunk-SFH2NJEJ.mjs +47 -0
  84. package/dist/{chunk-6AW4KJHE.mjs → chunk-SIVYAI3M.mjs} +12 -12
  85. package/dist/{chunk-5FQIKDKP.mjs → chunk-THVO2N47.mjs} +8 -8
  86. package/dist/{chunk-JMHR3YGZ.mjs → chunk-TLAWKTSA.mjs} +3 -3
  87. package/dist/{chunk-HVY6KCCF.mjs → chunk-TOWTPLRC.mjs} +68 -72
  88. package/dist/{chunk-6JQFUE5I.mjs → chunk-UALR6JGV.mjs} +2 -2
  89. package/dist/{chunk-N6TNTQL6.mjs → chunk-UJZ4UHWI.mjs} +9 -11
  90. package/dist/{chunk-MARPPFOJ.mjs → chunk-UNACI2YK.mjs} +2 -2
  91. package/dist/{chunk-3NCUZIFP.mjs → chunk-V6XGXYCJ.mjs} +7 -7
  92. package/dist/chunk-VB5M6OZQ.mjs +57 -0
  93. package/dist/{chunk-5IS7G74I.mjs → chunk-VY5NEUP7.mjs} +6 -6
  94. package/dist/{chunk-JHJHG4GO.mjs → chunk-WE4YKBDE.mjs} +2 -2
  95. package/dist/{chunk-BKNFWEH2.mjs → chunk-WL6WVV47.mjs} +3 -3
  96. package/dist/{chunk-FWCSY2DS.mjs → chunk-WNQUEZJF.mjs} +22 -1
  97. package/dist/{chunk-2Y7YJKPE.mjs → chunk-WZ6UJCBL.mjs} +1 -1
  98. package/dist/{chunk-UMTOX62O.mjs → chunk-XYPW2XA5.mjs} +13 -10
  99. package/dist/chunk-Y2MTAVAK.mjs +34 -0
  100. package/dist/{chunk-6CR5N2JW.mjs → chunk-YCWLFG27.mjs} +6 -6
  101. package/dist/{chunk-PU4YZQXV.mjs → chunk-YE67AALL.mjs} +12 -12
  102. package/dist/{chunk-M3FV7LOK.mjs → chunk-YEWNFK5S.mjs} +6 -1
  103. package/dist/{chunk-R3VSPKNP.mjs → chunk-YIZHS72Z.mjs} +11 -12
  104. package/dist/{chunk-7PYJD5JI.mjs → chunk-ZEDMKQK2.mjs} +2 -2
  105. package/dist/{chunk-N2PT566P.mjs → chunk-ZFCDYW6N.mjs} +4 -4
  106. package/dist/chunk-ZGQIVGIN.mjs +57 -0
  107. package/dist/{chunk-Q2BGOAMG.mjs → chunk-ZKWXDQDG.mjs} +4 -4
  108. package/dist/{chunk-GHC7LLUX.mjs → chunk-ZOWL2L5J.mjs} +5 -5
  109. package/dist/components/ui/accordion.mjs +3 -3
  110. package/dist/components/ui/add-column-modal.js +2 -2
  111. package/dist/components/ui/add-column-modal.mjs +10 -10
  112. package/dist/components/ui/add-lead-modal.js +424 -82
  113. package/dist/components/ui/add-lead-modal.mjs +12 -9
  114. package/dist/components/ui/advisor-card.js +2 -2
  115. package/dist/components/ui/advisor-card.mjs +8 -8
  116. package/dist/components/ui/ai-assistant-drawer.js +2 -2
  117. package/dist/components/ui/ai-assistant-drawer.mjs +9 -9
  118. package/dist/components/ui/ai-builder.js +958 -0
  119. package/dist/components/ui/ai-builder.mjs +25 -0
  120. package/dist/components/ui/ai-conversations.js +2045 -0
  121. package/dist/components/ui/ai-conversations.mjs +41 -0
  122. package/dist/components/ui/alert-dialog.js +2 -2
  123. package/dist/components/ui/alert-dialog.mjs +5 -5
  124. package/dist/components/ui/alert.mjs +3 -3
  125. package/dist/components/ui/appointment-action-dialogs.js +19 -3
  126. package/dist/components/ui/appointment-action-dialogs.mjs +15 -14
  127. package/dist/components/ui/appointment-availability-settings.js +181 -111
  128. package/dist/components/ui/appointment-availability-settings.mjs +20 -18
  129. package/dist/components/ui/appointment-book-dialog.js +113 -24
  130. package/dist/components/ui/appointment-book-dialog.mjs +21 -20
  131. package/dist/components/ui/appointment-calendar-view.js +19 -3
  132. package/dist/components/ui/appointment-calendar-view.mjs +10 -9
  133. package/dist/components/ui/appointment-detail-sheet.js +19 -3
  134. package/dist/components/ui/appointment-detail-sheet.mjs +18 -17
  135. package/dist/components/ui/appointment-gmail-connect.js +49 -89
  136. package/dist/components/ui/appointment-gmail-connect.mjs +8 -9
  137. package/dist/components/ui/appointment-mini-card.js +2 -2
  138. package/dist/components/ui/appointment-mini-card.mjs +6 -6
  139. package/dist/components/ui/appointment-time-slot-picker.mjs +6 -6
  140. package/dist/components/ui/appointment-upcoming-card.js +19 -3
  141. package/dist/components/ui/appointment-upcoming-card.mjs +15 -14
  142. package/dist/components/ui/auth-logo.js +95 -0
  143. package/dist/components/ui/auth-logo.mjs +8 -0
  144. package/dist/components/ui/auth-page-layout.js +108 -0
  145. package/dist/components/ui/auth-page-layout.mjs +8 -0
  146. package/dist/components/ui/avatar.mjs +3 -3
  147. package/dist/components/ui/backoffice-alert-history-chart.js +2 -2
  148. package/dist/components/ui/backoffice-alert-history-chart.mjs +9 -9
  149. package/dist/components/ui/backoffice-alerts-chart.js +2 -2
  150. package/dist/components/ui/backoffice-alerts-chart.mjs +11 -11
  151. package/dist/components/ui/backoffice-connections-chart.js +2 -2
  152. package/dist/components/ui/backoffice-connections-chart.mjs +11 -11
  153. package/dist/components/ui/backoffice-contact-history-chart.js +2 -2
  154. package/dist/components/ui/backoffice-contact-history-chart.mjs +9 -9
  155. package/dist/components/ui/badge.mjs +4 -4
  156. package/dist/components/ui/borrowing-capacity-line-chart.js +145 -132
  157. package/dist/components/ui/borrowing-capacity-line-chart.mjs +9 -9
  158. package/dist/components/ui/button.js +2 -2
  159. package/dist/components/ui/button.mjs +4 -4
  160. package/dist/components/ui/calendar.js +17 -3
  161. package/dist/components/ui/calendar.mjs +6 -5
  162. package/dist/components/ui/card.mjs +3 -3
  163. package/dist/components/ui/cash-balance-line-chart.js +158 -158
  164. package/dist/components/ui/cash-balance-line-chart.mjs +9 -9
  165. package/dist/components/ui/cashflow-bar-chart.js +2 -2
  166. package/dist/components/ui/cashflow-bar-chart.mjs +9 -9
  167. package/dist/components/ui/chat-widget-primitives.js +573 -0
  168. package/dist/components/ui/chat-widget-primitives.mjs +21 -0
  169. package/dist/components/ui/chat-widget.js +1268 -0
  170. package/dist/components/ui/chat-widget.mjs +29 -0
  171. package/dist/components/ui/checkbox.mjs +3 -3
  172. package/dist/components/ui/chip.js +2 -2
  173. package/dist/components/ui/chip.mjs +6 -6
  174. package/dist/components/ui/color-picker.js +2 -2
  175. package/dist/components/ui/color-picker.mjs +7 -7
  176. package/dist/components/ui/combobox.mjs +3 -3
  177. package/dist/components/ui/data-table.js +2 -2
  178. package/dist/components/ui/data-table.mjs +12 -12
  179. package/dist/components/ui/date-picker.js +22 -6
  180. package/dist/components/ui/date-picker.mjs +9 -8
  181. package/dist/components/ui/dialog.js +2 -2
  182. package/dist/components/ui/dialog.mjs +5 -5
  183. package/dist/components/ui/document-checklist-template.js +630 -0
  184. package/dist/components/ui/document-checklist-template.mjs +15 -0
  185. package/dist/components/ui/drawer.js +2 -2
  186. package/dist/components/ui/drawer.mjs +3 -3
  187. package/dist/components/ui/dropdown-menu.mjs +3 -3
  188. package/dist/components/ui/empty.mjs +3 -3
  189. package/dist/components/ui/expense-bar-chart.js +2 -2
  190. package/dist/components/ui/expense-bar-chart.mjs +9 -9
  191. package/dist/components/ui/field.mjs +5 -5
  192. package/dist/components/ui/financial-cards.js +431 -291
  193. package/dist/components/ui/financial-cards.mjs +10 -9
  194. package/dist/components/ui/financial-drawers.js +4 -4
  195. package/dist/components/ui/financial-drawers.mjs +8 -8
  196. package/dist/components/ui/financial-primitives.mjs +3 -3
  197. package/dist/components/ui/financial-sections.js +8 -9
  198. package/dist/components/ui/financial-sections.mjs +12 -12
  199. package/dist/components/ui/form-primitives.mjs +8 -8
  200. package/dist/components/ui/income-bar-chart.js +2 -2
  201. package/dist/components/ui/income-bar-chart.mjs +9 -9
  202. package/dist/components/ui/input-group.js +2 -2
  203. package/dist/components/ui/input-group.mjs +7 -7
  204. package/dist/components/ui/input-otp.mjs +3 -3
  205. package/dist/components/ui/input.mjs +3 -3
  206. package/dist/components/ui/kanban-column.js +19 -23
  207. package/dist/components/ui/kanban-column.mjs +14 -14
  208. package/dist/components/ui/label.mjs +3 -3
  209. package/dist/components/ui/onboarding-layout.js +476 -0
  210. package/dist/components/ui/onboarding-layout.mjs +11 -0
  211. package/dist/components/ui/opportunity-card.js +2 -2
  212. package/dist/components/ui/opportunity-card.mjs +12 -12
  213. package/dist/components/ui/opportunity-edit-modals.js +22 -6
  214. package/dist/components/ui/opportunity-edit-modals.mjs +21 -20
  215. package/dist/components/ui/opportunity-summary-tab.js +991 -674
  216. package/dist/components/ui/opportunity-summary-tab.mjs +26 -26
  217. package/dist/components/ui/page-header.mjs +3 -3
  218. package/dist/components/ui/page-top-bar.mjs +3 -3
  219. package/dist/components/ui/pagination.js +2 -2
  220. package/dist/components/ui/pagination.mjs +6 -6
  221. package/dist/components/ui/password-strength-tooltip.js +197 -0
  222. package/dist/components/ui/password-strength-tooltip.mjs +11 -0
  223. package/dist/components/ui/pipeline-alerts.mjs +3 -3
  224. package/dist/components/ui/pipeline-board.js +19 -23
  225. package/dist/components/ui/pipeline-board.mjs +18 -18
  226. package/dist/components/ui/pipeline-chart.js +12 -6
  227. package/dist/components/ui/pipeline-chart.mjs +4 -3
  228. package/dist/components/ui/pipeline-dialogs.js +28 -12
  229. package/dist/components/ui/pipeline-dialogs.mjs +14 -13
  230. package/dist/components/ui/pipeline-primitives.mjs +6 -6
  231. package/dist/components/ui/popover.mjs +3 -3
  232. package/dist/components/ui/progress.mjs +3 -3
  233. package/dist/components/ui/property-cashflow-doughnut-chart.js +2 -2
  234. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +9 -9
  235. package/dist/components/ui/property-debt-equity-doughnut-chart.js +2 -2
  236. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +9 -9
  237. package/dist/components/ui/property-mobile-estimate-line-chart.js +2 -2
  238. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +9 -9
  239. package/dist/components/ui/radio-group.mjs +3 -3
  240. package/dist/components/ui/select.mjs +3 -3
  241. package/dist/components/ui/separator.mjs +3 -3
  242. package/dist/components/ui/sheet.mjs +3 -3
  243. package/dist/components/ui/sidebar-nav.js +6 -5
  244. package/dist/components/ui/sidebar-nav.mjs +7 -7
  245. package/dist/components/ui/skeleton.mjs +3 -3
  246. package/dist/components/ui/slider.mjs +3 -3
  247. package/dist/components/ui/sonner.mjs +2 -2
  248. package/dist/components/ui/spinner.mjs +3 -3
  249. package/dist/components/ui/stage-timeline.mjs +10 -10
  250. package/dist/components/ui/stepper.mjs +3 -3
  251. package/dist/components/ui/switch.mjs +3 -3
  252. package/dist/components/ui/table.mjs +3 -3
  253. package/dist/components/ui/tabs.mjs +3 -3
  254. package/dist/components/ui/textarea.mjs +3 -3
  255. package/dist/components/ui/toggle-group.mjs +4 -4
  256. package/dist/components/ui/toggle.mjs +3 -3
  257. package/dist/components/ui/tooltip.mjs +3 -3
  258. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +2 -2
  259. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +9 -9
  260. package/dist/components/ui/transactions-income-expense-bar-chart.js +2 -2
  261. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +9 -9
  262. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +2 -2
  263. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +9 -9
  264. package/dist/components/ui/two-fa-setup-form.js +612 -0
  265. package/dist/components/ui/two-fa-setup-form.mjs +16 -0
  266. package/dist/components/ui/upload-card.js +187 -0
  267. package/dist/components/ui/upload-card.mjs +10 -0
  268. package/dist/components/ui/video-background.js +118 -0
  269. package/dist/components/ui/video-background.mjs +8 -0
  270. package/dist/index.js +12674 -9311
  271. package/dist/index.mjs +341 -245
  272. package/dist/lib/colors.mjs +1 -1
  273. package/dist/lib/theme-provider.mjs +1 -1
  274. package/dist/lib/typography.mjs +2 -2
  275. package/dist/lib/utils.js +8 -2
  276. package/dist/lib/utils.mjs +6 -4
  277. package/dist/styles.css +1 -1
  278. package/package.json +61 -1
  279. package/src/components/index.tsx +126 -1
  280. package/src/components/ui/add-lead-modal.tsx +101 -142
  281. package/src/components/ui/ai-builder.tsx +560 -0
  282. package/src/components/ui/ai-conversations.tsx +1690 -0
  283. package/src/components/ui/appointment-availability-settings.tsx +152 -101
  284. package/src/components/ui/appointment-book-dialog.tsx +138 -24
  285. package/src/components/ui/appointment-calendar-view.tsx +2 -3
  286. package/src/components/ui/appointment-gmail-connect.tsx +23 -42
  287. package/src/components/ui/auth-logo.tsx +50 -0
  288. package/src/components/ui/auth-page-layout.tsx +59 -0
  289. package/src/components/ui/borrowing-capacity-line-chart.tsx +10 -8
  290. package/src/components/ui/button.tsx +2 -2
  291. package/src/components/ui/calendar.tsx +2 -1
  292. package/src/components/ui/cash-balance-line-chart.tsx +11 -20
  293. package/src/components/ui/chart-shared.tsx +10 -0
  294. package/src/components/ui/chat-widget-primitives.tsx +336 -0
  295. package/src/components/ui/chat-widget.tsx +822 -0
  296. package/src/components/ui/document-checklist-template.tsx +264 -0
  297. package/src/components/ui/drawer.tsx +2 -2
  298. package/src/components/ui/financial-cards.tsx +176 -78
  299. package/src/components/ui/financial-drawers.tsx +2 -2
  300. package/src/components/ui/financial-sections.tsx +1 -1
  301. package/src/components/ui/kanban-column.tsx +2 -5
  302. package/src/components/ui/onboarding-layout.tsx +109 -0
  303. package/src/components/ui/opportunity-summary-tab.tsx +469 -142
  304. package/src/components/ui/password-strength-tooltip.tsx +70 -0
  305. package/src/components/ui/pipeline-chart.tsx +2 -6
  306. package/src/components/ui/sidebar-nav.tsx +1 -11
  307. package/src/components/ui/two-fa-setup-form.tsx +229 -0
  308. package/src/components/ui/upload-card.tsx +98 -0
  309. package/src/components/ui/video-background.tsx +55 -0
  310. package/src/lib/format-date.ts +26 -0
  311. package/src/lib/utils.ts +11 -0
  312. package/src/styles/styles-css.ts +1 -1
  313. package/tsup.config.ts +13 -0
@@ -0,0 +1,822 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import {
3
+ Home,
4
+ RefreshCw,
5
+ TrendingUp,
6
+ Key,
7
+ CalendarDays,
8
+ HelpCircle,
9
+ Lock,
10
+ Calendar,
11
+ CheckCircle2,
12
+ ArrowLeft,
13
+ ArrowRight,
14
+ ExternalLink,
15
+ Video,
16
+ Phone,
17
+ MapPin,
18
+ MessageSquare,
19
+ User,
20
+ } from "lucide-react";
21
+ import { cn } from "@/lib/utils";
22
+ import { Button } from "@/components/ui/button";
23
+ import { Input } from "@/components/ui/input";
24
+ import { Field, FieldLabel, FieldError } from "@/components/ui/field";
25
+ import {
26
+ ChatWidgetLauncher,
27
+ ChatWidgetHeader,
28
+ ChatWidgetMessage,
29
+ ChatWidgetInputBar,
30
+ type ChatWidgetMessageRole,
31
+ } from "@/components/ui/chat-widget-primitives";
32
+
33
+ /**
34
+ * ChatWidget — WealthX DS (Broker Website Embeddable Chat)
35
+ *
36
+ * Two-screen flow:
37
+ * Screen 1 — TopicGrid: lead selects a conversation topic
38
+ * Screen 2 — Chat: AI conversation based on selected topic
39
+ *
40
+ * Exports (atomic order):
41
+ * ChatWidgetIntakeForm — Molecule — Screen 1 contact form
42
+ * ChatWidgetTopicCard — Molecule — Single topic option card
43
+ * ChatWidgetTopicGrid — Organism — Screen 2 topic selection grid
44
+ * ChatWidgetInteractiveCard — Molecule — AI-embedded interactive cards
45
+ * ChatWidgetWindow — Template — 3-screen container + localStorage cache
46
+ * ChatWidget — Root — Floating widget with FAB launcher
47
+ *
48
+ * Pure display component for story control. Real API wiring done in the app layer.
49
+ */
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Shared types
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export interface ChatWidgetUser {
56
+ name: string;
57
+ phone: string;
58
+ email: string;
59
+ }
60
+
61
+ export interface ChatWidgetTopic {
62
+ id: string;
63
+ icon: React.ReactNode;
64
+ label: string;
65
+ description?: string;
66
+ }
67
+
68
+ export interface ChatWidgetChatMessage {
69
+ id: string;
70
+ role: ChatWidgetMessageRole;
71
+ content: string;
72
+ timestamp?: string;
73
+ isStreaming?: boolean;
74
+ /** Optional inline interactive card rendered below the message bubble. */
75
+ interactiveCard?: ChatWidgetInteractiveCardData;
76
+ }
77
+
78
+ export type ChatWidgetScreen = "intake" | "topics" | "chat" | "booking";
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Default topics
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export const DEFAULT_CHAT_WIDGET_TOPICS: ChatWidgetTopic[] = [
85
+ {
86
+ id: "buy_home",
87
+ icon: <Home className="size-5" />,
88
+ label: "Buy a Home",
89
+ description: "Purchase your first or next property",
90
+ },
91
+ {
92
+ id: "refinance",
93
+ icon: <RefreshCw className="size-5" />,
94
+ label: "Refinance",
95
+ description: "Get a better rate on your current loan",
96
+ },
97
+ {
98
+ id: "investment",
99
+ icon: <TrendingUp className="size-5" />,
100
+ label: "Investment Property",
101
+ description: "Grow your property portfolio",
102
+ },
103
+ {
104
+ id: "first_home_buyer",
105
+ icon: <Key className="size-5" />,
106
+ label: "First Home Buyer",
107
+ description: "First home buyer grants & schemes",
108
+ },
109
+ {
110
+ id: "book_meeting",
111
+ icon: <CalendarDays className="size-5" />,
112
+ label: "Book a Meeting",
113
+ description: "Talk directly with an advisor",
114
+ },
115
+ {
116
+ id: "general_question",
117
+ icon: <HelpCircle className="size-5" />,
118
+ label: "General Question",
119
+ description: "Ask anything about home loans",
120
+ },
121
+ ];
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // ChatWidgetIntakeForm (Molecule — Screen 1)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ export interface ChatWidgetIntakeFormProps {
128
+ brokerName?: string;
129
+ onSubmit: (user: ChatWidgetUser) => void;
130
+ isSubmitting?: boolean;
131
+ className?: string;
132
+ }
133
+
134
+ export function ChatWidgetIntakeForm({
135
+ brokerName,
136
+ onSubmit,
137
+ isSubmitting,
138
+ className,
139
+ }: ChatWidgetIntakeFormProps) {
140
+ const [name, setName] = useState("");
141
+ const [phone, setPhone] = useState("");
142
+ const [email, setEmail] = useState("");
143
+ const [errors, setErrors] = useState<
144
+ Partial<Record<keyof ChatWidgetUser, string>>
145
+ >({});
146
+
147
+ const clearError = (field: keyof ChatWidgetUser) =>
148
+ setErrors((prev) => ({ ...prev, [field]: undefined }));
149
+
150
+ const validate = (): Partial<Record<keyof ChatWidgetUser, string>> => {
151
+ const e: Partial<Record<keyof ChatWidgetUser, string>> = {};
152
+ if (!name.trim()) e.name = "Name is required";
153
+ if (!phone.trim()) e.phone = "Phone number is required";
154
+ if (!email.trim()) e.email = "Email is required";
155
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
156
+ e.email = "Enter a valid email address";
157
+ return e;
158
+ };
159
+
160
+ const handleSubmit = (e: React.FormEvent) => {
161
+ e.preventDefault();
162
+ const errs = validate();
163
+ if (Object.keys(errs).length > 0) {
164
+ setErrors(errs);
165
+ return;
166
+ }
167
+ onSubmit({ name: name.trim(), phone: phone.trim(), email: email.trim() });
168
+ };
169
+
170
+ return (
171
+ <div className={cn("flex flex-col gap-5 p-5", className)}>
172
+ <div>
173
+ <p className="text-body-large font-semibold text-foreground">
174
+ {brokerName ? `Hi from ${brokerName}!` : "Hi there!"}
175
+ </p>
176
+ <p className="mt-1 text-body-medium text-muted-foreground">
177
+ To get started, please share your contact details.
178
+ </p>
179
+ </div>
180
+
181
+ <form onSubmit={handleSubmit} noValidate className="flex flex-col gap-3">
182
+ <Field>
183
+ <FieldLabel>
184
+ Full Name <span className="text-destructive">*</span>
185
+ </FieldLabel>
186
+ <Input
187
+ value={name}
188
+ onChange={(e) => {
189
+ setName(e.target.value);
190
+ clearError("name");
191
+ }}
192
+ placeholder="Jane Smith"
193
+ disabled={isSubmitting}
194
+ aria-invalid={!!errors.name || undefined}
195
+ />
196
+ {errors.name && <FieldError>{errors.name}</FieldError>}
197
+ </Field>
198
+
199
+ <Field>
200
+ <FieldLabel>
201
+ Phone Number <span className="text-destructive">*</span>
202
+ </FieldLabel>
203
+ <Input
204
+ type="tel"
205
+ value={phone}
206
+ onChange={(e) => {
207
+ setPhone(e.target.value);
208
+ clearError("phone");
209
+ }}
210
+ placeholder="0400 000 000"
211
+ disabled={isSubmitting}
212
+ aria-invalid={!!errors.phone || undefined}
213
+ />
214
+ {errors.phone && <FieldError>{errors.phone}</FieldError>}
215
+ </Field>
216
+
217
+ <Field>
218
+ <FieldLabel>
219
+ Email Address <span className="text-destructive">*</span>
220
+ </FieldLabel>
221
+ <Input
222
+ type="email"
223
+ value={email}
224
+ onChange={(e) => {
225
+ setEmail(e.target.value);
226
+ clearError("email");
227
+ }}
228
+ placeholder="jane@example.com"
229
+ disabled={isSubmitting}
230
+ aria-invalid={!!errors.email || undefined}
231
+ />
232
+ {errors.email && <FieldError>{errors.email}</FieldError>}
233
+ </Field>
234
+
235
+ <Button
236
+ type="submit"
237
+ variant="default"
238
+ className="mt-1 w-full"
239
+ disabled={isSubmitting}
240
+ loading={isSubmitting}
241
+ >
242
+ Start Chat
243
+ <ArrowRight className="size-4" />
244
+ </Button>
245
+ </form>
246
+
247
+ <p className="flex items-center justify-center gap-1 text-center text-[11px] text-muted-foreground">
248
+ <Lock className="size-3" />
249
+ Your info is safe with us
250
+ </p>
251
+ </div>
252
+ );
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // ChatWidgetTopicCard (Molecule — single topic)
257
+ // ---------------------------------------------------------------------------
258
+
259
+ export interface ChatWidgetTopicCardProps {
260
+ topic: ChatWidgetTopic;
261
+ onClick: (topicId: string) => void;
262
+ className?: string;
263
+ }
264
+
265
+ export function ChatWidgetTopicCard({
266
+ topic,
267
+ onClick,
268
+ className,
269
+ }: ChatWidgetTopicCardProps) {
270
+ return (
271
+ <Button
272
+ variant="outline"
273
+ onClick={() => onClick(topic.id)}
274
+ className={cn(
275
+ "h-auto w-full flex-col items-start gap-1.5 whitespace-normal border-border px-3 py-3 text-left",
276
+ "hover:border-primary hover:bg-primary/5",
277
+ className,
278
+ )}
279
+ >
280
+ <span className="text-muted-foreground" aria-hidden="true">
281
+ {topic.icon}
282
+ </span>
283
+ <span className="text-body-medium font-semibold text-foreground">
284
+ {topic.label}
285
+ </span>
286
+ {topic.description && (
287
+ <span className="text-body-small leading-snug text-muted-foreground">
288
+ {topic.description}
289
+ </span>
290
+ )}
291
+ </Button>
292
+ );
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // ChatWidgetTopicGrid (Organism — Screen 2)
297
+ // ---------------------------------------------------------------------------
298
+
299
+ export interface ChatWidgetTopicGridProps {
300
+ userName: string;
301
+ topics?: ChatWidgetTopic[];
302
+ onTopicSelect: (topicId: string) => void;
303
+ className?: string;
304
+ }
305
+
306
+ export function ChatWidgetTopicGrid({
307
+ userName,
308
+ topics = DEFAULT_CHAT_WIDGET_TOPICS,
309
+ onTopicSelect,
310
+ className,
311
+ }: ChatWidgetTopicGridProps) {
312
+ return (
313
+ <div className={cn("flex flex-col gap-4 p-5", className)}>
314
+ <div>
315
+ <p className="text-body-large font-semibold text-foreground">
316
+ Hi {userName}!
317
+ </p>
318
+ <p className="mt-0.5 text-body-medium text-muted-foreground">
319
+ What would you like to discuss today?
320
+ </p>
321
+ </div>
322
+ <div className="grid grid-cols-2 gap-2">
323
+ {topics.map((topic) => (
324
+ <ChatWidgetTopicCard
325
+ key={topic.id}
326
+ topic={topic}
327
+ onClick={onTopicSelect}
328
+ />
329
+ ))}
330
+ </div>
331
+ </div>
332
+ );
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // ChatWidgetInteractiveCard (Molecule — AI-embedded cards)
337
+ // ---------------------------------------------------------------------------
338
+
339
+ export type ChatWidgetInteractiveCardType =
340
+ | "quick-reply"
341
+ | "appointment"
342
+ | "meeting-type"
343
+ | "confirmation";
344
+
345
+ export type ChatWidgetMeetingTypeId = "video" | "phone" | "in-person";
346
+
347
+ export interface ChatWidgetMeetingTypeOption {
348
+ id: ChatWidgetMeetingTypeId;
349
+ label: string;
350
+ }
351
+
352
+ export interface ChatWidgetQuickReplyOption {
353
+ id: string;
354
+ label: string;
355
+ }
356
+
357
+ export interface ChatWidgetAppointmentSlot {
358
+ id: string;
359
+ /** Human-readable datetime, e.g. "Mon 28 Apr · 2:00 PM" */
360
+ datetime: string;
361
+ type: "video" | "in-person" | "phone";
362
+ }
363
+
364
+ /** Data shape for an interactive card — no callbacks, safe to embed in message data. */
365
+ export interface ChatWidgetInteractiveCardData {
366
+ type: ChatWidgetInteractiveCardType;
367
+ options?: ChatWidgetQuickReplyOption[];
368
+ slots?: ChatWidgetAppointmentSlot[];
369
+ meetingTypes?: ChatWidgetMeetingTypeOption[];
370
+ confirmedSlot?: ChatWidgetAppointmentSlot;
371
+ meetingType?: ChatWidgetMeetingTypeId;
372
+ topic?: string;
373
+ advisorName?: string;
374
+ }
375
+
376
+ export interface ChatWidgetInteractiveCardProps extends ChatWidgetInteractiveCardData {
377
+ onQuickReply?: (optionId: string) => void;
378
+ onSlotSelect?: (slotId: string) => void;
379
+ onMeetingTypeSelect?: (typeId: ChatWidgetMeetingTypeId) => void;
380
+ className?: string;
381
+ }
382
+
383
+ const MEETING_TYPE_LABELS: Record<ChatWidgetMeetingTypeId, string> = {
384
+ video: "Video Call",
385
+ phone: "Phone Call",
386
+ "in-person": "In-Person Meeting",
387
+ };
388
+
389
+ function MeetingTypeIcon({
390
+ id,
391
+ className = "size-4",
392
+ }: {
393
+ id: ChatWidgetMeetingTypeId;
394
+ className?: string;
395
+ }) {
396
+ const Icon = id === "video" ? Video : id === "phone" ? Phone : MapPin;
397
+ return <Icon className={cn("shrink-0 text-muted-foreground", className)} />;
398
+ }
399
+
400
+ export function ChatWidgetInteractiveCard({
401
+ type,
402
+ options,
403
+ onQuickReply,
404
+ slots,
405
+ onSlotSelect,
406
+ meetingTypes,
407
+ onMeetingTypeSelect,
408
+ confirmedSlot,
409
+ meetingType,
410
+ topic,
411
+ advisorName,
412
+ className,
413
+ }: ChatWidgetInteractiveCardProps) {
414
+ if (type === "quick-reply") {
415
+ return (
416
+ <div className={cn("flex flex-wrap gap-2 pt-1", className)}>
417
+ {options?.map((opt) => (
418
+ <Button
419
+ key={opt.id}
420
+ variant="outline-primary"
421
+ size="sm"
422
+ onClick={() => onQuickReply?.(opt.id)}
423
+ >
424
+ {opt.label}
425
+ </Button>
426
+ ))}
427
+ </div>
428
+ );
429
+ }
430
+
431
+ if (type === "appointment") {
432
+ return (
433
+ <div
434
+ className={cn(
435
+ "flex flex-col gap-2 border border-border bg-card p-3",
436
+ className,
437
+ )}
438
+ >
439
+ <p className="text-body-small font-semibold text-foreground">
440
+ Choose a time
441
+ </p>
442
+ {slots?.map((slot) => (
443
+ <Button
444
+ key={slot.id}
445
+ variant="outline"
446
+ onClick={() => onSlotSelect?.(slot.id)}
447
+ className="h-auto w-full justify-start gap-2 px-3 py-2 hover:border-primary hover:bg-primary/5"
448
+ >
449
+ <Calendar className="size-4 shrink-0 text-muted-foreground" />
450
+ <span className="text-foreground">{slot.datetime}</span>
451
+ </Button>
452
+ ))}
453
+ </div>
454
+ );
455
+ }
456
+
457
+ if (type === "meeting-type") {
458
+ return (
459
+ <div
460
+ className={cn(
461
+ "flex flex-col gap-2 border border-border bg-card p-3",
462
+ className,
463
+ )}
464
+ >
465
+ <p className="text-body-small font-semibold text-foreground">
466
+ How would you prefer to meet?
467
+ </p>
468
+ {meetingTypes?.map((mt) => (
469
+ <Button
470
+ key={mt.id}
471
+ variant="outline"
472
+ onClick={() => onMeetingTypeSelect?.(mt.id)}
473
+ className="h-auto w-full justify-start gap-2 px-3 py-2 hover:border-primary hover:bg-primary/5"
474
+ >
475
+ <MeetingTypeIcon id={mt.id} />
476
+ <span className="text-foreground">{mt.label}</span>
477
+ </Button>
478
+ ))}
479
+ </div>
480
+ );
481
+ }
482
+
483
+ if (type === "confirmation") {
484
+ return (
485
+ <div
486
+ className={cn(
487
+ "flex flex-col gap-3 border border-border bg-card p-3",
488
+ className,
489
+ )}
490
+ >
491
+ <div className="flex items-center gap-2">
492
+ <CheckCircle2 className="size-4 shrink-0 text-green-600" />
493
+ <p className="text-body-medium font-semibold text-foreground">
494
+ Appointment Confirmed
495
+ </p>
496
+ </div>
497
+ <div className="flex flex-col gap-1.5 border-t border-border pt-2 text-body-small text-muted-foreground">
498
+ {confirmedSlot && (
499
+ <div className="flex items-center gap-2">
500
+ <Calendar className="size-3.5 shrink-0" />
501
+ <span>{confirmedSlot.datetime}</span>
502
+ </div>
503
+ )}
504
+ {meetingType && (
505
+ <div className="flex items-center gap-2">
506
+ <MeetingTypeIcon id={meetingType} className="size-3.5" />
507
+ <span>{MEETING_TYPE_LABELS[meetingType]}</span>
508
+ </div>
509
+ )}
510
+ {topic && (
511
+ <div className="flex items-center gap-2">
512
+ <MessageSquare className="size-3.5 shrink-0" />
513
+ <span>{topic}</span>
514
+ </div>
515
+ )}
516
+ {advisorName && (
517
+ <div className="flex items-center gap-2">
518
+ <User className="size-3.5 shrink-0" />
519
+ <span>with {advisorName}</span>
520
+ </div>
521
+ )}
522
+ </div>
523
+ </div>
524
+ );
525
+ }
526
+
527
+ return null;
528
+ }
529
+
530
+ // ---------------------------------------------------------------------------
531
+ // ChatWidgetWindow (Template — 3-screen container)
532
+ // ---------------------------------------------------------------------------
533
+
534
+ export interface ChatWidgetWindowProps {
535
+ isOpen: boolean;
536
+ brokerName?: string;
537
+ /** Solid brand color — sets --primary on the widget so all interactive elements match. */
538
+ brandColor?: string;
539
+ gradientFrom?: string;
540
+ gradientTo?: string;
541
+ onMinimize?: () => void;
542
+ // Screen 1
543
+ onIntakeSubmit?: (user: ChatWidgetUser) => void;
544
+ isSubmittingIntake?: boolean;
545
+ // Screen 2
546
+ topics?: ChatWidgetTopic[];
547
+ onTopicSelect?: (topicId: string) => void;
548
+ /** External booking URL. When provided and user selects "Book a Meeting", shows a booking screen instead of chat. */
549
+ bookingUrl?: string;
550
+ // Screen 3
551
+ messages?: ChatWidgetChatMessage[];
552
+ inputValue?: string;
553
+ onInputChange?: (value: string) => void;
554
+ onSend?: (value: string) => void;
555
+ isStreaming?: boolean;
556
+ onSlotSelect?: (slotId: string) => void;
557
+ onQuickReply?: (optionId: string) => void;
558
+ onMeetingTypeSelect?: (typeId: ChatWidgetMeetingTypeId) => void;
559
+ /** Override starting screen — used in stories to skip to a specific screen. */
560
+ initialScreen?: ChatWidgetScreen;
561
+ /** Override cached user — used in stories to pre-populate the user. */
562
+ initialUser?: ChatWidgetUser;
563
+ className?: string;
564
+ }
565
+
566
+ export function ChatWidgetWindow({
567
+ isOpen,
568
+ brokerName = "Your Broker",
569
+ brandColor,
570
+ gradientFrom,
571
+ gradientTo,
572
+ onMinimize,
573
+ onIntakeSubmit,
574
+ isSubmittingIntake,
575
+ topics,
576
+ onTopicSelect,
577
+ bookingUrl,
578
+ messages = [],
579
+ inputValue = "",
580
+ onInputChange,
581
+ onSend,
582
+ isStreaming,
583
+ onSlotSelect,
584
+ onQuickReply,
585
+ onMeetingTypeSelect,
586
+ initialScreen,
587
+ initialUser,
588
+ className,
589
+ }: ChatWidgetWindowProps) {
590
+ const [screen, setScreen] = useState<ChatWidgetScreen>(
591
+ () => initialScreen ?? "topics",
592
+ );
593
+
594
+ const [user, setUser] = useState<ChatWidgetUser | null>(
595
+ () => initialUser ?? null,
596
+ );
597
+
598
+ const handleIntakeSubmit = useCallback(
599
+ (data: ChatWidgetUser) => {
600
+ setUser(data);
601
+ setScreen("topics");
602
+ onIntakeSubmit?.(data);
603
+ },
604
+ [onIntakeSubmit],
605
+ );
606
+
607
+ const handleTopicSelect = useCallback(
608
+ (topicId: string) => {
609
+ setScreen(topicId === "book_meeting" && bookingUrl ? "booking" : "chat");
610
+ onTopicSelect?.(topicId);
611
+ },
612
+ [onTopicSelect, bookingUrl],
613
+ );
614
+
615
+ if (!isOpen) return null;
616
+
617
+ const showBackBar = screen === "chat" || screen === "booking";
618
+
619
+ return (
620
+ <div
621
+ className={cn(
622
+ "flex h-[600px] w-[360px] flex-col overflow-hidden border border-border bg-background shadow-xl",
623
+ className,
624
+ )}
625
+ style={
626
+ brandColor
627
+ ? ({ "--primary": brandColor } as React.CSSProperties)
628
+ : undefined
629
+ }
630
+ >
631
+ <ChatWidgetHeader
632
+ brokerName={brokerName}
633
+ gradientFrom={gradientFrom}
634
+ gradientTo={gradientTo}
635
+ onMinimize={onMinimize}
636
+ />
637
+
638
+ <div className="flex-1 overflow-y-auto" tabIndex={0}>
639
+ {screen === "intake" && (
640
+ <ChatWidgetIntakeForm
641
+ brokerName={brokerName}
642
+ onSubmit={handleIntakeSubmit}
643
+ isSubmitting={isSubmittingIntake}
644
+ />
645
+ )}
646
+ {screen === "topics" && (
647
+ <ChatWidgetTopicGrid
648
+ userName={user?.name ?? "there"}
649
+ topics={topics}
650
+ onTopicSelect={handleTopicSelect}
651
+ />
652
+ )}
653
+ {screen === "chat" && (
654
+ <div className="flex flex-col gap-3 p-4">
655
+ {messages.map((msg) => (
656
+ <React.Fragment key={msg.id}>
657
+ {(msg.content || msg.isStreaming) && (
658
+ <ChatWidgetMessage
659
+ role={msg.role}
660
+ content={msg.content}
661
+ timestamp={msg.timestamp}
662
+ isStreaming={msg.isStreaming}
663
+ />
664
+ )}
665
+ {msg.interactiveCard && (
666
+ <ChatWidgetInteractiveCard
667
+ {...msg.interactiveCard}
668
+ onSlotSelect={onSlotSelect}
669
+ onQuickReply={onQuickReply}
670
+ onMeetingTypeSelect={onMeetingTypeSelect}
671
+ />
672
+ )}
673
+ </React.Fragment>
674
+ ))}
675
+ </div>
676
+ )}
677
+ {screen === "booking" && bookingUrl && (
678
+ <div className="flex flex-col items-center gap-5 px-6 py-10 text-center">
679
+ <div className="flex size-14 items-center justify-center bg-primary/10">
680
+ <CalendarDays className="size-7 text-foreground" />
681
+ </div>
682
+ <div className="flex flex-col gap-1">
683
+ <p className="text-body-large font-semibold text-foreground">
684
+ Book an Appointment
685
+ </p>
686
+ <p className="text-body-medium text-muted-foreground">
687
+ Choose a time that works for you with one of our advisors.
688
+ </p>
689
+ </div>
690
+ <Button
691
+ variant="default"
692
+ className="w-full"
693
+ onClick={() => window.open(bookingUrl, "_blank")}
694
+ >
695
+ Open Booking Page
696
+ <ExternalLink className="size-4" />
697
+ </Button>
698
+ </div>
699
+ )}
700
+ </div>
701
+
702
+ {showBackBar && (
703
+ <div className="border-t border-border px-4 py-1.5 text-center">
704
+ <Button variant="ghost" size="sm" onClick={() => setScreen("topics")}>
705
+ <ArrowLeft className="size-3" />
706
+ Change topic
707
+ </Button>
708
+ </div>
709
+ )}
710
+ {screen === "chat" && onInputChange && onSend && (
711
+ <ChatWidgetInputBar
712
+ value={inputValue}
713
+ onChange={onInputChange}
714
+ onSend={onSend}
715
+ disabled={isStreaming}
716
+ />
717
+ )}
718
+ </div>
719
+ );
720
+ }
721
+
722
+ // ---------------------------------------------------------------------------
723
+ // ChatWidget (Root orchestrator)
724
+ // ---------------------------------------------------------------------------
725
+
726
+ export interface ChatWidgetProps {
727
+ brokerName?: string;
728
+ /** Primary color for the FAB launcher button. */
729
+ brandColor?: string;
730
+ gradientFrom?: string;
731
+ gradientTo?: string;
732
+ /** ID used to connect to the correct AI agent. */
733
+ agentId?: string;
734
+ /** External booking URL. When provided, "Book a Meeting" shows a direct booking screen instead of chat. */
735
+ bookingUrl?: string;
736
+ position?: "bottom-right" | "bottom-left";
737
+ /** Opens the widget by default — useful for stories. */
738
+ defaultOpen?: boolean;
739
+ /** Callback when a topic is selected. */
740
+ onTopicSelected?: (topicId: string) => void;
741
+ /** Callback when the user sends a message. */
742
+ onMessageSent?: (message: string) => void;
743
+ }
744
+
745
+ export function ChatWidget({
746
+ brokerName = "Your Broker",
747
+ brandColor,
748
+ gradientFrom,
749
+ gradientTo,
750
+ bookingUrl,
751
+ position = "bottom-right",
752
+ defaultOpen = false,
753
+ onTopicSelected,
754
+ onMessageSent,
755
+ }: ChatWidgetProps) {
756
+ const [isOpen, setIsOpen] = useState(defaultOpen);
757
+ const [messages, setMessages] = useState<ChatWidgetChatMessage[]>([]);
758
+ const [inputValue, setInputValue] = useState("");
759
+
760
+ const handleTopicSelect = useCallback(
761
+ (topicId: string) => {
762
+ if (topicId !== "book_meeting" || !bookingUrl) {
763
+ const topic = DEFAULT_CHAT_WIDGET_TOPICS.find((t) => t.id === topicId);
764
+ if (topic) {
765
+ setMessages([
766
+ {
767
+ id: "welcome",
768
+ role: "bot",
769
+ content: `Great choice! I can help you with ${topic.label.toLowerCase()}. What would you like to know?`,
770
+ },
771
+ ]);
772
+ }
773
+ }
774
+ onTopicSelected?.(topicId);
775
+ },
776
+ [onTopicSelected, bookingUrl],
777
+ );
778
+
779
+ const handleSend = useCallback(
780
+ (value: string) => {
781
+ if (!value.trim()) return;
782
+ setMessages((prev) => [
783
+ ...prev,
784
+ { id: `msg-${Date.now()}`, role: "user", content: value },
785
+ ]);
786
+ setInputValue("");
787
+ onMessageSent?.(value);
788
+ },
789
+ [onMessageSent],
790
+ );
791
+
792
+ return (
793
+ <div
794
+ className={cn(
795
+ "fixed bottom-6 z-50 flex flex-col items-end gap-3",
796
+ position === "bottom-right" ? "right-6" : "left-6",
797
+ )}
798
+ >
799
+ {isOpen && (
800
+ <ChatWidgetWindow
801
+ isOpen={isOpen}
802
+ brokerName={brokerName}
803
+ brandColor={brandColor}
804
+ gradientFrom={gradientFrom}
805
+ gradientTo={gradientTo}
806
+ bookingUrl={bookingUrl}
807
+ onMinimize={() => setIsOpen(false)}
808
+ messages={messages}
809
+ inputValue={inputValue}
810
+ onInputChange={setInputValue}
811
+ onSend={handleSend}
812
+ onTopicSelect={handleTopicSelect}
813
+ />
814
+ )}
815
+ <ChatWidgetLauncher
816
+ isOpen={isOpen}
817
+ onClick={() => setIsOpen((prev) => !prev)}
818
+ brandColor={brandColor}
819
+ />
820
+ </div>
821
+ );
822
+ }