@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,976 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
ShoppingCart,
|
|
8
|
+
Truck,
|
|
9
|
+
CreditCard,
|
|
10
|
+
ClipboardCheck,
|
|
11
|
+
PartyPopper,
|
|
12
|
+
ChevronLeft,
|
|
13
|
+
ChevronRight,
|
|
14
|
+
SkipForward,
|
|
15
|
+
AlertCircle,
|
|
16
|
+
Loader2,
|
|
17
|
+
LucideIcon,
|
|
18
|
+
} from "lucide-react"
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
export type CheckoutStepId = "cart" | "shipping" | "payment" | "review" | "confirmation"
|
|
25
|
+
|
|
26
|
+
export type CheckoutStepStatus = "pending" | "current" | "completed" | "error" | "skipped"
|
|
27
|
+
|
|
28
|
+
export interface CheckoutStepValidation {
|
|
29
|
+
/** Whether the step is valid */
|
|
30
|
+
isValid: boolean
|
|
31
|
+
/** Error message if invalid */
|
|
32
|
+
errorMessage?: string
|
|
33
|
+
/** Field-level errors */
|
|
34
|
+
fieldErrors?: Record<string, string>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CheckoutStep {
|
|
38
|
+
/** Unique step identifier */
|
|
39
|
+
id: CheckoutStepId | string
|
|
40
|
+
/** Step title */
|
|
41
|
+
title: string
|
|
42
|
+
/** Step description */
|
|
43
|
+
description?: string
|
|
44
|
+
/** Custom icon (optional) */
|
|
45
|
+
icon?: LucideIcon
|
|
46
|
+
/** Step content component */
|
|
47
|
+
content?: React.ReactNode
|
|
48
|
+
/** Whether step is optional (can be skipped) */
|
|
49
|
+
optional?: boolean
|
|
50
|
+
/** Whether step is disabled */
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
/** Validation function for the step */
|
|
53
|
+
validate?: () => CheckoutStepValidation | Promise<CheckoutStepValidation>
|
|
54
|
+
/** Summary data for the sidebar */
|
|
55
|
+
summary?: React.ReactNode
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CheckoutStepperProps {
|
|
59
|
+
/** List of checkout steps */
|
|
60
|
+
steps: CheckoutStep[]
|
|
61
|
+
/** Current step index (0-indexed) */
|
|
62
|
+
currentStep?: number
|
|
63
|
+
/** Callback when step changes */
|
|
64
|
+
onStepChange?: (stepIndex: number, stepId: string) => void
|
|
65
|
+
/** Callback when checkout is completed */
|
|
66
|
+
onComplete?: () => void
|
|
67
|
+
/** Layout orientation */
|
|
68
|
+
orientation?: "horizontal" | "vertical"
|
|
69
|
+
/** Size variant */
|
|
70
|
+
size?: "sm" | "md" | "lg"
|
|
71
|
+
/** Allow clicking on steps to navigate */
|
|
72
|
+
clickable?: boolean
|
|
73
|
+
/** Only allow navigation to previous steps */
|
|
74
|
+
allowPreviousOnly?: boolean
|
|
75
|
+
/** Show step content */
|
|
76
|
+
showContent?: boolean
|
|
77
|
+
/** Show summary sidebar */
|
|
78
|
+
showSummary?: boolean
|
|
79
|
+
/** Summary sidebar position */
|
|
80
|
+
summaryPosition?: "left" | "right"
|
|
81
|
+
/** Enable animated transitions */
|
|
82
|
+
animated?: boolean
|
|
83
|
+
/** Animation direction */
|
|
84
|
+
animationDirection?: "slide" | "fade" | "scale"
|
|
85
|
+
/** Loading state */
|
|
86
|
+
isLoading?: boolean
|
|
87
|
+
/** Custom class name */
|
|
88
|
+
className?: string
|
|
89
|
+
/** Navigation labels */
|
|
90
|
+
labels?: {
|
|
91
|
+
back?: string
|
|
92
|
+
next?: string
|
|
93
|
+
skip?: string
|
|
94
|
+
complete?: string
|
|
95
|
+
processing?: string
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface UseCheckoutStepperOptions {
|
|
100
|
+
/** Initial step index */
|
|
101
|
+
initialStep?: number
|
|
102
|
+
/** List of steps */
|
|
103
|
+
steps: CheckoutStep[]
|
|
104
|
+
/** Callback when step changes */
|
|
105
|
+
onStepChange?: (stepIndex: number, stepId: string) => void
|
|
106
|
+
/** Callback when checkout completes */
|
|
107
|
+
onComplete?: () => void
|
|
108
|
+
/** Validate before proceeding */
|
|
109
|
+
validateOnNext?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface UseCheckoutStepperReturn {
|
|
113
|
+
/** Current step index */
|
|
114
|
+
currentStep: number
|
|
115
|
+
/** Current step data */
|
|
116
|
+
currentStepData: CheckoutStep | undefined
|
|
117
|
+
/** All steps with status */
|
|
118
|
+
stepsWithStatus: (CheckoutStep & { status: CheckoutStepStatus })[]
|
|
119
|
+
/** Total number of steps */
|
|
120
|
+
totalSteps: number
|
|
121
|
+
/** Whether on first step */
|
|
122
|
+
isFirstStep: boolean
|
|
123
|
+
/** Whether on last step */
|
|
124
|
+
isLastStep: boolean
|
|
125
|
+
/** Whether checkout is complete */
|
|
126
|
+
isComplete: boolean
|
|
127
|
+
/** Whether currently validating */
|
|
128
|
+
isValidating: boolean
|
|
129
|
+
/** Current validation error */
|
|
130
|
+
validationError: string | null
|
|
131
|
+
/** Go to specific step */
|
|
132
|
+
goToStep: (index: number) => Promise<boolean>
|
|
133
|
+
/** Go to next step */
|
|
134
|
+
nextStep: () => Promise<boolean>
|
|
135
|
+
/** Go to previous step */
|
|
136
|
+
prevStep: () => void
|
|
137
|
+
/** Skip current step (if optional) */
|
|
138
|
+
skipStep: () => void
|
|
139
|
+
/** Complete checkout */
|
|
140
|
+
completeCheckout: () => Promise<boolean>
|
|
141
|
+
/** Reset stepper */
|
|
142
|
+
reset: () => void
|
|
143
|
+
/** Get step status */
|
|
144
|
+
getStepStatus: (index: number) => CheckoutStepStatus
|
|
145
|
+
/** Skipped steps */
|
|
146
|
+
skippedSteps: Set<number>
|
|
147
|
+
/** Mark step as completed manually */
|
|
148
|
+
markStepCompleted: (index: number) => void
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CheckoutStepperContextValue extends UseCheckoutStepperReturn {
|
|
152
|
+
orientation: "horizontal" | "vertical"
|
|
153
|
+
size: "sm" | "md" | "lg"
|
|
154
|
+
clickable: boolean
|
|
155
|
+
allowPreviousOnly: boolean
|
|
156
|
+
animated: boolean
|
|
157
|
+
animationDirection: "slide" | "fade" | "scale"
|
|
158
|
+
isLoading: boolean
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================
|
|
162
|
+
// DEFAULT STEPS
|
|
163
|
+
// ============================================
|
|
164
|
+
|
|
165
|
+
const defaultStepIcons: Record<CheckoutStepId, LucideIcon> = {
|
|
166
|
+
cart: ShoppingCart,
|
|
167
|
+
shipping: Truck,
|
|
168
|
+
payment: CreditCard,
|
|
169
|
+
review: ClipboardCheck,
|
|
170
|
+
confirmation: PartyPopper,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================
|
|
174
|
+
// HOOK: useCheckoutStepper
|
|
175
|
+
// ============================================
|
|
176
|
+
|
|
177
|
+
export function useCheckoutStepper({
|
|
178
|
+
initialStep = 0,
|
|
179
|
+
steps,
|
|
180
|
+
onStepChange,
|
|
181
|
+
onComplete,
|
|
182
|
+
validateOnNext = true,
|
|
183
|
+
}: UseCheckoutStepperOptions): UseCheckoutStepperReturn {
|
|
184
|
+
const [currentStep, setCurrentStep] = React.useState(initialStep)
|
|
185
|
+
const [completedSteps, setCompletedSteps] = React.useState<Set<number>>(new Set())
|
|
186
|
+
const [skippedSteps, setSkippedSteps] = React.useState<Set<number>>(new Set())
|
|
187
|
+
const [isComplete, setIsComplete] = React.useState(false)
|
|
188
|
+
const [isValidating, setIsValidating] = React.useState(false)
|
|
189
|
+
const [validationError, setValidationError] = React.useState<string | null>(null)
|
|
190
|
+
|
|
191
|
+
const totalSteps = steps.length
|
|
192
|
+
const isFirstStep = currentStep === 0
|
|
193
|
+
const isLastStep = currentStep === totalSteps - 1
|
|
194
|
+
const currentStepData = steps[currentStep]
|
|
195
|
+
|
|
196
|
+
const getStepStatus = React.useCallback(
|
|
197
|
+
(index: number): CheckoutStepStatus => {
|
|
198
|
+
if (skippedSteps.has(index)) return "skipped"
|
|
199
|
+
if (completedSteps.has(index)) return "completed"
|
|
200
|
+
if (index === currentStep) return "current"
|
|
201
|
+
if (index < currentStep) return "completed"
|
|
202
|
+
return "pending"
|
|
203
|
+
},
|
|
204
|
+
[currentStep, completedSteps, skippedSteps]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const stepsWithStatus = React.useMemo(
|
|
208
|
+
() =>
|
|
209
|
+
steps.map((step, index) => ({
|
|
210
|
+
...step,
|
|
211
|
+
status: getStepStatus(index),
|
|
212
|
+
})),
|
|
213
|
+
[steps, getStepStatus]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const validateCurrentStep = React.useCallback(async (): Promise<boolean> => {
|
|
217
|
+
const step = steps[currentStep]
|
|
218
|
+
if (!step?.validate) return true
|
|
219
|
+
|
|
220
|
+
setIsValidating(true)
|
|
221
|
+
setValidationError(null)
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const result = await step.validate()
|
|
225
|
+
if (!result.isValid) {
|
|
226
|
+
setValidationError(result.errorMessage || "Validation failed")
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
return true
|
|
230
|
+
} catch (error) {
|
|
231
|
+
setValidationError("An error occurred during validation")
|
|
232
|
+
return false
|
|
233
|
+
} finally {
|
|
234
|
+
setIsValidating(false)
|
|
235
|
+
}
|
|
236
|
+
}, [currentStep, steps])
|
|
237
|
+
|
|
238
|
+
const goToStep = React.useCallback(
|
|
239
|
+
async (index: number): Promise<boolean> => {
|
|
240
|
+
if (index < 0 || index >= totalSteps) return false
|
|
241
|
+
if (steps[index]?.disabled) return false
|
|
242
|
+
|
|
243
|
+
// If going forward, validate current step
|
|
244
|
+
if (index > currentStep && validateOnNext) {
|
|
245
|
+
const isValid = await validateCurrentStep()
|
|
246
|
+
if (!isValid) return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Mark current step as completed if moving forward
|
|
250
|
+
if (index > currentStep) {
|
|
251
|
+
setCompletedSteps((prev) => new Set(prev).add(currentStep))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setCurrentStep(index)
|
|
255
|
+
setValidationError(null)
|
|
256
|
+
onStepChange?.(index, steps[index].id)
|
|
257
|
+
return true
|
|
258
|
+
},
|
|
259
|
+
[currentStep, totalSteps, steps, validateOnNext, validateCurrentStep, onStepChange]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
const nextStep = React.useCallback(async (): Promise<boolean> => {
|
|
263
|
+
if (isLastStep) return false
|
|
264
|
+
return goToStep(currentStep + 1)
|
|
265
|
+
}, [isLastStep, currentStep, goToStep])
|
|
266
|
+
|
|
267
|
+
const prevStep = React.useCallback(() => {
|
|
268
|
+
if (isFirstStep) return
|
|
269
|
+
setCurrentStep((prev) => prev - 1)
|
|
270
|
+
setValidationError(null)
|
|
271
|
+
onStepChange?.(currentStep - 1, steps[currentStep - 1].id)
|
|
272
|
+
}, [isFirstStep, currentStep, steps, onStepChange])
|
|
273
|
+
|
|
274
|
+
const skipStep = React.useCallback(() => {
|
|
275
|
+
const step = steps[currentStep]
|
|
276
|
+
if (!step?.optional || isLastStep) return
|
|
277
|
+
|
|
278
|
+
setSkippedSteps((prev) => new Set(prev).add(currentStep))
|
|
279
|
+
setCurrentStep((prev) => prev + 1)
|
|
280
|
+
setValidationError(null)
|
|
281
|
+
onStepChange?.(currentStep + 1, steps[currentStep + 1].id)
|
|
282
|
+
}, [currentStep, steps, isLastStep, onStepChange])
|
|
283
|
+
|
|
284
|
+
const completeCheckout = React.useCallback(async (): Promise<boolean> => {
|
|
285
|
+
if (validateOnNext) {
|
|
286
|
+
const isValid = await validateCurrentStep()
|
|
287
|
+
if (!isValid) return false
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
setCompletedSteps((prev) => new Set(prev).add(currentStep))
|
|
291
|
+
setIsComplete(true)
|
|
292
|
+
onComplete?.()
|
|
293
|
+
return true
|
|
294
|
+
}, [currentStep, validateOnNext, validateCurrentStep, onComplete])
|
|
295
|
+
|
|
296
|
+
const reset = React.useCallback(() => {
|
|
297
|
+
setCurrentStep(initialStep)
|
|
298
|
+
setCompletedSteps(new Set())
|
|
299
|
+
setSkippedSteps(new Set())
|
|
300
|
+
setIsComplete(false)
|
|
301
|
+
setIsValidating(false)
|
|
302
|
+
setValidationError(null)
|
|
303
|
+
}, [initialStep])
|
|
304
|
+
|
|
305
|
+
const markStepCompleted = React.useCallback((index: number) => {
|
|
306
|
+
setCompletedSteps((prev) => new Set(prev).add(index))
|
|
307
|
+
}, [])
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
currentStep,
|
|
311
|
+
currentStepData,
|
|
312
|
+
stepsWithStatus,
|
|
313
|
+
totalSteps,
|
|
314
|
+
isFirstStep,
|
|
315
|
+
isLastStep,
|
|
316
|
+
isComplete,
|
|
317
|
+
isValidating,
|
|
318
|
+
validationError,
|
|
319
|
+
goToStep,
|
|
320
|
+
nextStep,
|
|
321
|
+
prevStep,
|
|
322
|
+
skipStep,
|
|
323
|
+
completeCheckout,
|
|
324
|
+
reset,
|
|
325
|
+
getStepStatus,
|
|
326
|
+
skippedSteps,
|
|
327
|
+
markStepCompleted,
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ============================================
|
|
332
|
+
// CONTEXT
|
|
333
|
+
// ============================================
|
|
334
|
+
|
|
335
|
+
const CheckoutStepperContext = React.createContext<CheckoutStepperContextValue | null>(null)
|
|
336
|
+
|
|
337
|
+
export function useCheckoutStepperContext() {
|
|
338
|
+
const context = React.useContext(CheckoutStepperContext)
|
|
339
|
+
if (!context) {
|
|
340
|
+
throw new Error("useCheckoutStepperContext must be used within a WakaCheckoutStepper")
|
|
341
|
+
}
|
|
342
|
+
return context
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================
|
|
346
|
+
// SUB-COMPONENTS
|
|
347
|
+
// ============================================
|
|
348
|
+
|
|
349
|
+
interface StepIndicatorProps {
|
|
350
|
+
step: CheckoutStep & { status: CheckoutStepStatus }
|
|
351
|
+
index: number
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function StepIndicator({ step, index }: StepIndicatorProps) {
|
|
355
|
+
const { size, animated } = useCheckoutStepperContext()
|
|
356
|
+
|
|
357
|
+
const sizeClasses = {
|
|
358
|
+
sm: "h-8 w-8 text-xs",
|
|
359
|
+
md: "h-10 w-10 text-sm",
|
|
360
|
+
lg: "h-12 w-12 text-base",
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const iconSizes = {
|
|
364
|
+
sm: "h-4 w-4",
|
|
365
|
+
md: "h-5 w-5",
|
|
366
|
+
lg: "h-6 w-6",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const statusClasses = {
|
|
370
|
+
pending: "bg-muted text-muted-foreground border-2 border-muted-foreground/30",
|
|
371
|
+
current: "bg-primary text-primary-foreground border-2 border-primary shadow-lg shadow-primary/25",
|
|
372
|
+
completed: "bg-green-500 text-white border-2 border-green-500",
|
|
373
|
+
error: "bg-destructive text-destructive-foreground border-2 border-destructive",
|
|
374
|
+
skipped: "bg-muted text-muted-foreground border-2 border-dashed border-muted-foreground/50",
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const Icon = step.icon || defaultStepIcons[step.id as CheckoutStepId]
|
|
378
|
+
|
|
379
|
+
const renderContent = () => {
|
|
380
|
+
if (step.status === "completed") {
|
|
381
|
+
return <Check className={iconSizes[size]} />
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (step.status === "error") {
|
|
385
|
+
return <AlertCircle className={iconSizes[size]} />
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (step.status === "skipped") {
|
|
389
|
+
return <SkipForward className={iconSizes[size]} />
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (Icon) {
|
|
393
|
+
return <Icon className={iconSizes[size]} />
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return <span className="font-semibold">{index + 1}</span>
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<div
|
|
401
|
+
className={cn(
|
|
402
|
+
"rounded-full flex items-center justify-center transition-all duration-300",
|
|
403
|
+
sizeClasses[size],
|
|
404
|
+
statusClasses[step.status],
|
|
405
|
+
animated && step.status === "current" && "animate-pulse"
|
|
406
|
+
)}
|
|
407
|
+
>
|
|
408
|
+
{renderContent()}
|
|
409
|
+
</div>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface StepConnectorProps {
|
|
414
|
+
status: CheckoutStepStatus
|
|
415
|
+
nextStatus: CheckoutStepStatus
|
|
416
|
+
orientation: "horizontal" | "vertical"
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function StepConnector({ status, nextStatus, orientation }: StepConnectorProps) {
|
|
420
|
+
const isCompleted = status === "completed" || status === "skipped"
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<div
|
|
424
|
+
className={cn(
|
|
425
|
+
"transition-all duration-500",
|
|
426
|
+
orientation === "horizontal"
|
|
427
|
+
? "flex-1 h-0.5 mx-2 min-w-[20px]"
|
|
428
|
+
: "w-0.5 min-h-[32px] mx-auto my-1",
|
|
429
|
+
isCompleted ? "bg-green-500" : "bg-muted-foreground/30"
|
|
430
|
+
)}
|
|
431
|
+
/>
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
interface StepItemProps {
|
|
436
|
+
step: CheckoutStep & { status: CheckoutStepStatus }
|
|
437
|
+
index: number
|
|
438
|
+
isLast: boolean
|
|
439
|
+
nextStatus?: CheckoutStepStatus
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function StepItem({ step, index, isLast, nextStatus }: StepItemProps) {
|
|
443
|
+
const {
|
|
444
|
+
orientation,
|
|
445
|
+
size,
|
|
446
|
+
clickable,
|
|
447
|
+
allowPreviousOnly,
|
|
448
|
+
currentStep,
|
|
449
|
+
goToStep,
|
|
450
|
+
} = useCheckoutStepperContext()
|
|
451
|
+
|
|
452
|
+
const isClickable =
|
|
453
|
+
clickable &&
|
|
454
|
+
!step.disabled &&
|
|
455
|
+
(allowPreviousOnly ? index < currentStep : index !== currentStep)
|
|
456
|
+
|
|
457
|
+
const labelSizes = {
|
|
458
|
+
sm: "text-xs",
|
|
459
|
+
md: "text-sm",
|
|
460
|
+
lg: "text-base",
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const descSizes = {
|
|
464
|
+
sm: "text-[10px]",
|
|
465
|
+
md: "text-xs",
|
|
466
|
+
lg: "text-sm",
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const handleClick = async () => {
|
|
470
|
+
if (isClickable) {
|
|
471
|
+
await goToStep(index)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (orientation === "vertical") {
|
|
476
|
+
return (
|
|
477
|
+
<div className="flex">
|
|
478
|
+
<div className="flex flex-col items-center">
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
onClick={handleClick}
|
|
482
|
+
disabled={!isClickable}
|
|
483
|
+
className={cn(
|
|
484
|
+
"flex items-center",
|
|
485
|
+
isClickable && "cursor-pointer hover:opacity-80",
|
|
486
|
+
!isClickable && "cursor-default"
|
|
487
|
+
)}
|
|
488
|
+
aria-current={step.status === "current" ? "step" : undefined}
|
|
489
|
+
>
|
|
490
|
+
<StepIndicator step={step} index={index} />
|
|
491
|
+
</button>
|
|
492
|
+
{!isLast && (
|
|
493
|
+
<StepConnector
|
|
494
|
+
status={step.status}
|
|
495
|
+
nextStatus={nextStatus || "pending"}
|
|
496
|
+
orientation="vertical"
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
<div className="ml-4 pb-8">
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
onClick={handleClick}
|
|
504
|
+
disabled={!isClickable}
|
|
505
|
+
className={cn(
|
|
506
|
+
"text-left",
|
|
507
|
+
isClickable && "cursor-pointer hover:opacity-80",
|
|
508
|
+
!isClickable && "cursor-default"
|
|
509
|
+
)}
|
|
510
|
+
>
|
|
511
|
+
<p
|
|
512
|
+
className={cn(
|
|
513
|
+
"font-semibold",
|
|
514
|
+
labelSizes[size],
|
|
515
|
+
step.status === "current"
|
|
516
|
+
? "text-foreground"
|
|
517
|
+
: step.status === "completed"
|
|
518
|
+
? "text-green-600 dark:text-green-400"
|
|
519
|
+
: "text-muted-foreground",
|
|
520
|
+
step.disabled && "opacity-50"
|
|
521
|
+
)}
|
|
522
|
+
>
|
|
523
|
+
{step.title}
|
|
524
|
+
{step.optional && (
|
|
525
|
+
<span className="ml-2 text-muted-foreground font-normal text-xs">
|
|
526
|
+
(Optional)
|
|
527
|
+
</span>
|
|
528
|
+
)}
|
|
529
|
+
</p>
|
|
530
|
+
{step.description && (
|
|
531
|
+
<p className={cn("text-muted-foreground mt-0.5", descSizes[size])}>
|
|
532
|
+
{step.description}
|
|
533
|
+
</p>
|
|
534
|
+
)}
|
|
535
|
+
</button>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Horizontal orientation
|
|
542
|
+
return (
|
|
543
|
+
<>
|
|
544
|
+
<div className="flex flex-col items-center flex-shrink-0">
|
|
545
|
+
<button
|
|
546
|
+
type="button"
|
|
547
|
+
onClick={handleClick}
|
|
548
|
+
disabled={!isClickable}
|
|
549
|
+
className={cn(
|
|
550
|
+
"flex flex-col items-center gap-2",
|
|
551
|
+
isClickable && "cursor-pointer hover:opacity-80",
|
|
552
|
+
!isClickable && "cursor-default"
|
|
553
|
+
)}
|
|
554
|
+
aria-current={step.status === "current" ? "step" : undefined}
|
|
555
|
+
>
|
|
556
|
+
<StepIndicator step={step} index={index} />
|
|
557
|
+
<div className="text-center min-w-[60px] max-w-[100px]">
|
|
558
|
+
<p
|
|
559
|
+
className={cn(
|
|
560
|
+
"font-semibold truncate",
|
|
561
|
+
labelSizes[size],
|
|
562
|
+
step.status === "current"
|
|
563
|
+
? "text-foreground"
|
|
564
|
+
: step.status === "completed"
|
|
565
|
+
? "text-green-600 dark:text-green-400"
|
|
566
|
+
: "text-muted-foreground",
|
|
567
|
+
step.disabled && "opacity-50"
|
|
568
|
+
)}
|
|
569
|
+
>
|
|
570
|
+
{step.title}
|
|
571
|
+
</p>
|
|
572
|
+
{step.description && (
|
|
573
|
+
<p
|
|
574
|
+
className={cn(
|
|
575
|
+
"text-muted-foreground mt-0.5 hidden md:block truncate",
|
|
576
|
+
descSizes[size]
|
|
577
|
+
)}
|
|
578
|
+
>
|
|
579
|
+
{step.description}
|
|
580
|
+
</p>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
</button>
|
|
584
|
+
</div>
|
|
585
|
+
{!isLast && (
|
|
586
|
+
<StepConnector
|
|
587
|
+
status={step.status}
|
|
588
|
+
nextStatus={nextStatus || "pending"}
|
|
589
|
+
orientation="horizontal"
|
|
590
|
+
/>
|
|
591
|
+
)}
|
|
592
|
+
</>
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ============================================
|
|
597
|
+
// STEP CONTENT WRAPPER
|
|
598
|
+
// ============================================
|
|
599
|
+
|
|
600
|
+
interface StepContentProps {
|
|
601
|
+
children: React.ReactNode
|
|
602
|
+
isActive: boolean
|
|
603
|
+
direction: "slide" | "fade" | "scale"
|
|
604
|
+
animated: boolean
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function StepContent({ children, isActive, direction, animated }: StepContentProps) {
|
|
608
|
+
const [shouldRender, setShouldRender] = React.useState(isActive)
|
|
609
|
+
|
|
610
|
+
React.useEffect(() => {
|
|
611
|
+
if (isActive) {
|
|
612
|
+
setShouldRender(true)
|
|
613
|
+
} else if (animated) {
|
|
614
|
+
const timer = setTimeout(() => setShouldRender(false), 300)
|
|
615
|
+
return () => clearTimeout(timer)
|
|
616
|
+
} else {
|
|
617
|
+
setShouldRender(false)
|
|
618
|
+
}
|
|
619
|
+
}, [isActive, animated])
|
|
620
|
+
|
|
621
|
+
if (!shouldRender) return null
|
|
622
|
+
|
|
623
|
+
const animationClasses = {
|
|
624
|
+
slide: cn(
|
|
625
|
+
"transition-all duration-300",
|
|
626
|
+
isActive
|
|
627
|
+
? "opacity-100 translate-x-0"
|
|
628
|
+
: "opacity-0 translate-x-8"
|
|
629
|
+
),
|
|
630
|
+
fade: cn(
|
|
631
|
+
"transition-opacity duration-300",
|
|
632
|
+
isActive ? "opacity-100" : "opacity-0"
|
|
633
|
+
),
|
|
634
|
+
scale: cn(
|
|
635
|
+
"transition-all duration-300",
|
|
636
|
+
isActive
|
|
637
|
+
? "opacity-100 scale-100"
|
|
638
|
+
: "opacity-0 scale-95"
|
|
639
|
+
),
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<div className={animated ? animationClasses[direction] : undefined}>
|
|
644
|
+
{children}
|
|
645
|
+
</div>
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ============================================
|
|
650
|
+
// SUMMARY SIDEBAR
|
|
651
|
+
// ============================================
|
|
652
|
+
|
|
653
|
+
interface SummarySidebarProps {
|
|
654
|
+
steps: (CheckoutStep & { status: CheckoutStepStatus })[]
|
|
655
|
+
position: "left" | "right"
|
|
656
|
+
className?: string
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function SummarySidebar({ steps, position, className }: SummarySidebarProps) {
|
|
660
|
+
const completedSteps = steps.filter(
|
|
661
|
+
(s) => s.status === "completed" || s.status === "skipped"
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
return (
|
|
665
|
+
<div
|
|
666
|
+
className={cn(
|
|
667
|
+
"w-full lg:w-80 p-4 bg-muted/50 rounded-lg border",
|
|
668
|
+
position === "left" ? "lg:order-first" : "lg:order-last",
|
|
669
|
+
className
|
|
670
|
+
)}
|
|
671
|
+
>
|
|
672
|
+
<h3 className="font-semibold text-lg mb-4">Order Summary</h3>
|
|
673
|
+
<div className="space-y-4">
|
|
674
|
+
{completedSteps.map((step) => (
|
|
675
|
+
<div key={step.id} className="pb-3 border-b border-border last:border-0">
|
|
676
|
+
<div className="flex items-center gap-2 mb-1">
|
|
677
|
+
<Check className="h-4 w-4 text-green-500" />
|
|
678
|
+
<span className="font-medium text-sm">{step.title}</span>
|
|
679
|
+
</div>
|
|
680
|
+
{step.summary && (
|
|
681
|
+
<div className="text-sm text-muted-foreground ml-6">
|
|
682
|
+
{step.summary}
|
|
683
|
+
</div>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
))}
|
|
687
|
+
{completedSteps.length === 0 && (
|
|
688
|
+
<p className="text-sm text-muted-foreground">
|
|
689
|
+
Complete steps to see your order summary.
|
|
690
|
+
</p>
|
|
691
|
+
)}
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ============================================
|
|
698
|
+
// NAVIGATION BUTTONS
|
|
699
|
+
// ============================================
|
|
700
|
+
|
|
701
|
+
export interface CheckoutNavigationProps {
|
|
702
|
+
/** Custom class name */
|
|
703
|
+
className?: string
|
|
704
|
+
/** Custom labels */
|
|
705
|
+
labels?: {
|
|
706
|
+
back?: string
|
|
707
|
+
next?: string
|
|
708
|
+
skip?: string
|
|
709
|
+
complete?: string
|
|
710
|
+
processing?: string
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function CheckoutNavigation({
|
|
715
|
+
className,
|
|
716
|
+
labels = {},
|
|
717
|
+
}: CheckoutNavigationProps) {
|
|
718
|
+
const {
|
|
719
|
+
currentStep,
|
|
720
|
+
currentStepData,
|
|
721
|
+
isFirstStep,
|
|
722
|
+
isLastStep,
|
|
723
|
+
isValidating,
|
|
724
|
+
isLoading,
|
|
725
|
+
validationError,
|
|
726
|
+
nextStep,
|
|
727
|
+
prevStep,
|
|
728
|
+
skipStep,
|
|
729
|
+
completeCheckout,
|
|
730
|
+
} = useCheckoutStepperContext()
|
|
731
|
+
|
|
732
|
+
const {
|
|
733
|
+
back = "Back",
|
|
734
|
+
next = "Continue",
|
|
735
|
+
skip = "Skip",
|
|
736
|
+
complete = "Complete Order",
|
|
737
|
+
processing = "Processing...",
|
|
738
|
+
} = labels
|
|
739
|
+
|
|
740
|
+
const isProcessing = isValidating || isLoading
|
|
741
|
+
|
|
742
|
+
return (
|
|
743
|
+
<div className={cn("space-y-4", className)}>
|
|
744
|
+
{validationError && (
|
|
745
|
+
<div className="flex items-center gap-2 p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
|
|
746
|
+
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
|
747
|
+
<span>{validationError}</span>
|
|
748
|
+
</div>
|
|
749
|
+
)}
|
|
750
|
+
<div className="flex items-center justify-between gap-4">
|
|
751
|
+
<button
|
|
752
|
+
type="button"
|
|
753
|
+
onClick={prevStep}
|
|
754
|
+
disabled={isFirstStep || isProcessing}
|
|
755
|
+
className={cn(
|
|
756
|
+
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg border transition-all",
|
|
757
|
+
isFirstStep || isProcessing
|
|
758
|
+
? "opacity-50 cursor-not-allowed bg-muted"
|
|
759
|
+
: "hover:bg-muted"
|
|
760
|
+
)}
|
|
761
|
+
>
|
|
762
|
+
<ChevronLeft className="h-4 w-4" />
|
|
763
|
+
{back}
|
|
764
|
+
</button>
|
|
765
|
+
|
|
766
|
+
<div className="flex items-center gap-2">
|
|
767
|
+
{currentStepData?.optional && !isLastStep && (
|
|
768
|
+
<button
|
|
769
|
+
type="button"
|
|
770
|
+
onClick={skipStep}
|
|
771
|
+
disabled={isProcessing}
|
|
772
|
+
className={cn(
|
|
773
|
+
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg transition-all",
|
|
774
|
+
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
775
|
+
isProcessing && "opacity-50 cursor-not-allowed"
|
|
776
|
+
)}
|
|
777
|
+
>
|
|
778
|
+
<SkipForward className="h-4 w-4" />
|
|
779
|
+
{skip}
|
|
780
|
+
</button>
|
|
781
|
+
)}
|
|
782
|
+
|
|
783
|
+
<button
|
|
784
|
+
type="button"
|
|
785
|
+
onClick={isLastStep ? completeCheckout : nextStep}
|
|
786
|
+
disabled={isProcessing}
|
|
787
|
+
className={cn(
|
|
788
|
+
"flex items-center gap-2 px-6 py-2.5 text-sm font-medium rounded-lg transition-all",
|
|
789
|
+
"bg-primary text-primary-foreground",
|
|
790
|
+
isProcessing
|
|
791
|
+
? "opacity-70 cursor-not-allowed"
|
|
792
|
+
: "hover:bg-primary/90"
|
|
793
|
+
)}
|
|
794
|
+
>
|
|
795
|
+
{isProcessing ? (
|
|
796
|
+
<>
|
|
797
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
798
|
+
{processing}
|
|
799
|
+
</>
|
|
800
|
+
) : isLastStep ? (
|
|
801
|
+
<>
|
|
802
|
+
{complete}
|
|
803
|
+
<Check className="h-4 w-4" />
|
|
804
|
+
</>
|
|
805
|
+
) : (
|
|
806
|
+
<>
|
|
807
|
+
{next}
|
|
808
|
+
<ChevronRight className="h-4 w-4" />
|
|
809
|
+
</>
|
|
810
|
+
)}
|
|
811
|
+
</button>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ============================================
|
|
819
|
+
// PROGRESS BAR
|
|
820
|
+
// ============================================
|
|
821
|
+
|
|
822
|
+
export interface CheckoutProgressBarProps {
|
|
823
|
+
className?: string
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function CheckoutProgressBar({ className }: CheckoutProgressBarProps) {
|
|
827
|
+
const { currentStep, totalSteps, stepsWithStatus } = useCheckoutStepperContext()
|
|
828
|
+
|
|
829
|
+
const completedCount = stepsWithStatus.filter(
|
|
830
|
+
(s) => s.status === "completed" || s.status === "skipped"
|
|
831
|
+
).length
|
|
832
|
+
|
|
833
|
+
const progress = Math.round((completedCount / totalSteps) * 100)
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<div className={cn("space-y-2", className)}>
|
|
837
|
+
<div className="flex justify-between text-sm">
|
|
838
|
+
<span className="text-muted-foreground">
|
|
839
|
+
Step {currentStep + 1} of {totalSteps}
|
|
840
|
+
</span>
|
|
841
|
+
<span className="font-medium">{progress}% complete</span>
|
|
842
|
+
</div>
|
|
843
|
+
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
|
844
|
+
<div
|
|
845
|
+
className="h-full bg-primary transition-all duration-500 ease-out rounded-full"
|
|
846
|
+
style={{ width: `${progress}%` }}
|
|
847
|
+
/>
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ============================================
|
|
854
|
+
// MAIN COMPONENT
|
|
855
|
+
// ============================================
|
|
856
|
+
|
|
857
|
+
export function WakaCheckoutStepper({
|
|
858
|
+
steps,
|
|
859
|
+
currentStep: controlledStep,
|
|
860
|
+
onStepChange,
|
|
861
|
+
onComplete,
|
|
862
|
+
orientation = "horizontal",
|
|
863
|
+
size = "md",
|
|
864
|
+
clickable = true,
|
|
865
|
+
allowPreviousOnly = true,
|
|
866
|
+
showContent = true,
|
|
867
|
+
showSummary = false,
|
|
868
|
+
summaryPosition = "right",
|
|
869
|
+
animated = true,
|
|
870
|
+
animationDirection = "slide",
|
|
871
|
+
isLoading = false,
|
|
872
|
+
className,
|
|
873
|
+
labels,
|
|
874
|
+
}: CheckoutStepperProps) {
|
|
875
|
+
const stepper = useCheckoutStepper({
|
|
876
|
+
initialStep: controlledStep ?? 0,
|
|
877
|
+
steps,
|
|
878
|
+
onStepChange,
|
|
879
|
+
onComplete,
|
|
880
|
+
validateOnNext: true,
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
// Sync with controlled step if provided
|
|
884
|
+
React.useEffect(() => {
|
|
885
|
+
if (controlledStep !== undefined && controlledStep !== stepper.currentStep) {
|
|
886
|
+
stepper.goToStep(controlledStep)
|
|
887
|
+
}
|
|
888
|
+
}, [controlledStep])
|
|
889
|
+
|
|
890
|
+
const contextValue: CheckoutStepperContextValue = {
|
|
891
|
+
...stepper,
|
|
892
|
+
orientation,
|
|
893
|
+
size,
|
|
894
|
+
clickable,
|
|
895
|
+
allowPreviousOnly,
|
|
896
|
+
animated,
|
|
897
|
+
animationDirection,
|
|
898
|
+
isLoading,
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
<CheckoutStepperContext.Provider value={contextValue}>
|
|
903
|
+
<div className={cn("w-full", className)}>
|
|
904
|
+
{/* Step indicators */}
|
|
905
|
+
<nav
|
|
906
|
+
aria-label="Checkout progress"
|
|
907
|
+
className={cn(
|
|
908
|
+
"mb-8",
|
|
909
|
+
orientation === "horizontal"
|
|
910
|
+
? "flex items-start justify-between overflow-x-auto pb-2"
|
|
911
|
+
: "flex flex-col"
|
|
912
|
+
)}
|
|
913
|
+
>
|
|
914
|
+
{stepper.stepsWithStatus.map((step, index) => (
|
|
915
|
+
<StepItem
|
|
916
|
+
key={step.id}
|
|
917
|
+
step={step}
|
|
918
|
+
index={index}
|
|
919
|
+
isLast={index === steps.length - 1}
|
|
920
|
+
nextStatus={stepper.stepsWithStatus[index + 1]?.status}
|
|
921
|
+
/>
|
|
922
|
+
))}
|
|
923
|
+
</nav>
|
|
924
|
+
|
|
925
|
+
{/* Content area */}
|
|
926
|
+
<div
|
|
927
|
+
className={cn(
|
|
928
|
+
"flex flex-col lg:flex-row gap-6",
|
|
929
|
+
showSummary && summaryPosition === "left" && "lg:flex-row-reverse"
|
|
930
|
+
)}
|
|
931
|
+
>
|
|
932
|
+
{/* Main content */}
|
|
933
|
+
{showContent && (
|
|
934
|
+
<div className="flex-1 min-w-0">
|
|
935
|
+
{steps.map((step, index) => (
|
|
936
|
+
<StepContent
|
|
937
|
+
key={step.id}
|
|
938
|
+
isActive={index === stepper.currentStep}
|
|
939
|
+
direction={animationDirection}
|
|
940
|
+
animated={animated}
|
|
941
|
+
>
|
|
942
|
+
<div className="p-6 rounded-lg border bg-card">
|
|
943
|
+
<h2 className="text-xl font-semibold mb-2">{step.title}</h2>
|
|
944
|
+
{step.description && (
|
|
945
|
+
<p className="text-muted-foreground mb-6">{step.description}</p>
|
|
946
|
+
)}
|
|
947
|
+
{step.content}
|
|
948
|
+
</div>
|
|
949
|
+
</StepContent>
|
|
950
|
+
))}
|
|
951
|
+
|
|
952
|
+
{/* Navigation */}
|
|
953
|
+
<div className="mt-6">
|
|
954
|
+
<CheckoutNavigation labels={labels} />
|
|
955
|
+
</div>
|
|
956
|
+
</div>
|
|
957
|
+
)}
|
|
958
|
+
|
|
959
|
+
{/* Summary sidebar */}
|
|
960
|
+
{showSummary && (
|
|
961
|
+
<SummarySidebar
|
|
962
|
+
steps={stepper.stepsWithStatus}
|
|
963
|
+
position={summaryPosition}
|
|
964
|
+
/>
|
|
965
|
+
)}
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
</CheckoutStepperContext.Provider>
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ============================================
|
|
973
|
+
// EXPORTS
|
|
974
|
+
// ============================================
|
|
975
|
+
|
|
976
|
+
export default WakaCheckoutStepper
|