@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,1067 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import {
6
+ Check,
7
+ ChevronDown,
8
+ ChevronRight,
9
+ Gift,
10
+ X,
11
+ Sparkles,
12
+ Trophy,
13
+ PartyPopper,
14
+ } from "lucide-react"
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export type TaskStatus = "pending" | "completed" | "skipped"
21
+
22
+ export interface ChecklistTask {
23
+ /** Unique identifier */
24
+ id: string
25
+ /** Task title */
26
+ title: string
27
+ /** Task description (shown when expanded) */
28
+ description?: string
29
+ /** Category/group the task belongs to */
30
+ category?: string
31
+ /** Action button label */
32
+ actionLabel?: string
33
+ /** Callback when action is triggered */
34
+ onAction?: () => void
35
+ /** Whether the task can be skipped */
36
+ skippable?: boolean
37
+ /** Custom icon */
38
+ icon?: React.ReactNode
39
+ /** Reward points for completing this task */
40
+ points?: number
41
+ /** Whether this task is required */
42
+ required?: boolean
43
+ }
44
+
45
+ export interface ChecklistCategory {
46
+ /** Category identifier */
47
+ id: string
48
+ /** Category label */
49
+ label: string
50
+ /** Category description */
51
+ description?: string
52
+ /** Custom icon */
53
+ icon?: React.ReactNode
54
+ }
55
+
56
+ export interface ChecklistState {
57
+ /** Map of task ID to status */
58
+ tasks: Record<string, TaskStatus>
59
+ /** Whether the checklist is collapsed */
60
+ collapsed: boolean
61
+ /** Set of expanded task IDs */
62
+ expandedTasks: Set<string>
63
+ }
64
+
65
+ export interface WakaChecklistProps {
66
+ /** Checklist identifier for localStorage persistence */
67
+ checklistId: string
68
+ /** List of tasks */
69
+ tasks: ChecklistTask[]
70
+ /** Category definitions */
71
+ categories?: ChecklistCategory[]
72
+ /** Title of the checklist */
73
+ title?: string
74
+ /** Subtitle/description */
75
+ subtitle?: string
76
+ /** Show progress bar */
77
+ showProgress?: boolean
78
+ /** Show completion percentage */
79
+ showPercentage?: boolean
80
+ /** Enable localStorage persistence */
81
+ persistent?: boolean
82
+ /** Show celebration on completion */
83
+ showCelebration?: boolean
84
+ /** Callback when all tasks are completed */
85
+ onComplete?: () => void
86
+ /** Callback when a task status changes */
87
+ onTaskChange?: (taskId: string, status: TaskStatus) => void
88
+ /** Initially collapsed */
89
+ defaultCollapsed?: boolean
90
+ /** Allow collapsing the checklist */
91
+ collapsible?: boolean
92
+ /** Size variant */
93
+ size?: "sm" | "default" | "lg"
94
+ /** Custom className */
95
+ className?: string
96
+ }
97
+
98
+ export interface UseChecklistOptions {
99
+ /** Checklist identifier for localStorage */
100
+ checklistId: string
101
+ /** List of tasks */
102
+ tasks: ChecklistTask[]
103
+ /** Enable localStorage persistence */
104
+ persistent?: boolean
105
+ /** Initially collapsed */
106
+ defaultCollapsed?: boolean
107
+ /** Callback when all tasks are completed */
108
+ onComplete?: () => void
109
+ /** Callback when a task status changes */
110
+ onTaskChange?: (taskId: string, status: TaskStatus) => void
111
+ }
112
+
113
+ export interface UseChecklistReturn {
114
+ /** Current state of all tasks */
115
+ taskStates: Record<string, TaskStatus>
116
+ /** Whether checklist is collapsed */
117
+ isCollapsed: boolean
118
+ /** Set of expanded task IDs */
119
+ expandedTasks: Set<string>
120
+ /** Mark a task as completed */
121
+ completeTask: (taskId: string) => void
122
+ /** Skip a task */
123
+ skipTask: (taskId: string) => void
124
+ /** Reset a task to pending */
125
+ resetTask: (taskId: string) => void
126
+ /** Toggle task expansion */
127
+ toggleTaskExpanded: (taskId: string) => void
128
+ /** Toggle checklist collapse */
129
+ toggleCollapsed: () => void
130
+ /** Reset entire checklist */
131
+ resetChecklist: () => void
132
+ /** Completion progress (0-100) */
133
+ progress: number
134
+ /** Number of completed tasks */
135
+ completedCount: number
136
+ /** Number of skipped tasks */
137
+ skippedCount: number
138
+ /** Total number of tasks */
139
+ totalCount: number
140
+ /** Whether all tasks are completed or skipped */
141
+ isComplete: boolean
142
+ /** Get status of a specific task */
143
+ getTaskStatus: (taskId: string) => TaskStatus
144
+ }
145
+
146
+ // ============================================================================
147
+ // localStorage Helpers
148
+ // ============================================================================
149
+
150
+ const STORAGE_KEY_PREFIX = "waka-checklist-"
151
+
152
+ function getStorageKey(checklistId: string): string {
153
+ return `${STORAGE_KEY_PREFIX}${checklistId}`
154
+ }
155
+
156
+ function loadFromStorage(checklistId: string): Partial<ChecklistState> | null {
157
+ if (typeof window === "undefined") return null
158
+ try {
159
+ const stored = localStorage.getItem(getStorageKey(checklistId))
160
+ if (stored) {
161
+ const parsed = JSON.parse(stored)
162
+ return {
163
+ ...parsed,
164
+ expandedTasks: new Set(parsed.expandedTasks || []),
165
+ }
166
+ }
167
+ } catch {
168
+ // Ignore storage errors
169
+ }
170
+ return null
171
+ }
172
+
173
+ function saveToStorage(checklistId: string, state: ChecklistState): void {
174
+ if (typeof window === "undefined") return
175
+ try {
176
+ localStorage.setItem(
177
+ getStorageKey(checklistId),
178
+ JSON.stringify({
179
+ ...state,
180
+ expandedTasks: Array.from(state.expandedTasks),
181
+ })
182
+ )
183
+ } catch {
184
+ // Ignore storage errors
185
+ }
186
+ }
187
+
188
+ // ============================================================================
189
+ // useChecklist Hook
190
+ // ============================================================================
191
+
192
+ export function useChecklist({
193
+ checklistId,
194
+ tasks,
195
+ persistent = true,
196
+ defaultCollapsed = false,
197
+ onComplete,
198
+ onTaskChange,
199
+ }: UseChecklistOptions): UseChecklistReturn {
200
+ // Initialize state from localStorage or defaults
201
+ const [state, setState] = React.useState<ChecklistState>(() => {
202
+ const stored = persistent ? loadFromStorage(checklistId) : null
203
+ return {
204
+ tasks: stored?.tasks || {},
205
+ collapsed: stored?.collapsed ?? defaultCollapsed,
206
+ expandedTasks: stored?.expandedTasks || new Set(),
207
+ }
208
+ })
209
+
210
+ const prevIsCompleteRef = React.useRef(false)
211
+
212
+ // Calculate derived values
213
+ const taskStates = state.tasks
214
+ const completedCount = Object.values(taskStates).filter(
215
+ (s) => s === "completed"
216
+ ).length
217
+ const skippedCount = Object.values(taskStates).filter(
218
+ (s) => s === "skipped"
219
+ ).length
220
+ const totalCount = tasks.length
221
+ const requiredTasks = tasks.filter((t) => t.required !== false)
222
+ const completedRequired = requiredTasks.filter(
223
+ (t) => taskStates[t.id] === "completed"
224
+ ).length
225
+ const skippedRequired = requiredTasks.filter(
226
+ (t) => taskStates[t.id] === "skipped"
227
+ ).length
228
+ const isComplete =
229
+ requiredTasks.length === 0 ||
230
+ completedRequired + skippedRequired >= requiredTasks.length
231
+ const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
232
+
233
+ // Persist state changes
234
+ React.useEffect(() => {
235
+ if (persistent) {
236
+ saveToStorage(checklistId, state)
237
+ }
238
+ }, [checklistId, persistent, state])
239
+
240
+ // Handle completion callback
241
+ React.useEffect(() => {
242
+ if (isComplete && !prevIsCompleteRef.current && completedCount > 0) {
243
+ onComplete?.()
244
+ }
245
+ prevIsCompleteRef.current = isComplete
246
+ }, [isComplete, completedCount, onComplete])
247
+
248
+ const completeTask = React.useCallback(
249
+ (taskId: string) => {
250
+ setState((prev) => ({
251
+ ...prev,
252
+ tasks: { ...prev.tasks, [taskId]: "completed" },
253
+ }))
254
+ onTaskChange?.(taskId, "completed")
255
+ },
256
+ [onTaskChange]
257
+ )
258
+
259
+ const skipTask = React.useCallback(
260
+ (taskId: string) => {
261
+ setState((prev) => ({
262
+ ...prev,
263
+ tasks: { ...prev.tasks, [taskId]: "skipped" },
264
+ }))
265
+ onTaskChange?.(taskId, "skipped")
266
+ },
267
+ [onTaskChange]
268
+ )
269
+
270
+ const resetTask = React.useCallback(
271
+ (taskId: string) => {
272
+ setState((prev) => {
273
+ const newTasks = { ...prev.tasks }
274
+ delete newTasks[taskId]
275
+ return { ...prev, tasks: newTasks }
276
+ })
277
+ onTaskChange?.(taskId, "pending")
278
+ },
279
+ [onTaskChange]
280
+ )
281
+
282
+ const toggleTaskExpanded = React.useCallback((taskId: string) => {
283
+ setState((prev) => {
284
+ const newExpanded = new Set(prev.expandedTasks)
285
+ if (newExpanded.has(taskId)) {
286
+ newExpanded.delete(taskId)
287
+ } else {
288
+ newExpanded.add(taskId)
289
+ }
290
+ return { ...prev, expandedTasks: newExpanded }
291
+ })
292
+ }, [])
293
+
294
+ const toggleCollapsed = React.useCallback(() => {
295
+ setState((prev) => ({ ...prev, collapsed: !prev.collapsed }))
296
+ }, [])
297
+
298
+ const resetChecklist = React.useCallback(() => {
299
+ setState({
300
+ tasks: {},
301
+ collapsed: defaultCollapsed,
302
+ expandedTasks: new Set(),
303
+ })
304
+ if (persistent) {
305
+ localStorage.removeItem(getStorageKey(checklistId))
306
+ }
307
+ }, [checklistId, defaultCollapsed, persistent])
308
+
309
+ const getTaskStatus = React.useCallback(
310
+ (taskId: string): TaskStatus => {
311
+ return taskStates[taskId] || "pending"
312
+ },
313
+ [taskStates]
314
+ )
315
+
316
+ return {
317
+ taskStates,
318
+ isCollapsed: state.collapsed,
319
+ expandedTasks: state.expandedTasks,
320
+ completeTask,
321
+ skipTask,
322
+ resetTask,
323
+ toggleTaskExpanded,
324
+ toggleCollapsed,
325
+ resetChecklist,
326
+ progress,
327
+ completedCount,
328
+ skippedCount,
329
+ totalCount,
330
+ isComplete,
331
+ getTaskStatus,
332
+ }
333
+ }
334
+
335
+ // ============================================================================
336
+ // Celebration Effect Component
337
+ // ============================================================================
338
+
339
+ interface CelebrationEffectProps {
340
+ active: boolean
341
+ onComplete?: () => void
342
+ }
343
+
344
+ function CelebrationEffect({ active, onComplete }: CelebrationEffectProps) {
345
+ const [isVisible, setIsVisible] = React.useState(false)
346
+
347
+ React.useEffect(() => {
348
+ if (active) {
349
+ setIsVisible(true)
350
+ const timer = setTimeout(() => {
351
+ setIsVisible(false)
352
+ onComplete?.()
353
+ }, 4000)
354
+ return () => clearTimeout(timer)
355
+ }
356
+ }, [active, onComplete])
357
+
358
+ if (!isVisible) return null
359
+
360
+ const confettiColors = [
361
+ "#fbbf24",
362
+ "#f59e0b",
363
+ "#ef4444",
364
+ "#8b5cf6",
365
+ "#06b6d4",
366
+ "#22c55e",
367
+ "#ec4899",
368
+ ]
369
+
370
+ return (
371
+ <div className="pointer-events-none fixed inset-0 z-50 overflow-hidden">
372
+ {/* Confetti burst */}
373
+ {[...Array(80)].map((_, i) => {
374
+ const angle = (i / 80) * 360
375
+ const distance = 150 + Math.random() * 250
376
+ const size = 6 + Math.random() * 10
377
+ const duration = 1.2 + Math.random() * 0.6
378
+
379
+ return (
380
+ <div
381
+ key={i}
382
+ className="absolute left-1/2 top-1/2 animate-confetti"
383
+ style={
384
+ {
385
+ width: size,
386
+ height: size,
387
+ backgroundColor:
388
+ confettiColors[
389
+ Math.floor(Math.random() * confettiColors.length)
390
+ ],
391
+ borderRadius: Math.random() > 0.5 ? "50%" : "2px",
392
+ "--angle": `${angle}deg`,
393
+ "--distance": `${distance}px`,
394
+ animationDuration: `${duration}s`,
395
+ animationDelay: `${Math.random() * 0.3}s`,
396
+ } as React.CSSProperties
397
+ }
398
+ />
399
+ )
400
+ })}
401
+
402
+ {/* Celebration message */}
403
+ <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 animate-celebration-popup">
404
+ <div className="flex flex-col items-center gap-4 rounded-2xl bg-gradient-to-br from-yellow-400 via-orange-500 to-pink-500 px-12 py-8 text-white shadow-2xl">
405
+ <div className="flex items-center gap-3">
406
+ <PartyPopper className="h-10 w-10 animate-bounce" />
407
+ <Trophy className="h-12 w-12 animate-pulse" />
408
+ <PartyPopper className="h-10 w-10 animate-bounce" />
409
+ </div>
410
+ <div className="text-center">
411
+ <h3 className="text-3xl font-bold">All Done!</h3>
412
+ <p className="mt-1 text-lg opacity-90">
413
+ You completed all the tasks
414
+ </p>
415
+ </div>
416
+ <Gift className="h-8 w-8 animate-bounce" />
417
+ </div>
418
+ </div>
419
+
420
+ {/* Sparkles */}
421
+ {[...Array(20)].map((_, i) => (
422
+ <Sparkles
423
+ key={`sparkle-${i}`}
424
+ className="absolute animate-sparkle text-yellow-400"
425
+ style={{
426
+ left: `${Math.random() * 100}%`,
427
+ top: `${Math.random() * 100}%`,
428
+ animationDelay: `${Math.random() * 2}s`,
429
+ fontSize: `${20 + Math.random() * 20}px`,
430
+ }}
431
+ />
432
+ ))}
433
+
434
+ <style>{`
435
+ @keyframes confetti {
436
+ 0% {
437
+ transform: translate(-50%, -50%) rotate(var(--angle)) translateX(0) scale(1);
438
+ opacity: 1;
439
+ }
440
+ 100% {
441
+ transform: translate(-50%, -50%) rotate(var(--angle)) translateX(var(--distance)) scale(0) rotate(720deg);
442
+ opacity: 0;
443
+ }
444
+ }
445
+ @keyframes celebration-popup {
446
+ 0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
447
+ 20% { transform: translate(-50%, -50%) scale(1.1); opacity: 1; }
448
+ 30% { transform: translate(-50%, -50%) scale(1); }
449
+ 80% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
450
+ 100% { transform: translate(-50%, -50%) scale(0.8); opacity: 0; }
451
+ }
452
+ @keyframes sparkle {
453
+ 0%, 100% { transform: scale(0) rotate(0deg); opacity: 0; }
454
+ 50% { transform: scale(1) rotate(180deg); opacity: 1; }
455
+ }
456
+ .animate-confetti {
457
+ animation: confetti 1.5s ease-out forwards;
458
+ }
459
+ .animate-celebration-popup {
460
+ animation: celebration-popup 4s ease-out forwards;
461
+ }
462
+ .animate-sparkle {
463
+ animation: sparkle 2s ease-in-out infinite;
464
+ }
465
+ `}</style>
466
+ </div>
467
+ )
468
+ }
469
+
470
+ // ============================================================================
471
+ // Progress Bar Component
472
+ // ============================================================================
473
+
474
+ interface ProgressBarProps {
475
+ progress: number
476
+ showPercentage?: boolean
477
+ size?: "sm" | "default" | "lg"
478
+ }
479
+
480
+ function ProgressBar({
481
+ progress,
482
+ showPercentage = true,
483
+ size = "default",
484
+ }: ProgressBarProps) {
485
+ const [displayProgress, setDisplayProgress] = React.useState(0)
486
+
487
+ React.useEffect(() => {
488
+ const timer = setTimeout(() => setDisplayProgress(progress), 100)
489
+ return () => clearTimeout(timer)
490
+ }, [progress])
491
+
492
+ const sizeClasses = {
493
+ sm: "h-1.5",
494
+ default: "h-2",
495
+ lg: "h-3",
496
+ }
497
+
498
+ const isComplete = progress >= 100
499
+
500
+ return (
501
+ <div className="w-full">
502
+ <div
503
+ className={cn(
504
+ "relative w-full overflow-hidden rounded-full bg-muted",
505
+ sizeClasses[size]
506
+ )}
507
+ >
508
+ <div
509
+ className={cn(
510
+ "h-full rounded-full transition-all duration-700 ease-out",
511
+ isComplete
512
+ ? "bg-gradient-to-r from-green-500 to-emerald-500"
513
+ : "bg-gradient-to-r from-blue-500 to-purple-500"
514
+ )}
515
+ style={{ width: `${displayProgress}%` }}
516
+ />
517
+ {/* Shine effect */}
518
+ <div
519
+ className="absolute inset-0 animate-progress-shine"
520
+ style={{ width: `${displayProgress}%` }}
521
+ />
522
+ </div>
523
+ {showPercentage && (
524
+ <div className="mt-1 flex items-center justify-between text-xs text-muted-foreground">
525
+ <span>{Math.round(displayProgress)}% complete</span>
526
+ {isComplete && (
527
+ <span className="flex items-center gap-1 text-green-600">
528
+ <Check className="h-3 w-3" />
529
+ Complete
530
+ </span>
531
+ )}
532
+ </div>
533
+ )}
534
+ <style>{`
535
+ @keyframes progress-shine {
536
+ 0% { background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%); background-position: -100% 0; }
537
+ 100% { background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%); background-position: 200% 0; }
538
+ }
539
+ .animate-progress-shine {
540
+ animation: progress-shine 2s ease-in-out infinite;
541
+ }
542
+ `}</style>
543
+ </div>
544
+ )
545
+ }
546
+
547
+ // ============================================================================
548
+ // Task Item Component
549
+ // ============================================================================
550
+
551
+ interface TaskItemProps {
552
+ task: ChecklistTask
553
+ status: TaskStatus
554
+ isExpanded: boolean
555
+ onToggleExpand: () => void
556
+ onComplete: () => void
557
+ onSkip: () => void
558
+ onReset: () => void
559
+ size?: "sm" | "default" | "lg"
560
+ }
561
+
562
+ function TaskItem({
563
+ task,
564
+ status,
565
+ isExpanded,
566
+ onToggleExpand,
567
+ onComplete,
568
+ onSkip,
569
+ onReset,
570
+ size = "default",
571
+ }: TaskItemProps) {
572
+ const isCompleted = status === "completed"
573
+ const isSkipped = status === "skipped"
574
+ const isPending = status === "pending"
575
+
576
+ const sizeClasses = {
577
+ sm: {
578
+ container: "px-3 py-2",
579
+ icon: "h-4 w-4",
580
+ checkIcon: "h-3 w-3",
581
+ title: "text-sm",
582
+ description: "text-xs",
583
+ button: "px-2 py-1 text-xs",
584
+ },
585
+ default: {
586
+ container: "px-4 py-3",
587
+ icon: "h-5 w-5",
588
+ checkIcon: "h-3.5 w-3.5",
589
+ title: "text-base",
590
+ description: "text-sm",
591
+ button: "px-3 py-1.5 text-sm",
592
+ },
593
+ lg: {
594
+ container: "px-5 py-4",
595
+ icon: "h-6 w-6",
596
+ checkIcon: "h-4 w-4",
597
+ title: "text-lg",
598
+ description: "text-base",
599
+ button: "px-4 py-2 text-base",
600
+ },
601
+ }
602
+
603
+ const sizes = sizeClasses[size]
604
+
605
+ return (
606
+ <div
607
+ className={cn(
608
+ "group rounded-lg border transition-all duration-200",
609
+ isCompleted && "border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950/30",
610
+ isSkipped && "border-muted bg-muted/30 opacity-60",
611
+ isPending && "border-border bg-card hover:border-primary/50 hover:shadow-sm"
612
+ )}
613
+ >
614
+ {/* Task header */}
615
+ <div className={cn("flex items-center gap-3", sizes.container)}>
616
+ {/* Status icon */}
617
+ <button
618
+ type="button"
619
+ onClick={isPending ? onComplete : onReset}
620
+ className={cn(
621
+ "flex-shrink-0 rounded-full border-2 transition-all duration-200",
622
+ sizes.icon,
623
+ "flex items-center justify-center",
624
+ isCompleted && "border-green-500 bg-green-500 text-white",
625
+ isSkipped && "border-muted-foreground/30 bg-muted",
626
+ isPending && "border-muted-foreground/30 hover:border-primary hover:bg-primary/10"
627
+ )}
628
+ aria-label={isPending ? "Complete task" : "Reset task"}
629
+ >
630
+ {isCompleted && <Check className={sizes.checkIcon} />}
631
+ {isSkipped && <X className={cn(sizes.checkIcon, "text-muted-foreground")} />}
632
+ </button>
633
+
634
+ {/* Task info */}
635
+ <div className="flex-1 min-w-0">
636
+ <div className="flex items-center gap-2">
637
+ {task.icon && (
638
+ <span className="text-muted-foreground">{task.icon}</span>
639
+ )}
640
+ <span
641
+ className={cn(
642
+ "font-medium transition-all duration-200",
643
+ sizes.title,
644
+ isCompleted && "text-green-700 line-through dark:text-green-400",
645
+ isSkipped && "text-muted-foreground line-through"
646
+ )}
647
+ >
648
+ {task.title}
649
+ </span>
650
+ {task.required === false && (
651
+ <span className="text-xs text-muted-foreground">(optional)</span>
652
+ )}
653
+ {task.points && isPending && (
654
+ <span className="inline-flex items-center gap-0.5 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-400">
655
+ +{task.points}
656
+ </span>
657
+ )}
658
+ </div>
659
+ </div>
660
+
661
+ {/* Expand/Action buttons */}
662
+ <div className="flex items-center gap-2">
663
+ {/* Skip button */}
664
+ {isPending && task.skippable && (
665
+ <button
666
+ type="button"
667
+ onClick={onSkip}
668
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
669
+ >
670
+ Skip
671
+ </button>
672
+ )}
673
+
674
+ {/* Action button */}
675
+ {isPending && task.actionLabel && task.onAction && (
676
+ <button
677
+ type="button"
678
+ onClick={(e) => {
679
+ e.stopPropagation()
680
+ task.onAction?.()
681
+ }}
682
+ className={cn(
683
+ "rounded-md bg-primary text-primary-foreground font-medium transition-colors hover:bg-primary/90",
684
+ sizes.button
685
+ )}
686
+ >
687
+ {task.actionLabel}
688
+ </button>
689
+ )}
690
+
691
+ {/* Expand toggle */}
692
+ {task.description && (
693
+ <button
694
+ type="button"
695
+ onClick={onToggleExpand}
696
+ className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
697
+ aria-label={isExpanded ? "Collapse details" : "Expand details"}
698
+ >
699
+ {isExpanded ? (
700
+ <ChevronDown className={sizes.icon} />
701
+ ) : (
702
+ <ChevronRight className={sizes.icon} />
703
+ )}
704
+ </button>
705
+ )}
706
+ </div>
707
+ </div>
708
+
709
+ {/* Expanded description */}
710
+ {task.description && (
711
+ <div
712
+ className={cn(
713
+ "overflow-hidden transition-all duration-200",
714
+ isExpanded ? "max-h-40 opacity-100" : "max-h-0 opacity-0"
715
+ )}
716
+ >
717
+ <div
718
+ className={cn(
719
+ "border-t pt-2 pb-3 px-4 ml-8",
720
+ sizes.description,
721
+ "text-muted-foreground"
722
+ )}
723
+ >
724
+ {task.description}
725
+ </div>
726
+ </div>
727
+ )}
728
+ </div>
729
+ )
730
+ }
731
+
732
+ // ============================================================================
733
+ // Category Group Component
734
+ // ============================================================================
735
+
736
+ interface CategoryGroupProps {
737
+ category: ChecklistCategory
738
+ tasks: ChecklistTask[]
739
+ taskStates: Record<string, TaskStatus>
740
+ expandedTasks: Set<string>
741
+ onCompleteTask: (taskId: string) => void
742
+ onSkipTask: (taskId: string) => void
743
+ onResetTask: (taskId: string) => void
744
+ onToggleTaskExpanded: (taskId: string) => void
745
+ size?: "sm" | "default" | "lg"
746
+ }
747
+
748
+ function CategoryGroup({
749
+ category,
750
+ tasks,
751
+ taskStates,
752
+ expandedTasks,
753
+ onCompleteTask,
754
+ onSkipTask,
755
+ onResetTask,
756
+ onToggleTaskExpanded,
757
+ size = "default",
758
+ }: CategoryGroupProps) {
759
+ const [isExpanded, setIsExpanded] = React.useState(true)
760
+
761
+ const completedCount = tasks.filter(
762
+ (t) => taskStates[t.id] === "completed"
763
+ ).length
764
+ const totalCount = tasks.length
765
+
766
+ return (
767
+ <div className="space-y-2">
768
+ {/* Category header */}
769
+ <button
770
+ type="button"
771
+ onClick={() => setIsExpanded((prev) => !prev)}
772
+ className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 hover:bg-muted/50 transition-colors"
773
+ >
774
+ {isExpanded ? (
775
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
776
+ ) : (
777
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
778
+ )}
779
+ {category.icon && (
780
+ <span className="text-muted-foreground">{category.icon}</span>
781
+ )}
782
+ <span className="font-medium">{category.label}</span>
783
+ <span className="ml-auto text-sm text-muted-foreground">
784
+ {completedCount}/{totalCount}
785
+ </span>
786
+ </button>
787
+
788
+ {/* Category tasks */}
789
+ {isExpanded && (
790
+ <div className="ml-4 space-y-2">
791
+ {tasks.map((task) => (
792
+ <TaskItem
793
+ key={task.id}
794
+ task={task}
795
+ status={taskStates[task.id] || "pending"}
796
+ isExpanded={expandedTasks.has(task.id)}
797
+ onToggleExpand={() => onToggleTaskExpanded(task.id)}
798
+ onComplete={() => onCompleteTask(task.id)}
799
+ onSkip={() => onSkipTask(task.id)}
800
+ onReset={() => onResetTask(task.id)}
801
+ size={size}
802
+ />
803
+ ))}
804
+ </div>
805
+ )}
806
+ </div>
807
+ )
808
+ }
809
+
810
+ // ============================================================================
811
+ // Main WakaChecklist Component
812
+ // ============================================================================
813
+
814
+ export function WakaChecklist({
815
+ checklistId,
816
+ tasks,
817
+ categories,
818
+ title = "Getting Started",
819
+ subtitle,
820
+ showProgress = true,
821
+ showPercentage = true,
822
+ persistent = true,
823
+ showCelebration = true,
824
+ onComplete,
825
+ onTaskChange,
826
+ defaultCollapsed = false,
827
+ collapsible = true,
828
+ size = "default",
829
+ className,
830
+ }: WakaChecklistProps) {
831
+ const [showCelebrationEffect, setShowCelebrationEffect] = React.useState(false)
832
+ const prevIsCompleteRef = React.useRef(false)
833
+
834
+ const checklist = useChecklist({
835
+ checklistId,
836
+ tasks,
837
+ persistent,
838
+ defaultCollapsed,
839
+ onComplete: () => {
840
+ if (showCelebration) {
841
+ setShowCelebrationEffect(true)
842
+ }
843
+ onComplete?.()
844
+ },
845
+ onTaskChange,
846
+ })
847
+
848
+ // Track completion for celebration
849
+ React.useEffect(() => {
850
+ if (checklist.isComplete && !prevIsCompleteRef.current && checklist.completedCount > 0) {
851
+ if (showCelebration) {
852
+ setShowCelebrationEffect(true)
853
+ }
854
+ }
855
+ prevIsCompleteRef.current = checklist.isComplete
856
+ }, [checklist.isComplete, checklist.completedCount, showCelebration])
857
+
858
+ // Group tasks by category
859
+ const groupedTasks = React.useMemo(() => {
860
+ if (!categories || categories.length === 0) {
861
+ return null
862
+ }
863
+
864
+ const groups: Record<string, ChecklistTask[]> = {}
865
+ const uncategorized: ChecklistTask[] = []
866
+
867
+ for (const task of tasks) {
868
+ if (task.category && categories.find((c) => c.id === task.category)) {
869
+ if (!groups[task.category]) {
870
+ groups[task.category] = []
871
+ }
872
+ groups[task.category].push(task)
873
+ } else {
874
+ uncategorized.push(task)
875
+ }
876
+ }
877
+
878
+ return { groups, uncategorized }
879
+ }, [tasks, categories])
880
+
881
+ const sizeClasses = {
882
+ sm: {
883
+ container: "p-3",
884
+ header: "gap-2",
885
+ title: "text-base",
886
+ subtitle: "text-xs",
887
+ },
888
+ default: {
889
+ container: "p-4",
890
+ header: "gap-3",
891
+ title: "text-lg",
892
+ subtitle: "text-sm",
893
+ },
894
+ lg: {
895
+ container: "p-6",
896
+ header: "gap-4",
897
+ title: "text-xl",
898
+ subtitle: "text-base",
899
+ },
900
+ }
901
+
902
+ const sizes = sizeClasses[size]
903
+
904
+ return (
905
+ <div
906
+ className={cn(
907
+ "rounded-xl border bg-card shadow-sm",
908
+ sizes.container,
909
+ className
910
+ )}
911
+ >
912
+ <CelebrationEffect
913
+ active={showCelebrationEffect}
914
+ onComplete={() => setShowCelebrationEffect(false)}
915
+ />
916
+
917
+ {/* Header */}
918
+ <div className={cn("flex items-start justify-between", sizes.header)}>
919
+ <div className="flex-1">
920
+ <div className="flex items-center gap-2">
921
+ {collapsible && (
922
+ <button
923
+ type="button"
924
+ onClick={checklist.toggleCollapsed}
925
+ className="rounded-md p-1 hover:bg-muted transition-colors"
926
+ aria-label={
927
+ checklist.isCollapsed ? "Expand checklist" : "Collapse checklist"
928
+ }
929
+ >
930
+ {checklist.isCollapsed ? (
931
+ <ChevronRight className="h-5 w-5" />
932
+ ) : (
933
+ <ChevronDown className="h-5 w-5" />
934
+ )}
935
+ </button>
936
+ )}
937
+ <h2 className={cn("font-semibold", sizes.title)}>{title}</h2>
938
+ {checklist.isComplete && (
939
+ <span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900/50 dark:text-green-400">
940
+ <Check className="h-3 w-3" />
941
+ Complete
942
+ </span>
943
+ )}
944
+ </div>
945
+ {subtitle && !checklist.isCollapsed && (
946
+ <p className={cn("text-muted-foreground mt-1", sizes.subtitle)}>
947
+ {subtitle}
948
+ </p>
949
+ )}
950
+ </div>
951
+
952
+ {/* Stats */}
953
+ <div className="text-right text-sm text-muted-foreground">
954
+ <span className="font-medium text-foreground">
955
+ {checklist.completedCount}
956
+ </span>
957
+ /{checklist.totalCount} tasks
958
+ </div>
959
+ </div>
960
+
961
+ {/* Progress bar */}
962
+ {showProgress && !checklist.isCollapsed && (
963
+ <div className="mt-3">
964
+ <ProgressBar
965
+ progress={checklist.progress}
966
+ showPercentage={showPercentage}
967
+ size={size}
968
+ />
969
+ </div>
970
+ )}
971
+
972
+ {/* Task list */}
973
+ {!checklist.isCollapsed && (
974
+ <div className="mt-4 space-y-3">
975
+ {groupedTasks ? (
976
+ <>
977
+ {/* Categorized tasks */}
978
+ {categories?.map((category) => {
979
+ const categoryTasks = groupedTasks.groups[category.id] || []
980
+ if (categoryTasks.length === 0) return null
981
+
982
+ return (
983
+ <CategoryGroup
984
+ key={category.id}
985
+ category={category}
986
+ tasks={categoryTasks}
987
+ taskStates={checklist.taskStates}
988
+ expandedTasks={checklist.expandedTasks}
989
+ onCompleteTask={checklist.completeTask}
990
+ onSkipTask={checklist.skipTask}
991
+ onResetTask={checklist.resetTask}
992
+ onToggleTaskExpanded={checklist.toggleTaskExpanded}
993
+ size={size}
994
+ />
995
+ )
996
+ })}
997
+
998
+ {/* Uncategorized tasks */}
999
+ {groupedTasks.uncategorized.length > 0 && (
1000
+ <div className="space-y-2">
1001
+ {groupedTasks.uncategorized.map((task) => (
1002
+ <TaskItem
1003
+ key={task.id}
1004
+ task={task}
1005
+ status={checklist.getTaskStatus(task.id)}
1006
+ isExpanded={checklist.expandedTasks.has(task.id)}
1007
+ onToggleExpand={() =>
1008
+ checklist.toggleTaskExpanded(task.id)
1009
+ }
1010
+ onComplete={() => checklist.completeTask(task.id)}
1011
+ onSkip={() => checklist.skipTask(task.id)}
1012
+ onReset={() => checklist.resetTask(task.id)}
1013
+ size={size}
1014
+ />
1015
+ ))}
1016
+ </div>
1017
+ )}
1018
+ </>
1019
+ ) : (
1020
+ /* Flat task list */
1021
+ <div className="space-y-2">
1022
+ {tasks.map((task) => (
1023
+ <TaskItem
1024
+ key={task.id}
1025
+ task={task}
1026
+ status={checklist.getTaskStatus(task.id)}
1027
+ isExpanded={checklist.expandedTasks.has(task.id)}
1028
+ onToggleExpand={() => checklist.toggleTaskExpanded(task.id)}
1029
+ onComplete={() => checklist.completeTask(task.id)}
1030
+ onSkip={() => checklist.skipTask(task.id)}
1031
+ onReset={() => checklist.resetTask(task.id)}
1032
+ size={size}
1033
+ />
1034
+ ))}
1035
+ </div>
1036
+ )}
1037
+ </div>
1038
+ )}
1039
+
1040
+ {/* Completed state with reward */}
1041
+ {checklist.isComplete && !checklist.isCollapsed && (
1042
+ <div className="mt-4 rounded-lg bg-gradient-to-r from-green-50 to-emerald-50 p-4 dark:from-green-950/30 dark:to-emerald-950/30">
1043
+ <div className="flex items-center gap-3">
1044
+ <div className="rounded-full bg-green-500 p-2 text-white">
1045
+ <Trophy className="h-5 w-5" />
1046
+ </div>
1047
+ <div>
1048
+ <p className="font-medium text-green-700 dark:text-green-400">
1049
+ Great job!
1050
+ </p>
1051
+ <p className="text-sm text-green-600 dark:text-green-500">
1052
+ You have completed the onboarding checklist
1053
+ </p>
1054
+ </div>
1055
+ <Gift className="ml-auto h-8 w-8 text-green-500" />
1056
+ </div>
1057
+ </div>
1058
+ )}
1059
+ </div>
1060
+ )
1061
+ }
1062
+
1063
+ // ============================================================================
1064
+ // Export
1065
+ // ============================================================================
1066
+
1067
+ export default WakaChecklist