@wakastellar/ui 2.0.0 → 2.1.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 (291) hide show
  1. package/README.md +71 -8
  2. package/dist/cli/commands/add.d.ts +7 -0
  3. package/dist/cli/commands/init.d.ts +6 -0
  4. package/dist/cli/commands/list.d.ts +5 -0
  5. package/dist/cli/commands/search.d.ts +1 -0
  6. package/dist/cli/index.cjs +6014 -0
  7. package/dist/cli/index.d.ts +1 -0
  8. package/dist/cli/utils/config.d.ts +29 -0
  9. package/dist/cli/utils/logger.d.ts +20 -0
  10. package/dist/cli/utils/registry.d.ts +23 -0
  11. package/package.json +14 -3
  12. package/src/blocks/activity-timeline/index.tsx +586 -0
  13. package/src/blocks/calendar-view/index.tsx +756 -0
  14. package/src/blocks/chat/index.tsx +1018 -0
  15. package/src/blocks/chat/widget.tsx +504 -0
  16. package/src/blocks/dashboard/index.tsx +522 -0
  17. package/src/blocks/empty-states/index.tsx +452 -0
  18. package/src/blocks/error-pages/index.tsx +426 -0
  19. package/src/blocks/faq/index.tsx +479 -0
  20. package/src/blocks/file-manager/index.tsx +890 -0
  21. package/src/blocks/footer/index.tsx +133 -0
  22. package/src/blocks/header/index.tsx +357 -0
  23. package/src/blocks/headtab/index.tsx +139 -0
  24. package/src/blocks/i18n-editor/index.tsx +1016 -0
  25. package/src/blocks/index.ts +80 -0
  26. package/src/blocks/kanban-board/index.tsx +779 -0
  27. package/src/blocks/landing/index.tsx +677 -0
  28. package/src/blocks/language-selector/index.tsx +88 -0
  29. package/src/blocks/layout/index.tsx +159 -0
  30. package/src/blocks/login/index.tsx +339 -0
  31. package/src/blocks/login/types.ts +131 -0
  32. package/src/blocks/pricing/index.tsx +564 -0
  33. package/src/blocks/profile/index.tsx +746 -0
  34. package/src/blocks/settings/index.tsx +558 -0
  35. package/src/blocks/sidebar/index.tsx +713 -0
  36. package/src/blocks/theme-creator-block/index.tsx +835 -0
  37. package/src/blocks/user-management/index.tsx +1037 -0
  38. package/src/blocks/wizard/index.tsx +719 -0
  39. package/src/components/DataTable/DataTable.tsx +406 -0
  40. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  41. package/src/components/DataTable/DataTableBody.tsx +216 -0
  42. package/src/components/DataTable/DataTableCell.tsx +172 -0
  43. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  44. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  45. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  46. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  47. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  48. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  49. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  50. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  51. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  52. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  53. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  54. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  55. package/src/components/DataTable/README.md +446 -0
  56. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  57. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  58. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  59. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  60. package/src/components/DataTable/formatters/index.ts +335 -0
  61. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  62. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  64. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  65. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  66. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  67. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  68. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  69. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  70. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  71. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  72. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  73. package/src/components/DataTable/index.ts +81 -0
  74. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  75. package/src/components/DataTable/templates/index.tsx +803 -0
  76. package/src/components/DataTable/types.ts +504 -0
  77. package/src/components/DataTable/utils.ts +164 -0
  78. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  79. package/src/components/accordion/index.tsx +61 -0
  80. package/src/components/alert/index.tsx +61 -0
  81. package/src/components/alert-dialog/index.tsx +146 -0
  82. package/src/components/aspect-ratio/index.tsx +12 -0
  83. package/src/components/avatar/index.tsx +54 -0
  84. package/src/components/badge/Badge.stories.tsx +64 -0
  85. package/src/components/badge/index.tsx +38 -0
  86. package/src/components/button/Button.stories.tsx +173 -0
  87. package/src/components/button/index.tsx +56 -0
  88. package/src/components/calendar/index.tsx +73 -0
  89. package/src/components/card/index.tsx +78 -0
  90. package/src/components/checkbox/index.tsx +34 -0
  91. package/src/components/code/index.tsx +229 -0
  92. package/src/components/collapsible/index.tsx +16 -0
  93. package/src/components/command/index.tsx +162 -0
  94. package/src/components/context-menu/index.tsx +204 -0
  95. package/src/components/dialog/index.tsx +126 -0
  96. package/src/components/dropdown-menu/index.tsx +204 -0
  97. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  98. package/src/components/error-boundary/index.ts +7 -0
  99. package/src/components/form/index.tsx +183 -0
  100. package/src/components/hover-card/index.tsx +33 -0
  101. package/src/components/index.ts +368 -0
  102. package/src/components/input/Input.stories.tsx +100 -0
  103. package/src/components/input/index.tsx +27 -0
  104. package/src/components/input-otp/index.tsx +277 -0
  105. package/src/components/label/index.tsx +30 -0
  106. package/src/components/language-selector/index.tsx +341 -0
  107. package/src/components/menubar/index.tsx +240 -0
  108. package/src/components/navigation-menu/index.tsx +134 -0
  109. package/src/components/popover/index.tsx +35 -0
  110. package/src/components/progress/index.tsx +32 -0
  111. package/src/components/radio-group/index.tsx +48 -0
  112. package/src/components/scroll-area/index.tsx +52 -0
  113. package/src/components/select/index.tsx +164 -0
  114. package/src/components/separator/index.tsx +35 -0
  115. package/src/components/sheet/index.tsx +147 -0
  116. package/src/components/skeleton/index.tsx +22 -0
  117. package/src/components/slider/index.tsx +32 -0
  118. package/src/components/switch/index.tsx +33 -0
  119. package/src/components/table/index.tsx +117 -0
  120. package/src/components/tabs/index.tsx +59 -0
  121. package/src/components/textarea/index.tsx +30 -0
  122. package/src/components/theme-selector/index.tsx +327 -0
  123. package/src/components/toast/index.tsx +133 -0
  124. package/src/components/toaster/index.tsx +34 -0
  125. package/src/components/toggle/index.tsx +49 -0
  126. package/src/components/tooltip/index.tsx +34 -0
  127. package/src/components/typography/index.tsx +276 -0
  128. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  129. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  130. package/src/components/waka-activity-feed/index.tsx +686 -0
  131. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  132. package/src/components/waka-admincrumb/index.tsx +349 -0
  133. package/src/components/waka-alert-stack/index.tsx +827 -0
  134. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  135. package/src/components/waka-approval-chain/index.tsx +766 -0
  136. package/src/components/waka-audit-log/index.tsx +1475 -0
  137. package/src/components/waka-autocomplete/index.tsx +358 -0
  138. package/src/components/waka-badge-showcase/index.tsx +704 -0
  139. package/src/components/waka-barcode/index.tsx +260 -0
  140. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  141. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  142. package/src/components/waka-breadcrumb/index.tsx +376 -0
  143. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  144. package/src/components/waka-budget-burn/index.tsx +1234 -0
  145. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  146. package/src/components/waka-carousel/index.tsx +893 -0
  147. package/src/components/waka-cart-summary/index.tsx +1055 -0
  148. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  149. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  150. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  151. package/src/components/waka-charts/WakaChart.tsx +124 -0
  152. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  153. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  154. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  155. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  156. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  157. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  158. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  159. package/src/components/waka-charts/index.ts +90 -0
  160. package/src/components/waka-charts/types.ts +330 -0
  161. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  162. package/src/components/waka-checklist/index.tsx +1067 -0
  163. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  164. package/src/components/waka-cohort-table/index.tsx +1011 -0
  165. package/src/components/waka-color-picker/index.tsx +447 -0
  166. package/src/components/waka-combo-counter/index.tsx +864 -0
  167. package/src/components/waka-combobox/index.tsx +497 -0
  168. package/src/components/waka-command-bar/index.tsx +403 -0
  169. package/src/components/waka-compare-period/index.tsx +1230 -0
  170. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  171. package/src/components/waka-contribution-graph/index.tsx +552 -0
  172. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  173. package/src/components/waka-coupon-input/index.tsx +592 -0
  174. package/src/components/waka-credit-card-input/index.tsx +982 -0
  175. package/src/components/waka-daily-reward/index.tsx +762 -0
  176. package/src/components/waka-date-range-picker/index.tsx +378 -0
  177. package/src/components/waka-datetime-picker/index.tsx +793 -0
  178. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  179. package/src/components/waka-deployment-lane/index.tsx +673 -0
  180. package/src/components/waka-device-trust/index.tsx +1259 -0
  181. package/src/components/waka-dock/index.tsx +285 -0
  182. package/src/components/waka-drawer/index.tsx +319 -0
  183. package/src/components/waka-empty-state/index.tsx +545 -0
  184. package/src/components/waka-error-shake/index.tsx +398 -0
  185. package/src/components/waka-feature-announcement/index.tsx +991 -0
  186. package/src/components/waka-file-upload/index.tsx +437 -0
  187. package/src/components/waka-floating-nav/index.tsx +413 -0
  188. package/src/components/waka-flow-diagram/index.tsx +508 -0
  189. package/src/components/waka-funnel-chart/index.tsx +823 -0
  190. package/src/components/waka-glow-card/index.tsx +246 -0
  191. package/src/components/waka-goal-progress/index.tsx +1025 -0
  192. package/src/components/waka-haptic-button/index.tsx +388 -0
  193. package/src/components/waka-health-pulse/index.tsx +451 -0
  194. package/src/components/waka-heatmap/index.tsx +1026 -0
  195. package/src/components/waka-hotspot/index.tsx +682 -0
  196. package/src/components/waka-image/index.tsx +373 -0
  197. package/src/components/waka-incident-timeline/index.tsx +686 -0
  198. package/src/components/waka-invoice-preview/index.tsx +829 -0
  199. package/src/components/waka-kanban/index.tsx +646 -0
  200. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  201. package/src/components/waka-leaderboard/index.tsx +746 -0
  202. package/src/components/waka-level-progress/index.tsx +665 -0
  203. package/src/components/waka-liquid-button/index.tsx +520 -0
  204. package/src/components/waka-loading-orbit/index.tsx +478 -0
  205. package/src/components/waka-loot-box/index.tsx +1091 -0
  206. package/src/components/waka-magic-link/index.tsx +321 -0
  207. package/src/components/waka-magnetic-button/index.tsx +567 -0
  208. package/src/components/waka-mention-input/index.tsx +953 -0
  209. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  210. package/src/components/waka-milestone-road/index.tsx +1064 -0
  211. package/src/components/waka-modal/index.tsx +374 -0
  212. package/src/components/waka-morph-button/index.tsx +495 -0
  213. package/src/components/waka-network-topology/index.tsx +801 -0
  214. package/src/components/waka-notifications/index.tsx +414 -0
  215. package/src/components/waka-number-input/index.tsx +373 -0
  216. package/src/components/waka-orbital-menu/index.tsx +445 -0
  217. package/src/components/waka-order-tracker/index.tsx +1041 -0
  218. package/src/components/waka-pagination/index.tsx +393 -0
  219. package/src/components/waka-password-strength/index.tsx +824 -0
  220. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  221. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  222. package/src/components/waka-phone-input/index.tsx +801 -0
  223. package/src/components/waka-pipeline-view/index.tsx +604 -0
  224. package/src/components/waka-player-card/index.tsx +691 -0
  225. package/src/components/waka-points-popup/index.tsx +366 -0
  226. package/src/components/waka-power-up/index.tsx +1155 -0
  227. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  228. package/src/components/waka-pricing-table/index.tsx +755 -0
  229. package/src/components/waka-product-card/index.tsx +786 -0
  230. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  231. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  232. package/src/components/waka-qrcode/index.tsx +232 -0
  233. package/src/components/waka-quest-card/index.tsx +1275 -0
  234. package/src/components/waka-quota-bar/index.tsx +693 -0
  235. package/src/components/waka-radar-score/index.tsx +512 -0
  236. package/src/components/waka-rank-badge/index.tsx +813 -0
  237. package/src/components/waka-rating-input/index.tsx +560 -0
  238. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  239. package/src/components/waka-region-map/index.tsx +730 -0
  240. package/src/components/waka-resource-gauge/index.tsx +654 -0
  241. package/src/components/waka-resource-pool/index.tsx +1035 -0
  242. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  243. package/src/components/waka-rollback-slider/index.tsx +891 -0
  244. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  245. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  246. package/src/components/waka-scratch-card/index.tsx +914 -0
  247. package/src/components/waka-season-pass/index.tsx +886 -0
  248. package/src/components/waka-security-score/index.tsx +1126 -0
  249. package/src/components/waka-segmented-control/index.tsx +238 -0
  250. package/src/components/waka-server-rack/index.tsx +764 -0
  251. package/src/components/waka-session-manager/index.tsx +815 -0
  252. package/src/components/waka-signature-pad/index.tsx +744 -0
  253. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  254. package/src/components/waka-skill-tree/index.tsx +1031 -0
  255. package/src/components/waka-sla-tracker/index.tsx +798 -0
  256. package/src/components/waka-slider-range/index.tsx +765 -0
  257. package/src/components/waka-spin-wheel/index.tsx +671 -0
  258. package/src/components/waka-spinner/index.tsx +284 -0
  259. package/src/components/waka-spotlight/index.tsx +410 -0
  260. package/src/components/waka-stat/index.tsx +428 -0
  261. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  262. package/src/components/waka-status-matrix/index.tsx +565 -0
  263. package/src/components/waka-stepper/index.tsx +489 -0
  264. package/src/components/waka-streak-counter/index.tsx +334 -0
  265. package/src/components/waka-success-explosion/index.tsx +453 -0
  266. package/src/components/waka-swipe-card/index.tsx +574 -0
  267. package/src/components/waka-tabs-morph/index.tsx +509 -0
  268. package/src/components/waka-tag-input/index.tsx +877 -0
  269. package/src/components/waka-team-banner/index.tsx +1183 -0
  270. package/src/components/waka-terminal-output/index.tsx +836 -0
  271. package/src/components/waka-theme-creator/index.tsx +762 -0
  272. package/src/components/waka-theme-manager/index.tsx +654 -0
  273. package/src/components/waka-thread-view/index.tsx +874 -0
  274. package/src/components/waka-tilt-card/index.tsx +250 -0
  275. package/src/components/waka-time-picker/index.tsx +479 -0
  276. package/src/components/waka-timeline/index.tsx +385 -0
  277. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  278. package/src/components/waka-tour-guide/index.tsx +920 -0
  279. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  280. package/src/components/waka-tree/index.tsx +557 -0
  281. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  282. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  283. package/src/components/waka-typewriter/index.tsx +566 -0
  284. package/src/components/waka-typing-indicator/index.tsx +649 -0
  285. package/src/components/waka-versus-card/index.tsx +1026 -0
  286. package/src/components/waka-video/index.tsx +557 -0
  287. package/src/components/waka-video-call/index.tsx +1087 -0
  288. package/src/components/waka-virtual-list/index.tsx +327 -0
  289. package/src/components/waka-voice-message/index.tsx +1019 -0
  290. package/src/components/waka-welcome-modal/index.tsx +790 -0
  291. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,1234 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { AlertTriangle, TrendingDown, TrendingUp, Calendar, DollarSign, Clock, Target } from "lucide-react"
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ export interface SpendDataPoint {
12
+ /** Date of the spend */
13
+ date: Date
14
+ /** Amount spent on this day */
15
+ amount: number
16
+ /** Category breakdown (optional) */
17
+ categories?: Record<string, number>
18
+ }
19
+
20
+ export interface BudgetCategory {
21
+ /** Unique identifier */
22
+ id: string
23
+ /** Display name */
24
+ name: string
25
+ /** Allocated budget for this category */
26
+ budget: number
27
+ /** Current spend for this category */
28
+ spent: number
29
+ /** Custom color */
30
+ color?: string
31
+ }
32
+
33
+ export interface BudgetBurnState {
34
+ /** Total budget amount */
35
+ totalBudget: number
36
+ /** Total amount spent */
37
+ totalSpent: number
38
+ /** Budget period start date */
39
+ startDate: Date
40
+ /** Budget period end date */
41
+ endDate: Date
42
+ /** Daily spending data */
43
+ spendHistory: SpendDataPoint[]
44
+ /** Budget categories */
45
+ categories: BudgetCategory[]
46
+ /** Current burn rate (spend per day) */
47
+ burnRate: number
48
+ /** Ideal burn rate to stay on budget */
49
+ idealBurnRate: number
50
+ /** Projected depletion date */
51
+ depletionDate: Date | null
52
+ /** Days remaining in period */
53
+ daysRemaining: number
54
+ /** Time elapsed percentage */
55
+ timeElapsedPercent: number
56
+ /** Budget used percentage */
57
+ budgetUsedPercent: number
58
+ /** Danger zone (burn rate is too high) */
59
+ isDangerZone: boolean
60
+ /** Warning zone (burn rate approaching danger) */
61
+ isWarningZone: boolean
62
+ }
63
+
64
+ export interface WakaBudgetBurnProps {
65
+ /** Total budget amount */
66
+ budget: number
67
+ /** Budget period start date */
68
+ startDate: Date
69
+ /** Budget period end date */
70
+ endDate: Date
71
+ /** Daily spending history */
72
+ spendHistory: SpendDataPoint[]
73
+ /** Budget categories (optional) */
74
+ categories?: BudgetCategory[]
75
+ /** Currency symbol */
76
+ currency?: string
77
+ /** Danger threshold (percentage over ideal burn rate) */
78
+ dangerThreshold?: number
79
+ /** Warning threshold (percentage over ideal burn rate) */
80
+ warningThreshold?: number
81
+ /** Show projected burn line */
82
+ showProjection?: boolean
83
+ /** Show ideal burn line */
84
+ showIdealLine?: boolean
85
+ /** Show category breakdown */
86
+ showCategories?: boolean
87
+ /** Show daily/weekly/monthly trends */
88
+ showTrends?: boolean
89
+ /** Enable animations */
90
+ animated?: boolean
91
+ /** Chart height in pixels */
92
+ height?: number
93
+ /** Custom value formatter */
94
+ formatValue?: (value: number) => string
95
+ /** Callback when danger zone is entered */
96
+ onDangerZone?: () => void
97
+ /** Callback when budget is depleted */
98
+ onBudgetDepleted?: () => void
99
+ /** Additional CSS classes */
100
+ className?: string
101
+ }
102
+
103
+ // ============================================================================
104
+ // Default Colors
105
+ // ============================================================================
106
+
107
+ const defaultCategoryColors = [
108
+ "#3b82f6", // blue
109
+ "#ef4444", // red
110
+ "#22c55e", // green
111
+ "#f59e0b", // amber
112
+ "#8b5cf6", // violet
113
+ "#ec4899", // pink
114
+ "#14b8a6", // teal
115
+ "#f97316", // orange
116
+ ]
117
+
118
+ // ============================================================================
119
+ // Utility Functions
120
+ // ============================================================================
121
+
122
+ function calculateBurnState(
123
+ budget: number,
124
+ startDate: Date,
125
+ endDate: Date,
126
+ spendHistory: SpendDataPoint[],
127
+ categories: BudgetCategory[],
128
+ dangerThreshold: number,
129
+ warningThreshold: number
130
+ ): BudgetBurnState {
131
+ const now = new Date()
132
+ const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
133
+ const daysElapsed = Math.max(0, Math.ceil((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)))
134
+ const daysRemaining = Math.max(0, totalDays - daysElapsed)
135
+
136
+ const totalSpent = spendHistory.reduce((sum, point) => sum + point.amount, 0)
137
+ const budgetRemaining = Math.max(0, budget - totalSpent)
138
+
139
+ const timeElapsedPercent = Math.min(100, (daysElapsed / totalDays) * 100)
140
+ const budgetUsedPercent = Math.min(100, (totalSpent / budget) * 100)
141
+
142
+ // Calculate burn rates
143
+ const burnRate = daysElapsed > 0 ? totalSpent / daysElapsed : 0
144
+ const idealBurnRate = budget / totalDays
145
+
146
+ // Project depletion date
147
+ let depletionDate: Date | null = null
148
+ if (burnRate > 0 && budgetRemaining > 0) {
149
+ const daysUntilDepletion = budgetRemaining / burnRate
150
+ depletionDate = new Date(now.getTime() + daysUntilDepletion * 24 * 60 * 60 * 1000)
151
+ } else if (totalSpent >= budget) {
152
+ depletionDate = now
153
+ }
154
+
155
+ // Determine zones
156
+ const burnRateRatio = idealBurnRate > 0 ? burnRate / idealBurnRate : 0
157
+ const isDangerZone = burnRateRatio >= 1 + dangerThreshold / 100
158
+ const isWarningZone = !isDangerZone && burnRateRatio >= 1 + warningThreshold / 100
159
+
160
+ return {
161
+ totalBudget: budget,
162
+ totalSpent,
163
+ startDate,
164
+ endDate,
165
+ spendHistory,
166
+ categories,
167
+ burnRate,
168
+ idealBurnRate,
169
+ depletionDate,
170
+ daysRemaining,
171
+ timeElapsedPercent,
172
+ budgetUsedPercent,
173
+ isDangerZone,
174
+ isWarningZone,
175
+ }
176
+ }
177
+
178
+ function formatDate(date: Date): string {
179
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" })
180
+ }
181
+
182
+ function calculateCumulativeSpend(spendHistory: SpendDataPoint[]): Array<{ date: Date; cumulative: number }> {
183
+ let cumulative = 0
184
+ return spendHistory
185
+ .sort((a, b) => a.date.getTime() - b.date.getTime())
186
+ .map((point) => {
187
+ cumulative += point.amount
188
+ return { date: point.date, cumulative }
189
+ })
190
+ }
191
+
192
+ function calculateTrend(values: number[]): { direction: "up" | "down" | "neutral"; percentage: number } {
193
+ if (values.length < 2) return { direction: "neutral", percentage: 0 }
194
+
195
+ const recentAvg = values.slice(-3).reduce((a, b) => a + b, 0) / Math.min(3, values.length)
196
+ const olderAvg = values.slice(0, -3).reduce((a, b) => a + b, 0) / Math.max(1, values.length - 3)
197
+
198
+ if (olderAvg === 0) return { direction: "neutral", percentage: 0 }
199
+
200
+ const change = ((recentAvg - olderAvg) / olderAvg) * 100
201
+
202
+ if (Math.abs(change) < 5) return { direction: "neutral", percentage: 0 }
203
+ return {
204
+ direction: change > 0 ? "up" : "down",
205
+ percentage: Math.abs(change),
206
+ }
207
+ }
208
+
209
+ // ============================================================================
210
+ // Burn Chart Component
211
+ // ============================================================================
212
+
213
+ interface BurnChartProps {
214
+ state: BudgetBurnState
215
+ width: number
216
+ height: number
217
+ showProjection: boolean
218
+ showIdealLine: boolean
219
+ animated: boolean
220
+ formatValue: (value: number) => string
221
+ }
222
+
223
+ function BurnChart({
224
+ state,
225
+ width,
226
+ height,
227
+ showProjection,
228
+ showIdealLine,
229
+ animated,
230
+ formatValue,
231
+ }: BurnChartProps) {
232
+ const [animationProgress, setAnimationProgress] = React.useState(animated ? 0 : 1)
233
+ const [hoveredPoint, setHoveredPoint] = React.useState<{
234
+ index: number
235
+ x: number
236
+ y: number
237
+ value: number
238
+ date: Date
239
+ } | null>(null)
240
+
241
+ const padding = { top: 20, right: 20, bottom: 30, left: 60 }
242
+ const chartWidth = width - padding.left - padding.right
243
+ const chartHeight = height - padding.top - padding.bottom
244
+
245
+ // Calculate cumulative spending
246
+ const cumulativeData = React.useMemo(
247
+ () => calculateCumulativeSpend(state.spendHistory),
248
+ [state.spendHistory]
249
+ )
250
+
251
+ // Calculate total days for x-axis
252
+ const totalDays = Math.ceil(
253
+ (state.endDate.getTime() - state.startDate.getTime()) / (1000 * 60 * 60 * 24)
254
+ )
255
+
256
+ // Scale functions
257
+ const xScale = (date: Date) => {
258
+ const dayIndex = Math.ceil(
259
+ (date.getTime() - state.startDate.getTime()) / (1000 * 60 * 60 * 24)
260
+ )
261
+ return padding.left + (dayIndex / totalDays) * chartWidth
262
+ }
263
+
264
+ const yScale = (value: number) => {
265
+ return padding.top + chartHeight - (value / state.totalBudget) * chartHeight
266
+ }
267
+
268
+ // Generate path for actual burn line
269
+ const burnPath = React.useMemo(() => {
270
+ if (cumulativeData.length === 0) return ""
271
+
272
+ const points = cumulativeData.map((d) => ({
273
+ x: xScale(d.date),
274
+ y: yScale(d.cumulative),
275
+ }))
276
+
277
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ")
278
+ }, [cumulativeData, chartWidth, chartHeight])
279
+
280
+ // Generate ideal burn line
281
+ const idealPath = React.useMemo(() => {
282
+ const startX = padding.left
283
+ const startY = yScale(0)
284
+ const endX = padding.left + chartWidth
285
+ const endY = yScale(state.totalBudget)
286
+ return `M ${startX} ${startY} L ${endX} ${endY}`
287
+ }, [chartWidth, chartHeight, state.totalBudget])
288
+
289
+ // Generate projection line
290
+ const projectionPath = React.useMemo(() => {
291
+ if (cumulativeData.length === 0 || state.burnRate === 0) return ""
292
+
293
+ const lastPoint = cumulativeData[cumulativeData.length - 1]
294
+ const startX = xScale(lastPoint.date)
295
+ const startY = yScale(lastPoint.cumulative)
296
+
297
+ // Project to budget depletion or end date
298
+ const endDate = state.depletionDate && state.depletionDate < state.endDate
299
+ ? state.depletionDate
300
+ : state.endDate
301
+
302
+ const daysToProject = Math.ceil(
303
+ (endDate.getTime() - lastPoint.date.getTime()) / (1000 * 60 * 60 * 24)
304
+ )
305
+ const projectedSpend = lastPoint.cumulative + state.burnRate * daysToProject
306
+
307
+ const endX = xScale(endDate)
308
+ const endY = yScale(Math.min(projectedSpend, state.totalBudget))
309
+
310
+ return `M ${startX} ${startY} L ${endX} ${endY}`
311
+ }, [cumulativeData, state.burnRate, state.depletionDate, state.endDate, chartWidth, chartHeight])
312
+
313
+ // Danger zone area (above ideal burn)
314
+ const dangerZonePath = React.useMemo(() => {
315
+ const topLeft = `${padding.left},${padding.top}`
316
+ const topRight = `${padding.left + chartWidth},${padding.top}`
317
+ const bottomRight = `${padding.left + chartWidth},${yScale(state.totalBudget)}`
318
+ const bottomLeft = `${padding.left},${yScale(0)}`
319
+ return `M ${topLeft} L ${topRight} L ${bottomRight} L ${bottomLeft} Z`
320
+ }, [chartWidth, chartHeight, state.totalBudget])
321
+
322
+ // Animation effect
323
+ React.useEffect(() => {
324
+ if (!animated) {
325
+ setAnimationProgress(1)
326
+ return
327
+ }
328
+
329
+ const duration = 1500
330
+ const startTime = Date.now()
331
+
332
+ const animate = () => {
333
+ const elapsed = Date.now() - startTime
334
+ const progress = Math.min(elapsed / duration, 1)
335
+ const eased = 1 - Math.pow(1 - progress, 3)
336
+ setAnimationProgress(eased)
337
+
338
+ if (progress < 1) {
339
+ requestAnimationFrame(animate)
340
+ }
341
+ }
342
+
343
+ requestAnimationFrame(animate)
344
+ }, [animated])
345
+
346
+ // Generate tick marks
347
+ const xTicks = React.useMemo(() => {
348
+ const ticks: Array<{ date: Date; x: number }> = []
349
+ const tickCount = Math.min(6, totalDays)
350
+
351
+ for (let i = 0; i <= tickCount; i++) {
352
+ const date = new Date(
353
+ state.startDate.getTime() + (i / tickCount) * (state.endDate.getTime() - state.startDate.getTime())
354
+ )
355
+ ticks.push({ date, x: xScale(date) })
356
+ }
357
+
358
+ return ticks
359
+ }, [state.startDate, state.endDate, totalDays, chartWidth])
360
+
361
+ const yTicks = React.useMemo(() => {
362
+ const ticks: Array<{ value: number; y: number }> = []
363
+ const tickCount = 4
364
+
365
+ for (let i = 0; i <= tickCount; i++) {
366
+ const value = (i / tickCount) * state.totalBudget
367
+ ticks.push({ value, y: yScale(value) })
368
+ }
369
+
370
+ return ticks
371
+ }, [state.totalBudget, chartHeight])
372
+
373
+ // Handle hover on burn line points
374
+ const handlePointHover = (index: number) => {
375
+ const point = cumulativeData[index]
376
+ if (point) {
377
+ setHoveredPoint({
378
+ index,
379
+ x: xScale(point.date),
380
+ y: yScale(point.cumulative),
381
+ value: point.cumulative,
382
+ date: point.date,
383
+ })
384
+ }
385
+ }
386
+
387
+ return (
388
+ <div className="relative">
389
+ <svg width={width} height={height} className="overflow-visible">
390
+ <defs>
391
+ {/* Gradient for danger zone */}
392
+ <linearGradient id="dangerZoneGradient" x1="0" y1="0" x2="0" y2="1">
393
+ <stop offset="0%" stopColor="#ef4444" stopOpacity="0.15" />
394
+ <stop offset="100%" stopColor="#ef4444" stopOpacity="0.02" />
395
+ </linearGradient>
396
+
397
+ {/* Gradient for burn line */}
398
+ <linearGradient id="burnLineGradient" x1="0" y1="0" x2="1" y2="0">
399
+ <stop offset="0%" stopColor="#3b82f6" />
400
+ <stop offset="100%" stopColor={state.isDangerZone ? "#ef4444" : state.isWarningZone ? "#f59e0b" : "#22c55e"} />
401
+ </linearGradient>
402
+
403
+ {/* Clip path for animation */}
404
+ <clipPath id="burnLineClip">
405
+ <rect
406
+ x={padding.left}
407
+ y={padding.top}
408
+ width={chartWidth * animationProgress}
409
+ height={chartHeight}
410
+ />
411
+ </clipPath>
412
+ </defs>
413
+
414
+ {/* Background grid */}
415
+ <g className="text-muted-foreground/20">
416
+ {yTicks.map((tick, i) => (
417
+ <line
418
+ key={i}
419
+ x1={padding.left}
420
+ y1={tick.y}
421
+ x2={padding.left + chartWidth}
422
+ y2={tick.y}
423
+ stroke="currentColor"
424
+ strokeDasharray="4,4"
425
+ />
426
+ ))}
427
+ </g>
428
+
429
+ {/* Danger zone fill */}
430
+ <path
431
+ d={`M ${padding.left} ${padding.top}
432
+ L ${padding.left + chartWidth} ${yScale(state.totalBudget)}
433
+ L ${padding.left + chartWidth} ${padding.top} Z`}
434
+ fill="url(#dangerZoneGradient)"
435
+ />
436
+
437
+ {/* Y-axis */}
438
+ <g className="text-muted-foreground text-xs">
439
+ {yTicks.map((tick, i) => (
440
+ <g key={i}>
441
+ <text
442
+ x={padding.left - 8}
443
+ y={tick.y}
444
+ textAnchor="end"
445
+ dominantBaseline="middle"
446
+ fill="currentColor"
447
+ >
448
+ {formatValue(tick.value)}
449
+ </text>
450
+ </g>
451
+ ))}
452
+ </g>
453
+
454
+ {/* X-axis */}
455
+ <g className="text-muted-foreground text-xs">
456
+ {xTicks.map((tick, i) => (
457
+ <text
458
+ key={i}
459
+ x={tick.x}
460
+ y={height - 8}
461
+ textAnchor="middle"
462
+ fill="currentColor"
463
+ >
464
+ {formatDate(tick.date)}
465
+ </text>
466
+ ))}
467
+ </g>
468
+
469
+ {/* Ideal burn line */}
470
+ {showIdealLine && (
471
+ <path
472
+ d={idealPath}
473
+ fill="none"
474
+ stroke="#94a3b8"
475
+ strokeWidth="2"
476
+ strokeDasharray="8,4"
477
+ opacity="0.6"
478
+ />
479
+ )}
480
+
481
+ {/* Actual burn line with animation */}
482
+ <g clipPath="url(#burnLineClip)">
483
+ <path
484
+ d={burnPath}
485
+ fill="none"
486
+ stroke="url(#burnLineGradient)"
487
+ strokeWidth="3"
488
+ strokeLinecap="round"
489
+ strokeLinejoin="round"
490
+ />
491
+ </g>
492
+
493
+ {/* Projection line */}
494
+ {showProjection && projectionPath && animationProgress === 1 && (
495
+ <path
496
+ d={projectionPath}
497
+ fill="none"
498
+ stroke={state.isDangerZone ? "#ef4444" : state.isWarningZone ? "#f59e0b" : "#22c55e"}
499
+ strokeWidth="2"
500
+ strokeDasharray="6,3"
501
+ opacity="0.6"
502
+ className="animate-burn-dash"
503
+ />
504
+ )}
505
+
506
+ {/* Current position marker */}
507
+ {cumulativeData.length > 0 && animationProgress === 1 && (
508
+ <g>
509
+ <circle
510
+ cx={xScale(cumulativeData[cumulativeData.length - 1].date)}
511
+ cy={yScale(cumulativeData[cumulativeData.length - 1].cumulative)}
512
+ r="6"
513
+ fill={state.isDangerZone ? "#ef4444" : state.isWarningZone ? "#f59e0b" : "#3b82f6"}
514
+ stroke="white"
515
+ strokeWidth="2"
516
+ className="animate-burn-pulse"
517
+ />
518
+ </g>
519
+ )}
520
+
521
+ {/* Budget line */}
522
+ <line
523
+ x1={padding.left}
524
+ y1={yScale(state.totalBudget)}
525
+ x2={padding.left + chartWidth}
526
+ y2={yScale(state.totalBudget)}
527
+ stroke="#ef4444"
528
+ strokeWidth="2"
529
+ strokeDasharray="4,2"
530
+ />
531
+
532
+ {/* Today marker */}
533
+ {state.timeElapsedPercent < 100 && (
534
+ <g>
535
+ <line
536
+ x1={padding.left + (state.timeElapsedPercent / 100) * chartWidth}
537
+ y1={padding.top}
538
+ x2={padding.left + (state.timeElapsedPercent / 100) * chartWidth}
539
+ y2={padding.top + chartHeight}
540
+ stroke="#6366f1"
541
+ strokeWidth="1"
542
+ strokeDasharray="4,4"
543
+ opacity="0.5"
544
+ />
545
+ <text
546
+ x={padding.left + (state.timeElapsedPercent / 100) * chartWidth}
547
+ y={padding.top - 6}
548
+ textAnchor="middle"
549
+ className="text-xs fill-indigo-500 font-medium"
550
+ >
551
+ Today
552
+ </text>
553
+ </g>
554
+ )}
555
+
556
+ {/* Hover points */}
557
+ {animationProgress === 1 && cumulativeData.map((point, i) => (
558
+ <circle
559
+ key={i}
560
+ cx={xScale(point.date)}
561
+ cy={yScale(point.cumulative)}
562
+ r="8"
563
+ fill="transparent"
564
+ className="cursor-pointer"
565
+ onMouseEnter={() => handlePointHover(i)}
566
+ onMouseLeave={() => setHoveredPoint(null)}
567
+ />
568
+ ))}
569
+
570
+ {/* Depletion date marker */}
571
+ {showProjection && state.depletionDate && state.depletionDate <= state.endDate && animationProgress === 1 && (
572
+ <g>
573
+ <line
574
+ x1={xScale(state.depletionDate)}
575
+ y1={padding.top}
576
+ x2={xScale(state.depletionDate)}
577
+ y2={padding.top + chartHeight}
578
+ stroke="#ef4444"
579
+ strokeWidth="2"
580
+ strokeDasharray="4,4"
581
+ />
582
+ <g transform={`translate(${xScale(state.depletionDate)}, ${padding.top + 10})`}>
583
+ <rect
584
+ x="-40"
585
+ y="-8"
586
+ width="80"
587
+ height="16"
588
+ rx="4"
589
+ fill="#ef4444"
590
+ />
591
+ <text
592
+ textAnchor="middle"
593
+ dominantBaseline="middle"
594
+ className="text-[10px] font-medium fill-white"
595
+ >
596
+ Depleted
597
+ </text>
598
+ </g>
599
+ </g>
600
+ )}
601
+ </svg>
602
+
603
+ {/* Tooltip */}
604
+ {hoveredPoint && (
605
+ <div
606
+ className={cn(
607
+ "absolute z-50 px-3 py-2 text-sm rounded-lg shadow-lg",
608
+ "bg-foreground text-background",
609
+ "animate-in fade-in-0 zoom-in-95 duration-100",
610
+ "pointer-events-none"
611
+ )}
612
+ style={{
613
+ left: hoveredPoint.x,
614
+ top: hoveredPoint.y - 60,
615
+ transform: "translateX(-50%)",
616
+ }}
617
+ >
618
+ <div className="font-semibold">{formatDate(hoveredPoint.date)}</div>
619
+ <div className="text-xs opacity-80">
620
+ Cumulative: {formatValue(hoveredPoint.value)}
621
+ </div>
622
+ <div
623
+ className="absolute left-1/2 -translate-x-1/2 -bottom-1.5 w-3 h-3 rotate-45 bg-foreground"
624
+ />
625
+ </div>
626
+ )}
627
+
628
+ {/* CSS Animations */}
629
+ <style>{`
630
+ @keyframes burn-pulse {
631
+ 0%, 100% { transform: scale(1); opacity: 1; }
632
+ 50% { transform: scale(1.2); opacity: 0.8; }
633
+ }
634
+ @keyframes burn-dash {
635
+ 0% { stroke-dashoffset: 0; }
636
+ 100% { stroke-dashoffset: 18; }
637
+ }
638
+ .animate-burn-pulse {
639
+ animation: burn-pulse 2s ease-in-out infinite;
640
+ transform-origin: center;
641
+ transform-box: fill-box;
642
+ }
643
+ .animate-burn-dash {
644
+ animation: burn-dash 1s linear infinite;
645
+ }
646
+ `}</style>
647
+ </div>
648
+ )
649
+ }
650
+
651
+ // ============================================================================
652
+ // Stats Panel Component
653
+ // ============================================================================
654
+
655
+ interface StatsPanelProps {
656
+ state: BudgetBurnState
657
+ formatValue: (value: number) => string
658
+ }
659
+
660
+ function StatsPanel({ state, formatValue }: StatsPanelProps) {
661
+ const budgetRemaining = state.totalBudget - state.totalSpent
662
+ const burnRateVsIdeal = state.idealBurnRate > 0
663
+ ? ((state.burnRate - state.idealBurnRate) / state.idealBurnRate) * 100
664
+ : 0
665
+
666
+ return (
667
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
668
+ {/* Budget Remaining */}
669
+ <div className="rounded-lg border bg-card p-4">
670
+ <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1">
671
+ <DollarSign className="h-4 w-4" />
672
+ <span>Remaining</span>
673
+ </div>
674
+ <div className={cn(
675
+ "text-2xl font-bold",
676
+ budgetRemaining < 0 && "text-red-600"
677
+ )}>
678
+ {formatValue(Math.max(0, budgetRemaining))}
679
+ </div>
680
+ <div className="text-xs text-muted-foreground mt-1">
681
+ {(100 - state.budgetUsedPercent).toFixed(1)}% of budget
682
+ </div>
683
+ </div>
684
+
685
+ {/* Burn Rate */}
686
+ <div className="rounded-lg border bg-card p-4">
687
+ <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1">
688
+ <TrendingDown className="h-4 w-4" />
689
+ <span>Burn Rate</span>
690
+ </div>
691
+ <div className={cn(
692
+ "text-2xl font-bold",
693
+ state.isDangerZone && "text-red-600",
694
+ state.isWarningZone && "text-amber-600"
695
+ )}>
696
+ {formatValue(state.burnRate)}/day
697
+ </div>
698
+ <div className={cn(
699
+ "text-xs mt-1 flex items-center gap-1",
700
+ burnRateVsIdeal > 0 ? "text-red-600" : "text-green-600"
701
+ )}>
702
+ {burnRateVsIdeal > 0 ? <TrendingUp className="h-3 w-3" /> : <TrendingDown className="h-3 w-3" />}
703
+ {Math.abs(burnRateVsIdeal).toFixed(1)}% vs ideal
704
+ </div>
705
+ </div>
706
+
707
+ {/* Days Remaining */}
708
+ <div className="rounded-lg border bg-card p-4">
709
+ <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1">
710
+ <Clock className="h-4 w-4" />
711
+ <span>Days Left</span>
712
+ </div>
713
+ <div className="text-2xl font-bold">
714
+ {state.daysRemaining}
715
+ </div>
716
+ <div className="text-xs text-muted-foreground mt-1">
717
+ {state.timeElapsedPercent.toFixed(1)}% elapsed
718
+ </div>
719
+ </div>
720
+
721
+ {/* Depletion Forecast */}
722
+ <div className="rounded-lg border bg-card p-4">
723
+ <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1">
724
+ <Calendar className="h-4 w-4" />
725
+ <span>Depletion Date</span>
726
+ </div>
727
+ <div className={cn(
728
+ "text-lg font-bold",
729
+ state.depletionDate && state.depletionDate < state.endDate && "text-red-600"
730
+ )}>
731
+ {state.depletionDate
732
+ ? formatDate(state.depletionDate)
733
+ : "On Track"}
734
+ </div>
735
+ {state.depletionDate && state.depletionDate < state.endDate && (
736
+ <div className="text-xs text-red-600 mt-1 flex items-center gap-1">
737
+ <AlertTriangle className="h-3 w-3" />
738
+ Before period ends
739
+ </div>
740
+ )}
741
+ </div>
742
+ </div>
743
+ )
744
+ }
745
+
746
+ // ============================================================================
747
+ // Trend Indicators Component
748
+ // ============================================================================
749
+
750
+ interface TrendIndicatorsProps {
751
+ state: BudgetBurnState
752
+ formatValue: (value: number) => string
753
+ }
754
+
755
+ function TrendIndicators({ state, formatValue }: TrendIndicatorsProps) {
756
+ const dailyAmounts = state.spendHistory.map((p) => p.amount)
757
+ const dailyTrend = calculateTrend(dailyAmounts)
758
+
759
+ // Calculate weekly averages
760
+ const weeklyAverages: number[] = []
761
+ for (let i = 0; i < dailyAmounts.length; i += 7) {
762
+ const weekData = dailyAmounts.slice(i, i + 7)
763
+ if (weekData.length > 0) {
764
+ weeklyAverages.push(weekData.reduce((a, b) => a + b, 0) / weekData.length)
765
+ }
766
+ }
767
+ const weeklyTrend = calculateTrend(weeklyAverages)
768
+
769
+ const avgDaily = dailyAmounts.length > 0
770
+ ? dailyAmounts.reduce((a, b) => a + b, 0) / dailyAmounts.length
771
+ : 0
772
+ const avgWeekly = avgDaily * 7
773
+
774
+ return (
775
+ <div className="flex flex-wrap gap-4">
776
+ {/* Daily Trend */}
777
+ <div className="flex items-center gap-3 px-4 py-2 rounded-lg bg-muted/50">
778
+ <div>
779
+ <div className="text-xs text-muted-foreground">Daily Avg</div>
780
+ <div className="font-semibold">{formatValue(avgDaily)}</div>
781
+ </div>
782
+ {dailyTrend.direction !== "neutral" && (
783
+ <div className={cn(
784
+ "flex items-center gap-1 text-sm font-medium",
785
+ dailyTrend.direction === "up" ? "text-red-600" : "text-green-600"
786
+ )}>
787
+ {dailyTrend.direction === "up" ? (
788
+ <TrendingUp className="h-4 w-4" />
789
+ ) : (
790
+ <TrendingDown className="h-4 w-4" />
791
+ )}
792
+ {dailyTrend.percentage.toFixed(1)}%
793
+ </div>
794
+ )}
795
+ </div>
796
+
797
+ {/* Weekly Trend */}
798
+ <div className="flex items-center gap-3 px-4 py-2 rounded-lg bg-muted/50">
799
+ <div>
800
+ <div className="text-xs text-muted-foreground">Weekly Avg</div>
801
+ <div className="font-semibold">{formatValue(avgWeekly)}</div>
802
+ </div>
803
+ {weeklyTrend.direction !== "neutral" && (
804
+ <div className={cn(
805
+ "flex items-center gap-1 text-sm font-medium",
806
+ weeklyTrend.direction === "up" ? "text-red-600" : "text-green-600"
807
+ )}>
808
+ {weeklyTrend.direction === "up" ? (
809
+ <TrendingUp className="h-4 w-4" />
810
+ ) : (
811
+ <TrendingDown className="h-4 w-4" />
812
+ )}
813
+ {weeklyTrend.percentage.toFixed(1)}%
814
+ </div>
815
+ )}
816
+ </div>
817
+
818
+ {/* Monthly projection */}
819
+ <div className="flex items-center gap-3 px-4 py-2 rounded-lg bg-muted/50">
820
+ <div>
821
+ <div className="text-xs text-muted-foreground">Monthly Proj.</div>
822
+ <div className="font-semibold">{formatValue(state.burnRate * 30)}</div>
823
+ </div>
824
+ <div className={cn(
825
+ "text-xs",
826
+ state.burnRate * 30 > state.totalBudget ? "text-red-600" : "text-green-600"
827
+ )}>
828
+ {state.burnRate * 30 > state.totalBudget ? "Over budget" : "On track"}
829
+ </div>
830
+ </div>
831
+ </div>
832
+ )
833
+ }
834
+
835
+ // ============================================================================
836
+ // Category Breakdown Component
837
+ // ============================================================================
838
+
839
+ interface CategoryBreakdownProps {
840
+ categories: BudgetCategory[]
841
+ formatValue: (value: number) => string
842
+ }
843
+
844
+ function CategoryBreakdown({ categories, formatValue }: CategoryBreakdownProps) {
845
+ return (
846
+ <div className="space-y-3">
847
+ <div className="text-sm font-medium text-muted-foreground flex items-center gap-2">
848
+ <Target className="h-4 w-4" />
849
+ Category Breakdown
850
+ </div>
851
+ <div className="space-y-2">
852
+ {categories.map((category, index) => {
853
+ const percentage = (category.spent / category.budget) * 100
854
+ const isOverBudget = category.spent > category.budget
855
+ const color = category.color || defaultCategoryColors[index % defaultCategoryColors.length]
856
+
857
+ return (
858
+ <div key={category.id} className="space-y-1">
859
+ <div className="flex items-center justify-between text-sm">
860
+ <div className="flex items-center gap-2">
861
+ <span
862
+ className="w-3 h-3 rounded-full"
863
+ style={{ backgroundColor: color }}
864
+ />
865
+ <span className="font-medium">{category.name}</span>
866
+ </div>
867
+ <div className="flex items-center gap-2">
868
+ <span className={cn(isOverBudget && "text-red-600")}>
869
+ {formatValue(category.spent)}
870
+ </span>
871
+ <span className="text-muted-foreground">
872
+ / {formatValue(category.budget)}
873
+ </span>
874
+ </div>
875
+ </div>
876
+ <div className="h-2 rounded-full bg-muted overflow-hidden">
877
+ <div
878
+ className={cn(
879
+ "h-full rounded-full transition-all duration-500",
880
+ isOverBudget && "animate-pulse"
881
+ )}
882
+ style={{
883
+ width: `${Math.min(percentage, 100)}%`,
884
+ backgroundColor: isOverBudget ? "#ef4444" : color,
885
+ }}
886
+ />
887
+ </div>
888
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
889
+ <span>{percentage.toFixed(1)}% used</span>
890
+ <span>
891
+ {isOverBudget
892
+ ? `${formatValue(category.spent - category.budget)} over`
893
+ : `${formatValue(category.budget - category.spent)} left`}
894
+ </span>
895
+ </div>
896
+ </div>
897
+ )
898
+ })}
899
+ </div>
900
+ </div>
901
+ )
902
+ }
903
+
904
+ // ============================================================================
905
+ // Alert Banner Component
906
+ // ============================================================================
907
+
908
+ interface AlertBannerProps {
909
+ state: BudgetBurnState
910
+ formatValue: (value: number) => string
911
+ }
912
+
913
+ function AlertBanner({ state, formatValue }: AlertBannerProps) {
914
+ if (!state.isDangerZone && !state.isWarningZone) return null
915
+
916
+ return (
917
+ <div className={cn(
918
+ "flex items-center gap-3 px-4 py-3 rounded-lg",
919
+ state.isDangerZone
920
+ ? "bg-red-100 dark:bg-red-950 text-red-800 dark:text-red-200"
921
+ : "bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-200"
922
+ )}>
923
+ <AlertTriangle className="h-5 w-5 flex-shrink-0" />
924
+ <div>
925
+ <div className="font-semibold">
926
+ {state.isDangerZone ? "Budget at Risk!" : "Budget Warning"}
927
+ </div>
928
+ <div className="text-sm opacity-90">
929
+ {state.isDangerZone
930
+ ? `At current burn rate of ${formatValue(state.burnRate)}/day, budget will deplete ${
931
+ state.depletionDate ? `on ${formatDate(state.depletionDate)}` : "before period ends"
932
+ }.`
933
+ : `Spending is ${((state.burnRate / state.idealBurnRate - 1) * 100).toFixed(0)}% above ideal burn rate. Consider reducing expenses.`}
934
+ </div>
935
+ </div>
936
+ </div>
937
+ )
938
+ }
939
+
940
+ // ============================================================================
941
+ // Main Component
942
+ // ============================================================================
943
+
944
+ export function WakaBudgetBurn({
945
+ budget,
946
+ startDate,
947
+ endDate,
948
+ spendHistory,
949
+ categories = [],
950
+ currency = "$",
951
+ dangerThreshold = 30,
952
+ warningThreshold = 15,
953
+ showProjection = true,
954
+ showIdealLine = true,
955
+ showCategories = true,
956
+ showTrends = true,
957
+ animated = true,
958
+ height = 300,
959
+ formatValue: customFormatValue,
960
+ onDangerZone,
961
+ onBudgetDepleted,
962
+ className,
963
+ }: WakaBudgetBurnProps) {
964
+ const containerRef = React.useRef<HTMLDivElement>(null)
965
+ const [containerWidth, setContainerWidth] = React.useState(600)
966
+
967
+ // Calculate burn state
968
+ const state = React.useMemo(
969
+ () => calculateBurnState(budget, startDate, endDate, spendHistory, categories, dangerThreshold, warningThreshold),
970
+ [budget, startDate, endDate, spendHistory, categories, dangerThreshold, warningThreshold]
971
+ )
972
+
973
+ // Default value formatter
974
+ const defaultFormatValue = React.useCallback(
975
+ (value: number) => {
976
+ if (value >= 1000000) {
977
+ return `${currency}${(value / 1000000).toFixed(1)}M`
978
+ }
979
+ if (value >= 1000) {
980
+ return `${currency}${(value / 1000).toFixed(1)}K`
981
+ }
982
+ return `${currency}${value.toFixed(2)}`
983
+ },
984
+ [currency]
985
+ )
986
+
987
+ const formatValue = customFormatValue || defaultFormatValue
988
+
989
+ // Track danger zone changes
990
+ const prevDangerZoneRef = React.useRef(state.isDangerZone)
991
+ const prevDepletedRef = React.useRef(state.totalSpent >= state.totalBudget)
992
+
993
+ React.useEffect(() => {
994
+ if (state.isDangerZone && !prevDangerZoneRef.current) {
995
+ onDangerZone?.()
996
+ }
997
+ prevDangerZoneRef.current = state.isDangerZone
998
+
999
+ const isDepleted = state.totalSpent >= state.totalBudget
1000
+ if (isDepleted && !prevDepletedRef.current) {
1001
+ onBudgetDepleted?.()
1002
+ }
1003
+ prevDepletedRef.current = isDepleted
1004
+ }, [state.isDangerZone, state.totalSpent, state.totalBudget, onDangerZone, onBudgetDepleted])
1005
+
1006
+ // Resize observer for responsive chart
1007
+ React.useEffect(() => {
1008
+ if (!containerRef.current) return
1009
+
1010
+ const observer = new ResizeObserver((entries) => {
1011
+ for (const entry of entries) {
1012
+ setContainerWidth(entry.contentRect.width)
1013
+ }
1014
+ })
1015
+
1016
+ observer.observe(containerRef.current)
1017
+ return () => observer.disconnect()
1018
+ }, [])
1019
+
1020
+ return (
1021
+ <div ref={containerRef} className={cn("space-y-6", className)}>
1022
+ {/* Alert Banner */}
1023
+ <AlertBanner state={state} formatValue={formatValue} />
1024
+
1025
+ {/* Stats Panel */}
1026
+ <StatsPanel state={state} formatValue={formatValue} />
1027
+
1028
+ {/* Burn Chart */}
1029
+ <div className="rounded-lg border bg-card p-4">
1030
+ <div className="flex items-center justify-between mb-4">
1031
+ <div className="text-sm font-medium">Budget Burn-down</div>
1032
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
1033
+ {showIdealLine && (
1034
+ <div className="flex items-center gap-2">
1035
+ <div className="w-6 h-0.5 bg-slate-400" style={{ backgroundImage: "repeating-linear-gradient(90deg, currentColor 0, currentColor 4px, transparent 4px, transparent 8px)" }} />
1036
+ <span>Ideal</span>
1037
+ </div>
1038
+ )}
1039
+ {showProjection && (
1040
+ <div className="flex items-center gap-2">
1041
+ <div className="w-6 h-0.5" style={{ backgroundImage: "repeating-linear-gradient(90deg, #22c55e 0, #22c55e 3px, transparent 3px, transparent 6px)" }} />
1042
+ <span>Projected</span>
1043
+ </div>
1044
+ )}
1045
+ <div className="flex items-center gap-2">
1046
+ <div className="w-6 h-0.5 bg-gradient-to-r from-blue-500 to-green-500 rounded" />
1047
+ <span>Actual</span>
1048
+ </div>
1049
+ </div>
1050
+ </div>
1051
+ <BurnChart
1052
+ state={state}
1053
+ width={containerWidth - 32}
1054
+ height={height}
1055
+ showProjection={showProjection}
1056
+ showIdealLine={showIdealLine}
1057
+ animated={animated}
1058
+ formatValue={formatValue}
1059
+ />
1060
+ </div>
1061
+
1062
+ {/* Trends */}
1063
+ {showTrends && spendHistory.length > 0 && (
1064
+ <TrendIndicators state={state} formatValue={formatValue} />
1065
+ )}
1066
+
1067
+ {/* Category Breakdown */}
1068
+ {showCategories && categories.length > 0 && (
1069
+ <div className="rounded-lg border bg-card p-4">
1070
+ <CategoryBreakdown categories={categories} formatValue={formatValue} />
1071
+ </div>
1072
+ )}
1073
+ </div>
1074
+ )
1075
+ }
1076
+
1077
+ // ============================================================================
1078
+ // Custom Hook: useBudgetBurn
1079
+ // ============================================================================
1080
+
1081
+ export interface UseBudgetBurnOptions {
1082
+ /** Total budget amount */
1083
+ budget: number
1084
+ /** Budget period start date */
1085
+ startDate: Date
1086
+ /** Budget period end date */
1087
+ endDate: Date
1088
+ /** Initial spending history */
1089
+ initialHistory?: SpendDataPoint[]
1090
+ /** Initial categories */
1091
+ initialCategories?: BudgetCategory[]
1092
+ /** Danger threshold percentage */
1093
+ dangerThreshold?: number
1094
+ /** Warning threshold percentage */
1095
+ warningThreshold?: number
1096
+ /** Callback when danger zone is entered */
1097
+ onDangerZone?: () => void
1098
+ /** Callback when budget is depleted */
1099
+ onBudgetDepleted?: () => void
1100
+ }
1101
+
1102
+ export function useBudgetBurn({
1103
+ budget,
1104
+ startDate,
1105
+ endDate,
1106
+ initialHistory = [],
1107
+ initialCategories = [],
1108
+ dangerThreshold = 30,
1109
+ warningThreshold = 15,
1110
+ onDangerZone,
1111
+ onBudgetDepleted,
1112
+ }: UseBudgetBurnOptions) {
1113
+ const [spendHistory, setSpendHistory] = React.useState<SpendDataPoint[]>(initialHistory)
1114
+ const [categories, setCategories] = React.useState<BudgetCategory[]>(initialCategories)
1115
+
1116
+ // Calculate state
1117
+ const state = React.useMemo(
1118
+ () => calculateBurnState(budget, startDate, endDate, spendHistory, categories, dangerThreshold, warningThreshold),
1119
+ [budget, startDate, endDate, spendHistory, categories, dangerThreshold, warningThreshold]
1120
+ )
1121
+
1122
+ // Track danger zone and depletion
1123
+ const prevDangerZoneRef = React.useRef(state.isDangerZone)
1124
+ const prevDepletedRef = React.useRef(state.totalSpent >= state.totalBudget)
1125
+
1126
+ React.useEffect(() => {
1127
+ if (state.isDangerZone && !prevDangerZoneRef.current) {
1128
+ onDangerZone?.()
1129
+ }
1130
+ prevDangerZoneRef.current = state.isDangerZone
1131
+
1132
+ const isDepleted = state.totalSpent >= state.totalBudget
1133
+ if (isDepleted && !prevDepletedRef.current) {
1134
+ onBudgetDepleted?.()
1135
+ }
1136
+ prevDepletedRef.current = isDepleted
1137
+ }, [state.isDangerZone, state.totalSpent, state.totalBudget, onDangerZone, onBudgetDepleted])
1138
+
1139
+ // Add spend entry
1140
+ const addSpend = React.useCallback((date: Date, amount: number, categoryBreakdown?: Record<string, number>) => {
1141
+ setSpendHistory((prev) => [
1142
+ ...prev,
1143
+ { date, amount, categories: categoryBreakdown },
1144
+ ])
1145
+
1146
+ // Update categories if provided
1147
+ if (categoryBreakdown) {
1148
+ setCategories((prev) =>
1149
+ prev.map((cat) => ({
1150
+ ...cat,
1151
+ spent: cat.spent + (categoryBreakdown[cat.id] || 0),
1152
+ }))
1153
+ )
1154
+ }
1155
+ }, [])
1156
+
1157
+ // Remove spend entry
1158
+ const removeSpend = React.useCallback((date: Date) => {
1159
+ setSpendHistory((prev) => {
1160
+ const entry = prev.find((p) => p.date.getTime() === date.getTime())
1161
+ if (entry?.categories) {
1162
+ setCategories((cats) =>
1163
+ cats.map((cat) => ({
1164
+ ...cat,
1165
+ spent: Math.max(0, cat.spent - (entry.categories?.[cat.id] || 0)),
1166
+ }))
1167
+ )
1168
+ }
1169
+ return prev.filter((p) => p.date.getTime() !== date.getTime())
1170
+ })
1171
+ }, [])
1172
+
1173
+ // Update category
1174
+ const updateCategory = React.useCallback((id: string, updates: Partial<BudgetCategory>) => {
1175
+ setCategories((prev) =>
1176
+ prev.map((cat) => (cat.id === id ? { ...cat, ...updates } : cat))
1177
+ )
1178
+ }, [])
1179
+
1180
+ // Add category
1181
+ const addCategory = React.useCallback((category: BudgetCategory) => {
1182
+ setCategories((prev) => [...prev, category])
1183
+ }, [])
1184
+
1185
+ // Remove category
1186
+ const removeCategory = React.useCallback((id: string) => {
1187
+ setCategories((prev) => prev.filter((cat) => cat.id !== id))
1188
+ }, [])
1189
+
1190
+ // Reset all spending
1191
+ const reset = React.useCallback(() => {
1192
+ setSpendHistory([])
1193
+ setCategories((prev) => prev.map((cat) => ({ ...cat, spent: 0 })))
1194
+ }, [])
1195
+
1196
+ // Get forecast
1197
+ const getForecast = React.useCallback((days: number) => {
1198
+ const projectedSpend = state.totalSpent + state.burnRate * days
1199
+ const projectedRemaining = Math.max(0, budget - projectedSpend)
1200
+ const projectedPercentUsed = Math.min(100, (projectedSpend / budget) * 100)
1201
+
1202
+ return {
1203
+ projectedSpend,
1204
+ projectedRemaining,
1205
+ projectedPercentUsed,
1206
+ willDeplete: projectedSpend >= budget,
1207
+ daysUntilDepletion: state.burnRate > 0
1208
+ ? Math.ceil((budget - state.totalSpent) / state.burnRate)
1209
+ : Infinity,
1210
+ }
1211
+ }, [budget, state.totalSpent, state.burnRate])
1212
+
1213
+ return {
1214
+ // State
1215
+ ...state,
1216
+ spendHistory,
1217
+ categories,
1218
+
1219
+ // Actions
1220
+ addSpend,
1221
+ removeSpend,
1222
+ updateCategory,
1223
+ addCategory,
1224
+ removeCategory,
1225
+ reset,
1226
+ setSpendHistory,
1227
+ setCategories,
1228
+
1229
+ // Utilities
1230
+ getForecast,
1231
+ }
1232
+ }
1233
+
1234
+ export default WakaBudgetBurn