@wakastellar/ui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/cli/commands/add.d.ts +7 -0
  2. package/dist/cli/commands/init.d.ts +6 -0
  3. package/dist/cli/commands/list.d.ts +5 -0
  4. package/dist/cli/commands/search.d.ts +1 -0
  5. package/dist/cli/index.cjs +4844 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/utils/config.d.ts +29 -0
  8. package/dist/cli/utils/logger.d.ts +20 -0
  9. package/dist/cli/utils/registry.d.ts +23 -0
  10. package/package.json +14 -3
  11. package/src/blocks/activity-timeline/index.tsx +586 -0
  12. package/src/blocks/calendar-view/index.tsx +756 -0
  13. package/src/blocks/chat/index.tsx +1018 -0
  14. package/src/blocks/chat/widget.tsx +504 -0
  15. package/src/blocks/dashboard/index.tsx +522 -0
  16. package/src/blocks/empty-states/index.tsx +452 -0
  17. package/src/blocks/error-pages/index.tsx +426 -0
  18. package/src/blocks/faq/index.tsx +479 -0
  19. package/src/blocks/file-manager/index.tsx +890 -0
  20. package/src/blocks/footer/index.tsx +133 -0
  21. package/src/blocks/header/index.tsx +357 -0
  22. package/src/blocks/headtab/index.tsx +139 -0
  23. package/src/blocks/i18n-editor/index.tsx +1016 -0
  24. package/src/blocks/index.ts +80 -0
  25. package/src/blocks/kanban-board/index.tsx +779 -0
  26. package/src/blocks/landing/index.tsx +677 -0
  27. package/src/blocks/language-selector/index.tsx +88 -0
  28. package/src/blocks/layout/index.tsx +159 -0
  29. package/src/blocks/login/index.tsx +339 -0
  30. package/src/blocks/login/types.ts +131 -0
  31. package/src/blocks/pricing/index.tsx +564 -0
  32. package/src/blocks/profile/index.tsx +746 -0
  33. package/src/blocks/settings/index.tsx +558 -0
  34. package/src/blocks/sidebar/index.tsx +713 -0
  35. package/src/blocks/theme-creator-block/index.tsx +835 -0
  36. package/src/blocks/user-management/index.tsx +1037 -0
  37. package/src/blocks/wizard/index.tsx +719 -0
  38. package/src/components/DataTable/DataTable.tsx +406 -0
  39. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  40. package/src/components/DataTable/DataTableBody.tsx +216 -0
  41. package/src/components/DataTable/DataTableCell.tsx +172 -0
  42. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  43. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  44. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  45. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  46. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  47. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  48. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  49. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  50. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  51. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  52. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  53. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  54. package/src/components/DataTable/README.md +446 -0
  55. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  56. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  57. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  58. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  59. package/src/components/DataTable/formatters/index.ts +335 -0
  60. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  61. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  62. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  64. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  65. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  66. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  67. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  68. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  69. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  70. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  71. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  72. package/src/components/DataTable/index.ts +81 -0
  73. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  74. package/src/components/DataTable/templates/index.tsx +803 -0
  75. package/src/components/DataTable/types.ts +504 -0
  76. package/src/components/DataTable/utils.ts +164 -0
  77. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  78. package/src/components/accordion/index.tsx +61 -0
  79. package/src/components/alert/index.tsx +61 -0
  80. package/src/components/alert-dialog/index.tsx +146 -0
  81. package/src/components/aspect-ratio/index.tsx +12 -0
  82. package/src/components/avatar/index.tsx +54 -0
  83. package/src/components/badge/Badge.stories.tsx +64 -0
  84. package/src/components/badge/index.tsx +38 -0
  85. package/src/components/button/Button.stories.tsx +173 -0
  86. package/src/components/button/index.tsx +56 -0
  87. package/src/components/calendar/index.tsx +73 -0
  88. package/src/components/card/index.tsx +78 -0
  89. package/src/components/checkbox/index.tsx +34 -0
  90. package/src/components/code/index.tsx +229 -0
  91. package/src/components/collapsible/index.tsx +16 -0
  92. package/src/components/command/index.tsx +162 -0
  93. package/src/components/context-menu/index.tsx +204 -0
  94. package/src/components/dialog/index.tsx +126 -0
  95. package/src/components/dropdown-menu/index.tsx +204 -0
  96. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  97. package/src/components/error-boundary/index.ts +7 -0
  98. package/src/components/form/index.tsx +183 -0
  99. package/src/components/hover-card/index.tsx +33 -0
  100. package/src/components/index.ts +368 -0
  101. package/src/components/input/Input.stories.tsx +100 -0
  102. package/src/components/input/index.tsx +27 -0
  103. package/src/components/input-otp/index.tsx +277 -0
  104. package/src/components/label/index.tsx +30 -0
  105. package/src/components/language-selector/index.tsx +341 -0
  106. package/src/components/menubar/index.tsx +240 -0
  107. package/src/components/navigation-menu/index.tsx +134 -0
  108. package/src/components/popover/index.tsx +35 -0
  109. package/src/components/progress/index.tsx +32 -0
  110. package/src/components/radio-group/index.tsx +48 -0
  111. package/src/components/scroll-area/index.tsx +52 -0
  112. package/src/components/select/index.tsx +164 -0
  113. package/src/components/separator/index.tsx +35 -0
  114. package/src/components/sheet/index.tsx +147 -0
  115. package/src/components/skeleton/index.tsx +22 -0
  116. package/src/components/slider/index.tsx +32 -0
  117. package/src/components/switch/index.tsx +33 -0
  118. package/src/components/table/index.tsx +117 -0
  119. package/src/components/tabs/index.tsx +59 -0
  120. package/src/components/textarea/index.tsx +30 -0
  121. package/src/components/theme-selector/index.tsx +327 -0
  122. package/src/components/toast/index.tsx +133 -0
  123. package/src/components/toaster/index.tsx +34 -0
  124. package/src/components/toggle/index.tsx +49 -0
  125. package/src/components/tooltip/index.tsx +34 -0
  126. package/src/components/typography/index.tsx +276 -0
  127. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  128. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  129. package/src/components/waka-activity-feed/index.tsx +686 -0
  130. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  131. package/src/components/waka-admincrumb/index.tsx +349 -0
  132. package/src/components/waka-alert-stack/index.tsx +827 -0
  133. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  134. package/src/components/waka-approval-chain/index.tsx +766 -0
  135. package/src/components/waka-audit-log/index.tsx +1475 -0
  136. package/src/components/waka-autocomplete/index.tsx +358 -0
  137. package/src/components/waka-badge-showcase/index.tsx +704 -0
  138. package/src/components/waka-barcode/index.tsx +260 -0
  139. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  140. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  141. package/src/components/waka-breadcrumb/index.tsx +376 -0
  142. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  143. package/src/components/waka-budget-burn/index.tsx +1234 -0
  144. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  145. package/src/components/waka-carousel/index.tsx +893 -0
  146. package/src/components/waka-cart-summary/index.tsx +1055 -0
  147. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  148. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  149. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  150. package/src/components/waka-charts/WakaChart.tsx +124 -0
  151. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  152. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  153. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  154. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  155. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  156. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  157. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  158. package/src/components/waka-charts/index.ts +90 -0
  159. package/src/components/waka-charts/types.ts +330 -0
  160. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  161. package/src/components/waka-checklist/index.tsx +1067 -0
  162. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  163. package/src/components/waka-cohort-table/index.tsx +1011 -0
  164. package/src/components/waka-color-picker/index.tsx +447 -0
  165. package/src/components/waka-combo-counter/index.tsx +864 -0
  166. package/src/components/waka-combobox/index.tsx +497 -0
  167. package/src/components/waka-command-bar/index.tsx +403 -0
  168. package/src/components/waka-compare-period/index.tsx +1230 -0
  169. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  170. package/src/components/waka-contribution-graph/index.tsx +552 -0
  171. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  172. package/src/components/waka-coupon-input/index.tsx +592 -0
  173. package/src/components/waka-credit-card-input/index.tsx +982 -0
  174. package/src/components/waka-daily-reward/index.tsx +762 -0
  175. package/src/components/waka-date-range-picker/index.tsx +378 -0
  176. package/src/components/waka-datetime-picker/index.tsx +793 -0
  177. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  178. package/src/components/waka-deployment-lane/index.tsx +673 -0
  179. package/src/components/waka-device-trust/index.tsx +1259 -0
  180. package/src/components/waka-dock/index.tsx +285 -0
  181. package/src/components/waka-drawer/index.tsx +319 -0
  182. package/src/components/waka-empty-state/index.tsx +545 -0
  183. package/src/components/waka-error-shake/index.tsx +398 -0
  184. package/src/components/waka-feature-announcement/index.tsx +991 -0
  185. package/src/components/waka-file-upload/index.tsx +437 -0
  186. package/src/components/waka-floating-nav/index.tsx +413 -0
  187. package/src/components/waka-flow-diagram/index.tsx +508 -0
  188. package/src/components/waka-funnel-chart/index.tsx +823 -0
  189. package/src/components/waka-glow-card/index.tsx +246 -0
  190. package/src/components/waka-goal-progress/index.tsx +1025 -0
  191. package/src/components/waka-haptic-button/index.tsx +388 -0
  192. package/src/components/waka-health-pulse/index.tsx +451 -0
  193. package/src/components/waka-heatmap/index.tsx +1026 -0
  194. package/src/components/waka-hotspot/index.tsx +682 -0
  195. package/src/components/waka-image/index.tsx +373 -0
  196. package/src/components/waka-incident-timeline/index.tsx +686 -0
  197. package/src/components/waka-invoice-preview/index.tsx +829 -0
  198. package/src/components/waka-kanban/index.tsx +646 -0
  199. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  200. package/src/components/waka-leaderboard/index.tsx +746 -0
  201. package/src/components/waka-level-progress/index.tsx +665 -0
  202. package/src/components/waka-liquid-button/index.tsx +520 -0
  203. package/src/components/waka-loading-orbit/index.tsx +478 -0
  204. package/src/components/waka-loot-box/index.tsx +1091 -0
  205. package/src/components/waka-magic-link/index.tsx +321 -0
  206. package/src/components/waka-magnetic-button/index.tsx +567 -0
  207. package/src/components/waka-mention-input/index.tsx +953 -0
  208. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  209. package/src/components/waka-milestone-road/index.tsx +1064 -0
  210. package/src/components/waka-modal/index.tsx +374 -0
  211. package/src/components/waka-morph-button/index.tsx +495 -0
  212. package/src/components/waka-network-topology/index.tsx +801 -0
  213. package/src/components/waka-notifications/index.tsx +414 -0
  214. package/src/components/waka-number-input/index.tsx +373 -0
  215. package/src/components/waka-orbital-menu/index.tsx +445 -0
  216. package/src/components/waka-order-tracker/index.tsx +1041 -0
  217. package/src/components/waka-pagination/index.tsx +393 -0
  218. package/src/components/waka-password-strength/index.tsx +824 -0
  219. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  220. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  221. package/src/components/waka-phone-input/index.tsx +801 -0
  222. package/src/components/waka-pipeline-view/index.tsx +604 -0
  223. package/src/components/waka-player-card/index.tsx +691 -0
  224. package/src/components/waka-points-popup/index.tsx +366 -0
  225. package/src/components/waka-power-up/index.tsx +1155 -0
  226. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  227. package/src/components/waka-pricing-table/index.tsx +755 -0
  228. package/src/components/waka-product-card/index.tsx +786 -0
  229. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  230. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  231. package/src/components/waka-qrcode/index.tsx +232 -0
  232. package/src/components/waka-quest-card/index.tsx +1275 -0
  233. package/src/components/waka-quota-bar/index.tsx +693 -0
  234. package/src/components/waka-radar-score/index.tsx +512 -0
  235. package/src/components/waka-rank-badge/index.tsx +813 -0
  236. package/src/components/waka-rating-input/index.tsx +560 -0
  237. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  238. package/src/components/waka-region-map/index.tsx +730 -0
  239. package/src/components/waka-resource-gauge/index.tsx +654 -0
  240. package/src/components/waka-resource-pool/index.tsx +1035 -0
  241. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  242. package/src/components/waka-rollback-slider/index.tsx +891 -0
  243. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  244. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  245. package/src/components/waka-scratch-card/index.tsx +914 -0
  246. package/src/components/waka-season-pass/index.tsx +886 -0
  247. package/src/components/waka-security-score/index.tsx +1126 -0
  248. package/src/components/waka-segmented-control/index.tsx +238 -0
  249. package/src/components/waka-server-rack/index.tsx +764 -0
  250. package/src/components/waka-session-manager/index.tsx +815 -0
  251. package/src/components/waka-signature-pad/index.tsx +744 -0
  252. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  253. package/src/components/waka-skill-tree/index.tsx +1031 -0
  254. package/src/components/waka-sla-tracker/index.tsx +798 -0
  255. package/src/components/waka-slider-range/index.tsx +765 -0
  256. package/src/components/waka-spin-wheel/index.tsx +671 -0
  257. package/src/components/waka-spinner/index.tsx +284 -0
  258. package/src/components/waka-spotlight/index.tsx +410 -0
  259. package/src/components/waka-stat/index.tsx +428 -0
  260. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  261. package/src/components/waka-status-matrix/index.tsx +565 -0
  262. package/src/components/waka-stepper/index.tsx +489 -0
  263. package/src/components/waka-streak-counter/index.tsx +334 -0
  264. package/src/components/waka-success-explosion/index.tsx +453 -0
  265. package/src/components/waka-swipe-card/index.tsx +574 -0
  266. package/src/components/waka-tabs-morph/index.tsx +509 -0
  267. package/src/components/waka-tag-input/index.tsx +877 -0
  268. package/src/components/waka-team-banner/index.tsx +1183 -0
  269. package/src/components/waka-terminal-output/index.tsx +836 -0
  270. package/src/components/waka-theme-creator/index.tsx +762 -0
  271. package/src/components/waka-theme-manager/index.tsx +654 -0
  272. package/src/components/waka-thread-view/index.tsx +874 -0
  273. package/src/components/waka-tilt-card/index.tsx +250 -0
  274. package/src/components/waka-time-picker/index.tsx +479 -0
  275. package/src/components/waka-timeline/index.tsx +385 -0
  276. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  277. package/src/components/waka-tour-guide/index.tsx +920 -0
  278. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  279. package/src/components/waka-tree/index.tsx +557 -0
  280. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  281. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  282. package/src/components/waka-typewriter/index.tsx +566 -0
  283. package/src/components/waka-typing-indicator/index.tsx +649 -0
  284. package/src/components/waka-versus-card/index.tsx +1026 -0
  285. package/src/components/waka-video/index.tsx +557 -0
  286. package/src/components/waka-video-call/index.tsx +1087 -0
  287. package/src/components/waka-virtual-list/index.tsx +327 -0
  288. package/src/components/waka-voice-message/index.tsx +1019 -0
  289. package/src/components/waka-welcome-modal/index.tsx +790 -0
  290. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,827 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import {
6
+ AlertTriangle,
7
+ AlertCircle,
8
+ Info,
9
+ Check,
10
+ Clock,
11
+ ChevronDown,
12
+ ChevronUp,
13
+ X,
14
+ Bell,
15
+ BellOff,
16
+ Volume2,
17
+ } from "lucide-react"
18
+
19
+ // ============================================
20
+ // TYPES
21
+ // ============================================
22
+
23
+ export type AlertSeverity = "critical" | "warning" | "info"
24
+
25
+ export interface Alert {
26
+ id: string
27
+ title: string
28
+ description?: string
29
+ severity: AlertSeverity
30
+ source?: string
31
+ timestamp: Date
32
+ acknowledged?: boolean
33
+ acknowledgedBy?: string
34
+ snoozedUntil?: Date
35
+ count?: number
36
+ }
37
+
38
+ export interface WakaAlertStackProps {
39
+ /** Array of alerts to display */
40
+ alerts: Alert[]
41
+ /** Callback when an alert is acknowledged */
42
+ onAcknowledge?: (alertId: string) => void
43
+ /** Callback when an alert is snoozed */
44
+ onSnooze?: (alertId: string, duration: number) => void
45
+ /** Callback when an alert is dismissed */
46
+ onDismiss?: (alertId: string) => void
47
+ /** Maximum number of visible alerts in the stack */
48
+ maxVisible?: number
49
+ /** Whether to group similar alerts */
50
+ groupSimilar?: boolean
51
+ /** Whether to animate entry/exit */
52
+ animated?: boolean
53
+ /** Additional CSS classes */
54
+ className?: string
55
+ }
56
+
57
+ // ============================================
58
+ // SEVERITY CONFIG
59
+ // ============================================
60
+
61
+ const severityConfig: Record<
62
+ AlertSeverity,
63
+ {
64
+ icon: React.ElementType
65
+ color: string
66
+ bgColor: string
67
+ borderColor: string
68
+ ringColor: string
69
+ label: string
70
+ priority: number
71
+ }
72
+ > = {
73
+ critical: {
74
+ icon: AlertTriangle,
75
+ color: "text-red-600 dark:text-red-300",
76
+ bgColor: "bg-red-50 dark:bg-red-900/40",
77
+ borderColor: "border-red-500 dark:border-red-400",
78
+ ringColor: "ring-red-500/20 dark:ring-red-400/30",
79
+ label: "Critical",
80
+ priority: 3,
81
+ },
82
+ warning: {
83
+ icon: AlertCircle,
84
+ color: "text-amber-600 dark:text-amber-300",
85
+ bgColor: "bg-amber-50 dark:bg-amber-900/40",
86
+ borderColor: "border-amber-500 dark:border-amber-400",
87
+ ringColor: "ring-amber-500/20 dark:ring-amber-400/30",
88
+ label: "Warning",
89
+ priority: 2,
90
+ },
91
+ info: {
92
+ icon: Info,
93
+ color: "text-blue-600 dark:text-blue-300",
94
+ bgColor: "bg-blue-50 dark:bg-blue-900/40",
95
+ borderColor: "border-blue-500 dark:border-blue-400",
96
+ ringColor: "ring-blue-500/20 dark:ring-blue-400/30",
97
+ label: "Info",
98
+ priority: 1,
99
+ },
100
+ }
101
+
102
+ // ============================================
103
+ // SNOOZE DURATIONS
104
+ // ============================================
105
+
106
+ const snoozeDurations = [
107
+ { label: "5 min", value: 5 * 60 * 1000 },
108
+ { label: "15 min", value: 15 * 60 * 1000 },
109
+ { label: "30 min", value: 30 * 60 * 1000 },
110
+ { label: "1 hour", value: 60 * 60 * 1000 },
111
+ { label: "4 hours", value: 4 * 60 * 60 * 1000 },
112
+ ]
113
+
114
+ // ============================================
115
+ // UTILITY FUNCTIONS
116
+ // ============================================
117
+
118
+ function formatRelativeTime(date: Date): string {
119
+ const now = new Date()
120
+ const diffMs = now.getTime() - date.getTime()
121
+ const diffMins = Math.floor(diffMs / 60000)
122
+ const diffHours = Math.floor(diffMs / 3600000)
123
+ const diffDays = Math.floor(diffMs / 86400000)
124
+
125
+ if (diffMins < 1) return "Just now"
126
+ if (diffMins < 60) return `${diffMins}m ago`
127
+ if (diffHours < 24) return `${diffHours}h ago`
128
+ if (diffDays < 7) return `${diffDays}d ago`
129
+ return date.toLocaleDateString()
130
+ }
131
+
132
+ function formatDuration(date: Date): string {
133
+ const now = new Date()
134
+ const diffMs = now.getTime() - date.getTime()
135
+ const diffMins = Math.floor(diffMs / 60000)
136
+ const diffHours = Math.floor(diffMs / 3600000)
137
+
138
+ if (diffMins < 60) return `${diffMins}m`
139
+ if (diffHours < 24) return `${diffHours}h ${diffMins % 60}m`
140
+ return `${Math.floor(diffHours / 24)}d ${diffHours % 24}h`
141
+ }
142
+
143
+ function formatSnoozeRemaining(snoozedUntil: Date): string {
144
+ const now = new Date()
145
+ const diffMs = snoozedUntil.getTime() - now.getTime()
146
+
147
+ if (diffMs <= 0) return "Expired"
148
+
149
+ const diffMins = Math.floor(diffMs / 60000)
150
+ const diffHours = Math.floor(diffMs / 3600000)
151
+
152
+ if (diffMins < 60) return `${diffMins}m remaining`
153
+ return `${diffHours}h ${diffMins % 60}m remaining`
154
+ }
155
+
156
+ // ============================================
157
+ // HOOKS
158
+ // ============================================
159
+
160
+ export interface UseSoundNotificationOptions {
161
+ enabled?: boolean
162
+ volume?: number
163
+ soundUrl?: string
164
+ }
165
+
166
+ export function useSoundNotification(options: UseSoundNotificationOptions = {}) {
167
+ const { enabled = true, volume = 0.5, soundUrl } = options
168
+ const audioRef = React.useRef<HTMLAudioElement | null>(null)
169
+ const [isMuted, setIsMuted] = React.useState(!enabled)
170
+
171
+ React.useEffect(() => {
172
+ if (typeof window !== "undefined" && soundUrl) {
173
+ audioRef.current = new Audio(soundUrl)
174
+ audioRef.current.volume = volume
175
+ }
176
+ return () => {
177
+ if (audioRef.current) {
178
+ audioRef.current.pause()
179
+ audioRef.current = null
180
+ }
181
+ }
182
+ }, [soundUrl, volume])
183
+
184
+ const playSound = React.useCallback(() => {
185
+ if (!isMuted && audioRef.current) {
186
+ audioRef.current.currentTime = 0
187
+ audioRef.current.play().catch(() => {
188
+ // Autoplay may be blocked
189
+ })
190
+ }
191
+ }, [isMuted])
192
+
193
+ const toggleMute = React.useCallback(() => {
194
+ setIsMuted((prev) => !prev)
195
+ }, [])
196
+
197
+ return {
198
+ playSound,
199
+ isMuted,
200
+ toggleMute,
201
+ }
202
+ }
203
+
204
+ export interface UseAlertStackOptions {
205
+ onNewAlert?: (alert: Alert) => void
206
+ playSoundOnCritical?: boolean
207
+ }
208
+
209
+ export function useAlertStack(
210
+ initialAlerts: Alert[] = [],
211
+ options: UseAlertStackOptions = {}
212
+ ) {
213
+ const [alerts, setAlerts] = React.useState<Alert[]>(initialAlerts)
214
+ const { onNewAlert, playSoundOnCritical = true } = options
215
+ const prevAlertsRef = React.useRef<Set<string>>(new Set(initialAlerts.map((a) => a.id)))
216
+
217
+ const addAlert = React.useCallback(
218
+ (alert: Omit<Alert, "id" | "timestamp">) => {
219
+ const newAlert: Alert = {
220
+ ...alert,
221
+ id: `alert-${Date.now()}-${Math.random().toString(36).slice(2)}`,
222
+ timestamp: new Date(),
223
+ }
224
+ setAlerts((prev) => [newAlert, ...prev])
225
+ onNewAlert?.(newAlert)
226
+ return newAlert
227
+ },
228
+ [onNewAlert]
229
+ )
230
+
231
+ const acknowledgeAlert = React.useCallback((alertId: string, acknowledgedBy?: string) => {
232
+ setAlerts((prev) =>
233
+ prev.map((a) =>
234
+ a.id === alertId
235
+ ? { ...a, acknowledged: true, acknowledgedBy }
236
+ : a
237
+ )
238
+ )
239
+ }, [])
240
+
241
+ const snoozeAlert = React.useCallback((alertId: string, duration: number) => {
242
+ const snoozedUntil = new Date(Date.now() + duration)
243
+ setAlerts((prev) =>
244
+ prev.map((a) =>
245
+ a.id === alertId ? { ...a, snoozedUntil } : a
246
+ )
247
+ )
248
+ }, [])
249
+
250
+ const dismissAlert = React.useCallback((alertId: string) => {
251
+ setAlerts((prev) => prev.filter((a) => a.id !== alertId))
252
+ }, [])
253
+
254
+ const clearAcknowledged = React.useCallback(() => {
255
+ setAlerts((prev) => prev.filter((a) => !a.acknowledged))
256
+ }, [])
257
+
258
+ const clearAll = React.useCallback(() => {
259
+ setAlerts([])
260
+ }, [])
261
+
262
+ // Check for new alerts
263
+ React.useEffect(() => {
264
+ const currentIds = new Set(alerts.map((a) => a.id))
265
+ const newAlerts = alerts.filter((a) => !prevAlertsRef.current.has(a.id))
266
+
267
+ if (playSoundOnCritical) {
268
+ const hasCritical = newAlerts.some((a) => a.severity === "critical")
269
+ if (hasCritical) {
270
+ // Sound notification hook should be used externally
271
+ }
272
+ }
273
+
274
+ prevAlertsRef.current = currentIds
275
+ }, [alerts, playSoundOnCritical])
276
+
277
+ const activeAlerts = React.useMemo(() => {
278
+ const now = new Date()
279
+ return alerts.filter((a) => {
280
+ if (a.snoozedUntil && a.snoozedUntil > now) return false
281
+ return true
282
+ })
283
+ }, [alerts])
284
+
285
+ const snoozedAlerts = React.useMemo(() => {
286
+ const now = new Date()
287
+ return alerts.filter((a) => a.snoozedUntil && a.snoozedUntil > now)
288
+ }, [alerts])
289
+
290
+ return {
291
+ alerts,
292
+ activeAlerts,
293
+ snoozedAlerts,
294
+ addAlert,
295
+ acknowledgeAlert,
296
+ snoozeAlert,
297
+ dismissAlert,
298
+ clearAcknowledged,
299
+ clearAll,
300
+ }
301
+ }
302
+
303
+ // ============================================
304
+ // SUB-COMPONENTS
305
+ // ============================================
306
+
307
+ interface AlertCardProps {
308
+ alert: Alert
309
+ onAcknowledge?: (alertId: string) => void
310
+ onSnooze?: (alertId: string, duration: number) => void
311
+ onDismiss?: (alertId: string) => void
312
+ isStacked?: boolean
313
+ stackIndex?: number
314
+ animated?: boolean
315
+ }
316
+
317
+ function AlertCard({
318
+ alert,
319
+ onAcknowledge,
320
+ onSnooze,
321
+ onDismiss,
322
+ isStacked = false,
323
+ stackIndex = 0,
324
+ animated = true,
325
+ }: AlertCardProps) {
326
+ const [isExpanded, setIsExpanded] = React.useState(false)
327
+ const [showSnoozeMenu, setShowSnoozeMenu] = React.useState(false)
328
+ const snoozeMenuRef = React.useRef<HTMLDivElement>(null)
329
+
330
+ const config = severityConfig[alert.severity]
331
+ const Icon = config.icon
332
+ const isSnoozed = alert.snoozedUntil && alert.snoozedUntil > new Date()
333
+
334
+ // Close snooze menu when clicking outside
335
+ React.useEffect(() => {
336
+ function handleClickOutside(event: MouseEvent) {
337
+ if (snoozeMenuRef.current && !snoozeMenuRef.current.contains(event.target as Node)) {
338
+ setShowSnoozeMenu(false)
339
+ }
340
+ }
341
+ document.addEventListener("mousedown", handleClickOutside)
342
+ return () => document.removeEventListener("mousedown", handleClickOutside)
343
+ }, [])
344
+
345
+ const stackStyles = isStacked
346
+ ? {
347
+ transform: `translateY(${stackIndex * 8}px) scale(${1 - stackIndex * 0.02})`,
348
+ zIndex: 100 - stackIndex,
349
+ opacity: 1 - stackIndex * 0.15,
350
+ }
351
+ : {}
352
+
353
+ return (
354
+ <div
355
+ className={cn(
356
+ "relative w-full rounded-lg border-l-4 shadow-lg transition-all duration-300",
357
+ config.borderColor,
358
+ config.bgColor,
359
+ "ring-1",
360
+ config.ringColor,
361
+ alert.acknowledged && "opacity-60",
362
+ isSnoozed && "opacity-50",
363
+ animated && "animate-in slide-in-from-right-5 fade-in duration-300",
364
+ isStacked && stackIndex > 0 && "absolute top-0 left-0"
365
+ )}
366
+ style={stackStyles}
367
+ >
368
+ {/* Main Content */}
369
+ <div className="p-4">
370
+ {/* Header Row */}
371
+ <div className="flex items-start gap-3">
372
+ {/* Icon */}
373
+ <div
374
+ className={cn(
375
+ "flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center",
376
+ config.bgColor,
377
+ "ring-2",
378
+ config.ringColor
379
+ )}
380
+ >
381
+ <Icon className={cn("h-5 w-5", config.color)} />
382
+ </div>
383
+
384
+ {/* Content */}
385
+ <div className="flex-1 min-w-0">
386
+ <div className="flex items-start justify-between gap-2">
387
+ <div className="flex-1">
388
+ <div className="flex items-center gap-2">
389
+ <h4 className="font-semibold text-sm text-foreground">
390
+ {alert.title}
391
+ </h4>
392
+ {alert.count && alert.count > 1 && (
393
+ <span
394
+ className={cn(
395
+ "inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-medium",
396
+ config.bgColor,
397
+ config.color,
398
+ "ring-1",
399
+ config.ringColor
400
+ )}
401
+ >
402
+ {alert.count}
403
+ </span>
404
+ )}
405
+ </div>
406
+ {alert.source && (
407
+ <p className="text-xs text-muted-foreground mt-0.5">
408
+ {alert.source}
409
+ </p>
410
+ )}
411
+ </div>
412
+
413
+ {/* Dismiss Button */}
414
+ {onDismiss && (
415
+ <button
416
+ onClick={() => onDismiss(alert.id)}
417
+ className="flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
418
+ aria-label="Dismiss alert"
419
+ >
420
+ <X className="h-4 w-4 text-muted-foreground" />
421
+ </button>
422
+ )}
423
+ </div>
424
+
425
+ {/* Timestamp and Duration */}
426
+ <div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
427
+ <span>{formatRelativeTime(alert.timestamp)}</span>
428
+ <span className="text-muted-foreground/50">|</span>
429
+ <span className="flex items-center gap-1">
430
+ <Clock className="h-3 w-3" />
431
+ {formatDuration(alert.timestamp)}
432
+ </span>
433
+ {isSnoozed && alert.snoozedUntil && (
434
+ <>
435
+ <span className="text-muted-foreground/50">|</span>
436
+ <span className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
437
+ <BellOff className="h-3 w-3" />
438
+ {formatSnoozeRemaining(alert.snoozedUntil)}
439
+ </span>
440
+ </>
441
+ )}
442
+ </div>
443
+
444
+ {/* Acknowledged Info */}
445
+ {alert.acknowledged && (
446
+ <div className="flex items-center gap-1 mt-1 text-xs text-green-600 dark:text-green-400">
447
+ <Check className="h-3 w-3" />
448
+ <span>
449
+ Acknowledged
450
+ {alert.acknowledgedBy && ` by ${alert.acknowledgedBy}`}
451
+ </span>
452
+ </div>
453
+ )}
454
+ </div>
455
+ </div>
456
+
457
+ {/* Description (Expandable) */}
458
+ {alert.description && (
459
+ <div className="mt-3">
460
+ <button
461
+ onClick={() => setIsExpanded(!isExpanded)}
462
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
463
+ >
464
+ {isExpanded ? (
465
+ <ChevronUp className="h-3 w-3" />
466
+ ) : (
467
+ <ChevronDown className="h-3 w-3" />
468
+ )}
469
+ {isExpanded ? "Hide details" : "Show details"}
470
+ </button>
471
+
472
+ {isExpanded && (
473
+ <div className="mt-2 p-3 rounded-md bg-black/5 dark:bg-white/5 animate-in slide-in-from-top-2 fade-in duration-200">
474
+ <p className="text-sm text-muted-foreground whitespace-pre-wrap">
475
+ {alert.description}
476
+ </p>
477
+ </div>
478
+ )}
479
+ </div>
480
+ )}
481
+
482
+ {/* Action Buttons */}
483
+ {(!alert.acknowledged || onSnooze) && (
484
+ <div className="flex items-center gap-2 mt-3">
485
+ {/* Acknowledge Button */}
486
+ {onAcknowledge && !alert.acknowledged && (
487
+ <button
488
+ onClick={() => onAcknowledge(alert.id)}
489
+ className={cn(
490
+ "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors",
491
+ "bg-green-100 text-green-700 hover:bg-green-200",
492
+ "dark:bg-green-900/50 dark:text-green-300 dark:hover:bg-green-900/70"
493
+ )}
494
+ >
495
+ <Check className="h-3.5 w-3.5" />
496
+ Acknowledge
497
+ </button>
498
+ )}
499
+
500
+ {/* Snooze Button */}
501
+ {onSnooze && !isSnoozed && (
502
+ <div className="relative" ref={snoozeMenuRef}>
503
+ <button
504
+ onClick={() => setShowSnoozeMenu(!showSnoozeMenu)}
505
+ className={cn(
506
+ "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors",
507
+ "bg-amber-100 text-amber-700 hover:bg-amber-200",
508
+ "dark:bg-amber-900/50 dark:text-amber-300 dark:hover:bg-amber-900/70"
509
+ )}
510
+ >
511
+ <BellOff className="h-3.5 w-3.5" />
512
+ Snooze
513
+ <ChevronDown className="h-3 w-3" />
514
+ </button>
515
+
516
+ {/* Snooze Menu */}
517
+ {showSnoozeMenu && (
518
+ <div className="absolute bottom-full left-0 mb-1 py-1 bg-popover border rounded-md shadow-lg min-w-[120px] z-50 animate-in slide-in-from-bottom-2 fade-in duration-150">
519
+ {snoozeDurations.map((duration) => (
520
+ <button
521
+ key={duration.value}
522
+ onClick={() => {
523
+ onSnooze(alert.id, duration.value)
524
+ setShowSnoozeMenu(false)
525
+ }}
526
+ className="w-full px-3 py-1.5 text-xs text-left hover:bg-muted transition-colors"
527
+ >
528
+ {duration.label}
529
+ </button>
530
+ ))}
531
+ </div>
532
+ )}
533
+ </div>
534
+ )}
535
+ </div>
536
+ )}
537
+ </div>
538
+ </div>
539
+ )
540
+ }
541
+
542
+ interface StackedAlertsPreviewProps {
543
+ alerts: Alert[]
544
+ maxVisible: number
545
+ onExpand: () => void
546
+ }
547
+
548
+ function StackedAlertsPreview({
549
+ alerts,
550
+ maxVisible,
551
+ onExpand,
552
+ }: StackedAlertsPreviewProps) {
553
+ const visibleAlerts = alerts.slice(0, maxVisible)
554
+ const hiddenCount = alerts.length - maxVisible
555
+
556
+ return (
557
+ <div className="relative">
558
+ {/* Stacked cards effect */}
559
+ <div className="relative" style={{ height: `${56 + (Math.min(maxVisible, alerts.length) - 1) * 8}px` }}>
560
+ {visibleAlerts.map((alert, index) => {
561
+ const config = severityConfig[alert.severity]
562
+ const Icon = config.icon
563
+
564
+ return (
565
+ <div
566
+ key={alert.id}
567
+ className={cn(
568
+ "absolute left-0 right-0 h-14 rounded-lg border-l-4 shadow-md transition-all duration-300",
569
+ config.borderColor,
570
+ config.bgColor,
571
+ "ring-1",
572
+ config.ringColor,
573
+ "cursor-pointer hover:translate-y-[-2px]"
574
+ )}
575
+ style={{
576
+ transform: `translateY(${index * 8}px) scale(${1 - index * 0.02})`,
577
+ zIndex: 100 - index,
578
+ opacity: 1 - index * 0.15,
579
+ }}
580
+ onClick={onExpand}
581
+ >
582
+ <div className="flex items-center gap-3 p-3 h-full">
583
+ <Icon className={cn("h-5 w-5 flex-shrink-0", config.color)} />
584
+ <div className="flex-1 min-w-0">
585
+ <p className="text-sm font-medium truncate">{alert.title}</p>
586
+ <p className="text-xs text-muted-foreground">
587
+ {formatRelativeTime(alert.timestamp)}
588
+ </p>
589
+ </div>
590
+ {alert.count && alert.count > 1 && (
591
+ <span
592
+ className={cn(
593
+ "flex-shrink-0 inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-medium",
594
+ config.color
595
+ )}
596
+ >
597
+ {alert.count}
598
+ </span>
599
+ )}
600
+ </div>
601
+ </div>
602
+ )
603
+ })}
604
+ </div>
605
+
606
+ {/* Hidden count badge */}
607
+ {hiddenCount > 0 && (
608
+ <button
609
+ onClick={onExpand}
610
+ className="absolute -bottom-2 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-muted rounded-full text-xs font-medium text-muted-foreground hover:bg-muted/80 transition-colors shadow-sm"
611
+ >
612
+ +{hiddenCount} more
613
+ </button>
614
+ )}
615
+ </div>
616
+ )
617
+ }
618
+
619
+ // ============================================
620
+ // MAIN COMPONENT
621
+ // ============================================
622
+
623
+ export function WakaAlertStack({
624
+ alerts,
625
+ onAcknowledge,
626
+ onSnooze,
627
+ onDismiss,
628
+ maxVisible = 5,
629
+ groupSimilar = true,
630
+ animated = true,
631
+ className,
632
+ }: WakaAlertStackProps) {
633
+ const [isExpanded, setIsExpanded] = React.useState(false)
634
+
635
+ // Process and sort alerts by priority
636
+ const processedAlerts = React.useMemo(() => {
637
+ const now = new Date()
638
+
639
+ // Filter out snoozed alerts
640
+ let filteredAlerts = alerts.filter((a) => {
641
+ if (a.snoozedUntil && a.snoozedUntil > now) return false
642
+ return true
643
+ })
644
+
645
+ // Group similar alerts if enabled
646
+ if (groupSimilar) {
647
+ const grouped = new Map<string, Alert>()
648
+
649
+ filteredAlerts.forEach((alert) => {
650
+ const key = `${alert.severity}-${alert.title}-${alert.source || ""}`
651
+ const existing = grouped.get(key)
652
+
653
+ if (existing) {
654
+ // Update count and use most recent timestamp
655
+ grouped.set(key, {
656
+ ...existing,
657
+ count: (existing.count || 1) + 1,
658
+ timestamp:
659
+ alert.timestamp > existing.timestamp
660
+ ? alert.timestamp
661
+ : existing.timestamp,
662
+ })
663
+ } else {
664
+ grouped.set(key, { ...alert, count: alert.count || 1 })
665
+ }
666
+ })
667
+
668
+ filteredAlerts = Array.from(grouped.values())
669
+ }
670
+
671
+ // Sort by priority (critical first) then by timestamp (newest first)
672
+ return filteredAlerts.sort((a, b) => {
673
+ const priorityDiff =
674
+ severityConfig[b.severity].priority -
675
+ severityConfig[a.severity].priority
676
+ if (priorityDiff !== 0) return priorityDiff
677
+ return b.timestamp.getTime() - a.timestamp.getTime()
678
+ })
679
+ }, [alerts, groupSimilar])
680
+
681
+ // Get snoozed alerts for display
682
+ const snoozedAlerts = React.useMemo(() => {
683
+ const now = new Date()
684
+ return alerts.filter((a) => a.snoozedUntil && a.snoozedUntil > now)
685
+ }, [alerts])
686
+
687
+ // Summary stats
688
+ const stats = React.useMemo(() => {
689
+ return {
690
+ critical: processedAlerts.filter((a) => a.severity === "critical").length,
691
+ warning: processedAlerts.filter((a) => a.severity === "warning").length,
692
+ info: processedAlerts.filter((a) => a.severity === "info").length,
693
+ snoozed: snoozedAlerts.length,
694
+ }
695
+ }, [processedAlerts, snoozedAlerts])
696
+
697
+ if (alerts.length === 0) {
698
+ return (
699
+ <div
700
+ className={cn(
701
+ "flex flex-col items-center justify-center py-8 text-center rounded-lg border border-dashed",
702
+ className
703
+ )}
704
+ >
705
+ <Bell className="h-10 w-10 text-muted-foreground/50 mb-3" />
706
+ <p className="text-sm text-muted-foreground">No active alerts</p>
707
+ </div>
708
+ )
709
+ }
710
+
711
+ return (
712
+ <div className={cn("w-full", className)}>
713
+ {/* Summary Header */}
714
+ <div className="flex items-center justify-between mb-4">
715
+ <div className="flex items-center gap-3">
716
+ <h3 className="font-semibold text-sm">Alerts</h3>
717
+ <div className="flex items-center gap-2">
718
+ {stats.critical > 0 && (
719
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300">
720
+ <AlertTriangle className="h-3 w-3" />
721
+ {stats.critical}
722
+ </span>
723
+ )}
724
+ {stats.warning > 0 && (
725
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300">
726
+ <AlertCircle className="h-3 w-3" />
727
+ {stats.warning}
728
+ </span>
729
+ )}
730
+ {stats.info > 0 && (
731
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
732
+ <Info className="h-3 w-3" />
733
+ {stats.info}
734
+ </span>
735
+ )}
736
+ {stats.snoozed > 0 && (
737
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground">
738
+ <BellOff className="h-3 w-3" />
739
+ {stats.snoozed}
740
+ </span>
741
+ )}
742
+ </div>
743
+ </div>
744
+
745
+ {/* Expand/Collapse Toggle */}
746
+ {processedAlerts.length > 1 && (
747
+ <button
748
+ onClick={() => setIsExpanded(!isExpanded)}
749
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
750
+ >
751
+ {isExpanded ? (
752
+ <>
753
+ <ChevronUp className="h-3 w-3" />
754
+ Collapse
755
+ </>
756
+ ) : (
757
+ <>
758
+ <ChevronDown className="h-3 w-3" />
759
+ Expand all
760
+ </>
761
+ )}
762
+ </button>
763
+ )}
764
+ </div>
765
+
766
+ {/* Alert Stack */}
767
+ {isExpanded ? (
768
+ // Expanded view - show all alerts
769
+ <div className="space-y-3">
770
+ {processedAlerts.map((alert, index) => (
771
+ <AlertCard
772
+ key={alert.id}
773
+ alert={alert}
774
+ onAcknowledge={onAcknowledge}
775
+ onSnooze={onSnooze}
776
+ onDismiss={onDismiss}
777
+ animated={animated}
778
+ />
779
+ ))}
780
+
781
+ {/* Snoozed Alerts Section */}
782
+ {snoozedAlerts.length > 0 && (
783
+ <div className="pt-3 border-t">
784
+ <p className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
785
+ <BellOff className="h-3 w-3" />
786
+ Snoozed ({snoozedAlerts.length})
787
+ </p>
788
+ <div className="space-y-2">
789
+ {snoozedAlerts.map((alert) => (
790
+ <AlertCard
791
+ key={alert.id}
792
+ alert={alert}
793
+ onAcknowledge={onAcknowledge}
794
+ onSnooze={onSnooze}
795
+ onDismiss={onDismiss}
796
+ animated={false}
797
+ />
798
+ ))}
799
+ </div>
800
+ </div>
801
+ )}
802
+ </div>
803
+ ) : (
804
+ // Stacked view
805
+ <>
806
+ {processedAlerts.length === 1 ? (
807
+ <AlertCard
808
+ alert={processedAlerts[0]}
809
+ onAcknowledge={onAcknowledge}
810
+ onSnooze={onSnooze}
811
+ onDismiss={onDismiss}
812
+ animated={animated}
813
+ />
814
+ ) : (
815
+ <StackedAlertsPreview
816
+ alerts={processedAlerts}
817
+ maxVisible={maxVisible}
818
+ onExpand={() => setIsExpanded(true)}
819
+ />
820
+ )}
821
+ </>
822
+ )}
823
+ </div>
824
+ )
825
+ }
826
+
827
+ export default WakaAlertStack