@wealthx/shadcn 1.2.1 → 1.3.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 (247) hide show
  1. package/.turbo/turbo-build.log +203 -150
  2. package/CHANGELOG.md +29 -0
  3. package/dist/{chunk-4Y6R4WEC.mjs → chunk-2A5RRQGG.mjs} +9 -22
  4. package/dist/{chunk-TS2ZX2VS.mjs → chunk-2UM72RJ7.mjs} +11 -15
  5. package/dist/{chunk-A56YQQHG.mjs → chunk-3NCUZIFP.mjs} +2 -2
  6. package/dist/chunk-3OYFOX3X.mjs +79 -0
  7. package/dist/{chunk-RP3SQYA3.mjs → chunk-3TTACBDP.mjs} +9 -4
  8. package/dist/chunk-4GAWMKMI.mjs +710 -0
  9. package/dist/{chunk-SYOD63OZ.mjs → chunk-5FQIKDKP.mjs} +6 -6
  10. package/dist/{chunk-K3JYD4IU.mjs → chunk-5IS7G74I.mjs} +11 -4
  11. package/dist/chunk-6AW4KJHE.mjs +235 -0
  12. package/dist/chunk-6CR5N2JW.mjs +302 -0
  13. package/dist/{chunk-XIRTEFKH.mjs → chunk-6DZEXFNB.mjs} +36 -8
  14. package/dist/chunk-6O6KD7CE.mjs +271 -0
  15. package/dist/chunk-7PV3IWCN.mjs +33 -0
  16. package/dist/{chunk-SPJ5KXW7.mjs → chunk-7S5AESZO.mjs} +5 -5
  17. package/dist/{chunk-RYCLWMZ7.mjs → chunk-ABFDMHOR.mjs} +9 -7
  18. package/dist/{chunk-SWGT756Z.mjs → chunk-AMQZRHEZ.mjs} +10 -4
  19. package/dist/{chunk-WOEHFRGB.mjs → chunk-BDYZCBRT.mjs} +4 -4
  20. package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
  21. package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
  22. package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
  23. package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
  24. package/dist/chunk-DOEO3CDL.mjs +27 -0
  25. package/dist/{chunk-KUDCQ4FI.mjs → chunk-DUJTAXMH.mjs} +9 -4
  26. package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
  27. package/dist/{chunk-PMB3A7V3.mjs → chunk-EI5F6FMT.mjs} +1 -1
  28. package/dist/chunk-EWRB4PAD.mjs +468 -0
  29. package/dist/chunk-FAKPBKLT.mjs +253 -0
  30. package/dist/chunk-FNQXOAYJ.mjs +169 -0
  31. package/dist/{chunk-4CX4SBRO.mjs → chunk-GHC7LLUX.mjs} +14 -5
  32. package/dist/chunk-HBZLGDIN.mjs +507 -0
  33. package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
  34. package/dist/{chunk-PR6V5XKM.mjs → chunk-HVY6KCCF.mjs} +7 -4
  35. package/dist/chunk-I3RZS7V2.mjs +136 -0
  36. package/dist/chunk-IAE3F7DR.mjs +1962 -0
  37. package/dist/{chunk-ZRO5JO3H.mjs → chunk-IHMFS7NZ.mjs} +81 -84
  38. package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
  39. package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
  40. package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
  41. package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
  42. package/dist/{chunk-CSDO6VBW.mjs → chunk-LBMRIB3G.mjs} +10 -10
  43. package/dist/chunk-LV35NGVG.mjs +272 -0
  44. package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
  45. package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
  46. package/dist/chunk-MIZQHHUO.mjs +441 -0
  47. package/dist/chunk-MN5NYQCL.mjs +29 -0
  48. package/dist/chunk-NL3ZO62D.mjs +31 -0
  49. package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
  50. package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
  51. package/dist/chunk-PBL4OQV2.mjs +283 -0
  52. package/dist/{chunk-3WMX6KWS.mjs → chunk-PU4YZQXV.mjs} +11 -12
  53. package/dist/chunk-QMY3AZJH.mjs +80 -0
  54. package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
  55. package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
  56. package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
  57. package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
  58. package/dist/chunk-U4NDAF2P.mjs +207 -0
  59. package/dist/{chunk-DOH3EHX7.mjs → chunk-U5X52X37.mjs} +1 -1
  60. package/dist/chunk-UMTOX62O.mjs +415 -0
  61. package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
  62. package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
  63. package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
  64. package/dist/chunk-ZC45IGZO.mjs +388 -0
  65. package/dist/components/ui/add-column-modal.js +42 -14
  66. package/dist/components/ui/add-column-modal.mjs +4 -4
  67. package/dist/components/ui/add-lead-modal.js +42 -11
  68. package/dist/components/ui/add-lead-modal.mjs +3 -3
  69. package/dist/components/ui/advisor-card.js +497 -0
  70. package/dist/components/ui/advisor-card.mjs +13 -0
  71. package/dist/components/ui/ai-assistant-drawer.js +11 -10
  72. package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
  73. package/dist/components/ui/alert-dialog.js +2 -2
  74. package/dist/components/ui/alert-dialog.mjs +2 -2
  75. package/dist/components/ui/appointment-action-dialogs.js +1160 -0
  76. package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
  77. package/dist/components/ui/appointment-availability-settings.js +1590 -0
  78. package/dist/components/ui/appointment-availability-settings.mjs +23 -0
  79. package/dist/components/ui/appointment-book-dialog.js +1744 -0
  80. package/dist/components/ui/appointment-book-dialog.mjs +27 -0
  81. package/dist/components/ui/appointment-calendar-view.js +833 -0
  82. package/dist/components/ui/appointment-calendar-view.mjs +14 -0
  83. package/dist/components/ui/appointment-detail-sheet.js +1517 -0
  84. package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
  85. package/dist/components/ui/appointment-gmail-connect.js +467 -0
  86. package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
  87. package/dist/components/ui/appointment-mini-card.js +345 -0
  88. package/dist/components/ui/appointment-mini-card.mjs +11 -0
  89. package/dist/components/ui/appointment-time-slot-picker.js +311 -0
  90. package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
  91. package/dist/components/ui/appointment-upcoming-card.js +1268 -0
  92. package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
  93. package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
  94. package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
  95. package/dist/components/ui/backoffice-alerts-chart.js +786 -0
  96. package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
  97. package/dist/components/ui/backoffice-connections-chart.js +817 -0
  98. package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
  99. package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
  100. package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
  101. package/dist/components/ui/badge.js +6 -6
  102. package/dist/components/ui/badge.mjs +1 -1
  103. package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
  104. package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
  105. package/dist/components/ui/button.js +2 -2
  106. package/dist/components/ui/button.mjs +1 -1
  107. package/dist/components/ui/calendar.js +2 -2
  108. package/dist/components/ui/calendar.mjs +2 -2
  109. package/dist/components/ui/card.js +1 -1
  110. package/dist/components/ui/card.mjs +1 -1
  111. package/dist/components/ui/cash-balance-line-chart.js +31 -23
  112. package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
  113. package/dist/components/ui/cashflow-bar-chart.js +12 -5
  114. package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
  115. package/dist/components/ui/chip.js +97 -18
  116. package/dist/components/ui/chip.mjs +3 -2
  117. package/dist/components/ui/color-picker.js +547 -0
  118. package/dist/components/ui/color-picker.mjs +24 -0
  119. package/dist/components/ui/data-table.js +182 -129
  120. package/dist/components/ui/data-table.mjs +3 -2
  121. package/dist/components/ui/date-picker.js +48 -27
  122. package/dist/components/ui/date-picker.mjs +4 -3
  123. package/dist/components/ui/dialog.js +37 -9
  124. package/dist/components/ui/dialog.mjs +2 -2
  125. package/dist/components/ui/expense-bar-chart.js +12 -5
  126. package/dist/components/ui/expense-bar-chart.mjs +5 -4
  127. package/dist/components/ui/field.mjs +2 -2
  128. package/dist/components/ui/financial-cards.js +322 -155
  129. package/dist/components/ui/financial-cards.mjs +5 -3
  130. package/dist/components/ui/financial-drawers.js +2 -2
  131. package/dist/components/ui/financial-drawers.mjs +3 -3
  132. package/dist/components/ui/financial-sections.js +14 -10
  133. package/dist/components/ui/financial-sections.mjs +6 -5
  134. package/dist/components/ui/form-primitives.js +4 -4
  135. package/dist/components/ui/form-primitives.mjs +3 -3
  136. package/dist/components/ui/income-bar-chart.js +12 -5
  137. package/dist/components/ui/income-bar-chart.mjs +5 -4
  138. package/dist/components/ui/input-group.js +2 -2
  139. package/dist/components/ui/input-group.mjs +2 -2
  140. package/dist/components/ui/kanban-column.js +52 -44
  141. package/dist/components/ui/kanban-column.mjs +7 -5
  142. package/dist/components/ui/opportunity-card.js +52 -44
  143. package/dist/components/ui/opportunity-card.mjs +6 -4
  144. package/dist/components/ui/opportunity-edit-modals.js +1371 -1267
  145. package/dist/components/ui/opportunity-edit-modals.mjs +10 -10
  146. package/dist/components/ui/opportunity-summary-tab.js +2748 -2161
  147. package/dist/components/ui/opportunity-summary-tab.mjs +16 -16
  148. package/dist/components/ui/page-header.js +92 -0
  149. package/dist/components/ui/page-header.mjs +8 -0
  150. package/dist/components/ui/page-top-bar.js +88 -0
  151. package/dist/components/ui/page-top-bar.mjs +8 -0
  152. package/dist/components/ui/pagination.js +303 -19
  153. package/dist/components/ui/pagination.mjs +11 -4
  154. package/dist/components/ui/pipeline-board.js +209 -195
  155. package/dist/components/ui/pipeline-board.mjs +10 -8
  156. package/dist/components/ui/pipeline-dialogs.js +118 -69
  157. package/dist/components/ui/pipeline-dialogs.mjs +8 -7
  158. package/dist/components/ui/pipeline-primitives.js +6 -6
  159. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  160. package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
  161. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
  162. package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
  163. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
  164. package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
  165. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
  166. package/dist/components/ui/sidebar-nav.js +679 -0
  167. package/dist/components/ui/sidebar-nav.mjs +14 -0
  168. package/dist/components/ui/stage-timeline.js +6 -6
  169. package/dist/components/ui/stage-timeline.mjs +3 -3
  170. package/dist/components/ui/stepper.js +283 -0
  171. package/dist/components/ui/stepper.mjs +18 -0
  172. package/dist/components/ui/toggle-group.js +4 -4
  173. package/dist/components/ui/toggle-group.mjs +2 -2
  174. package/dist/components/ui/toggle.js +4 -4
  175. package/dist/components/ui/toggle.mjs +1 -1
  176. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
  177. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
  178. package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
  179. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
  180. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
  181. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
  182. package/dist/index.js +12927 -8522
  183. package/dist/index.mjs +288 -190
  184. package/dist/lib/typography.js +10 -10
  185. package/dist/lib/typography.mjs +1 -1
  186. package/dist/styles.css +1 -1
  187. package/package.json +86 -1
  188. package/src/components/index.tsx +146 -0
  189. package/src/components/ui/add-column-modal.tsx +7 -7
  190. package/src/components/ui/add-lead-modal.tsx +6 -3
  191. package/src/components/ui/advisor-card.tsx +227 -0
  192. package/src/components/ui/ai-assistant-drawer.tsx +4 -3
  193. package/src/components/ui/appointment-action-dialogs.tsx +297 -0
  194. package/src/components/ui/appointment-availability-settings.tsx +645 -0
  195. package/src/components/ui/appointment-book-dialog.tsx +618 -0
  196. package/src/components/ui/appointment-calendar-view.tsx +510 -0
  197. package/src/components/ui/appointment-detail-sheet.tsx +415 -0
  198. package/src/components/ui/appointment-gmail-connect.tsx +188 -0
  199. package/src/components/ui/appointment-mini-card.tsx +104 -0
  200. package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
  201. package/src/components/ui/appointment-upcoming-card.tsx +635 -0
  202. package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
  203. package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
  204. package/src/components/ui/backoffice-connections-chart.tsx +339 -0
  205. package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
  206. package/src/components/ui/badge.tsx +12 -6
  207. package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
  208. package/src/components/ui/button.tsx +2 -2
  209. package/src/components/ui/card.tsx +1 -1
  210. package/src/components/ui/cash-balance-line-chart.tsx +4 -23
  211. package/src/components/ui/cashflow-bar-chart.tsx +9 -2
  212. package/src/components/ui/chart-shared.tsx +4 -11
  213. package/src/components/ui/chip.tsx +23 -19
  214. package/src/components/ui/color-picker.tsx +309 -0
  215. package/src/components/ui/data-table.tsx +117 -83
  216. package/src/components/ui/date-picker.tsx +42 -37
  217. package/src/components/ui/dialog.tsx +72 -6
  218. package/src/components/ui/expense-bar-chart.tsx +11 -2
  219. package/src/components/ui/financial-cards.tsx +99 -10
  220. package/src/components/ui/income-bar-chart.tsx +11 -2
  221. package/src/components/ui/opportunity-card.tsx +10 -39
  222. package/src/components/ui/opportunity-edit-modals.tsx +98 -36
  223. package/src/components/ui/opportunity-summary-tab.tsx +548 -232
  224. package/src/components/ui/page-header.tsx +57 -0
  225. package/src/components/ui/page-top-bar.tsx +48 -0
  226. package/src/components/ui/pagination.tsx +171 -22
  227. package/src/components/ui/pipeline-board.tsx +12 -5
  228. package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
  229. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
  230. package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
  231. package/src/components/ui/sidebar-nav.tsx +516 -0
  232. package/src/components/ui/stepper.tsx +347 -0
  233. package/src/components/ui/toggle.tsx +4 -4
  234. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
  235. package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
  236. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
  237. package/src/lib/format-currency.ts +44 -0
  238. package/src/lib/format-date.ts +50 -0
  239. package/src/lib/opportunity-constants.ts +12 -0
  240. package/src/lib/typography.ts +11 -11
  241. package/src/styles/globals.css +36 -34
  242. package/src/styles/styles-css.ts +1 -1
  243. package/tsup.config.ts +17 -0
  244. package/dist/chunk-PG6K5XEC.mjs +0 -475
  245. package/dist/chunk-WA6O6EUR.mjs +0 -1885
  246. package/dist/chunk-WNGWBVLV.mjs +0 -148
  247. package/dist/{chunk-LLVQKSU3.mjs → chunk-GD4BJDJR.mjs} +3 -3
@@ -25,6 +25,7 @@ import {
25
25
  ChartLegendItem,
26
26
  formatAbbrev,
27
27
  } from "./chart-shared";
28
+ import { formatCurrency } from "@/lib/format-currency";
28
29
 
29
30
  ChartJS.register(
30
31
  CategoryScale,
@@ -123,14 +124,6 @@ const DASH_PATTERN: number[] = [6, 4];
123
124
  // Helpers
124
125
  // ---------------------------------------------------------------------------
125
126
 
126
- function formatCurrencyFull(dollars: number): string {
127
- return new Intl.NumberFormat("en-AU", {
128
- style: "currency",
129
- currency: "AUD",
130
- maximumFractionDigits: 0,
131
- }).format(dollars);
132
- }
133
-
134
127
  /** "2025-01" or "2025-01-15" → "Jan '25" for x-axis ticks */
135
128
  function formatDateLabel(iso: string): string {
136
129
  const d = new Date(`${iso.slice(0, 7)}-01T00:00:00`);
@@ -246,7 +239,7 @@ export function BorrowingCapacityLineChart({
246
239
  },
247
240
  label: (ctx) => {
248
241
  const dollars = ctx.parsed.y ?? 0;
249
- return ` ${ctx.dataset.label}: ${formatCurrencyFull(dollars)}`;
242
+ return ` ${ctx.dataset.label}: ${formatCurrency(dollars)}`;
250
243
  },
251
244
  },
252
245
  },
@@ -319,12 +312,12 @@ export function BorrowingCapacityLineChart({
319
312
  >
320
313
  <CardHeader className="px-3 sm:px-6">
321
314
  <div className="flex items-start justify-between gap-4">
322
- <CardTitle className="text-sm sm:text-base uppercase tracking-wider">
315
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
323
316
  {title}
324
317
  </CardTitle>
325
318
  {kpiValue != null && (
326
319
  <span className="shrink-0 text-xl font-bold tabular-nums">
327
- {formatCurrencyFull(kpiValue / 100)}
320
+ {formatCurrency(kpiValue / 100)}
328
321
  </span>
329
322
  )}
330
323
  </div>
@@ -23,13 +23,13 @@ const buttonVariants = cva(
23
23
  destructive:
24
24
  "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
25
25
  outline:
26
- "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
26
+ "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:ring-border/50 dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
27
27
  "outline-primary":
28
28
  "border border-primary text-foreground bg-transparent shadow-xs hover:bg-primary/5 focus-visible:ring-primary/50",
29
29
  "outline-secondary":
30
30
  "border border-brand-secondary text-brand-secondary bg-transparent shadow-xs hover:bg-brand-secondary/10 focus-visible:ring-brand-secondary/30",
31
31
  ghost:
32
- "hover:bg-accent hover:text-accent-foreground hover:shadow-xs dark:hover:bg-accent/50",
32
+ "hover:bg-accent hover:text-accent-foreground hover:shadow-xs focus-visible:ring-border/50 dark:hover:bg-accent/50",
33
33
  link: "text-primary underline-offset-4 hover:underline",
34
34
  },
35
35
  size: {
@@ -23,7 +23,7 @@ function CardHeader({ className, ...props }: CardHeaderProps): ReactElement {
23
23
  return (
24
24
  <div
25
25
  className={cn(
26
- "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
26
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-center gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
27
27
  className,
28
28
  )}
29
29
  data-slot="card-header"
@@ -21,7 +21,9 @@ import {
21
21
  FALLBACK_PRIMARY,
22
22
  FALLBACK_BG,
23
23
  ChartLegendItem,
24
+ formatAbbrev,
24
25
  } from "./chart-shared";
26
+ import { formatCurrency } from "@/lib/format-currency";
25
27
 
26
28
  ChartJS.register(
27
29
  CategoryScale,
@@ -36,27 +38,6 @@ ChartJS.register(
36
38
  // Helpers
37
39
  // ---------------------------------------------------------------------------
38
40
 
39
- /** Abbreviate a dollar value: $1.2K, $4.5M, $1.1B */
40
- function formatAbbrev(value: number): string {
41
- const abs = Math.abs(value);
42
- const sign = value < 0 ? "-" : "";
43
- if (abs >= 1_000_000_000)
44
- return `${sign}$${(abs / 1_000_000_000).toFixed(1)}B`;
45
- if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(1)}M`;
46
- if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(1)}K`;
47
- return `${sign}$${abs.toFixed(0)}`;
48
- }
49
-
50
- /** Format a balance value for prominent display: $12,345.67 */
51
- function formatBalanceFull(value: number): string {
52
- const abs = Math.abs(value);
53
- const sign = value < 0 ? "-" : "";
54
- return `${sign}$${abs.toLocaleString(undefined, {
55
- minimumFractionDigits: 2,
56
- maximumFractionDigits: 2,
57
- })}`;
58
- }
59
-
60
41
  /** Format ISO date string to "MMM dd" label e.g. "Jan 15" */
61
42
  function formatDateLabel(iso: string): string {
62
43
  const d = new Date(iso);
@@ -244,12 +225,12 @@ export function CashBalanceLineChart({
244
225
  >
245
226
  <CardHeader className="px-3 sm:px-6">
246
227
  <div className="flex flex-col gap-0.5">
247
- <CardTitle className="text-sm sm:text-base uppercase tracking-wider">
228
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
248
229
  {title}
249
230
  </CardTitle>
250
231
  {showBalanceValue && latestValue !== null && (
251
232
  <p className="text-2xl font-bold tabular-nums leading-tight">
252
- {formatBalanceFull(latestValue)}
233
+ {formatCurrency(latestValue, { decimals: 2 })}
253
234
  </p>
254
235
  )}
255
236
  </div>
@@ -25,7 +25,14 @@ import {
25
25
  ChartPeriodButton,
26
26
  } from "./chart-shared";
27
27
 
28
- ChartJS.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip, Legend);
28
+ ChartJS.register(
29
+ CategoryScale,
30
+ LinearScale,
31
+ BarController,
32
+ BarElement,
33
+ Tooltip,
34
+ Legend,
35
+ );
29
36
 
30
37
  // ---------------------------------------------------------------------------
31
38
  // Types
@@ -313,7 +320,7 @@ export function CashflowBarChart({
313
320
  style={{ maxWidth: width, fontFamily }}
314
321
  >
315
322
  <CardHeader className="px-3 sm:px-6">
316
- <CardTitle className="text-sm sm:text-base uppercase tracking-wider">
323
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
317
324
  {title}
318
325
  </CardTitle>
319
326
  {showPeriodSelector && (
@@ -90,18 +90,11 @@ export const SEVERITY_COLORS = {
90
90
  } as const;
91
91
 
92
92
  /**
93
- * Format a dollar amount as an abbreviated string for chart axis ticks.
94
- * e.g. 1_200_000 "$1.2M", 580_000 "$580K", -250_000 "-$250K"
93
+ * Re-export abbreviated currency formatter from shared lib.
94
+ * Kept as `formatAbbrev` for backward compatibility with existing chart consumers.
95
95
  */
96
- export function formatAbbrev(value: number): string {
97
- const abs = Math.abs(value);
98
- const sign = value < 0 ? "-" : "";
99
- if (abs >= 1_000_000_000)
100
- return `${sign}$${(abs / 1_000_000_000).toFixed(1)}B`;
101
- if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(1)}M`;
102
- if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(0)}K`;
103
- return `${sign}$${abs.toFixed(0)}`;
104
- }
96
+ import { formatCurrencyAbbrev as formatAbbrev } from "@/lib/format-currency";
97
+ export { formatAbbrev };
105
98
 
106
99
  // ---------------------------------------------------------------------------
107
100
  // Tooltip date format
@@ -7,19 +7,19 @@
7
7
  // <Chip>Label</Chip>
8
8
  // <Chip onRemove={() => handleRemove(id)}>Label</Chip>
9
9
  // <Chip variant="outline" disabled onRemove={...}>Label</Chip>
10
- import { type ReactElement } from "react"
11
- import * as React from "react"
12
- import { X } from "lucide-react"
13
- import type { VariantProps } from "class-variance-authority"
14
- import { cn } from "@/lib/utils"
15
- import { badgeVariants } from "@/components/ui/badge"
10
+ import { type ReactElement } from "react";
11
+ import * as React from "react";
12
+ import { X } from "lucide-react";
13
+ import { Button } from "./button";
14
+ import type { VariantProps } from "class-variance-authority";
15
+ import { cn } from "@/lib/utils";
16
+ import { badgeVariants } from "@/components/ui/badge";
16
17
 
17
18
  export interface ChipProps
18
- extends React.ComponentProps<"span">,
19
- VariantProps<typeof badgeVariants> {
19
+ extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {
20
20
  /** When provided, renders a dismiss (×) button inside the chip. */
21
- onRemove?: () => void
22
- disabled?: boolean
21
+ onRemove?: () => void;
22
+ disabled?: boolean;
23
23
  }
24
24
 
25
25
  function Chip({
@@ -38,28 +38,32 @@ function Chip({
38
38
  // extra right padding only when dismiss button is present
39
39
  onRemove && "pr-1",
40
40
  disabled && "pointer-events-none opacity-50",
41
- className
41
+ className,
42
42
  )}
43
43
  data-slot="chip"
44
44
  data-variant={variant}
45
45
  {...props}
46
46
  >
47
47
  {children}
48
- {onRemove ? <button
48
+ {onRemove ? (
49
+ <Button
50
+ type="button"
51
+ variant="ghost"
52
+ size="icon"
49
53
  aria-label="Remove"
50
- className="ml-0.5 inline-flex shrink-0 items-center justify-center rounded-full p-0.5 opacity-60 hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none"
54
+ className="ml-0.5 size-4 shrink-0 rounded-full p-0.5 opacity-60 hover:opacity-100 disabled:pointer-events-none"
51
55
  data-slot="chip-remove"
52
56
  disabled={disabled}
53
57
  onClick={(e) => {
54
- e.stopPropagation()
55
- onRemove()
58
+ e.stopPropagation();
59
+ onRemove();
56
60
  }}
57
- type="button"
58
61
  >
59
62
  <X className="size-3" />
60
- </button> : null}
63
+ </Button>
64
+ ) : null}
61
65
  </span>
62
- )
66
+ );
63
67
  }
64
68
 
65
- export { Chip }
69
+ export { Chip };
@@ -0,0 +1,309 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import {
4
+ Popover,
5
+ PopoverContent,
6
+ PopoverTrigger,
7
+ } from "@/components/ui/popover";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Button } from "@/components/ui/button";
10
+
11
+ /**
12
+ * ColorPicker — WealthX Design System
13
+ *
14
+ * Popover-based color picker with preset swatches and a hex input field.
15
+ * Designed for tenant white-label brand color configuration.
16
+ *
17
+ * Figma: https://www.figma.com/design/9V9F0NGVsif8LGmEhVjOcT/Design-System---shadcn
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Preset palettes
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export const COLOR_PICKER_PRESETS = [
25
+ // Blues
26
+ "#1E40AF",
27
+ "#2563EB",
28
+ "#3B82F6",
29
+ "#60A5FA",
30
+ // Purples
31
+ "#7C3AED",
32
+ "#8B5CF6",
33
+ "#A78BFA",
34
+ "#C4B5FD",
35
+ // Greens
36
+ "#15803D",
37
+ "#16A34A",
38
+ "#22C55E",
39
+ "#4ADE80",
40
+ // Reds
41
+ "#B91C1C",
42
+ "#DC2626",
43
+ "#EF4444",
44
+ "#F87171",
45
+ // Oranges
46
+ "#C2410C",
47
+ "#EA580C",
48
+ "#F97316",
49
+ "#FB923C",
50
+ // Teals
51
+ "#0F766E",
52
+ "#0D9488",
53
+ "#14B8A6",
54
+ "#2DD4BF",
55
+ // Neutrals
56
+ "#111827",
57
+ "#374151",
58
+ "#6B7280",
59
+ "#D1D5DB",
60
+ ];
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Utility
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /** Returns true if the string is a valid 6-digit or 3-digit hex color. */
67
+ function isValidHex(value: string): boolean {
68
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value);
69
+ }
70
+
71
+ /** Ensures value always has a leading `#`. */
72
+ function normalizeHex(value: string): string {
73
+ const stripped = value.replace(/^#/, "");
74
+ return `#${stripped}`;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // ColorSwatch
79
+ // ---------------------------------------------------------------------------
80
+
81
+ interface ColorSwatchProps {
82
+ color: string;
83
+ selected?: boolean;
84
+ size?: "sm" | "md";
85
+ onClick?: (color: string) => void;
86
+ className?: string;
87
+ }
88
+
89
+ function ColorSwatch({
90
+ color,
91
+ selected,
92
+ size = "md",
93
+ onClick,
94
+ className,
95
+ }: ColorSwatchProps) {
96
+ return (
97
+ <Button
98
+ type="button"
99
+ variant="ghost"
100
+ title={color}
101
+ aria-label={`Select color ${color}`}
102
+ aria-pressed={selected}
103
+ onClick={() => onClick?.(color)}
104
+ className={cn(
105
+ "relative shrink-0 p-0 transition-all outline-none shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
106
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
107
+ size === "md" ? "size-7" : "size-5",
108
+ selected &&
109
+ "ring-2 ring-foreground ring-offset-1 ring-offset-background",
110
+ className,
111
+ )}
112
+ style={{ backgroundColor: color }}
113
+ />
114
+ );
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // ColorPickerContent (inner panel shown in popover)
119
+ // ---------------------------------------------------------------------------
120
+
121
+ interface ColorPickerContentProps {
122
+ value: string;
123
+ onChange: (color: string) => void;
124
+ presets?: string[];
125
+ }
126
+
127
+ function ColorPickerContent({
128
+ value,
129
+ onChange,
130
+ presets = COLOR_PICKER_PRESETS,
131
+ }: ColorPickerContentProps) {
132
+ const [hexInput, setHexInput] = React.useState(value);
133
+
134
+ React.useEffect(() => {
135
+ setHexInput(value);
136
+ }, [value]);
137
+
138
+ function handleHexInputChange(e: React.ChangeEvent<HTMLInputElement>) {
139
+ const raw = e.target.value;
140
+ setHexInput(raw);
141
+ const normalized = normalizeHex(raw);
142
+ if (isValidHex(normalized)) {
143
+ onChange(normalized);
144
+ }
145
+ }
146
+
147
+ function handleHexBlur() {
148
+ const normalized = normalizeHex(hexInput);
149
+ if (isValidHex(normalized)) {
150
+ setHexInput(normalized);
151
+ if (normalized !== value) onChange(normalized);
152
+ } else {
153
+ setHexInput(value);
154
+ }
155
+ }
156
+
157
+ const normalizedInput = normalizeHex(hexInput);
158
+ const isValid = isValidHex(normalizedInput);
159
+
160
+ return (
161
+ <div data-slot="color-picker-content" className="flex flex-col gap-4">
162
+ <div>
163
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
164
+ Presets
165
+ </p>
166
+ <div className="grid grid-cols-7 gap-1.5">
167
+ {presets.map((color) => (
168
+ <ColorSwatch
169
+ key={color}
170
+ color={color}
171
+ selected={value.toLowerCase() === color.toLowerCase()}
172
+ onClick={onChange}
173
+ />
174
+ ))}
175
+ </div>
176
+ </div>
177
+
178
+ <div>
179
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
180
+ Custom hex
181
+ </p>
182
+ <div className="flex items-center gap-2">
183
+ <label
184
+ className="relative size-9 shrink-0 cursor-pointer border border-border"
185
+ title="Open color picker"
186
+ style={{
187
+ backgroundColor: isValid ? normalizedInput : value,
188
+ }}
189
+ >
190
+ <input
191
+ type="color"
192
+ aria-label="Native color picker"
193
+ value={isValid ? normalizedInput : value}
194
+ onChange={(e) => {
195
+ setHexInput(e.target.value);
196
+ onChange(e.target.value);
197
+ }}
198
+ className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
199
+ />
200
+ </label>
201
+ <Input
202
+ aria-label="Hex color value"
203
+ placeholder="#000000"
204
+ value={hexInput}
205
+ onChange={handleHexInputChange}
206
+ onBlur={handleHexBlur}
207
+ maxLength={7}
208
+ className="h-9 flex-1 font-mono text-sm uppercase"
209
+ />
210
+ </div>
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // ColorPicker (popover trigger + content)
218
+ // ---------------------------------------------------------------------------
219
+
220
+ interface ColorPickerProps {
221
+ /** The current color value (hex string, e.g. `"#3B82F6"`). */
222
+ value?: string;
223
+ /** Default value when uncontrolled. */
224
+ defaultValue?: string;
225
+ /** Called whenever the selected color changes. */
226
+ onChange?: (color: string) => void;
227
+ /** Preset colors shown in the swatch grid. Defaults to `COLOR_PICKER_PRESETS`. */
228
+ presets?: string[];
229
+ /** Disable the picker. */
230
+ disabled?: boolean;
231
+ /** Optional label shown above the trigger. */
232
+ label?: string;
233
+ className?: string;
234
+ }
235
+
236
+ function ColorPicker({
237
+ value: controlledValue,
238
+ defaultValue = "#3B82F6",
239
+ onChange,
240
+ presets,
241
+ disabled,
242
+ label,
243
+ className,
244
+ }: ColorPickerProps) {
245
+ const isControlled = controlledValue !== undefined;
246
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
247
+ const color = isControlled ? controlledValue : internalValue;
248
+ const [open, setOpen] = React.useState(false);
249
+
250
+ React.useEffect(() => {
251
+ if (disabled) setOpen(false);
252
+ }, [disabled]);
253
+
254
+ function handleChange(newColor: string) {
255
+ if (!isControlled) setInternalValue(newColor);
256
+ onChange?.(newColor);
257
+ }
258
+
259
+ return (
260
+ <div
261
+ data-slot="color-picker"
262
+ className={cn("inline-flex flex-col gap-1.5 font-sans", className)}
263
+ >
264
+ {label && (
265
+ <span className="text-sm font-medium text-foreground">{label}</span>
266
+ )}
267
+ <Popover open={open} onOpenChange={setOpen}>
268
+ <PopoverTrigger
269
+ disabled={disabled}
270
+ className={cn(
271
+ "flex h-9 min-w-[140px] cursor-pointer items-center gap-2.5 border border-input bg-background px-3 text-sm outline-none transition-colors",
272
+ "hover:border-ring",
273
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
274
+ "disabled:cursor-not-allowed disabled:opacity-50",
275
+ )}
276
+ >
277
+ <span
278
+ aria-hidden
279
+ className="size-5 shrink-0 border border-border/50"
280
+ style={{ backgroundColor: color }}
281
+ />
282
+ <span className="font-mono text-xs uppercase tracking-wide">
283
+ {color}
284
+ </span>
285
+ </PopoverTrigger>
286
+ <PopoverContent
287
+ className="w-[220px] p-4"
288
+ align="start"
289
+ data-shadcn-scope
290
+ >
291
+ <ColorPickerContent
292
+ value={color}
293
+ onChange={handleChange}
294
+ presets={presets}
295
+ />
296
+ </PopoverContent>
297
+ </Popover>
298
+ </div>
299
+ );
300
+ }
301
+
302
+ export {
303
+ ColorPicker,
304
+ ColorPickerContent,
305
+ ColorSwatch,
306
+ isValidHex,
307
+ normalizeHex,
308
+ };
309
+ export type { ColorPickerProps, ColorPickerContentProps, ColorSwatchProps };