@wakastellar/ui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/cli/commands/add.d.ts +7 -0
  2. package/dist/cli/commands/init.d.ts +6 -0
  3. package/dist/cli/commands/list.d.ts +5 -0
  4. package/dist/cli/commands/search.d.ts +1 -0
  5. package/dist/cli/index.cjs +4844 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/utils/config.d.ts +29 -0
  8. package/dist/cli/utils/logger.d.ts +20 -0
  9. package/dist/cli/utils/registry.d.ts +23 -0
  10. package/package.json +14 -3
  11. package/src/blocks/activity-timeline/index.tsx +586 -0
  12. package/src/blocks/calendar-view/index.tsx +756 -0
  13. package/src/blocks/chat/index.tsx +1018 -0
  14. package/src/blocks/chat/widget.tsx +504 -0
  15. package/src/blocks/dashboard/index.tsx +522 -0
  16. package/src/blocks/empty-states/index.tsx +452 -0
  17. package/src/blocks/error-pages/index.tsx +426 -0
  18. package/src/blocks/faq/index.tsx +479 -0
  19. package/src/blocks/file-manager/index.tsx +890 -0
  20. package/src/blocks/footer/index.tsx +133 -0
  21. package/src/blocks/header/index.tsx +357 -0
  22. package/src/blocks/headtab/index.tsx +139 -0
  23. package/src/blocks/i18n-editor/index.tsx +1016 -0
  24. package/src/blocks/index.ts +80 -0
  25. package/src/blocks/kanban-board/index.tsx +779 -0
  26. package/src/blocks/landing/index.tsx +677 -0
  27. package/src/blocks/language-selector/index.tsx +88 -0
  28. package/src/blocks/layout/index.tsx +159 -0
  29. package/src/blocks/login/index.tsx +339 -0
  30. package/src/blocks/login/types.ts +131 -0
  31. package/src/blocks/pricing/index.tsx +564 -0
  32. package/src/blocks/profile/index.tsx +746 -0
  33. package/src/blocks/settings/index.tsx +558 -0
  34. package/src/blocks/sidebar/index.tsx +713 -0
  35. package/src/blocks/theme-creator-block/index.tsx +835 -0
  36. package/src/blocks/user-management/index.tsx +1037 -0
  37. package/src/blocks/wizard/index.tsx +719 -0
  38. package/src/components/DataTable/DataTable.tsx +406 -0
  39. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  40. package/src/components/DataTable/DataTableBody.tsx +216 -0
  41. package/src/components/DataTable/DataTableCell.tsx +172 -0
  42. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  43. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  44. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  45. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  46. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  47. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  48. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  49. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  50. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  51. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  52. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  53. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  54. package/src/components/DataTable/README.md +446 -0
  55. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  56. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  57. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  58. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  59. package/src/components/DataTable/formatters/index.ts +335 -0
  60. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  61. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  62. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  64. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  65. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  66. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  67. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  68. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  69. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  70. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  71. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  72. package/src/components/DataTable/index.ts +81 -0
  73. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  74. package/src/components/DataTable/templates/index.tsx +803 -0
  75. package/src/components/DataTable/types.ts +504 -0
  76. package/src/components/DataTable/utils.ts +164 -0
  77. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  78. package/src/components/accordion/index.tsx +61 -0
  79. package/src/components/alert/index.tsx +61 -0
  80. package/src/components/alert-dialog/index.tsx +146 -0
  81. package/src/components/aspect-ratio/index.tsx +12 -0
  82. package/src/components/avatar/index.tsx +54 -0
  83. package/src/components/badge/Badge.stories.tsx +64 -0
  84. package/src/components/badge/index.tsx +38 -0
  85. package/src/components/button/Button.stories.tsx +173 -0
  86. package/src/components/button/index.tsx +56 -0
  87. package/src/components/calendar/index.tsx +73 -0
  88. package/src/components/card/index.tsx +78 -0
  89. package/src/components/checkbox/index.tsx +34 -0
  90. package/src/components/code/index.tsx +229 -0
  91. package/src/components/collapsible/index.tsx +16 -0
  92. package/src/components/command/index.tsx +162 -0
  93. package/src/components/context-menu/index.tsx +204 -0
  94. package/src/components/dialog/index.tsx +126 -0
  95. package/src/components/dropdown-menu/index.tsx +204 -0
  96. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  97. package/src/components/error-boundary/index.ts +7 -0
  98. package/src/components/form/index.tsx +183 -0
  99. package/src/components/hover-card/index.tsx +33 -0
  100. package/src/components/index.ts +368 -0
  101. package/src/components/input/Input.stories.tsx +100 -0
  102. package/src/components/input/index.tsx +27 -0
  103. package/src/components/input-otp/index.tsx +277 -0
  104. package/src/components/label/index.tsx +30 -0
  105. package/src/components/language-selector/index.tsx +341 -0
  106. package/src/components/menubar/index.tsx +240 -0
  107. package/src/components/navigation-menu/index.tsx +134 -0
  108. package/src/components/popover/index.tsx +35 -0
  109. package/src/components/progress/index.tsx +32 -0
  110. package/src/components/radio-group/index.tsx +48 -0
  111. package/src/components/scroll-area/index.tsx +52 -0
  112. package/src/components/select/index.tsx +164 -0
  113. package/src/components/separator/index.tsx +35 -0
  114. package/src/components/sheet/index.tsx +147 -0
  115. package/src/components/skeleton/index.tsx +22 -0
  116. package/src/components/slider/index.tsx +32 -0
  117. package/src/components/switch/index.tsx +33 -0
  118. package/src/components/table/index.tsx +117 -0
  119. package/src/components/tabs/index.tsx +59 -0
  120. package/src/components/textarea/index.tsx +30 -0
  121. package/src/components/theme-selector/index.tsx +327 -0
  122. package/src/components/toast/index.tsx +133 -0
  123. package/src/components/toaster/index.tsx +34 -0
  124. package/src/components/toggle/index.tsx +49 -0
  125. package/src/components/tooltip/index.tsx +34 -0
  126. package/src/components/typography/index.tsx +276 -0
  127. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  128. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  129. package/src/components/waka-activity-feed/index.tsx +686 -0
  130. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  131. package/src/components/waka-admincrumb/index.tsx +349 -0
  132. package/src/components/waka-alert-stack/index.tsx +827 -0
  133. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  134. package/src/components/waka-approval-chain/index.tsx +766 -0
  135. package/src/components/waka-audit-log/index.tsx +1475 -0
  136. package/src/components/waka-autocomplete/index.tsx +358 -0
  137. package/src/components/waka-badge-showcase/index.tsx +704 -0
  138. package/src/components/waka-barcode/index.tsx +260 -0
  139. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  140. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  141. package/src/components/waka-breadcrumb/index.tsx +376 -0
  142. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  143. package/src/components/waka-budget-burn/index.tsx +1234 -0
  144. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  145. package/src/components/waka-carousel/index.tsx +893 -0
  146. package/src/components/waka-cart-summary/index.tsx +1055 -0
  147. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  148. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  149. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  150. package/src/components/waka-charts/WakaChart.tsx +124 -0
  151. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  152. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  153. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  154. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  155. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  156. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  157. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  158. package/src/components/waka-charts/index.ts +90 -0
  159. package/src/components/waka-charts/types.ts +330 -0
  160. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  161. package/src/components/waka-checklist/index.tsx +1067 -0
  162. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  163. package/src/components/waka-cohort-table/index.tsx +1011 -0
  164. package/src/components/waka-color-picker/index.tsx +447 -0
  165. package/src/components/waka-combo-counter/index.tsx +864 -0
  166. package/src/components/waka-combobox/index.tsx +497 -0
  167. package/src/components/waka-command-bar/index.tsx +403 -0
  168. package/src/components/waka-compare-period/index.tsx +1230 -0
  169. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  170. package/src/components/waka-contribution-graph/index.tsx +552 -0
  171. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  172. package/src/components/waka-coupon-input/index.tsx +592 -0
  173. package/src/components/waka-credit-card-input/index.tsx +982 -0
  174. package/src/components/waka-daily-reward/index.tsx +762 -0
  175. package/src/components/waka-date-range-picker/index.tsx +378 -0
  176. package/src/components/waka-datetime-picker/index.tsx +793 -0
  177. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  178. package/src/components/waka-deployment-lane/index.tsx +673 -0
  179. package/src/components/waka-device-trust/index.tsx +1259 -0
  180. package/src/components/waka-dock/index.tsx +285 -0
  181. package/src/components/waka-drawer/index.tsx +319 -0
  182. package/src/components/waka-empty-state/index.tsx +545 -0
  183. package/src/components/waka-error-shake/index.tsx +398 -0
  184. package/src/components/waka-feature-announcement/index.tsx +991 -0
  185. package/src/components/waka-file-upload/index.tsx +437 -0
  186. package/src/components/waka-floating-nav/index.tsx +413 -0
  187. package/src/components/waka-flow-diagram/index.tsx +508 -0
  188. package/src/components/waka-funnel-chart/index.tsx +823 -0
  189. package/src/components/waka-glow-card/index.tsx +246 -0
  190. package/src/components/waka-goal-progress/index.tsx +1025 -0
  191. package/src/components/waka-haptic-button/index.tsx +388 -0
  192. package/src/components/waka-health-pulse/index.tsx +451 -0
  193. package/src/components/waka-heatmap/index.tsx +1026 -0
  194. package/src/components/waka-hotspot/index.tsx +682 -0
  195. package/src/components/waka-image/index.tsx +373 -0
  196. package/src/components/waka-incident-timeline/index.tsx +686 -0
  197. package/src/components/waka-invoice-preview/index.tsx +829 -0
  198. package/src/components/waka-kanban/index.tsx +646 -0
  199. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  200. package/src/components/waka-leaderboard/index.tsx +746 -0
  201. package/src/components/waka-level-progress/index.tsx +665 -0
  202. package/src/components/waka-liquid-button/index.tsx +520 -0
  203. package/src/components/waka-loading-orbit/index.tsx +478 -0
  204. package/src/components/waka-loot-box/index.tsx +1091 -0
  205. package/src/components/waka-magic-link/index.tsx +321 -0
  206. package/src/components/waka-magnetic-button/index.tsx +567 -0
  207. package/src/components/waka-mention-input/index.tsx +953 -0
  208. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  209. package/src/components/waka-milestone-road/index.tsx +1064 -0
  210. package/src/components/waka-modal/index.tsx +374 -0
  211. package/src/components/waka-morph-button/index.tsx +495 -0
  212. package/src/components/waka-network-topology/index.tsx +801 -0
  213. package/src/components/waka-notifications/index.tsx +414 -0
  214. package/src/components/waka-number-input/index.tsx +373 -0
  215. package/src/components/waka-orbital-menu/index.tsx +445 -0
  216. package/src/components/waka-order-tracker/index.tsx +1041 -0
  217. package/src/components/waka-pagination/index.tsx +393 -0
  218. package/src/components/waka-password-strength/index.tsx +824 -0
  219. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  220. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  221. package/src/components/waka-phone-input/index.tsx +801 -0
  222. package/src/components/waka-pipeline-view/index.tsx +604 -0
  223. package/src/components/waka-player-card/index.tsx +691 -0
  224. package/src/components/waka-points-popup/index.tsx +366 -0
  225. package/src/components/waka-power-up/index.tsx +1155 -0
  226. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  227. package/src/components/waka-pricing-table/index.tsx +755 -0
  228. package/src/components/waka-product-card/index.tsx +786 -0
  229. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  230. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  231. package/src/components/waka-qrcode/index.tsx +232 -0
  232. package/src/components/waka-quest-card/index.tsx +1275 -0
  233. package/src/components/waka-quota-bar/index.tsx +693 -0
  234. package/src/components/waka-radar-score/index.tsx +512 -0
  235. package/src/components/waka-rank-badge/index.tsx +813 -0
  236. package/src/components/waka-rating-input/index.tsx +560 -0
  237. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  238. package/src/components/waka-region-map/index.tsx +730 -0
  239. package/src/components/waka-resource-gauge/index.tsx +654 -0
  240. package/src/components/waka-resource-pool/index.tsx +1035 -0
  241. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  242. package/src/components/waka-rollback-slider/index.tsx +891 -0
  243. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  244. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  245. package/src/components/waka-scratch-card/index.tsx +914 -0
  246. package/src/components/waka-season-pass/index.tsx +886 -0
  247. package/src/components/waka-security-score/index.tsx +1126 -0
  248. package/src/components/waka-segmented-control/index.tsx +238 -0
  249. package/src/components/waka-server-rack/index.tsx +764 -0
  250. package/src/components/waka-session-manager/index.tsx +815 -0
  251. package/src/components/waka-signature-pad/index.tsx +744 -0
  252. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  253. package/src/components/waka-skill-tree/index.tsx +1031 -0
  254. package/src/components/waka-sla-tracker/index.tsx +798 -0
  255. package/src/components/waka-slider-range/index.tsx +765 -0
  256. package/src/components/waka-spin-wheel/index.tsx +671 -0
  257. package/src/components/waka-spinner/index.tsx +284 -0
  258. package/src/components/waka-spotlight/index.tsx +410 -0
  259. package/src/components/waka-stat/index.tsx +428 -0
  260. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  261. package/src/components/waka-status-matrix/index.tsx +565 -0
  262. package/src/components/waka-stepper/index.tsx +489 -0
  263. package/src/components/waka-streak-counter/index.tsx +334 -0
  264. package/src/components/waka-success-explosion/index.tsx +453 -0
  265. package/src/components/waka-swipe-card/index.tsx +574 -0
  266. package/src/components/waka-tabs-morph/index.tsx +509 -0
  267. package/src/components/waka-tag-input/index.tsx +877 -0
  268. package/src/components/waka-team-banner/index.tsx +1183 -0
  269. package/src/components/waka-terminal-output/index.tsx +836 -0
  270. package/src/components/waka-theme-creator/index.tsx +762 -0
  271. package/src/components/waka-theme-manager/index.tsx +654 -0
  272. package/src/components/waka-thread-view/index.tsx +874 -0
  273. package/src/components/waka-tilt-card/index.tsx +250 -0
  274. package/src/components/waka-time-picker/index.tsx +479 -0
  275. package/src/components/waka-timeline/index.tsx +385 -0
  276. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  277. package/src/components/waka-tour-guide/index.tsx +920 -0
  278. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  279. package/src/components/waka-tree/index.tsx +557 -0
  280. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  281. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  282. package/src/components/waka-typewriter/index.tsx +566 -0
  283. package/src/components/waka-typing-indicator/index.tsx +649 -0
  284. package/src/components/waka-versus-card/index.tsx +1026 -0
  285. package/src/components/waka-video/index.tsx +557 -0
  286. package/src/components/waka-video-call/index.tsx +1087 -0
  287. package/src/components/waka-virtual-list/index.tsx +327 -0
  288. package/src/components/waka-voice-message/index.tsx +1019 -0
  289. package/src/components/waka-welcome-modal/index.tsx +790 -0
  290. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,982 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { CreditCard, Eye, EyeOff, Check, AlertCircle } from "lucide-react"
6
+ import { Input } from "../input"
7
+
8
+ // ============================================
9
+ // TYPES
10
+ // ============================================
11
+
12
+ export type CardType =
13
+ | "visa"
14
+ | "mastercard"
15
+ | "amex"
16
+ | "discover"
17
+ | "diners"
18
+ | "jcb"
19
+ | "unionpay"
20
+ | "maestro"
21
+ | "unknown"
22
+
23
+ export interface CardData {
24
+ /** Card number (formatted with spaces) */
25
+ cardNumber: string
26
+ /** Raw card number (no spaces) */
27
+ rawCardNumber: string
28
+ /** Card holder name */
29
+ cardHolder: string
30
+ /** Expiry date (MM/YY) */
31
+ expiry: string
32
+ /** CVV code */
33
+ cvv: string
34
+ /** Detected card type */
35
+ cardType: CardType
36
+ }
37
+
38
+ export interface CardErrors {
39
+ cardNumber?: string
40
+ cardHolder?: string
41
+ expiry?: string
42
+ cvv?: string
43
+ }
44
+
45
+ export interface WakaCreditCardInputProps {
46
+ /** Current card data */
47
+ value?: Partial<CardData>
48
+ /** Callback when card data changes */
49
+ onChange?: (data: CardData) => void
50
+ /** Callback when validation changes */
51
+ onValidationChange?: (isValid: boolean, errors: CardErrors) => void
52
+ /** Disabled state */
53
+ disabled?: boolean
54
+ /** Read-only state */
55
+ readOnly?: boolean
56
+ /** Combined single-field mode */
57
+ singleFieldMode?: boolean
58
+ /** Show card flip preview animation */
59
+ showCardPreview?: boolean
60
+ /** Show card type icon */
61
+ showCardTypeIcon?: boolean
62
+ /** Placeholder texts */
63
+ placeholders?: {
64
+ cardNumber?: string
65
+ cardHolder?: string
66
+ expiry?: string
67
+ cvv?: string
68
+ }
69
+ /** Labels for fields */
70
+ labels?: {
71
+ cardNumber?: string
72
+ cardHolder?: string
73
+ expiry?: string
74
+ cvv?: string
75
+ }
76
+ /** Custom error messages */
77
+ errorMessages?: {
78
+ cardNumber?: string
79
+ cardHolder?: string
80
+ expiry?: string
81
+ cvv?: string
82
+ luhn?: string
83
+ }
84
+ /** Size variant */
85
+ size?: "sm" | "md" | "lg"
86
+ /** Additional class names */
87
+ className?: string
88
+ /** ID for accessibility */
89
+ id?: string
90
+ /** Name prefix for form fields */
91
+ name?: string
92
+ /** Auto-focus first field */
93
+ autoFocus?: boolean
94
+ }
95
+
96
+ // ============================================
97
+ // CARD TYPE DETECTION
98
+ // ============================================
99
+
100
+ const CARD_PATTERNS: { type: CardType; pattern: RegExp; lengths: number[]; cvvLength: number }[] = [
101
+ { type: "amex", pattern: /^3[47]/, lengths: [15], cvvLength: 4 },
102
+ { type: "visa", pattern: /^4/, lengths: [13, 16, 19], cvvLength: 3 },
103
+ { type: "mastercard", pattern: /^(5[1-5]|2[2-7])/, lengths: [16], cvvLength: 3 },
104
+ { type: "discover", pattern: /^(6011|65|64[4-9])/, lengths: [16, 19], cvvLength: 3 },
105
+ { type: "diners", pattern: /^(36|38|30[0-5])/, lengths: [14], cvvLength: 3 },
106
+ { type: "jcb", pattern: /^35/, lengths: [16, 19], cvvLength: 3 },
107
+ { type: "unionpay", pattern: /^62/, lengths: [16, 17, 18, 19], cvvLength: 3 },
108
+ { type: "maestro", pattern: /^(50|5[6-9]|6[0-9])/, lengths: [12, 13, 14, 15, 16, 17, 18, 19], cvvLength: 3 },
109
+ ]
110
+
111
+ function detectCardType(cardNumber: string): CardType {
112
+ const cleaned = cardNumber.replace(/\D/g, "")
113
+ for (const card of CARD_PATTERNS) {
114
+ if (card.pattern.test(cleaned)) {
115
+ return card.type
116
+ }
117
+ }
118
+ return "unknown"
119
+ }
120
+
121
+ function getCardConfig(type: CardType) {
122
+ const config = CARD_PATTERNS.find((c) => c.type === type)
123
+ return config || { type: "unknown" as CardType, pattern: /.*/, lengths: [16], cvvLength: 3 }
124
+ }
125
+
126
+ // ============================================
127
+ // LUHN VALIDATION
128
+ // ============================================
129
+
130
+ function luhnCheck(cardNumber: string): boolean {
131
+ const cleaned = cardNumber.replace(/\D/g, "")
132
+ if (cleaned.length === 0) return false
133
+
134
+ let sum = 0
135
+ let isEven = false
136
+
137
+ for (let i = cleaned.length - 1; i >= 0; i--) {
138
+ let digit = parseInt(cleaned[i], 10)
139
+
140
+ if (isEven) {
141
+ digit *= 2
142
+ if (digit > 9) {
143
+ digit -= 9
144
+ }
145
+ }
146
+
147
+ sum += digit
148
+ isEven = !isEven
149
+ }
150
+
151
+ return sum % 10 === 0
152
+ }
153
+
154
+ // ============================================
155
+ // FORMATTING HELPERS
156
+ // ============================================
157
+
158
+ function formatCardNumber(value: string, cardType: CardType): string {
159
+ const cleaned = value.replace(/\D/g, "")
160
+
161
+ // Amex has 4-6-5 format
162
+ if (cardType === "amex") {
163
+ const parts = [cleaned.slice(0, 4), cleaned.slice(4, 10), cleaned.slice(10, 15)]
164
+ return parts.filter(Boolean).join(" ")
165
+ }
166
+
167
+ // Standard 4-4-4-4 format
168
+ const parts = []
169
+ for (let i = 0; i < cleaned.length; i += 4) {
170
+ parts.push(cleaned.slice(i, i + 4))
171
+ }
172
+ return parts.join(" ")
173
+ }
174
+
175
+ function formatExpiry(value: string): string {
176
+ const cleaned = value.replace(/\D/g, "")
177
+ if (cleaned.length >= 2) {
178
+ return `${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}`
179
+ }
180
+ return cleaned
181
+ }
182
+
183
+ function parseExpiry(value: string): { month: number; year: number } | null {
184
+ const match = value.match(/^(\d{2})\/(\d{2})$/)
185
+ if (!match) return null
186
+
187
+ const month = parseInt(match[1], 10)
188
+ const year = parseInt(match[2], 10) + 2000
189
+
190
+ return { month, year }
191
+ }
192
+
193
+ function isExpiryValid(expiry: string): boolean {
194
+ const parsed = parseExpiry(expiry)
195
+ if (!parsed) return false
196
+
197
+ const { month, year } = parsed
198
+ if (month < 1 || month > 12) return false
199
+
200
+ const now = new Date()
201
+ const currentYear = now.getFullYear()
202
+ const currentMonth = now.getMonth() + 1
203
+
204
+ if (year < currentYear) return false
205
+ if (year === currentYear && month < currentMonth) return false
206
+
207
+ return true
208
+ }
209
+
210
+ // ============================================
211
+ // CARD BRAND ICONS
212
+ // ============================================
213
+
214
+ function CardBrandIcon({ type, className }: { type: CardType; className?: string }) {
215
+ const baseClass = cn("w-8 h-6", className)
216
+
217
+ switch (type) {
218
+ case "visa":
219
+ return (
220
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
221
+ <rect width="48" height="32" rx="4" fill="#1A1F71" />
222
+ <path d="M20.5 21.5L22.5 10.5H25.5L23.5 21.5H20.5Z" fill="white" />
223
+ <path d="M32.5 10.7C31.9 10.5 31 10.2 29.8 10.2C26.8 10.2 24.7 11.7 24.7 13.9C24.7 15.5 26.2 16.4 27.3 16.9C28.5 17.5 28.9 17.9 28.9 18.4C28.9 19.2 27.9 19.5 27 19.5C25.8 19.5 25.1 19.3 24.1 18.9L23.7 18.7L23.3 21.3C24 21.6 25.3 21.8 26.6 21.8C29.8 21.8 31.8 20.3 31.9 18C31.9 16.7 31.1 15.7 29.4 14.9C28.4 14.4 27.8 14 27.8 13.5C27.8 13 28.4 12.5 29.5 12.5C30.5 12.5 31.2 12.7 31.7 12.9L32 13L32.5 10.7Z" fill="white" />
224
+ <path d="M37.5 10.5H35.2C34.5 10.5 34 10.7 33.7 11.4L29.5 21.5H32.7L33.3 19.8H37.2L37.6 21.5H40.5L37.5 10.5ZM34.2 17.5C34.5 16.7 35.5 14 35.5 14C35.5 14 35.8 13.2 36 12.7L36.2 13.9C36.2 13.9 36.8 16.7 36.9 17.5H34.2Z" fill="white" />
225
+ <path d="M18.5 10.5L15.6 18.1L15.3 16.8C14.7 15 13 13 11 12L13.7 21.5H17L21.9 10.5H18.5Z" fill="white" />
226
+ <path d="M13 10.5H8.1L8 10.7C11.8 11.6 14.3 14 15.2 16.9L14.2 11.5C14.1 10.8 13.6 10.5 13 10.5Z" fill="#F9A51A" />
227
+ </svg>
228
+ )
229
+ case "mastercard":
230
+ return (
231
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
232
+ <rect width="48" height="32" rx="4" fill="#000000" />
233
+ <circle cx="18" cy="16" r="8" fill="#EB001B" />
234
+ <circle cx="30" cy="16" r="8" fill="#F79E1B" />
235
+ <path d="M24 10.8C25.8 12.2 27 14.5 27 16C27 17.5 25.8 19.8 24 21.2C22.2 19.8 21 17.5 21 16C21 14.5 22.2 12.2 24 10.8Z" fill="#FF5F00" />
236
+ </svg>
237
+ )
238
+ case "amex":
239
+ return (
240
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
241
+ <rect width="48" height="32" rx="4" fill="#006FCF" />
242
+ <path d="M8 16H12L13 13.5L14 16H18L15.5 19.5H18V21H8V19.5H10.5L8 16Z" fill="white" />
243
+ <text x="20" y="20" fill="white" fontSize="8" fontWeight="bold" fontFamily="Arial">AMEX</text>
244
+ </svg>
245
+ )
246
+ case "discover":
247
+ return (
248
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
249
+ <rect width="48" height="32" rx="4" fill="#FFFFFF" stroke="#E5E5E5" />
250
+ <circle cx="30" cy="16" r="6" fill="#FF6600" />
251
+ <text x="8" y="20" fill="#000000" fontSize="7" fontWeight="bold" fontFamily="Arial">DISCOVER</text>
252
+ </svg>
253
+ )
254
+ case "diners":
255
+ return (
256
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
257
+ <rect width="48" height="32" rx="4" fill="#FFFFFF" stroke="#E5E5E5" />
258
+ <circle cx="24" cy="16" r="8" fill="#0079BE" />
259
+ <path d="M19 16C19 13.2 20.5 10.8 22.7 9.6V22.4C20.5 21.2 19 18.8 19 16Z" fill="white" />
260
+ <path d="M25.3 9.6C27.5 10.8 29 13.2 29 16C29 18.8 27.5 21.2 25.3 22.4V9.6Z" fill="white" />
261
+ </svg>
262
+ )
263
+ case "jcb":
264
+ return (
265
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
266
+ <rect width="48" height="32" rx="4" fill="#FFFFFF" stroke="#E5E5E5" />
267
+ <rect x="8" y="8" width="10" height="16" rx="2" fill="#0066CC" />
268
+ <rect x="19" y="8" width="10" height="16" rx="2" fill="#E30613" />
269
+ <rect x="30" y="8" width="10" height="16" rx="2" fill="#00A651" />
270
+ </svg>
271
+ )
272
+ case "unionpay":
273
+ return (
274
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
275
+ <rect width="48" height="32" rx="4" fill="#D71F27" />
276
+ <text x="10" y="20" fill="white" fontSize="8" fontWeight="bold" fontFamily="Arial">UnionPay</text>
277
+ </svg>
278
+ )
279
+ case "maestro":
280
+ return (
281
+ <svg className={baseClass} viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
282
+ <rect width="48" height="32" rx="4" fill="#000000" />
283
+ <circle cx="18" cy="16" r="8" fill="#0066CC" />
284
+ <circle cx="30" cy="16" r="8" fill="#CC0000" />
285
+ <path d="M24 10C26 11.5 27.5 13.5 27.5 16C27.5 18.5 26 20.5 24 22C22 20.5 20.5 18.5 20.5 16C20.5 13.5 22 11.5 24 10Z" fill="#6C4899" />
286
+ </svg>
287
+ )
288
+ default:
289
+ return <CreditCard className={cn("w-5 h-5 text-muted-foreground", className)} />
290
+ }
291
+ }
292
+
293
+ // ============================================
294
+ // CARD PREVIEW COMPONENT
295
+ // ============================================
296
+
297
+ interface CardPreviewProps {
298
+ cardData: CardData
299
+ isFlipped: boolean
300
+ className?: string
301
+ }
302
+
303
+ function CardPreview({ cardData, isFlipped, className }: CardPreviewProps) {
304
+ return (
305
+ <div className={cn("perspective-1000", className)}>
306
+ <div
307
+ className={cn(
308
+ "relative w-full h-48 transition-transform duration-700 transform-style-3d",
309
+ isFlipped && "rotate-y-180"
310
+ )}
311
+ style={{
312
+ transformStyle: "preserve-3d",
313
+ transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)",
314
+ }}
315
+ >
316
+ {/* Front of card */}
317
+ <div
318
+ className={cn(
319
+ "absolute inset-0 w-full h-full rounded-xl p-6 text-white backface-hidden",
320
+ "bg-gradient-to-br shadow-xl",
321
+ cardData.cardType === "visa" && "from-blue-600 to-blue-800",
322
+ cardData.cardType === "mastercard" && "from-red-600 to-orange-600",
323
+ cardData.cardType === "amex" && "from-cyan-600 to-cyan-800",
324
+ cardData.cardType === "discover" && "from-orange-500 to-orange-700",
325
+ cardData.cardType === "unknown" && "from-gray-600 to-gray-800"
326
+ )}
327
+ style={{ backfaceVisibility: "hidden" }}
328
+ >
329
+ <div className="flex justify-between items-start mb-8">
330
+ <div className="w-12 h-8 bg-gradient-to-br from-yellow-300 to-yellow-500 rounded opacity-90" />
331
+ <CardBrandIcon type={cardData.cardType} className="w-12 h-8" />
332
+ </div>
333
+
334
+ <div className="font-mono text-xl tracking-wider mb-6">
335
+ {cardData.cardNumber || "#### #### #### ####"}
336
+ </div>
337
+
338
+ <div className="flex justify-between items-end">
339
+ <div>
340
+ <div className="text-xs uppercase opacity-70 mb-1">Card Holder</div>
341
+ <div className="font-medium tracking-wide uppercase">
342
+ {cardData.cardHolder || "YOUR NAME"}
343
+ </div>
344
+ </div>
345
+ <div className="text-right">
346
+ <div className="text-xs uppercase opacity-70 mb-1">Expires</div>
347
+ <div className="font-mono">
348
+ {cardData.expiry || "MM/YY"}
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ {/* Back of card */}
355
+ <div
356
+ className={cn(
357
+ "absolute inset-0 w-full h-full rounded-xl text-white backface-hidden rotate-y-180",
358
+ "bg-gradient-to-br shadow-xl",
359
+ cardData.cardType === "visa" && "from-blue-700 to-blue-900",
360
+ cardData.cardType === "mastercard" && "from-red-700 to-orange-700",
361
+ cardData.cardType === "amex" && "from-cyan-700 to-cyan-900",
362
+ cardData.cardType === "discover" && "from-orange-600 to-orange-800",
363
+ cardData.cardType === "unknown" && "from-gray-700 to-gray-900"
364
+ )}
365
+ style={{
366
+ backfaceVisibility: "hidden",
367
+ transform: "rotateY(180deg)",
368
+ }}
369
+ >
370
+ <div className="w-full h-12 bg-black/40 mt-6" />
371
+
372
+ <div className="px-6 mt-6">
373
+ <div className="flex items-center gap-2">
374
+ <div className="flex-1 h-10 bg-white/20 rounded flex items-center justify-end pr-4">
375
+ <span className="font-mono text-lg italic">
376
+ {cardData.cvv || "###"}
377
+ </span>
378
+ </div>
379
+ <div className="text-xs opacity-70">CVV</div>
380
+ </div>
381
+ </div>
382
+
383
+ <div className="absolute bottom-4 right-4">
384
+ <CardBrandIcon type={cardData.cardType} className="w-10 h-6 opacity-50" />
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </div>
389
+ )
390
+ }
391
+
392
+ // ============================================
393
+ // HOOK
394
+ // ============================================
395
+
396
+ export interface UseCreditCardInputOptions {
397
+ /** Initial card data */
398
+ initialValue?: Partial<CardData>
399
+ /** Validation on change */
400
+ validateOnChange?: boolean
401
+ /** Custom error messages */
402
+ errorMessages?: WakaCreditCardInputProps["errorMessages"]
403
+ }
404
+
405
+ export function useCreditCardInput(options: UseCreditCardInputOptions = {}) {
406
+ const { initialValue, validateOnChange = true, errorMessages } = options
407
+
408
+ const [cardData, setCardData] = React.useState<CardData>({
409
+ cardNumber: initialValue?.cardNumber || "",
410
+ rawCardNumber: initialValue?.rawCardNumber || "",
411
+ cardHolder: initialValue?.cardHolder || "",
412
+ expiry: initialValue?.expiry || "",
413
+ cvv: initialValue?.cvv || "",
414
+ cardType: initialValue?.cardType || "unknown",
415
+ })
416
+
417
+ const [errors, setErrors] = React.useState<CardErrors>({})
418
+ const [touched, setTouched] = React.useState<Record<keyof CardErrors, boolean>>({
419
+ cardNumber: false,
420
+ cardHolder: false,
421
+ expiry: false,
422
+ cvv: false,
423
+ })
424
+
425
+ const validate = React.useCallback(
426
+ (data: CardData): CardErrors => {
427
+ const newErrors: CardErrors = {}
428
+ const config = getCardConfig(data.cardType)
429
+
430
+ // Card number validation
431
+ if (!data.rawCardNumber) {
432
+ newErrors.cardNumber = errorMessages?.cardNumber || "Card number is required"
433
+ } else if (!config.lengths.includes(data.rawCardNumber.length)) {
434
+ newErrors.cardNumber = errorMessages?.cardNumber || "Invalid card number length"
435
+ } else if (!luhnCheck(data.rawCardNumber)) {
436
+ newErrors.cardNumber = errorMessages?.luhn || "Invalid card number"
437
+ }
438
+
439
+ // Card holder validation
440
+ if (!data.cardHolder.trim()) {
441
+ newErrors.cardHolder = errorMessages?.cardHolder || "Cardholder name is required"
442
+ } else if (data.cardHolder.trim().length < 2) {
443
+ newErrors.cardHolder = errorMessages?.cardHolder || "Name is too short"
444
+ }
445
+
446
+ // Expiry validation
447
+ if (!data.expiry) {
448
+ newErrors.expiry = errorMessages?.expiry || "Expiry date is required"
449
+ } else if (!isExpiryValid(data.expiry)) {
450
+ newErrors.expiry = errorMessages?.expiry || "Invalid or expired date"
451
+ }
452
+
453
+ // CVV validation
454
+ if (!data.cvv) {
455
+ newErrors.cvv = errorMessages?.cvv || "CVV is required"
456
+ } else if (data.cvv.length !== config.cvvLength) {
457
+ newErrors.cvv = errorMessages?.cvv || `CVV must be ${config.cvvLength} digits`
458
+ }
459
+
460
+ return newErrors
461
+ },
462
+ [errorMessages]
463
+ )
464
+
465
+ const updateCardNumber = React.useCallback(
466
+ (value: string) => {
467
+ const raw = value.replace(/\D/g, "").slice(0, 19)
468
+ const cardType = detectCardType(raw)
469
+ const formatted = formatCardNumber(raw, cardType)
470
+
471
+ const newData = {
472
+ ...cardData,
473
+ cardNumber: formatted,
474
+ rawCardNumber: raw,
475
+ cardType,
476
+ }
477
+
478
+ setCardData(newData)
479
+ setTouched((prev) => ({ ...prev, cardNumber: true }))
480
+
481
+ if (validateOnChange) {
482
+ setErrors(validate(newData))
483
+ }
484
+ },
485
+ [cardData, validate, validateOnChange]
486
+ )
487
+
488
+ const updateCardHolder = React.useCallback(
489
+ (value: string) => {
490
+ const newData = {
491
+ ...cardData,
492
+ cardHolder: value.toUpperCase(),
493
+ }
494
+
495
+ setCardData(newData)
496
+ setTouched((prev) => ({ ...prev, cardHolder: true }))
497
+
498
+ if (validateOnChange) {
499
+ setErrors(validate(newData))
500
+ }
501
+ },
502
+ [cardData, validate, validateOnChange]
503
+ )
504
+
505
+ const updateExpiry = React.useCallback(
506
+ (value: string) => {
507
+ const cleaned = value.replace(/\D/g, "").slice(0, 4)
508
+ const formatted = formatExpiry(cleaned)
509
+
510
+ const newData = {
511
+ ...cardData,
512
+ expiry: formatted,
513
+ }
514
+
515
+ setCardData(newData)
516
+ setTouched((prev) => ({ ...prev, expiry: true }))
517
+
518
+ if (validateOnChange) {
519
+ setErrors(validate(newData))
520
+ }
521
+ },
522
+ [cardData, validate, validateOnChange]
523
+ )
524
+
525
+ const updateCvv = React.useCallback(
526
+ (value: string) => {
527
+ const config = getCardConfig(cardData.cardType)
528
+ const cleaned = value.replace(/\D/g, "").slice(0, config.cvvLength)
529
+
530
+ const newData = {
531
+ ...cardData,
532
+ cvv: cleaned,
533
+ }
534
+
535
+ setCardData(newData)
536
+ setTouched((prev) => ({ ...prev, cvv: true }))
537
+
538
+ if (validateOnChange) {
539
+ setErrors(validate(newData))
540
+ }
541
+ },
542
+ [cardData, validate, validateOnChange]
543
+ )
544
+
545
+ const reset = React.useCallback(() => {
546
+ setCardData({
547
+ cardNumber: "",
548
+ rawCardNumber: "",
549
+ cardHolder: "",
550
+ expiry: "",
551
+ cvv: "",
552
+ cardType: "unknown",
553
+ })
554
+ setErrors({})
555
+ setTouched({
556
+ cardNumber: false,
557
+ cardHolder: false,
558
+ expiry: false,
559
+ cvv: false,
560
+ })
561
+ }, [])
562
+
563
+ const isValid = React.useMemo(() => {
564
+ const validationErrors = validate(cardData)
565
+ return Object.keys(validationErrors).length === 0
566
+ }, [cardData, validate])
567
+
568
+ const visibleErrors = React.useMemo(() => {
569
+ const result: CardErrors = {}
570
+ if (touched.cardNumber && errors.cardNumber) result.cardNumber = errors.cardNumber
571
+ if (touched.cardHolder && errors.cardHolder) result.cardHolder = errors.cardHolder
572
+ if (touched.expiry && errors.expiry) result.expiry = errors.expiry
573
+ if (touched.cvv && errors.cvv) result.cvv = errors.cvv
574
+ return result
575
+ }, [errors, touched])
576
+
577
+ return {
578
+ cardData,
579
+ errors: visibleErrors,
580
+ touched,
581
+ isValid,
582
+ updateCardNumber,
583
+ updateCardHolder,
584
+ updateExpiry,
585
+ updateCvv,
586
+ validate: () => validate(cardData),
587
+ reset,
588
+ setTouched,
589
+ }
590
+ }
591
+
592
+ // ============================================
593
+ // MAIN COMPONENT
594
+ // ============================================
595
+
596
+ export function WakaCreditCardInput({
597
+ value,
598
+ onChange,
599
+ onValidationChange,
600
+ disabled = false,
601
+ readOnly = false,
602
+ singleFieldMode = false,
603
+ showCardPreview = false,
604
+ showCardTypeIcon = true,
605
+ placeholders,
606
+ labels,
607
+ errorMessages,
608
+ size = "md",
609
+ className,
610
+ id,
611
+ name,
612
+ autoFocus = false,
613
+ }: WakaCreditCardInputProps) {
614
+ const {
615
+ cardData,
616
+ errors,
617
+ isValid,
618
+ updateCardNumber,
619
+ updateCardHolder,
620
+ updateExpiry,
621
+ updateCvv,
622
+ } = useCreditCardInput({
623
+ initialValue: value,
624
+ errorMessages,
625
+ })
626
+
627
+ const [cvvVisible, setCvvVisible] = React.useState(false)
628
+ const [isFlipped, setIsFlipped] = React.useState(false)
629
+ const [focusedField, setFocusedField] = React.useState<string | null>(null)
630
+
631
+ const cardNumberRef = React.useRef<HTMLInputElement>(null)
632
+ const cardHolderRef = React.useRef<HTMLInputElement>(null)
633
+ const expiryRef = React.useRef<HTMLInputElement>(null)
634
+ const cvvRef = React.useRef<HTMLInputElement>(null)
635
+
636
+ // Notify parent of changes
637
+ React.useEffect(() => {
638
+ onChange?.(cardData)
639
+ }, [cardData, onChange])
640
+
641
+ // Notify parent of validation changes
642
+ React.useEffect(() => {
643
+ onValidationChange?.(isValid, errors)
644
+ }, [isValid, errors, onValidationChange])
645
+
646
+ // Auto-focus
647
+ React.useEffect(() => {
648
+ if (autoFocus && cardNumberRef.current) {
649
+ cardNumberRef.current.focus()
650
+ }
651
+ }, [autoFocus])
652
+
653
+ // Handle card number change with auto-advance
654
+ const handleCardNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
655
+ updateCardNumber(e.target.value)
656
+
657
+ // Auto-advance to next field
658
+ const config = getCardConfig(detectCardType(e.target.value.replace(/\D/g, "")))
659
+ const raw = e.target.value.replace(/\D/g, "")
660
+ if (config.lengths.includes(raw.length) && expiryRef.current) {
661
+ expiryRef.current.focus()
662
+ }
663
+ }
664
+
665
+ // Handle expiry change with auto-advance
666
+ const handleExpiryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
667
+ updateExpiry(e.target.value)
668
+
669
+ // Auto-advance to CVV
670
+ const cleaned = e.target.value.replace(/\D/g, "")
671
+ if (cleaned.length === 4 && cvvRef.current) {
672
+ cvvRef.current.focus()
673
+ }
674
+ }
675
+
676
+ // Handle CVV focus/blur for card flip
677
+ const handleCvvFocus = () => {
678
+ setFocusedField("cvv")
679
+ setIsFlipped(true)
680
+ }
681
+
682
+ const handleCvvBlur = () => {
683
+ setFocusedField(null)
684
+ setIsFlipped(false)
685
+ }
686
+
687
+ // Size classes
688
+ const sizeClasses = {
689
+ sm: "h-8 text-xs",
690
+ md: "h-9 text-sm",
691
+ lg: "h-10 text-base",
692
+ }
693
+
694
+ const labelSizeClasses = {
695
+ sm: "text-xs",
696
+ md: "text-sm",
697
+ lg: "text-base",
698
+ }
699
+
700
+ // Field wrapper with error state
701
+ const FieldWrapper = ({
702
+ children,
703
+ label,
704
+ error,
705
+ htmlFor,
706
+ }: {
707
+ children: React.ReactNode
708
+ label?: string
709
+ error?: string
710
+ htmlFor?: string
711
+ }) => (
712
+ <div className="space-y-1.5">
713
+ {label && (
714
+ <label
715
+ htmlFor={htmlFor}
716
+ className={cn(
717
+ "block font-medium text-foreground",
718
+ labelSizeClasses[size]
719
+ )}
720
+ >
721
+ {label}
722
+ </label>
723
+ )}
724
+ {children}
725
+ {error && (
726
+ <div className="flex items-center gap-1 text-destructive">
727
+ <AlertCircle className="h-3 w-3" />
728
+ <span className="text-xs">{error}</span>
729
+ </div>
730
+ )}
731
+ </div>
732
+ )
733
+
734
+ // Single field mode renders a combined input
735
+ if (singleFieldMode) {
736
+ return (
737
+ <div className={cn("space-y-4", className)}>
738
+ <FieldWrapper
739
+ label={labels?.cardNumber || "Card Details"}
740
+ error={errors.cardNumber}
741
+ htmlFor={id ? `${id}-combined` : undefined}
742
+ >
743
+ <div className="flex items-center gap-2">
744
+ {showCardTypeIcon && (
745
+ <CardBrandIcon type={cardData.cardType} className="flex-shrink-0" />
746
+ )}
747
+ <div className="flex-1 flex gap-2">
748
+ <Input
749
+ ref={cardNumberRef}
750
+ id={id ? `${id}-combined` : undefined}
751
+ name={name ? `${name}-number` : undefined}
752
+ type="text"
753
+ inputMode="numeric"
754
+ autoComplete="cc-number"
755
+ placeholder={placeholders?.cardNumber || "Card number"}
756
+ value={cardData.cardNumber}
757
+ onChange={handleCardNumberChange}
758
+ disabled={disabled}
759
+ readOnly={readOnly}
760
+ className={cn(
761
+ sizeClasses[size],
762
+ "flex-1",
763
+ errors.cardNumber && "border-destructive focus-visible:ring-destructive"
764
+ )}
765
+ />
766
+ <Input
767
+ ref={expiryRef}
768
+ name={name ? `${name}-expiry` : undefined}
769
+ type="text"
770
+ inputMode="numeric"
771
+ autoComplete="cc-exp"
772
+ placeholder={placeholders?.expiry || "MM/YY"}
773
+ value={cardData.expiry}
774
+ onChange={handleExpiryChange}
775
+ disabled={disabled}
776
+ readOnly={readOnly}
777
+ className={cn(
778
+ sizeClasses[size],
779
+ "w-20",
780
+ errors.expiry && "border-destructive focus-visible:ring-destructive"
781
+ )}
782
+ />
783
+ <div className="relative w-16">
784
+ <Input
785
+ ref={cvvRef}
786
+ name={name ? `${name}-cvv` : undefined}
787
+ type={cvvVisible ? "text" : "password"}
788
+ inputMode="numeric"
789
+ autoComplete="cc-csc"
790
+ placeholder={placeholders?.cvv || "CVV"}
791
+ value={cardData.cvv}
792
+ onChange={(e) => updateCvv(e.target.value)}
793
+ onFocus={handleCvvFocus}
794
+ onBlur={handleCvvBlur}
795
+ disabled={disabled}
796
+ readOnly={readOnly}
797
+ className={cn(
798
+ sizeClasses[size],
799
+ "pr-8",
800
+ errors.cvv && "border-destructive focus-visible:ring-destructive"
801
+ )}
802
+ />
803
+ <button
804
+ type="button"
805
+ onClick={() => setCvvVisible(!cvvVisible)}
806
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
807
+ tabIndex={-1}
808
+ >
809
+ {cvvVisible ? (
810
+ <EyeOff className="h-4 w-4" />
811
+ ) : (
812
+ <Eye className="h-4 w-4" />
813
+ )}
814
+ </button>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ </FieldWrapper>
819
+ </div>
820
+ )
821
+ }
822
+
823
+ // Full mode with all fields
824
+ return (
825
+ <div className={cn("space-y-4", className)}>
826
+ {showCardPreview && (
827
+ <CardPreview
828
+ cardData={cardData}
829
+ isFlipped={isFlipped}
830
+ className="mb-6"
831
+ />
832
+ )}
833
+
834
+ <div className="grid gap-4">
835
+ {/* Card Number */}
836
+ <FieldWrapper
837
+ label={labels?.cardNumber || "Card Number"}
838
+ error={errors.cardNumber}
839
+ htmlFor={id ? `${id}-number` : undefined}
840
+ >
841
+ <div className="relative">
842
+ <Input
843
+ ref={cardNumberRef}
844
+ id={id ? `${id}-number` : undefined}
845
+ name={name ? `${name}-number` : undefined}
846
+ type="text"
847
+ inputMode="numeric"
848
+ autoComplete="cc-number"
849
+ placeholder={placeholders?.cardNumber || "1234 5678 9012 3456"}
850
+ value={cardData.cardNumber}
851
+ onChange={handleCardNumberChange}
852
+ onFocus={() => setFocusedField("cardNumber")}
853
+ onBlur={() => setFocusedField(null)}
854
+ disabled={disabled}
855
+ readOnly={readOnly}
856
+ className={cn(
857
+ sizeClasses[size],
858
+ "pr-12",
859
+ errors.cardNumber && "border-destructive focus-visible:ring-destructive"
860
+ )}
861
+ />
862
+ {showCardTypeIcon && (
863
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
864
+ <CardBrandIcon type={cardData.cardType} />
865
+ </div>
866
+ )}
867
+ {cardData.rawCardNumber && !errors.cardNumber && (
868
+ <Check className="absolute right-12 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
869
+ )}
870
+ </div>
871
+ </FieldWrapper>
872
+
873
+ {/* Card Holder */}
874
+ <FieldWrapper
875
+ label={labels?.cardHolder || "Cardholder Name"}
876
+ error={errors.cardHolder}
877
+ htmlFor={id ? `${id}-holder` : undefined}
878
+ >
879
+ <Input
880
+ ref={cardHolderRef}
881
+ id={id ? `${id}-holder` : undefined}
882
+ name={name ? `${name}-holder` : undefined}
883
+ type="text"
884
+ autoComplete="cc-name"
885
+ placeholder={placeholders?.cardHolder || "JOHN DOE"}
886
+ value={cardData.cardHolder}
887
+ onChange={(e) => updateCardHolder(e.target.value)}
888
+ onFocus={() => setFocusedField("cardHolder")}
889
+ onBlur={() => setFocusedField(null)}
890
+ disabled={disabled}
891
+ readOnly={readOnly}
892
+ className={cn(
893
+ sizeClasses[size],
894
+ errors.cardHolder && "border-destructive focus-visible:ring-destructive"
895
+ )}
896
+ />
897
+ </FieldWrapper>
898
+
899
+ {/* Expiry and CVV */}
900
+ <div className="grid grid-cols-2 gap-4">
901
+ <FieldWrapper
902
+ label={labels?.expiry || "Expiry Date"}
903
+ error={errors.expiry}
904
+ htmlFor={id ? `${id}-expiry` : undefined}
905
+ >
906
+ <Input
907
+ ref={expiryRef}
908
+ id={id ? `${id}-expiry` : undefined}
909
+ name={name ? `${name}-expiry` : undefined}
910
+ type="text"
911
+ inputMode="numeric"
912
+ autoComplete="cc-exp"
913
+ placeholder={placeholders?.expiry || "MM/YY"}
914
+ value={cardData.expiry}
915
+ onChange={handleExpiryChange}
916
+ onFocus={() => setFocusedField("expiry")}
917
+ onBlur={() => setFocusedField(null)}
918
+ disabled={disabled}
919
+ readOnly={readOnly}
920
+ className={cn(
921
+ sizeClasses[size],
922
+ errors.expiry && "border-destructive focus-visible:ring-destructive"
923
+ )}
924
+ />
925
+ </FieldWrapper>
926
+
927
+ <FieldWrapper
928
+ label={labels?.cvv || "CVV"}
929
+ error={errors.cvv}
930
+ htmlFor={id ? `${id}-cvv` : undefined}
931
+ >
932
+ <div className="relative">
933
+ <Input
934
+ ref={cvvRef}
935
+ id={id ? `${id}-cvv` : undefined}
936
+ name={name ? `${name}-cvv` : undefined}
937
+ type={cvvVisible ? "text" : "password"}
938
+ inputMode="numeric"
939
+ autoComplete="cc-csc"
940
+ placeholder={placeholders?.cvv || cardData.cardType === "amex" ? "1234" : "123"}
941
+ value={cardData.cvv}
942
+ onChange={(e) => updateCvv(e.target.value)}
943
+ onFocus={handleCvvFocus}
944
+ onBlur={handleCvvBlur}
945
+ disabled={disabled}
946
+ readOnly={readOnly}
947
+ className={cn(
948
+ sizeClasses[size],
949
+ "pr-10",
950
+ errors.cvv && "border-destructive focus-visible:ring-destructive"
951
+ )}
952
+ />
953
+ <button
954
+ type="button"
955
+ onClick={() => setCvvVisible(!cvvVisible)}
956
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
957
+ tabIndex={-1}
958
+ aria-label={cvvVisible ? "Hide CVV" : "Show CVV"}
959
+ >
960
+ {cvvVisible ? (
961
+ <EyeOff className="h-4 w-4" />
962
+ ) : (
963
+ <Eye className="h-4 w-4" />
964
+ )}
965
+ </button>
966
+ </div>
967
+ </FieldWrapper>
968
+ </div>
969
+ </div>
970
+
971
+ {/* Validation summary */}
972
+ {isValid && cardData.rawCardNumber && (
973
+ <div className="flex items-center gap-2 text-sm text-green-600">
974
+ <Check className="h-4 w-4" />
975
+ <span>Card details are valid</span>
976
+ </div>
977
+ )}
978
+ </div>
979
+ )
980
+ }
981
+
982
+ export default WakaCreditCardInput