@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.
- package/README.md +71 -8
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +6014 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/package.json +14 -3
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- 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
|