@wakastellar/ui 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +71 -8
  2. package/dist/cli/commands/add.d.ts +7 -0
  3. package/dist/cli/commands/init.d.ts +6 -0
  4. package/dist/cli/commands/list.d.ts +5 -0
  5. package/dist/cli/commands/search.d.ts +1 -0
  6. package/dist/cli/index.cjs +6014 -0
  7. package/dist/cli/index.d.ts +1 -0
  8. package/dist/cli/utils/config.d.ts +29 -0
  9. package/dist/cli/utils/logger.d.ts +20 -0
  10. package/dist/cli/utils/registry.d.ts +23 -0
  11. package/package.json +14 -3
  12. package/src/blocks/activity-timeline/index.tsx +586 -0
  13. package/src/blocks/calendar-view/index.tsx +756 -0
  14. package/src/blocks/chat/index.tsx +1018 -0
  15. package/src/blocks/chat/widget.tsx +504 -0
  16. package/src/blocks/dashboard/index.tsx +522 -0
  17. package/src/blocks/empty-states/index.tsx +452 -0
  18. package/src/blocks/error-pages/index.tsx +426 -0
  19. package/src/blocks/faq/index.tsx +479 -0
  20. package/src/blocks/file-manager/index.tsx +890 -0
  21. package/src/blocks/footer/index.tsx +133 -0
  22. package/src/blocks/header/index.tsx +357 -0
  23. package/src/blocks/headtab/index.tsx +139 -0
  24. package/src/blocks/i18n-editor/index.tsx +1016 -0
  25. package/src/blocks/index.ts +80 -0
  26. package/src/blocks/kanban-board/index.tsx +779 -0
  27. package/src/blocks/landing/index.tsx +677 -0
  28. package/src/blocks/language-selector/index.tsx +88 -0
  29. package/src/blocks/layout/index.tsx +159 -0
  30. package/src/blocks/login/index.tsx +339 -0
  31. package/src/blocks/login/types.ts +131 -0
  32. package/src/blocks/pricing/index.tsx +564 -0
  33. package/src/blocks/profile/index.tsx +746 -0
  34. package/src/blocks/settings/index.tsx +558 -0
  35. package/src/blocks/sidebar/index.tsx +713 -0
  36. package/src/blocks/theme-creator-block/index.tsx +835 -0
  37. package/src/blocks/user-management/index.tsx +1037 -0
  38. package/src/blocks/wizard/index.tsx +719 -0
  39. package/src/components/DataTable/DataTable.tsx +406 -0
  40. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  41. package/src/components/DataTable/DataTableBody.tsx +216 -0
  42. package/src/components/DataTable/DataTableCell.tsx +172 -0
  43. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  44. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  45. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  46. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  47. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  48. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  49. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  50. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  51. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  52. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  53. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  54. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  55. package/src/components/DataTable/README.md +446 -0
  56. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  57. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  58. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  59. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  60. package/src/components/DataTable/formatters/index.ts +335 -0
  61. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  62. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  64. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  65. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  66. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  67. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  68. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  69. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  70. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  71. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  72. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  73. package/src/components/DataTable/index.ts +81 -0
  74. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  75. package/src/components/DataTable/templates/index.tsx +803 -0
  76. package/src/components/DataTable/types.ts +504 -0
  77. package/src/components/DataTable/utils.ts +164 -0
  78. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  79. package/src/components/accordion/index.tsx +61 -0
  80. package/src/components/alert/index.tsx +61 -0
  81. package/src/components/alert-dialog/index.tsx +146 -0
  82. package/src/components/aspect-ratio/index.tsx +12 -0
  83. package/src/components/avatar/index.tsx +54 -0
  84. package/src/components/badge/Badge.stories.tsx +64 -0
  85. package/src/components/badge/index.tsx +38 -0
  86. package/src/components/button/Button.stories.tsx +173 -0
  87. package/src/components/button/index.tsx +56 -0
  88. package/src/components/calendar/index.tsx +73 -0
  89. package/src/components/card/index.tsx +78 -0
  90. package/src/components/checkbox/index.tsx +34 -0
  91. package/src/components/code/index.tsx +229 -0
  92. package/src/components/collapsible/index.tsx +16 -0
  93. package/src/components/command/index.tsx +162 -0
  94. package/src/components/context-menu/index.tsx +204 -0
  95. package/src/components/dialog/index.tsx +126 -0
  96. package/src/components/dropdown-menu/index.tsx +204 -0
  97. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  98. package/src/components/error-boundary/index.ts +7 -0
  99. package/src/components/form/index.tsx +183 -0
  100. package/src/components/hover-card/index.tsx +33 -0
  101. package/src/components/index.ts +368 -0
  102. package/src/components/input/Input.stories.tsx +100 -0
  103. package/src/components/input/index.tsx +27 -0
  104. package/src/components/input-otp/index.tsx +277 -0
  105. package/src/components/label/index.tsx +30 -0
  106. package/src/components/language-selector/index.tsx +341 -0
  107. package/src/components/menubar/index.tsx +240 -0
  108. package/src/components/navigation-menu/index.tsx +134 -0
  109. package/src/components/popover/index.tsx +35 -0
  110. package/src/components/progress/index.tsx +32 -0
  111. package/src/components/radio-group/index.tsx +48 -0
  112. package/src/components/scroll-area/index.tsx +52 -0
  113. package/src/components/select/index.tsx +164 -0
  114. package/src/components/separator/index.tsx +35 -0
  115. package/src/components/sheet/index.tsx +147 -0
  116. package/src/components/skeleton/index.tsx +22 -0
  117. package/src/components/slider/index.tsx +32 -0
  118. package/src/components/switch/index.tsx +33 -0
  119. package/src/components/table/index.tsx +117 -0
  120. package/src/components/tabs/index.tsx +59 -0
  121. package/src/components/textarea/index.tsx +30 -0
  122. package/src/components/theme-selector/index.tsx +327 -0
  123. package/src/components/toast/index.tsx +133 -0
  124. package/src/components/toaster/index.tsx +34 -0
  125. package/src/components/toggle/index.tsx +49 -0
  126. package/src/components/tooltip/index.tsx +34 -0
  127. package/src/components/typography/index.tsx +276 -0
  128. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  129. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  130. package/src/components/waka-activity-feed/index.tsx +686 -0
  131. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  132. package/src/components/waka-admincrumb/index.tsx +349 -0
  133. package/src/components/waka-alert-stack/index.tsx +827 -0
  134. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  135. package/src/components/waka-approval-chain/index.tsx +766 -0
  136. package/src/components/waka-audit-log/index.tsx +1475 -0
  137. package/src/components/waka-autocomplete/index.tsx +358 -0
  138. package/src/components/waka-badge-showcase/index.tsx +704 -0
  139. package/src/components/waka-barcode/index.tsx +260 -0
  140. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  141. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  142. package/src/components/waka-breadcrumb/index.tsx +376 -0
  143. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  144. package/src/components/waka-budget-burn/index.tsx +1234 -0
  145. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  146. package/src/components/waka-carousel/index.tsx +893 -0
  147. package/src/components/waka-cart-summary/index.tsx +1055 -0
  148. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  149. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  150. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  151. package/src/components/waka-charts/WakaChart.tsx +124 -0
  152. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  153. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  154. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  155. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  156. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  157. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  158. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  159. package/src/components/waka-charts/index.ts +90 -0
  160. package/src/components/waka-charts/types.ts +330 -0
  161. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  162. package/src/components/waka-checklist/index.tsx +1067 -0
  163. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  164. package/src/components/waka-cohort-table/index.tsx +1011 -0
  165. package/src/components/waka-color-picker/index.tsx +447 -0
  166. package/src/components/waka-combo-counter/index.tsx +864 -0
  167. package/src/components/waka-combobox/index.tsx +497 -0
  168. package/src/components/waka-command-bar/index.tsx +403 -0
  169. package/src/components/waka-compare-period/index.tsx +1230 -0
  170. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  171. package/src/components/waka-contribution-graph/index.tsx +552 -0
  172. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  173. package/src/components/waka-coupon-input/index.tsx +592 -0
  174. package/src/components/waka-credit-card-input/index.tsx +982 -0
  175. package/src/components/waka-daily-reward/index.tsx +762 -0
  176. package/src/components/waka-date-range-picker/index.tsx +378 -0
  177. package/src/components/waka-datetime-picker/index.tsx +793 -0
  178. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  179. package/src/components/waka-deployment-lane/index.tsx +673 -0
  180. package/src/components/waka-device-trust/index.tsx +1259 -0
  181. package/src/components/waka-dock/index.tsx +285 -0
  182. package/src/components/waka-drawer/index.tsx +319 -0
  183. package/src/components/waka-empty-state/index.tsx +545 -0
  184. package/src/components/waka-error-shake/index.tsx +398 -0
  185. package/src/components/waka-feature-announcement/index.tsx +991 -0
  186. package/src/components/waka-file-upload/index.tsx +437 -0
  187. package/src/components/waka-floating-nav/index.tsx +413 -0
  188. package/src/components/waka-flow-diagram/index.tsx +508 -0
  189. package/src/components/waka-funnel-chart/index.tsx +823 -0
  190. package/src/components/waka-glow-card/index.tsx +246 -0
  191. package/src/components/waka-goal-progress/index.tsx +1025 -0
  192. package/src/components/waka-haptic-button/index.tsx +388 -0
  193. package/src/components/waka-health-pulse/index.tsx +451 -0
  194. package/src/components/waka-heatmap/index.tsx +1026 -0
  195. package/src/components/waka-hotspot/index.tsx +682 -0
  196. package/src/components/waka-image/index.tsx +373 -0
  197. package/src/components/waka-incident-timeline/index.tsx +686 -0
  198. package/src/components/waka-invoice-preview/index.tsx +829 -0
  199. package/src/components/waka-kanban/index.tsx +646 -0
  200. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  201. package/src/components/waka-leaderboard/index.tsx +746 -0
  202. package/src/components/waka-level-progress/index.tsx +665 -0
  203. package/src/components/waka-liquid-button/index.tsx +520 -0
  204. package/src/components/waka-loading-orbit/index.tsx +478 -0
  205. package/src/components/waka-loot-box/index.tsx +1091 -0
  206. package/src/components/waka-magic-link/index.tsx +321 -0
  207. package/src/components/waka-magnetic-button/index.tsx +567 -0
  208. package/src/components/waka-mention-input/index.tsx +953 -0
  209. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  210. package/src/components/waka-milestone-road/index.tsx +1064 -0
  211. package/src/components/waka-modal/index.tsx +374 -0
  212. package/src/components/waka-morph-button/index.tsx +495 -0
  213. package/src/components/waka-network-topology/index.tsx +801 -0
  214. package/src/components/waka-notifications/index.tsx +414 -0
  215. package/src/components/waka-number-input/index.tsx +373 -0
  216. package/src/components/waka-orbital-menu/index.tsx +445 -0
  217. package/src/components/waka-order-tracker/index.tsx +1041 -0
  218. package/src/components/waka-pagination/index.tsx +393 -0
  219. package/src/components/waka-password-strength/index.tsx +824 -0
  220. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  221. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  222. package/src/components/waka-phone-input/index.tsx +801 -0
  223. package/src/components/waka-pipeline-view/index.tsx +604 -0
  224. package/src/components/waka-player-card/index.tsx +691 -0
  225. package/src/components/waka-points-popup/index.tsx +366 -0
  226. package/src/components/waka-power-up/index.tsx +1155 -0
  227. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  228. package/src/components/waka-pricing-table/index.tsx +755 -0
  229. package/src/components/waka-product-card/index.tsx +786 -0
  230. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  231. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  232. package/src/components/waka-qrcode/index.tsx +232 -0
  233. package/src/components/waka-quest-card/index.tsx +1275 -0
  234. package/src/components/waka-quota-bar/index.tsx +693 -0
  235. package/src/components/waka-radar-score/index.tsx +512 -0
  236. package/src/components/waka-rank-badge/index.tsx +813 -0
  237. package/src/components/waka-rating-input/index.tsx +560 -0
  238. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  239. package/src/components/waka-region-map/index.tsx +730 -0
  240. package/src/components/waka-resource-gauge/index.tsx +654 -0
  241. package/src/components/waka-resource-pool/index.tsx +1035 -0
  242. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  243. package/src/components/waka-rollback-slider/index.tsx +891 -0
  244. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  245. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  246. package/src/components/waka-scratch-card/index.tsx +914 -0
  247. package/src/components/waka-season-pass/index.tsx +886 -0
  248. package/src/components/waka-security-score/index.tsx +1126 -0
  249. package/src/components/waka-segmented-control/index.tsx +238 -0
  250. package/src/components/waka-server-rack/index.tsx +764 -0
  251. package/src/components/waka-session-manager/index.tsx +815 -0
  252. package/src/components/waka-signature-pad/index.tsx +744 -0
  253. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  254. package/src/components/waka-skill-tree/index.tsx +1031 -0
  255. package/src/components/waka-sla-tracker/index.tsx +798 -0
  256. package/src/components/waka-slider-range/index.tsx +765 -0
  257. package/src/components/waka-spin-wheel/index.tsx +671 -0
  258. package/src/components/waka-spinner/index.tsx +284 -0
  259. package/src/components/waka-spotlight/index.tsx +410 -0
  260. package/src/components/waka-stat/index.tsx +428 -0
  261. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  262. package/src/components/waka-status-matrix/index.tsx +565 -0
  263. package/src/components/waka-stepper/index.tsx +489 -0
  264. package/src/components/waka-streak-counter/index.tsx +334 -0
  265. package/src/components/waka-success-explosion/index.tsx +453 -0
  266. package/src/components/waka-swipe-card/index.tsx +574 -0
  267. package/src/components/waka-tabs-morph/index.tsx +509 -0
  268. package/src/components/waka-tag-input/index.tsx +877 -0
  269. package/src/components/waka-team-banner/index.tsx +1183 -0
  270. package/src/components/waka-terminal-output/index.tsx +836 -0
  271. package/src/components/waka-theme-creator/index.tsx +762 -0
  272. package/src/components/waka-theme-manager/index.tsx +654 -0
  273. package/src/components/waka-thread-view/index.tsx +874 -0
  274. package/src/components/waka-tilt-card/index.tsx +250 -0
  275. package/src/components/waka-time-picker/index.tsx +479 -0
  276. package/src/components/waka-timeline/index.tsx +385 -0
  277. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  278. package/src/components/waka-tour-guide/index.tsx +920 -0
  279. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  280. package/src/components/waka-tree/index.tsx +557 -0
  281. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  282. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  283. package/src/components/waka-typewriter/index.tsx +566 -0
  284. package/src/components/waka-typing-indicator/index.tsx +649 -0
  285. package/src/components/waka-versus-card/index.tsx +1026 -0
  286. package/src/components/waka-video/index.tsx +557 -0
  287. package/src/components/waka-video-call/index.tsx +1087 -0
  288. package/src/components/waka-virtual-list/index.tsx +327 -0
  289. package/src/components/waka-voice-message/index.tsx +1019 -0
  290. package/src/components/waka-welcome-modal/index.tsx +790 -0
  291. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,592 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Tag, Check, X, Loader2, Percent, Ticket } from "lucide-react"
6
+ import { Button } from "../button"
7
+ import { Input } from "../input"
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ export interface Coupon {
14
+ /** Unique coupon code */
15
+ code: string
16
+ /** Discount value (percentage or fixed amount) */
17
+ discountValue: number
18
+ /** Discount type */
19
+ discountType: "percentage" | "fixed"
20
+ /** Optional description */
21
+ description?: string
22
+ /** Optional expiration date */
23
+ expiresAt?: Date
24
+ }
25
+
26
+ export interface CouponValidationResult {
27
+ /** Whether the coupon is valid */
28
+ isValid: boolean
29
+ /** The coupon data if valid */
30
+ coupon?: Coupon
31
+ /** Error message if invalid */
32
+ errorMessage?: string
33
+ }
34
+
35
+ export interface WakaCouponInputProps {
36
+ /** Currently applied coupons */
37
+ appliedCoupons?: Coupon[]
38
+ /** Callback when a coupon is applied */
39
+ onApply?: (coupon: Coupon) => void
40
+ /** Callback when a coupon is removed */
41
+ onRemove?: (code: string) => void
42
+ /** Validation function that returns a promise */
43
+ validateCoupon?: (code: string) => Promise<CouponValidationResult>
44
+ /** Allow multiple coupons */
45
+ allowMultiple?: boolean
46
+ /** Maximum number of coupons (when allowMultiple is true) */
47
+ maxCoupons?: number
48
+ /** Placeholder text */
49
+ placeholder?: string
50
+ /** Apply button text */
51
+ applyButtonText?: string
52
+ /** Currency symbol for fixed discounts */
53
+ currencySymbol?: string
54
+ /** Disabled state */
55
+ disabled?: boolean
56
+ /** Size variant */
57
+ size?: "sm" | "md" | "lg"
58
+ /** Custom class name */
59
+ className?: string
60
+ /** Custom input class name */
61
+ inputClassName?: string
62
+ /** Custom button class name */
63
+ buttonClassName?: string
64
+ /** Show applied coupons list */
65
+ showAppliedCoupons?: boolean
66
+ /** Format discount display */
67
+ formatDiscount?: (coupon: Coupon) => string
68
+ /** ID for accessibility */
69
+ id?: string
70
+ /** Name attribute */
71
+ name?: string
72
+ }
73
+
74
+ export type CouponInputStatus = "idle" | "loading" | "success" | "error"
75
+
76
+ // ============================================================================
77
+ // Hook: useCouponInput
78
+ // ============================================================================
79
+
80
+ export interface UseCouponInputOptions {
81
+ /** Initial applied coupons */
82
+ initialCoupons?: Coupon[]
83
+ /** Validation function */
84
+ validateCoupon?: (code: string) => Promise<CouponValidationResult>
85
+ /** Allow multiple coupons */
86
+ allowMultiple?: boolean
87
+ /** Maximum coupons */
88
+ maxCoupons?: number
89
+ /** Callback when coupon is applied */
90
+ onApply?: (coupon: Coupon) => void
91
+ /** Callback when coupon is removed */
92
+ onRemove?: (code: string) => void
93
+ /** Reset delay after success/error (ms) */
94
+ statusResetDelay?: number
95
+ }
96
+
97
+ export interface UseCouponInputReturn {
98
+ /** Current input value */
99
+ inputValue: string
100
+ /** Set input value */
101
+ setInputValue: (value: string) => void
102
+ /** Current status */
103
+ status: CouponInputStatus
104
+ /** Error message if any */
105
+ errorMessage: string | null
106
+ /** Applied coupons */
107
+ appliedCoupons: Coupon[]
108
+ /** Apply the current input value as a coupon */
109
+ applyCoupon: () => Promise<void>
110
+ /** Remove a coupon by code */
111
+ removeCoupon: (code: string) => void
112
+ /** Clear all coupons */
113
+ clearAllCoupons: () => void
114
+ /** Reset input and status */
115
+ resetInput: () => void
116
+ /** Check if can apply more coupons */
117
+ canApplyMore: boolean
118
+ /** Check if a code is already applied */
119
+ isCodeApplied: (code: string) => boolean
120
+ }
121
+
122
+ export function useCouponInput(
123
+ options: UseCouponInputOptions = {}
124
+ ): UseCouponInputReturn {
125
+ const {
126
+ initialCoupons = [],
127
+ validateCoupon,
128
+ allowMultiple = false,
129
+ maxCoupons = 5,
130
+ onApply,
131
+ onRemove,
132
+ statusResetDelay = 2000,
133
+ } = options
134
+
135
+ const [inputValue, setInputValue] = React.useState("")
136
+ const [status, setStatus] = React.useState<CouponInputStatus>("idle")
137
+ const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
138
+ const [appliedCoupons, setAppliedCoupons] =
139
+ React.useState<Coupon[]>(initialCoupons)
140
+
141
+ const statusTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
142
+
143
+ // Cleanup timeout on unmount
144
+ React.useEffect(() => {
145
+ return () => {
146
+ if (statusTimeoutRef.current) {
147
+ clearTimeout(statusTimeoutRef.current)
148
+ }
149
+ }
150
+ }, [])
151
+
152
+ const resetStatus = React.useCallback(() => {
153
+ if (statusTimeoutRef.current) {
154
+ clearTimeout(statusTimeoutRef.current)
155
+ }
156
+ statusTimeoutRef.current = setTimeout(() => {
157
+ setStatus("idle")
158
+ setErrorMessage(null)
159
+ }, statusResetDelay)
160
+ }, [statusResetDelay])
161
+
162
+ const canApplyMore = React.useMemo(() => {
163
+ if (!allowMultiple) {
164
+ return appliedCoupons.length === 0
165
+ }
166
+ return appliedCoupons.length < maxCoupons
167
+ }, [allowMultiple, appliedCoupons.length, maxCoupons])
168
+
169
+ const isCodeApplied = React.useCallback(
170
+ (code: string) => {
171
+ return appliedCoupons.some(
172
+ (c) => c.code.toUpperCase() === code.toUpperCase()
173
+ )
174
+ },
175
+ [appliedCoupons]
176
+ )
177
+
178
+ const applyCoupon = React.useCallback(async () => {
179
+ const code = inputValue.trim().toUpperCase()
180
+
181
+ if (!code) {
182
+ setStatus("error")
183
+ setErrorMessage("Please enter a coupon code")
184
+ resetStatus()
185
+ return
186
+ }
187
+
188
+ if (isCodeApplied(code)) {
189
+ setStatus("error")
190
+ setErrorMessage("This coupon is already applied")
191
+ resetStatus()
192
+ return
193
+ }
194
+
195
+ if (!canApplyMore) {
196
+ setStatus("error")
197
+ setErrorMessage(
198
+ allowMultiple
199
+ ? `Maximum ${maxCoupons} coupons allowed`
200
+ : "Only one coupon can be applied"
201
+ )
202
+ resetStatus()
203
+ return
204
+ }
205
+
206
+ setStatus("loading")
207
+ setErrorMessage(null)
208
+
209
+ try {
210
+ if (validateCoupon) {
211
+ const result = await validateCoupon(code)
212
+
213
+ if (result.isValid && result.coupon) {
214
+ setAppliedCoupons((prev) => [...prev, result.coupon!])
215
+ onApply?.(result.coupon)
216
+ setStatus("success")
217
+ setInputValue("")
218
+ resetStatus()
219
+ } else {
220
+ setStatus("error")
221
+ setErrorMessage(result.errorMessage || "Invalid coupon code")
222
+ resetStatus()
223
+ }
224
+ } else {
225
+ // Default behavior without validation: create a mock coupon
226
+ const mockCoupon: Coupon = {
227
+ code,
228
+ discountValue: 10,
229
+ discountType: "percentage",
230
+ description: "Discount applied",
231
+ }
232
+ setAppliedCoupons((prev) => [...prev, mockCoupon])
233
+ onApply?.(mockCoupon)
234
+ setStatus("success")
235
+ setInputValue("")
236
+ resetStatus()
237
+ }
238
+ } catch (error) {
239
+ setStatus("error")
240
+ setErrorMessage(
241
+ error instanceof Error ? error.message : "Failed to validate coupon"
242
+ )
243
+ resetStatus()
244
+ }
245
+ }, [
246
+ inputValue,
247
+ isCodeApplied,
248
+ canApplyMore,
249
+ allowMultiple,
250
+ maxCoupons,
251
+ validateCoupon,
252
+ onApply,
253
+ resetStatus,
254
+ ])
255
+
256
+ const removeCoupon = React.useCallback(
257
+ (code: string) => {
258
+ setAppliedCoupons((prev) =>
259
+ prev.filter((c) => c.code.toUpperCase() !== code.toUpperCase())
260
+ )
261
+ onRemove?.(code)
262
+ },
263
+ [onRemove]
264
+ )
265
+
266
+ const clearAllCoupons = React.useCallback(() => {
267
+ const codes = appliedCoupons.map((c) => c.code)
268
+ setAppliedCoupons([])
269
+ codes.forEach((code) => onRemove?.(code))
270
+ }, [appliedCoupons, onRemove])
271
+
272
+ const resetInput = React.useCallback(() => {
273
+ setInputValue("")
274
+ setStatus("idle")
275
+ setErrorMessage(null)
276
+ if (statusTimeoutRef.current) {
277
+ clearTimeout(statusTimeoutRef.current)
278
+ }
279
+ }, [])
280
+
281
+ return {
282
+ inputValue,
283
+ setInputValue,
284
+ status,
285
+ errorMessage,
286
+ appliedCoupons,
287
+ applyCoupon,
288
+ removeCoupon,
289
+ clearAllCoupons,
290
+ resetInput,
291
+ canApplyMore,
292
+ isCodeApplied,
293
+ }
294
+ }
295
+
296
+ // ============================================================================
297
+ // Size configurations
298
+ // ============================================================================
299
+
300
+ const sizeConfig = {
301
+ sm: {
302
+ input: "h-8 text-xs",
303
+ button: "h-8 px-3 text-xs",
304
+ icon: "h-3.5 w-3.5",
305
+ tag: "text-xs py-1 px-2",
306
+ tagIcon: "h-3 w-3",
307
+ },
308
+ md: {
309
+ input: "h-9 text-sm",
310
+ button: "h-9 px-4 text-sm",
311
+ icon: "h-4 w-4",
312
+ tag: "text-sm py-1.5 px-3",
313
+ tagIcon: "h-3.5 w-3.5",
314
+ },
315
+ lg: {
316
+ input: "h-10 text-base",
317
+ button: "h-10 px-5 text-base",
318
+ icon: "h-5 w-5",
319
+ tag: "text-base py-2 px-4",
320
+ tagIcon: "h-4 w-4",
321
+ },
322
+ }
323
+
324
+ // ============================================================================
325
+ // Component: WakaCouponInput
326
+ // ============================================================================
327
+
328
+ export function WakaCouponInput({
329
+ appliedCoupons: controlledAppliedCoupons,
330
+ onApply,
331
+ onRemove,
332
+ validateCoupon,
333
+ allowMultiple = false,
334
+ maxCoupons = 5,
335
+ placeholder = "Enter coupon code",
336
+ applyButtonText = "Apply",
337
+ currencySymbol = "$",
338
+ disabled = false,
339
+ size = "md",
340
+ className,
341
+ inputClassName,
342
+ buttonClassName,
343
+ showAppliedCoupons = true,
344
+ formatDiscount,
345
+ id,
346
+ name,
347
+ }: WakaCouponInputProps) {
348
+ const {
349
+ inputValue,
350
+ setInputValue,
351
+ status,
352
+ errorMessage,
353
+ appliedCoupons: hookAppliedCoupons,
354
+ applyCoupon,
355
+ removeCoupon,
356
+ canApplyMore,
357
+ } = useCouponInput({
358
+ initialCoupons: controlledAppliedCoupons,
359
+ validateCoupon,
360
+ allowMultiple,
361
+ maxCoupons,
362
+ onApply,
363
+ onRemove,
364
+ })
365
+
366
+ // Use controlled coupons if provided, otherwise use hook state
367
+ const appliedCoupons = controlledAppliedCoupons ?? hookAppliedCoupons
368
+
369
+ const [shakeKey, setShakeKey] = React.useState(0)
370
+ const inputRef = React.useRef<HTMLInputElement>(null)
371
+
372
+ const config = sizeConfig[size]
373
+
374
+ // Trigger shake animation on error
375
+ React.useEffect(() => {
376
+ if (status === "error") {
377
+ setShakeKey((prev) => prev + 1)
378
+ }
379
+ }, [status])
380
+
381
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
382
+ // Auto-uppercase
383
+ setInputValue(e.target.value.toUpperCase())
384
+ }
385
+
386
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
387
+ if (e.key === "Enter") {
388
+ e.preventDefault()
389
+ applyCoupon()
390
+ }
391
+ }
392
+
393
+ const handleApply = () => {
394
+ applyCoupon()
395
+ }
396
+
397
+ const handleRemove = (code: string) => {
398
+ removeCoupon(code)
399
+ }
400
+
401
+ const getDiscountDisplay = (coupon: Coupon): string => {
402
+ if (formatDiscount) {
403
+ return formatDiscount(coupon)
404
+ }
405
+ if (coupon.discountType === "percentage") {
406
+ return `${coupon.discountValue}% off`
407
+ }
408
+ return `${currencySymbol}${coupon.discountValue} off`
409
+ }
410
+
411
+ const getButtonContent = () => {
412
+ switch (status) {
413
+ case "loading":
414
+ return <Loader2 className={cn(config.icon, "animate-spin")} />
415
+ case "success":
416
+ return <Check className={config.icon} />
417
+ default:
418
+ return applyButtonText
419
+ }
420
+ }
421
+
422
+ const isInputDisabled = disabled || status === "loading" || !canApplyMore
423
+
424
+ return (
425
+ <div className={cn("space-y-3", className)}>
426
+ {/* Input Row */}
427
+ <div className="flex gap-2">
428
+ <div
429
+ key={shakeKey}
430
+ className={cn(
431
+ "relative flex-1",
432
+ status === "error" && "waka-coupon-shake"
433
+ )}
434
+ >
435
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
436
+ <Ticket className={config.icon} />
437
+ </div>
438
+ <Input
439
+ ref={inputRef}
440
+ id={id}
441
+ name={name}
442
+ type="text"
443
+ value={inputValue}
444
+ onChange={handleInputChange}
445
+ onKeyDown={handleKeyDown}
446
+ placeholder={placeholder}
447
+ disabled={isInputDisabled}
448
+ className={cn(
449
+ config.input,
450
+ "pl-10 font-mono uppercase tracking-wider",
451
+ status === "error" &&
452
+ "border-destructive focus-visible:ring-destructive",
453
+ status === "success" &&
454
+ "border-green-500 focus-visible:ring-green-500",
455
+ inputClassName
456
+ )}
457
+ aria-invalid={status === "error"}
458
+ aria-describedby={errorMessage ? "coupon-error" : undefined}
459
+ />
460
+ {/* Success checkmark inside input */}
461
+ {status === "success" && (
462
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
463
+ <div className="waka-coupon-success-check rounded-full bg-green-500 p-0.5">
464
+ <Check className="h-3 w-3 text-white" />
465
+ </div>
466
+ </div>
467
+ )}
468
+ </div>
469
+
470
+ <Button
471
+ type="button"
472
+ onClick={handleApply}
473
+ disabled={isInputDisabled || !inputValue.trim()}
474
+ className={cn(
475
+ config.button,
476
+ status === "success" && "bg-green-500 hover:bg-green-600",
477
+ buttonClassName
478
+ )}
479
+ >
480
+ {getButtonContent()}
481
+ </Button>
482
+ </div>
483
+
484
+ {/* Error Message */}
485
+ {status === "error" && errorMessage && (
486
+ <p
487
+ id="coupon-error"
488
+ className="text-sm text-destructive flex items-center gap-1.5"
489
+ role="alert"
490
+ >
491
+ <X className="h-3.5 w-3.5" />
492
+ {errorMessage}
493
+ </p>
494
+ )}
495
+
496
+ {/* Applied Coupons List */}
497
+ {showAppliedCoupons && appliedCoupons.length > 0 && (
498
+ <div className="space-y-2">
499
+ {appliedCoupons.map((coupon) => (
500
+ <div
501
+ key={coupon.code}
502
+ className={cn(
503
+ "flex items-center justify-between rounded-lg border bg-muted/50 waka-coupon-applied",
504
+ config.tag
505
+ )}
506
+ >
507
+ <div className="flex items-center gap-2">
508
+ <Tag className={cn(config.tagIcon, "text-primary")} />
509
+ <span className="font-mono font-medium">{coupon.code}</span>
510
+ <span className="text-muted-foreground">-</span>
511
+ <span className="flex items-center gap-1 text-green-600 font-medium">
512
+ {coupon.discountType === "percentage" ? (
513
+ <Percent className={config.tagIcon} />
514
+ ) : null}
515
+ {getDiscountDisplay(coupon)}
516
+ </span>
517
+ </div>
518
+ <Button
519
+ type="button"
520
+ variant="ghost"
521
+ size="icon"
522
+ onClick={() => handleRemove(coupon.code)}
523
+ disabled={disabled}
524
+ className="h-6 w-6 text-muted-foreground hover:text-destructive"
525
+ aria-label={`Remove coupon ${coupon.code}`}
526
+ >
527
+ <X className="h-3.5 w-3.5" />
528
+ </Button>
529
+ </div>
530
+ ))}
531
+ </div>
532
+ )}
533
+
534
+ {/* Multiple coupon info */}
535
+ {allowMultiple && appliedCoupons.length > 0 && canApplyMore && (
536
+ <p className="text-xs text-muted-foreground">
537
+ {appliedCoupons.length} of {maxCoupons} coupons applied
538
+ </p>
539
+ )}
540
+
541
+ {/* Animation Styles */}
542
+ <style
543
+ dangerouslySetInnerHTML={{
544
+ __html: `
545
+ @keyframes waka-coupon-shake {
546
+ 0%, 100% { transform: translateX(0); }
547
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
548
+ 20%, 40%, 60%, 80% { transform: translateX(4px); }
549
+ }
550
+
551
+ @keyframes waka-coupon-success-pop {
552
+ 0% { transform: scale(0); opacity: 0; }
553
+ 50% { transform: scale(1.2); }
554
+ 100% { transform: scale(1); opacity: 1; }
555
+ }
556
+
557
+ @keyframes waka-coupon-slide-in {
558
+ 0% {
559
+ opacity: 0;
560
+ transform: translateY(-10px);
561
+ }
562
+ 100% {
563
+ opacity: 1;
564
+ transform: translateY(0);
565
+ }
566
+ }
567
+
568
+ .waka-coupon-shake {
569
+ animation: waka-coupon-shake 0.4s ease-in-out;
570
+ }
571
+
572
+ .waka-coupon-success-check {
573
+ animation: waka-coupon-success-pop 0.3s ease-out;
574
+ }
575
+
576
+ .waka-coupon-applied {
577
+ animation: waka-coupon-slide-in 0.3s ease-out;
578
+ }
579
+ `,
580
+ }}
581
+ />
582
+ </div>
583
+ )
584
+ }
585
+
586
+ WakaCouponInput.displayName = "WakaCouponInput"
587
+
588
+ // ============================================================================
589
+ // Exports
590
+ // ============================================================================
591
+
592
+ export default WakaCouponInput