@wealthx/shadcn 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.turbo/turbo-build.log +193 -149
  2. package/CHANGELOG.md +28 -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-VGSESELX.mjs → chunk-5FQIKDKP.mjs} +5 -5
  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-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
  20. package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
  21. package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
  22. package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
  23. package/dist/chunk-DOEO3CDL.mjs +27 -0
  24. package/dist/{chunk-5MEWU56Z.mjs → chunk-DUJTAXMH.mjs} +11 -6
  25. package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
  26. package/dist/chunk-EWRB4PAD.mjs +468 -0
  27. package/dist/{chunk-ZSHYDDRB.mjs → chunk-FAKPBKLT.mjs} +6 -2
  28. package/dist/{chunk-A6AAWBPF.mjs → chunk-GHC7LLUX.mjs} +13 -4
  29. package/dist/chunk-HBZLGDIN.mjs +507 -0
  30. package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
  31. package/dist/{chunk-CGH4DRNG.mjs → chunk-HVY6KCCF.mjs} +10 -7
  32. package/dist/chunk-I3RZS7V2.mjs +136 -0
  33. package/dist/chunk-IAE3F7DR.mjs +1962 -0
  34. package/dist/{chunk-UT4KJR7V.mjs → chunk-IHMFS7NZ.mjs} +35 -74
  35. package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
  36. package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
  37. package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
  38. package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
  39. package/dist/chunk-LV35NGVG.mjs +272 -0
  40. package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
  41. package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
  42. package/dist/chunk-MIZQHHUO.mjs +441 -0
  43. package/dist/chunk-MLNEWRWV.mjs +449 -0
  44. package/dist/chunk-MN5NYQCL.mjs +29 -0
  45. package/dist/chunk-NL3ZO62D.mjs +31 -0
  46. package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
  47. package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
  48. package/dist/chunk-PBL4OQV2.mjs +283 -0
  49. package/dist/{chunk-Y4QFWRNR.mjs → chunk-PU4YZQXV.mjs} +17 -18
  50. package/dist/chunk-Q2BGOAMG.mjs +202 -0
  51. package/dist/chunk-QMY3AZJH.mjs +80 -0
  52. package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
  53. package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
  54. package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
  55. package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
  56. package/dist/chunk-UMTOX62O.mjs +415 -0
  57. package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
  58. package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
  59. package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
  60. package/dist/components/ui/add-column-modal.js +42 -14
  61. package/dist/components/ui/add-column-modal.mjs +5 -5
  62. package/dist/components/ui/add-lead-modal.js +42 -11
  63. package/dist/components/ui/add-lead-modal.mjs +3 -3
  64. package/dist/components/ui/advisor-card.js +530 -0
  65. package/dist/components/ui/advisor-card.mjs +15 -0
  66. package/dist/components/ui/ai-assistant-drawer.js +11 -10
  67. package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
  68. package/dist/components/ui/alert-dialog.js +2 -2
  69. package/dist/components/ui/alert-dialog.mjs +2 -2
  70. package/dist/components/ui/appointment-action-dialogs.js +1160 -0
  71. package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
  72. package/dist/components/ui/appointment-availability-settings.js +1590 -0
  73. package/dist/components/ui/appointment-availability-settings.mjs +23 -0
  74. package/dist/components/ui/appointment-book-dialog.js +1744 -0
  75. package/dist/components/ui/appointment-book-dialog.mjs +27 -0
  76. package/dist/components/ui/appointment-calendar-view.js +833 -0
  77. package/dist/components/ui/appointment-calendar-view.mjs +14 -0
  78. package/dist/components/ui/appointment-detail-sheet.js +1517 -0
  79. package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
  80. package/dist/components/ui/appointment-gmail-connect.js +467 -0
  81. package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
  82. package/dist/components/ui/appointment-mini-card.js +345 -0
  83. package/dist/components/ui/appointment-mini-card.mjs +11 -0
  84. package/dist/components/ui/appointment-time-slot-picker.js +311 -0
  85. package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
  86. package/dist/components/ui/appointment-upcoming-card.js +1268 -0
  87. package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
  88. package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
  89. package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
  90. package/dist/components/ui/backoffice-alerts-chart.js +786 -0
  91. package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
  92. package/dist/components/ui/backoffice-connections-chart.js +817 -0
  93. package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
  95. package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
  96. package/dist/components/ui/badge.js +6 -6
  97. package/dist/components/ui/badge.mjs +1 -1
  98. package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
  99. package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
  100. package/dist/components/ui/button.js +2 -2
  101. package/dist/components/ui/button.mjs +1 -1
  102. package/dist/components/ui/calendar.js +2 -2
  103. package/dist/components/ui/calendar.mjs +2 -2
  104. package/dist/components/ui/card.js +1 -1
  105. package/dist/components/ui/card.mjs +1 -1
  106. package/dist/components/ui/cash-balance-line-chart.js +31 -23
  107. package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
  108. package/dist/components/ui/cashflow-bar-chart.js +12 -5
  109. package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
  110. package/dist/components/ui/chip.js +97 -18
  111. package/dist/components/ui/chip.mjs +3 -2
  112. package/dist/components/ui/color-picker.js +158 -28
  113. package/dist/components/ui/color-picker.mjs +3 -1
  114. package/dist/components/ui/data-table.js +140 -119
  115. package/dist/components/ui/data-table.mjs +3 -2
  116. package/dist/components/ui/date-picker.js +48 -27
  117. package/dist/components/ui/date-picker.mjs +4 -3
  118. package/dist/components/ui/dialog.js +37 -9
  119. package/dist/components/ui/dialog.mjs +2 -2
  120. package/dist/components/ui/expense-bar-chart.js +12 -5
  121. package/dist/components/ui/expense-bar-chart.mjs +5 -4
  122. package/dist/components/ui/field.mjs +2 -2
  123. package/dist/components/ui/financial-cards.js +322 -155
  124. package/dist/components/ui/financial-cards.mjs +5 -3
  125. package/dist/components/ui/financial-drawers.js +2 -2
  126. package/dist/components/ui/financial-drawers.mjs +3 -3
  127. package/dist/components/ui/financial-sections.js +14 -10
  128. package/dist/components/ui/financial-sections.mjs +6 -5
  129. package/dist/components/ui/income-bar-chart.js +12 -5
  130. package/dist/components/ui/income-bar-chart.mjs +5 -4
  131. package/dist/components/ui/input-group.js +2 -2
  132. package/dist/components/ui/input-group.mjs +2 -2
  133. package/dist/components/ui/kanban-column.js +52 -44
  134. package/dist/components/ui/kanban-column.mjs +7 -5
  135. package/dist/components/ui/opportunity-card.js +52 -44
  136. package/dist/components/ui/opportunity-card.mjs +6 -4
  137. package/dist/components/ui/opportunity-edit-modals.js +1367 -1263
  138. package/dist/components/ui/opportunity-edit-modals.mjs +8 -8
  139. package/dist/components/ui/opportunity-summary-tab.js +2744 -2157
  140. package/dist/components/ui/opportunity-summary-tab.mjs +14 -14
  141. package/dist/components/ui/page-header.js +92 -0
  142. package/dist/components/ui/page-header.mjs +8 -0
  143. package/dist/components/ui/page-top-bar.js +88 -0
  144. package/dist/components/ui/page-top-bar.mjs +8 -0
  145. package/dist/components/ui/pagination.js +303 -19
  146. package/dist/components/ui/pagination.mjs +11 -4
  147. package/dist/components/ui/pipeline-board.js +205 -191
  148. package/dist/components/ui/pipeline-board.mjs +9 -7
  149. package/dist/components/ui/pipeline-dialogs.js +114 -65
  150. package/dist/components/ui/pipeline-dialogs.mjs +7 -6
  151. package/dist/components/ui/pipeline-primitives.js +6 -6
  152. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  153. package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
  154. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
  155. package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
  156. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
  157. package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
  158. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
  159. package/dist/components/ui/sidebar-nav.js +426 -191
  160. package/dist/components/ui/sidebar-nav.mjs +5 -1
  161. package/dist/components/ui/stage-timeline.js +6 -6
  162. package/dist/components/ui/stage-timeline.mjs +3 -3
  163. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
  164. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
  165. package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
  166. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
  167. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
  168. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
  169. package/dist/index.js +12258 -8611
  170. package/dist/index.mjs +258 -190
  171. package/dist/styles.css +1 -1
  172. package/package.json +71 -1
  173. package/src/components/index.tsx +115 -9
  174. package/src/components/ui/add-column-modal.tsx +7 -7
  175. package/src/components/ui/add-lead-modal.tsx +6 -3
  176. package/src/components/ui/advisor-card.tsx +284 -0
  177. package/src/components/ui/ai-assistant-drawer.tsx +4 -3
  178. package/src/components/ui/appointment-action-dialogs.tsx +297 -0
  179. package/src/components/ui/appointment-availability-settings.tsx +645 -0
  180. package/src/components/ui/appointment-book-dialog.tsx +618 -0
  181. package/src/components/ui/appointment-calendar-view.tsx +510 -0
  182. package/src/components/ui/appointment-detail-sheet.tsx +415 -0
  183. package/src/components/ui/appointment-gmail-connect.tsx +188 -0
  184. package/src/components/ui/appointment-mini-card.tsx +104 -0
  185. package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
  186. package/src/components/ui/appointment-upcoming-card.tsx +635 -0
  187. package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
  188. package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
  189. package/src/components/ui/backoffice-connections-chart.tsx +339 -0
  190. package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
  191. package/src/components/ui/badge.tsx +12 -6
  192. package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
  193. package/src/components/ui/button.tsx +2 -2
  194. package/src/components/ui/card.tsx +1 -1
  195. package/src/components/ui/cash-balance-line-chart.tsx +4 -23
  196. package/src/components/ui/cashflow-bar-chart.tsx +9 -2
  197. package/src/components/ui/chart-shared.tsx +4 -11
  198. package/src/components/ui/chip.tsx +23 -19
  199. package/src/components/ui/color-picker.tsx +4 -2
  200. package/src/components/ui/data-table.tsx +28 -74
  201. package/src/components/ui/date-picker.tsx +42 -37
  202. package/src/components/ui/dialog.tsx +72 -6
  203. package/src/components/ui/expense-bar-chart.tsx +11 -2
  204. package/src/components/ui/financial-cards.tsx +99 -10
  205. package/src/components/ui/income-bar-chart.tsx +11 -2
  206. package/src/components/ui/opportunity-card.tsx +10 -39
  207. package/src/components/ui/opportunity-edit-modals.tsx +98 -36
  208. package/src/components/ui/opportunity-summary-tab.tsx +548 -232
  209. package/src/components/ui/page-header.tsx +57 -0
  210. package/src/components/ui/page-top-bar.tsx +48 -0
  211. package/src/components/ui/pagination.tsx +171 -22
  212. package/src/components/ui/pipeline-board.tsx +12 -5
  213. package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
  214. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
  215. package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
  216. package/src/components/ui/sidebar-nav.tsx +213 -157
  217. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
  218. package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
  219. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
  220. package/src/lib/format-currency.ts +44 -0
  221. package/src/lib/format-date.ts +50 -0
  222. package/src/lib/opportunity-constants.ts +12 -0
  223. package/src/styles/globals.css +17 -15
  224. package/src/styles/styles-css.ts +1 -1
  225. package/tsup.config.ts +14 -0
  226. package/dist/chunk-S4QRUQNW.mjs +0 -475
  227. package/dist/chunk-URGMJAE3.mjs +0 -1885
  228. package/dist/chunk-WNGWBVLV.mjs +0 -148
  229. package/dist/chunk-ZRSDX6OW.mjs +0 -385
  230. package/dist/{chunk-LLVQKSU3.mjs → chunk-GD4BJDJR.mjs} +3 -3
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * - All icons must be Lucide icons (LucideIcon type).
11
11
  * - Supports collapsible sub-items (accordion).
12
- * - Collapsed state: icon-only, metrics hidden.
12
+ * - Collapsed state: icon-only, metrics animated out.
13
13
  * - metricsGroups: optional financial summary rows (Frontend sidebar only).
14
14
  * - No internal navigation — consumers wire onNavigate / onLogout.
15
15
  */
@@ -23,7 +23,11 @@ import {
23
23
  PanelLeftOpen,
24
24
  } from "lucide-react";
25
25
  import type { LucideIcon } from "lucide-react";
26
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
26
27
  import { cn } from "@/lib/utils";
28
+ import { formatCurrency } from "@/lib/format-currency";
29
+ import { Accordion, AccordionContent, AccordionItem } from "./accordion";
30
+ import { Button } from "./button";
27
31
  import {
28
32
  Tooltip,
29
33
  TooltipContent,
@@ -106,20 +110,6 @@ function getInitials(name: string): string {
106
110
  .slice(0, 2);
107
111
  }
108
112
 
109
- function formatCurrency(value: number, isNetItem = false): string {
110
- const abs = Math.abs(value);
111
- const formatted = new Intl.NumberFormat("en-AU", {
112
- style: "currency",
113
- currency: "AUD",
114
- minimumFractionDigits: 0,
115
- maximumFractionDigits: 0,
116
- }).format(abs);
117
- if (!isNetItem) return formatted;
118
- if (value > 0) return `+${formatted}`;
119
- if (value < 0) return `-${formatted}`;
120
- return formatted;
121
- }
122
-
123
113
  function navIconCn(isActive: boolean): string {
124
114
  return cn(
125
115
  "shrink-0 transition-colors",
@@ -186,7 +176,7 @@ function MetricsGroup({ group }: MetricsGroupProps) {
186
176
  item.isNetItem && item.value < 0 && "text-destructive",
187
177
  )}
188
178
  >
189
- {formatCurrency(item.value, item.isNetItem)}
179
+ {formatCurrency(item.value, { showSign: item.isNetItem })}
190
180
  </span>
191
181
  </div>
192
182
  ))}
@@ -211,16 +201,17 @@ function SidebarNavItemView({
211
201
 
212
202
  return (
213
203
  <NavTooltip label={item.title} collapsed={collapsed}>
214
- <button
204
+ <Button
215
205
  type="button"
206
+ variant="ghost"
216
207
  onClick={() => onNavigate?.(item.href)}
217
208
  className={cn(
218
- "group flex w-full items-center gap-3 py-2.5 text-base font-medium transition-colors",
209
+ "group h-auto w-full items-center gap-3 py-2.5 text-base font-medium transition-colors",
219
210
  "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground",
220
211
  collapsed
221
212
  ? "justify-center px-2"
222
213
  : cn(
223
- "px-3 border-l-4",
214
+ "justify-start px-3 border-l-4",
224
215
  item.isActive
225
216
  ? "bg-white/15 text-brand-secondary-foreground border-primary"
226
217
  : "border-transparent",
@@ -229,11 +220,11 @@ function SidebarNavItemView({
229
220
  >
230
221
  <Icon
231
222
  className={navIconCn(item.isActive ?? false)}
232
- size={18}
223
+ size={24}
233
224
  strokeWidth={1.75}
234
225
  />
235
226
  {!collapsed && <span className="truncate">{item.title}</span>}
236
- </button>
227
+ </Button>
237
228
  </NavTooltip>
238
229
  );
239
230
  }
@@ -262,83 +253,92 @@ function CollapsibleNavItem({
262
253
  if (collapsed) {
263
254
  return (
264
255
  <NavTooltip label={item.title} collapsed={collapsed}>
265
- <button
256
+ <Button
266
257
  type="button"
258
+ variant="ghost"
267
259
  onClick={() => onNavigate?.(item.href)}
268
260
  className={cn(
269
- "group flex w-full items-center justify-center px-2 py-2.5 transition-colors",
261
+ "group h-auto w-full justify-center px-2 py-2.5 transition-colors",
270
262
  "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground",
271
263
  hasActiveChild && "bg-white/15 text-brand-secondary-foreground",
272
264
  )}
273
265
  >
274
266
  <Icon
275
267
  className={navIconCn(hasActiveChild)}
276
- size={18}
268
+ size={24}
277
269
  strokeWidth={1.75}
278
270
  />
279
- </button>
271
+ </Button>
280
272
  </NavTooltip>
281
273
  );
282
274
  }
283
275
 
284
276
  return (
285
- <div>
286
- <button
287
- type="button"
288
- onClick={() => setOpen((prev) => !prev)}
289
- className={cn(
290
- "group flex w-full items-center gap-3 px-3 py-2.5 text-base font-medium transition-colors",
291
- "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground",
292
- "border-l-4 border-transparent",
293
- hasActiveChild &&
294
- "bg-white/15 text-brand-secondary-foreground border-primary",
295
- )}
296
- >
297
- <Icon
298
- className={navIconCn(hasActiveChild)}
299
- size={18}
300
- strokeWidth={1.75}
301
- />
302
- <span className="flex-1 truncate text-left">{item.title}</span>
303
- <ChevronDown
304
- className={cn(
305
- "ml-auto shrink-0 text-brand-secondary-foreground/40 transition-transform duration-200",
306
- open && "rotate-180",
307
- )}
308
- size={14}
309
- strokeWidth={2}
310
- />
311
- </button>
312
-
313
- {open && item.subItems && (
314
- <div className="ml-9 border-l border-white/15 pl-3">
315
- {item.subItems.map((sub) => (
316
- <button
317
- key={sub.href}
318
- type="button"
319
- onClick={() => onNavigate?.(sub.href)}
277
+ <Accordion
278
+ value={open ? [item.href] : []}
279
+ onValueChange={(values) => setOpen(values.length > 0)}
280
+ >
281
+ <AccordionItem className="border-none" value={item.href}>
282
+ <AccordionPrimitive.Header className="flex">
283
+ <AccordionPrimitive.Trigger
284
+ className={cn(
285
+ "group flex h-auto w-full items-center justify-start gap-3 px-3 py-2.5 text-base font-medium transition-colors",
286
+ "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground",
287
+ "border-l-4 border-transparent",
288
+ hasActiveChild &&
289
+ "bg-white/15 text-brand-secondary-foreground border-primary",
290
+ )}
291
+ >
292
+ <Icon
293
+ className={navIconCn(hasActiveChild)}
294
+ size={24}
295
+ strokeWidth={1.75}
296
+ />
297
+ <span className="flex-1 truncate text-left">{item.title}</span>
298
+ <ChevronDown
320
299
  className={cn(
321
- "flex w-full items-center gap-2 py-1.5 pl-1 text-sm transition-colors",
322
- "text-brand-secondary-foreground/50 hover:text-brand-secondary-foreground",
323
- sub.isActive && "text-primary font-medium",
300
+ "ml-auto shrink-0 text-brand-secondary-foreground/40 transition-transform duration-200",
301
+ "group-data-[panel-open]:rotate-180",
324
302
  )}
325
- >
326
- <ChevronRight
327
- size={11}
328
- strokeWidth={2}
329
- className={cn(
330
- "shrink-0",
331
- sub.isActive
332
- ? "text-primary"
333
- : "text-brand-secondary-foreground/30",
334
- )}
335
- />
336
- <span className="truncate">{sub.title}</span>
337
- </button>
338
- ))}
339
- </div>
340
- )}
341
- </div>
303
+ size={14}
304
+ strokeWidth={2}
305
+ />
306
+ </AccordionPrimitive.Trigger>
307
+ </AccordionPrimitive.Header>
308
+
309
+ {item.subItems && (
310
+ <AccordionContent className="p-0 text-inherit">
311
+ <div className="ml-9 border-l border-white/15 pl-3">
312
+ {item.subItems.map((sub) => (
313
+ <Button
314
+ key={sub.href}
315
+ type="button"
316
+ variant="ghost"
317
+ onClick={() => onNavigate?.(sub.href)}
318
+ className={cn(
319
+ "h-auto w-full justify-start gap-2 py-1.5 pl-1 text-sm transition-colors",
320
+ "text-brand-secondary-foreground/50 hover:text-brand-secondary-foreground",
321
+ sub.isActive && "text-primary font-medium",
322
+ )}
323
+ >
324
+ <ChevronRight
325
+ size={11}
326
+ strokeWidth={2}
327
+ className={cn(
328
+ "shrink-0",
329
+ sub.isActive
330
+ ? "text-primary"
331
+ : "text-brand-secondary-foreground/30",
332
+ )}
333
+ />
334
+ <span className="truncate">{sub.title}</span>
335
+ </Button>
336
+ ))}
337
+ </div>
338
+ </AccordionContent>
339
+ )}
340
+ </AccordionItem>
341
+ </Accordion>
342
342
  );
343
343
  }
344
344
 
@@ -357,6 +357,27 @@ export function SidebarNav({
357
357
  className,
358
358
  }: SidebarNavProps) {
359
359
  const [userMenuOpen, setUserMenuOpen] = React.useState(false);
360
+ const navScrollRef = React.useRef<HTMLDivElement>(null);
361
+ const expandedScrollRef = React.useRef(0);
362
+
363
+ React.useEffect(() => {
364
+ if (collapsed) setUserMenuOpen(false);
365
+ }, [collapsed]);
366
+
367
+ // Preserve nav items scroll position across collapse/expand transitions.
368
+ // Cleanup saves scrollTop before the DOM changes; setup restores it when expanding.
369
+ React.useLayoutEffect(() => {
370
+ const nav = navScrollRef.current;
371
+ if (!nav) return;
372
+ if (!collapsed) {
373
+ nav.scrollTop = expandedScrollRef.current;
374
+ }
375
+ return () => {
376
+ if (!collapsed && nav) {
377
+ expandedScrollRef.current = nav.scrollTop;
378
+ }
379
+ };
380
+ }, [collapsed]);
360
381
 
361
382
  return (
362
383
  <TooltipProvider>
@@ -364,100 +385,134 @@ export function SidebarNav({
364
385
  data-slot="sidebar-nav"
365
386
  data-collapsed={collapsed}
366
387
  className={cn(
367
- "flex h-full flex-col bg-brand-secondary text-brand-secondary-foreground",
388
+ // Force dark-mode CSS variable resolution — sidebar is always dark-backgrounded
389
+ // regardless of system theme, so semantic tokens (destructive, success, etc.)
390
+ // must use their dark-mode values to maintain WCAG contrast.
391
+ "dark flex h-full flex-col bg-brand-secondary text-brand-secondary-foreground",
368
392
  "transition-all duration-200 ease-in-out",
369
393
  collapsed ? "w-14" : "w-[279px]",
370
394
  className,
371
395
  )}
372
396
  >
373
- {/* Logo */}
374
- {!collapsed && logo && (
375
- <div className="flex items-center border-b border-white/15 px-5 py-4">
376
- <img
377
- src={logo}
378
- alt="Logo"
379
- className="h-8 w-auto object-contain object-left"
380
- style={{ filter: "brightness(0) invert(1)" }}
381
- />
397
+ {/* Logo — crossfade between full and icon variant */}
398
+ {(logo || logoCollapsed) && (
399
+ <div className="relative flex items-center border-b border-white/15 py-4 overflow-hidden">
400
+ {logo && (
401
+ <img
402
+ src={logo}
403
+ alt="Logo"
404
+ className={cn(
405
+ "h-8 w-auto object-contain object-left px-5 transition-opacity duration-200",
406
+ collapsed ? "opacity-0" : "opacity-100",
407
+ )}
408
+ style={{ filter: "brightness(0) invert(1)" }}
409
+ />
410
+ )}
411
+ {logoCollapsed && (
412
+ <img
413
+ src={logoCollapsed}
414
+ alt="Logo"
415
+ className={cn(
416
+ "absolute inset-y-0 left-0 right-0 m-auto h-8 w-8 object-contain transition-opacity duration-200",
417
+ collapsed ? "opacity-100" : "opacity-0",
418
+ )}
419
+ style={{ filter: "brightness(0) invert(1)" }}
420
+ />
421
+ )}
382
422
  </div>
383
423
  )}
384
- {collapsed && logoCollapsed && (
385
- <div className="flex items-center justify-center border-b border-white/15 py-4">
386
- <img
387
- src={logoCollapsed}
388
- alt="Logo"
389
- className="h-8 w-8 object-contain"
390
- style={{ filter: "brightness(0) invert(1)" }}
391
- />
424
+
425
+ {/* User section crossfade between expanded and collapsed */}
426
+ <div className="relative border-b border-white/15">
427
+ {/* Expanded — in flow (defines height), no transition (overlay handles it) */}
428
+ <div
429
+ className={cn(
430
+ collapsed ? "opacity-0 pointer-events-none" : "opacity-100",
431
+ )}
432
+ >
433
+ <Accordion
434
+ value={userMenuOpen ? ["user-menu"] : []}
435
+ onValueChange={(values) => setUserMenuOpen(values.length > 0)}
436
+ >
437
+ <AccordionItem className="border-none" value="user-menu">
438
+ <AccordionPrimitive.Header className="flex">
439
+ <AccordionPrimitive.Trigger
440
+ className={cn(
441
+ "group flex h-auto w-full items-center justify-start gap-3 px-5 py-5 text-base transition-colors",
442
+ "text-brand-secondary-foreground hover:bg-white/10",
443
+ )}
444
+ >
445
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center font-semibold text-xs bg-primary text-primary-foreground">
446
+ {getInitials(userName)}
447
+ </div>
448
+ <span className="flex-1 truncate text-left font-medium text-brand-secondary-foreground">
449
+ {userName}
450
+ </span>
451
+ <ChevronDown
452
+ className="ml-auto shrink-0 text-brand-secondary-foreground/50 transition-transform duration-200 group-data-[panel-open]:rotate-180"
453
+ size={16}
454
+ strokeWidth={2}
455
+ />
456
+ </AccordionPrimitive.Trigger>
457
+ </AccordionPrimitive.Header>
458
+
459
+ <AccordionContent className="p-0 text-inherit">
460
+ <div className="border-t border-white/15 bg-black/20">
461
+ <Button
462
+ type="button"
463
+ variant="ghost"
464
+ onClick={onLogout}
465
+ className={cn(
466
+ "h-auto w-full justify-start gap-3 px-5 py-3 text-base",
467
+ "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground transition-colors",
468
+ )}
469
+ >
470
+ <LogOut
471
+ size={16}
472
+ strokeWidth={1.75}
473
+ className="shrink-0 text-destructive"
474
+ />
475
+ <span>Logout</span>
476
+ </Button>
477
+ </div>
478
+ </AccordionContent>
479
+ </AccordionItem>
480
+ </Accordion>
392
481
  </div>
393
- )}
394
482
 
395
- {/* User section */}
396
- <div className="border-b border-white/15">
483
+ {/* Collapsed absolute overlay, centered avatar */}
397
484
  <NavTooltip label={userName} collapsed={collapsed}>
398
- <button
399
- type="button"
400
- onClick={() => !collapsed && setUserMenuOpen((prev) => !prev)}
485
+ <div
401
486
  className={cn(
402
- "group flex w-full items-center gap-3 px-5 py-5 text-base transition-colors",
403
- "text-brand-secondary-foreground hover:bg-white/10",
404
- collapsed && "justify-center px-2 py-4",
487
+ "absolute inset-0 flex items-center justify-center transition-opacity duration-200",
488
+ collapsed ? "opacity-100" : "opacity-0 pointer-events-none",
405
489
  )}
406
490
  >
407
491
  <div className="flex h-8 w-8 shrink-0 items-center justify-center font-semibold text-xs bg-primary text-primary-foreground">
408
492
  {getInitials(userName)}
409
493
  </div>
410
- {!collapsed && (
411
- <>
412
- <span className="flex-1 truncate text-left font-medium text-brand-secondary-foreground">
413
- {userName}
414
- </span>
415
- <ChevronDown
416
- className={cn(
417
- "shrink-0 text-brand-secondary-foreground/50 transition-transform duration-200",
418
- userMenuOpen && "rotate-180",
419
- )}
420
- size={16}
421
- strokeWidth={2}
422
- />
423
- </>
424
- )}
425
- </button>
426
- </NavTooltip>
427
-
428
- {/* Logout dropdown */}
429
- {!collapsed && userMenuOpen && (
430
- <div className="border-t border-white/15 bg-black/20">
431
- <button
432
- type="button"
433
- onClick={onLogout}
434
- className={cn(
435
- "flex w-full items-center gap-3 px-5 py-3 text-base",
436
- "text-brand-secondary-foreground/70 hover:bg-white/10 hover:text-brand-secondary-foreground transition-colors",
437
- )}
438
- >
439
- <LogOut
440
- size={16}
441
- strokeWidth={1.75}
442
- className="shrink-0 text-destructive"
443
- />
444
- <span>Logout</span>
445
- </button>
446
494
  </div>
447
- )}
495
+ </NavTooltip>
448
496
  </div>
449
497
 
450
- {/* Financial metrics (Frontend sidebar only, hidden when collapsed) */}
451
- {!collapsed && !!metricsGroups?.length && (
452
- <div>
453
- {metricsGroups.map((group, i) => (
454
- <MetricsGroup key={i} group={group} />
455
- ))}
456
- </div>
498
+ {/* Financial metrics animated in/out with sidebar collapse to prevent nav items jumping */}
499
+ {!!metricsGroups?.length && (
500
+ <Accordion
501
+ value={!collapsed ? ["metrics"] : []}
502
+ onValueChange={() => {}}
503
+ >
504
+ <AccordionItem className="border-none" value="metrics">
505
+ <AccordionContent className="p-0 text-inherit">
506
+ {metricsGroups.map((group, i) => (
507
+ <MetricsGroup key={i} group={group} />
508
+ ))}
509
+ </AccordionContent>
510
+ </AccordionItem>
511
+ </Accordion>
457
512
  )}
458
513
 
459
514
  {/* Nav items */}
460
- <div className="flex flex-col overflow-y-auto py-3">
515
+ <div ref={navScrollRef} className="flex flex-col overflow-y-auto py-3">
461
516
  {items.map((item) =>
462
517
  item.isCollapsible ? (
463
518
  <CollapsibleNavItem
@@ -484,30 +539,31 @@ export function SidebarNav({
484
539
  label={collapsed ? "Expand" : "Collapse"}
485
540
  collapsed={collapsed}
486
541
  >
487
- <button
542
+ <Button
488
543
  type="button"
544
+ variant="ghost"
489
545
  onClick={() => onCollapsedChange(!collapsed)}
490
546
  className={cn(
491
- "flex w-full items-center gap-3 px-3 py-3 transition-colors",
547
+ "h-12 w-full justify-start gap-3 px-3 py-3 transition-colors",
492
548
  "text-brand-secondary-foreground/80 hover:bg-white/10 hover:text-brand-secondary-foreground",
493
549
  collapsed && "justify-center px-2",
494
550
  )}
495
551
  >
496
552
  {collapsed ? (
497
553
  <PanelLeftOpen
498
- size={18}
554
+ size={24}
499
555
  strokeWidth={1.75}
500
556
  className="shrink-0"
501
557
  />
502
558
  ) : (
503
559
  <PanelLeftClose
504
- size={18}
560
+ size={24}
505
561
  strokeWidth={1.75}
506
562
  className="shrink-0"
507
563
  />
508
564
  )}
509
565
  {!collapsed && <span className="text-sm">Collapse</span>}
510
- </button>
566
+ </Button>
511
567
  </NavTooltip>
512
568
  </div>
513
569
  )}
@@ -129,7 +129,9 @@ export function TransactionsExpenseCategoriesDoughnutChart({
129
129
  style={{ maxWidth: width, fontFamily }}
130
130
  >
131
131
  <CardHeader className="px-3 sm:px-6">
132
- <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
132
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
133
+ {title}
134
+ </CardTitle>
133
135
  </CardHeader>
134
136
 
135
137
  <CardContent className="px-3 sm:px-6">
@@ -16,13 +16,20 @@ import { Card, CardContent, CardHeader, CardTitle } from "./card";
16
16
  import { Empty, EmptyDescription } from "./empty";
17
17
  import { Spinner } from "./spinner";
18
18
  import { cn } from "@/lib/utils";
19
+ import { formatCurrency } from "@/lib/format-currency";
19
20
  import {
20
21
  FALLBACK_TICK,
21
22
  FALLBACK_PRIMARY,
22
23
  FALLBACK_SECONDARY,
23
24
  } from "./chart-shared";
24
25
 
25
- ChartJS.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip);
26
+ ChartJS.register(
27
+ CategoryScale,
28
+ LinearScale,
29
+ BarController,
30
+ BarElement,
31
+ Tooltip,
32
+ );
26
33
 
27
34
  // ---------------------------------------------------------------------------
28
35
  // Constants
@@ -32,12 +39,6 @@ ChartJS.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip)
32
39
  // Helpers
33
40
  // ---------------------------------------------------------------------------
34
41
 
35
- function formatDollar(value: number): string {
36
- return `$${value.toLocaleString(undefined, {
37
- minimumFractionDigits: 2,
38
- maximumFractionDigits: 2,
39
- })}`;
40
- }
41
42
 
42
43
  // ---------------------------------------------------------------------------
43
44
  // Types
@@ -110,7 +111,7 @@ export function TransactionsIncomeExpenseBarChart({
110
111
  color: FALLBACK_SECONDARY,
111
112
  textAlign: "left",
112
113
  // Returns array for multi-line: dollar value on line 1, blank on line 2
113
- formatter: (v: number) => [formatDollar(v), ""],
114
+ formatter: (v: number) => [formatCurrency(v, { decimals: 2 }), ""],
114
115
  },
115
116
  name: {
116
117
  anchor: "end",
@@ -173,7 +174,9 @@ export function TransactionsIncomeExpenseBarChart({
173
174
  style={{ maxWidth: width, fontFamily }}
174
175
  >
175
176
  <CardHeader className="px-3 sm:px-6">
176
- <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
177
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
178
+ {title}
179
+ </CardTitle>
177
180
  </CardHeader>
178
181
 
179
182
  <CardContent className="px-3 sm:px-6">
@@ -129,7 +129,9 @@ export function TransactionsLiabilitiesBreakdownChart({
129
129
  style={{ maxWidth: width, fontFamily }}
130
130
  >
131
131
  <CardHeader className="px-3 sm:px-6">
132
- <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
132
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
133
+ {title}
134
+ </CardTitle>
133
135
  </CardHeader>
134
136
 
135
137
  <CardContent className="px-3 sm:px-6">
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Currency formatting utilities for the WealthX design system.
3
+ * All formatters default to AUD (en-AU locale).
4
+ */
5
+
6
+ /**
7
+ * Format a number as AUD currency using Intl.NumberFormat.
8
+ *
9
+ * @param value The numeric value to format.
10
+ * @param options Optional formatting overrides.
11
+ * @param options.decimals Decimal places (default: 0).
12
+ * @param options.showSign Prefix with +/− for positive/negative (default: false).
13
+ */
14
+ export function formatCurrency(
15
+ value: number,
16
+ options?: { decimals?: number; showSign?: boolean },
17
+ ): string {
18
+ const { decimals = 0, showSign = false } = options ?? {};
19
+ const abs = Math.abs(value);
20
+ const formatted = new Intl.NumberFormat("en-AU", {
21
+ style: "currency",
22
+ currency: "AUD",
23
+ minimumFractionDigits: decimals,
24
+ maximumFractionDigits: decimals,
25
+ }).format(abs);
26
+ if (!showSign) return value < 0 ? `-${formatted}` : formatted;
27
+ if (value > 0) return `+${formatted}`;
28
+ if (value < 0) return `-${formatted}`;
29
+ return formatted;
30
+ }
31
+
32
+ /**
33
+ * Abbreviated currency: $1.2B, $3.5M, $580K, $42.
34
+ * Used primarily for chart axis ticks.
35
+ */
36
+ export function formatCurrencyAbbrev(value: number): string {
37
+ const abs = Math.abs(value);
38
+ const sign = value < 0 ? "-" : "";
39
+ if (abs >= 1_000_000_000)
40
+ return `${sign}$${(abs / 1_000_000_000).toFixed(1)}B`;
41
+ if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(1)}M`;
42
+ if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(0)}K`;
43
+ return `${sign}$${abs.toFixed(0)}`;
44
+ }