@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,277 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+
6
+ export interface InputOTPProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'size'> {
7
+ /** Nombre de champs OTP */
8
+ length?: number
9
+ /** Valeur contrôlée */
10
+ value?: string
11
+ /** Callback lors du changement */
12
+ onChange?: (value: string) => void
13
+ /** Callback quand l'OTP est complet */
14
+ onComplete?: (value: string) => void
15
+ /** Type de caractères acceptés */
16
+ pattern?: 'numeric' | 'alphanumeric' | 'alpha'
17
+ /** Afficher le contenu en clair ou masqué */
18
+ secure?: boolean
19
+ /** Désactiver le composant */
20
+ disabled?: boolean
21
+ /** Classe CSS personnalisée */
22
+ className?: string
23
+ /** Message d'erreur */
24
+ error?: string
25
+ /** Taille du composant */
26
+ size?: 'sm' | 'md' | 'lg'
27
+ /** Séparer les groupes de champs */
28
+ separator?: number
29
+ /** Élément de séparation personnalisé */
30
+ separatorElement?: React.ReactNode
31
+ }
32
+
33
+ /**
34
+ * InputOTP - Composant de saisie de code à usage unique (OTP)
35
+ *
36
+ * Composant accessible pour la saisie de codes de vérification, mots de passe temporaires, etc.
37
+ * Gère automatiquement le focus, la navigation au clavier et la validation des entrées.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * // OTP numérique simple
42
+ * <InputOTP length={6} onChange={(value) => console.log(value)} />
43
+ *
44
+ * // OTP avec séparateur
45
+ * <InputOTP
46
+ * length={6}
47
+ * separator={3}
48
+ * separatorElement={<span>-</span>}
49
+ * onComplete={(code) => verifyCode(code)}
50
+ * />
51
+ *
52
+ * // OTP sécurisé
53
+ * <InputOTP length={4} secure pattern="numeric" />
54
+ * ```
55
+ */
56
+ export const InputOTP = React.forwardRef<HTMLDivElement, InputOTPProps>(
57
+ ({
58
+ length = 6,
59
+ value = "",
60
+ onChange,
61
+ onComplete,
62
+ pattern = 'numeric',
63
+ secure = false,
64
+ disabled = false,
65
+ className,
66
+ error,
67
+ size = 'md',
68
+ separator,
69
+ separatorElement = <span className="text-muted-foreground">-</span>,
70
+ ...props
71
+ }, ref) => {
72
+ const [internalValue, setInternalValue] = React.useState<string[]>(
73
+ value.split('').slice(0, length)
74
+ )
75
+ const inputsRef = React.useRef<(HTMLInputElement | null)[]>([])
76
+
77
+ // Synchroniser avec la valeur externe
78
+ React.useEffect(() => {
79
+ const newValue = value.split('').slice(0, length)
80
+ setInternalValue(newValue)
81
+ }, [value, length])
82
+
83
+ // Obtenir le pattern de regex selon le type
84
+ const getPattern = () => {
85
+ switch (pattern) {
86
+ case 'numeric':
87
+ return /^[0-9]$/
88
+ case 'alpha':
89
+ return /^[a-zA-Z]$/
90
+ case 'alphanumeric':
91
+ return /^[a-zA-Z0-9]$/
92
+ default:
93
+ return /^.$/
94
+ }
95
+ }
96
+
97
+ const regex = getPattern()
98
+
99
+ // Gérer le changement de valeur
100
+ const handleChange = (index: number, inputValue: string) => {
101
+ if (disabled) return
102
+
103
+ // Ne garder que le dernier caractère saisi
104
+ const char = inputValue.slice(-1)
105
+
106
+ // Vérifier si le caractère correspond au pattern
107
+ if (char && !regex.test(char)) return
108
+
109
+ const newValue = [...internalValue]
110
+ newValue[index] = char
111
+
112
+ setInternalValue(newValue)
113
+
114
+ // Appeler le callback onChange
115
+ const stringValue = newValue.join('')
116
+ onChange?.(stringValue)
117
+
118
+ // Si un caractère a été saisi et que ce n'est pas le dernier champ, passer au suivant
119
+ if (char && index < length - 1) {
120
+ inputsRef.current[index + 1]?.focus()
121
+ }
122
+
123
+ // Si l'OTP est complet, appeler onComplete
124
+ if (newValue.every(v => v !== '') && newValue.length === length) {
125
+ onComplete?.(stringValue)
126
+ }
127
+ }
128
+
129
+ // Gérer la suppression
130
+ const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
131
+ if (disabled) return
132
+
133
+ // Backspace
134
+ if (e.key === 'Backspace') {
135
+ e.preventDefault()
136
+ const newValue = [...internalValue]
137
+
138
+ if (newValue[index]) {
139
+ // Supprimer le caractère actuel
140
+ newValue[index] = ''
141
+ setInternalValue(newValue)
142
+ onChange?.(newValue.join(''))
143
+ } else if (index > 0) {
144
+ // Si le champ est vide, aller au précédent et supprimer
145
+ newValue[index - 1] = ''
146
+ setInternalValue(newValue)
147
+ onChange?.(newValue.join(''))
148
+ inputsRef.current[index - 1]?.focus()
149
+ }
150
+ }
151
+
152
+ // Delete
153
+ if (e.key === 'Delete') {
154
+ e.preventDefault()
155
+ const newValue = [...internalValue]
156
+ newValue[index] = ''
157
+ setInternalValue(newValue)
158
+ onChange?.(newValue.join(''))
159
+ }
160
+
161
+ // Flèches gauche/droite
162
+ if (e.key === 'ArrowLeft' && index > 0) {
163
+ e.preventDefault()
164
+ inputsRef.current[index - 1]?.focus()
165
+ }
166
+
167
+ if (e.key === 'ArrowRight' && index < length - 1) {
168
+ e.preventDefault()
169
+ inputsRef.current[index + 1]?.focus()
170
+ }
171
+
172
+ // Home / End
173
+ if (e.key === 'Home') {
174
+ e.preventDefault()
175
+ inputsRef.current[0]?.focus()
176
+ }
177
+
178
+ if (e.key === 'End') {
179
+ e.preventDefault()
180
+ inputsRef.current[length - 1]?.focus()
181
+ }
182
+ }
183
+
184
+ // Gérer le collage
185
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
186
+ e.preventDefault()
187
+ if (disabled) return
188
+
189
+ const pastedData = e.clipboardData.getData('text/plain')
190
+ const chars = pastedData.split('').filter(char => regex.test(char)).slice(0, length)
191
+
192
+ const newValue = [...internalValue]
193
+ chars.forEach((char, i) => {
194
+ newValue[i] = char
195
+ })
196
+
197
+ setInternalValue(newValue)
198
+ onChange?.(newValue.join(''))
199
+
200
+ // Déplacer le focus sur le prochain champ vide ou le dernier
201
+ const nextEmptyIndex = newValue.findIndex(v => !v)
202
+ if (nextEmptyIndex !== -1) {
203
+ inputsRef.current[nextEmptyIndex]?.focus()
204
+ } else {
205
+ inputsRef.current[length - 1]?.focus()
206
+ }
207
+
208
+ // Si l'OTP est complet, appeler onComplete
209
+ if (newValue.every(v => v !== '') && newValue.length === length) {
210
+ onComplete?.(newValue.join(''))
211
+ }
212
+ }
213
+
214
+ // Gérer le focus - sélectionner tout le contenu
215
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
216
+ e.target.select()
217
+ }
218
+
219
+ // Tailles
220
+ const sizeClasses = {
221
+ sm: "h-8 w-8 text-sm",
222
+ md: "h-12 w-12 text-base",
223
+ lg: "h-16 w-16 text-lg",
224
+ }[size]
225
+
226
+ return (
227
+ <div ref={ref} className={cn("flex flex-col gap-2", className)}>
228
+ <div className="flex items-center gap-2">
229
+ {Array.from({ length }).map((_, index) => (
230
+ <React.Fragment key={index}>
231
+ <input
232
+ ref={(el) => (inputsRef.current[index] = el)}
233
+ type={secure ? 'password' : 'text'}
234
+ inputMode={pattern === 'numeric' ? 'numeric' : 'text'}
235
+ maxLength={1}
236
+ value={internalValue[index] || ''}
237
+ onChange={(e) => handleChange(index, e.target.value)}
238
+ onKeyDown={(e) => handleKeyDown(index, e)}
239
+ onPaste={handlePaste}
240
+ onFocus={handleFocus}
241
+ disabled={disabled}
242
+ className={cn(
243
+ // Classes de base du thème
244
+ "flex items-center justify-center rounded-md border border-input bg-background text-center font-medium transition-colors",
245
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
246
+ "disabled:cursor-not-allowed disabled:opacity-50",
247
+ // Taille
248
+ sizeClasses,
249
+ // Erreur
250
+ error && "border-destructive focus-visible:ring-destructive",
251
+ // Valeur présente
252
+ internalValue[index] && "border-primary"
253
+ )}
254
+ aria-label={`Caractère ${index + 1} sur ${length}`}
255
+ {...props}
256
+ />
257
+ {/* Séparateur */}
258
+ {separator && (index + 1) % separator === 0 && index < length - 1 && (
259
+ <div className="flex items-center justify-center">
260
+ {separatorElement}
261
+ </div>
262
+ )}
263
+ </React.Fragment>
264
+ ))}
265
+ </div>
266
+ {error && (
267
+ <p className="text-sm text-destructive" role="alert">
268
+ {error}
269
+ </p>
270
+ )}
271
+ </div>
272
+ )
273
+ }
274
+ )
275
+
276
+ InputOTP.displayName = "InputOTP"
277
+
@@ -0,0 +1,30 @@
1
+ import * as React from "react"
2
+ import * as LabelPrimitive from "@radix-ui/react-label"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { cn } from "../../utils/cn"
5
+
6
+ const labelVariants = cva(
7
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
8
+ )
9
+
10
+ const Label = React.forwardRef<
11
+ React.ElementRef<typeof LabelPrimitive.Root>,
12
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
13
+ VariantProps<typeof labelVariants>
14
+ >(({ className, ...props }, ref) => (
15
+ <LabelPrimitive.Root
16
+ ref={ref}
17
+ className={cn(labelVariants(), className)}
18
+ {...props}
19
+ />
20
+ ))
21
+ Label.displayName = LabelPrimitive.Root.displayName
22
+
23
+ export { Label }
24
+
25
+
26
+
27
+
28
+
29
+
30
+
@@ -0,0 +1,341 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "../select"
12
+ import { Check, ChevronDown, Globe } from "lucide-react"
13
+ import { createPortal } from "react-dom"
14
+
15
+ export interface Language {
16
+ code: string
17
+ label: string
18
+ flag?: string // Emoji ou URL d'image
19
+ flagEmoji?: string // Emoji du drapeau
20
+ }
21
+
22
+ export interface LanguageSelectorProps {
23
+ /** Liste des langues disponibles */
24
+ languages: Language[]
25
+
26
+ /** Langue sélectionnée (code) */
27
+ value?: string
28
+
29
+ /** Callback quand la langue change */
30
+ onChange?: (code: string) => void
31
+
32
+ /** Afficher les drapeaux */
33
+ showFlags?: boolean
34
+
35
+ /** Afficher les labels */
36
+ showLabels?: boolean
37
+
38
+ /** Variant du composant */
39
+ variant?: "default" | "compact" | "minimal"
40
+
41
+ /** État de chargement */
42
+ isLoading?: boolean
43
+
44
+ /** Classe CSS personnalisée */
45
+ className?: string
46
+
47
+ /** Texte du placeholder */
48
+ placeholder?: string
49
+
50
+ /** Texte pour le screen reader */
51
+ ariaLabel?: string
52
+ }
53
+
54
+ /**
55
+ * LanguageSelector
56
+ *
57
+ * Sélecteur de langue avec drapeaux et support du chargement dynamique.
58
+ *
59
+ * Features:
60
+ * - Drapeaux emoji ou images
61
+ * - 3 variantes : default (select), compact (bouton), minimal (icon only)
62
+ * - Support du chargement asynchrone
63
+ * - Intégration avec LanguageProvider pour S3
64
+ */
65
+ export function LanguageSelector({
66
+ languages,
67
+ value,
68
+ onChange,
69
+ showFlags = true,
70
+ showLabels = true,
71
+ variant = "default",
72
+ isLoading = false,
73
+ className,
74
+ placeholder = "Select language",
75
+ ariaLabel = "Select language",
76
+ }: LanguageSelectorProps) {
77
+ const [isOpen, setIsOpen] = React.useState(false)
78
+ const [mounted, setMounted] = React.useState(false)
79
+ const buttonRef = React.useRef<HTMLButtonElement>(null)
80
+ const dropdownRef = React.useRef<HTMLDivElement>(null)
81
+
82
+ React.useEffect(() => {
83
+ setMounted(true)
84
+ }, [])
85
+
86
+ const selectedLanguage = languages.find(lang => lang.code === value)
87
+
88
+ const handleLanguageChange = (code: string) => {
89
+ onChange?.(code)
90
+ setIsOpen(false)
91
+ }
92
+
93
+ // Position du dropdown
94
+ const [dropdownPosition, setDropdownPosition] = React.useState({ top: 0, left: 0 })
95
+
96
+ React.useEffect(() => {
97
+ if (isOpen && buttonRef.current && mounted) {
98
+ const rect = buttonRef.current.getBoundingClientRect()
99
+ setDropdownPosition({
100
+ top: rect.bottom + 8,
101
+ left: rect.left
102
+ })
103
+ }
104
+ }, [isOpen, mounted])
105
+
106
+ // Variant default - Select standard
107
+ if (variant === "default") {
108
+ return (
109
+ <Select value={value} onValueChange={onChange} disabled={isLoading}>
110
+ <SelectTrigger className={cn("w-[180px]", className)}>
111
+ <SelectValue placeholder={placeholder}>
112
+ {selectedLanguage && (
113
+ <div className="flex items-center gap-2">
114
+ {showFlags && selectedLanguage.flagEmoji && (
115
+ <span className="text-lg">{selectedLanguage.flagEmoji}</span>
116
+ )}
117
+ {showFlags && selectedLanguage.flag && !selectedLanguage.flagEmoji && (
118
+ <img
119
+ src={selectedLanguage.flag}
120
+ alt={selectedLanguage.label}
121
+ className="h-4 w-6 object-cover rounded"
122
+ />
123
+ )}
124
+ {!showFlags && <Globe className="h-4 w-4 text-muted-foreground" />}
125
+ {showLabels && <span>{selectedLanguage.label}</span>}
126
+ </div>
127
+ )}
128
+ </SelectValue>
129
+ </SelectTrigger>
130
+ <SelectContent>
131
+ {languages.map((language) => (
132
+ <SelectItem key={language.code} value={language.code}>
133
+ <div className="flex items-center gap-2">
134
+ {showFlags && language.flagEmoji && (
135
+ <span className="text-lg">{language.flagEmoji}</span>
136
+ )}
137
+ {showFlags && language.flag && !language.flagEmoji && (
138
+ <img
139
+ src={language.flag}
140
+ alt={language.label}
141
+ className="h-4 w-6 object-cover rounded"
142
+ />
143
+ )}
144
+ {showLabels && language.label}
145
+ </div>
146
+ </SelectItem>
147
+ ))}
148
+ </SelectContent>
149
+ </Select>
150
+ )
151
+ }
152
+
153
+ // Variant minimal - Icon only avec dropdown
154
+ if (variant === "minimal") {
155
+ const dropdownContent = isOpen && mounted ? (
156
+ <div
157
+ ref={dropdownRef}
158
+ className="fixed z-[9999] min-w-[160px] rounded-md border border-border bg-popover text-popover-foreground p-2 shadow-lg"
159
+ style={{
160
+ top: `${dropdownPosition.top}px`,
161
+ left: `${dropdownPosition.left}px`,
162
+ }}
163
+ onMouseDown={(e) => e.stopPropagation()}
164
+ >
165
+ <div className="grid gap-1">
166
+ {languages.map((language) => (
167
+ <button
168
+ key={language.code}
169
+ onMouseDown={(e) => {
170
+ e.preventDefault()
171
+ e.stopPropagation()
172
+ handleLanguageChange(language.code)
173
+ }}
174
+ type="button"
175
+ className={cn(
176
+ "flex items-center gap-2 rounded-sm px-2 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer",
177
+ value === language.code && "bg-accent"
178
+ )}
179
+ >
180
+ {language.flagEmoji && (
181
+ <span className="text-lg">{language.flagEmoji}</span>
182
+ )}
183
+ {language.flag && !language.flagEmoji && (
184
+ <img
185
+ src={language.flag}
186
+ alt={language.label}
187
+ className="h-4 w-6 object-cover rounded"
188
+ />
189
+ )}
190
+ <span className="flex-1 text-left">{language.label}</span>
191
+ {value === language.code && (
192
+ <Check className="h-4 w-4 shrink-0" />
193
+ )}
194
+ </button>
195
+ ))}
196
+ </div>
197
+ </div>
198
+ ) : null
199
+
200
+ return (
201
+ <>
202
+ <button
203
+ ref={buttonRef}
204
+ onClick={(e) => {
205
+ e.preventDefault()
206
+ e.stopPropagation()
207
+ setIsOpen(!isOpen)
208
+ }}
209
+ className={cn(
210
+ "flex h-9 w-9 items-center justify-center rounded-md border border-input bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors",
211
+ className
212
+ )}
213
+ title="Changer de langue"
214
+ type="button"
215
+ disabled={isLoading}
216
+ >
217
+ {selectedLanguage?.flagEmoji ? (
218
+ <span className="text-lg">{selectedLanguage.flagEmoji}</span>
219
+ ) : (
220
+ <Globe className="h-4 w-4" />
221
+ )}
222
+ <span className="sr-only">{ariaLabel}</span>
223
+ </button>
224
+
225
+ {mounted && typeof document !== "undefined" && dropdownContent && createPortal(
226
+ <>
227
+ <div
228
+ className="fixed inset-0 z-[9998]"
229
+ onMouseDown={(e) => {
230
+ e.preventDefault()
231
+ setIsOpen(false)
232
+ }}
233
+ />
234
+ {dropdownContent}
235
+ </>,
236
+ document.body
237
+ )}
238
+ </>
239
+ )
240
+ }
241
+
242
+ // Variant compact - Bouton avec drapeau et chevron avec dropdown
243
+ const dropdownContent = isOpen && mounted ? (
244
+ <div
245
+ ref={dropdownRef}
246
+ className="fixed z-[9999] min-w-[180px] rounded-md border border-border bg-popover text-popover-foreground p-2 shadow-lg"
247
+ style={{
248
+ top: `${dropdownPosition.top}px`,
249
+ left: `${dropdownPosition.left}px`,
250
+ }}
251
+ onMouseDown={(e) => e.stopPropagation()}
252
+ >
253
+ <div className="grid gap-1">
254
+ {languages.map((language) => (
255
+ <button
256
+ key={language.code}
257
+ onMouseDown={(e) => {
258
+ e.preventDefault()
259
+ e.stopPropagation()
260
+ handleLanguageChange(language.code)
261
+ }}
262
+ type="button"
263
+ className={cn(
264
+ "flex items-center gap-2 rounded-sm px-2 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors cursor-pointer",
265
+ value === language.code && "bg-accent"
266
+ )}
267
+ >
268
+ {showFlags && language.flagEmoji && (
269
+ <span className="text-lg">{language.flagEmoji}</span>
270
+ )}
271
+ {showFlags && language.flag && !language.flagEmoji && (
272
+ <img
273
+ src={language.flag}
274
+ alt={language.label}
275
+ className="h-4 w-6 object-cover rounded"
276
+ />
277
+ )}
278
+ <span className="flex-1 text-left">{language.label}</span>
279
+ {value === language.code && (
280
+ <Check className="h-4 w-4 shrink-0" />
281
+ )}
282
+ </button>
283
+ ))}
284
+ </div>
285
+ </div>
286
+ ) : null
287
+
288
+ return (
289
+ <>
290
+ <button
291
+ ref={buttonRef}
292
+ onClick={(e) => {
293
+ e.preventDefault()
294
+ e.stopPropagation()
295
+ if (!isLoading) {
296
+ setIsOpen(!isOpen)
297
+ }
298
+ }}
299
+ className={cn(
300
+ "inline-flex items-center gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors",
301
+ isLoading && "opacity-50 cursor-not-allowed",
302
+ className
303
+ )}
304
+ disabled={isLoading}
305
+ type="button"
306
+ aria-label={ariaLabel}
307
+ >
308
+ {showFlags && selectedLanguage?.flagEmoji && (
309
+ <span className="text-lg">{selectedLanguage.flagEmoji}</span>
310
+ )}
311
+ {showFlags && selectedLanguage?.flag && !selectedLanguage?.flagEmoji && (
312
+ <img
313
+ src={selectedLanguage.flag}
314
+ alt={selectedLanguage.label}
315
+ className="h-4 w-6 object-cover rounded"
316
+ />
317
+ )}
318
+ {!showFlags && <Globe className="h-4 w-4" />}
319
+ {showLabels && (
320
+ <span>{selectedLanguage?.label || "Langue"}</span>
321
+ )}
322
+ <ChevronDown className="h-4 w-4" />
323
+ </button>
324
+
325
+ {mounted && typeof document !== "undefined" && dropdownContent && createPortal(
326
+ <>
327
+ <div
328
+ className="fixed inset-0 z-[9998]"
329
+ onMouseDown={(e) => {
330
+ e.preventDefault()
331
+ setIsOpen(false)
332
+ }}
333
+ />
334
+ {dropdownContent}
335
+ </>,
336
+ document.body
337
+ )}
338
+ </>
339
+ )
340
+ }
341
+