@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,497 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Check, ChevronsUpDown, Plus, X, Loader2 } from "lucide-react"
6
+ import { Button } from "../button"
7
+ import {
8
+ Command,
9
+ CommandEmpty,
10
+ CommandGroup,
11
+ CommandInput,
12
+ CommandItem,
13
+ CommandList,
14
+ CommandSeparator,
15
+ } from "../command"
16
+ import {
17
+ Popover,
18
+ PopoverContent,
19
+ PopoverTrigger,
20
+ } from "../popover"
21
+ import { Badge } from "../badge"
22
+
23
+ // ============================================
24
+ // TYPES
25
+ // ============================================
26
+
27
+ export interface ComboboxOption<T = string> {
28
+ /** Valeur unique de l'option */
29
+ value: T
30
+ /** Libellé affiché */
31
+ label: string
32
+ /** Description secondaire */
33
+ description?: string
34
+ /** Icône */
35
+ icon?: React.ReactNode
36
+ /** Désactiver l'option */
37
+ disabled?: boolean
38
+ /** Groupe de l'option */
39
+ group?: string
40
+ }
41
+
42
+ export interface WakaComboboxProps<T = string> {
43
+ /** Options disponibles */
44
+ options: ComboboxOption<T>[]
45
+ /** Valeur sélectionnée (single) ou valeurs (multiple) */
46
+ value?: T | T[]
47
+ /** Callback lors du changement */
48
+ onChange?: (value: T | T[] | undefined) => void
49
+ /** Placeholder */
50
+ placeholder?: string
51
+ /** Placeholder de recherche */
52
+ searchPlaceholder?: string
53
+ /** Message si aucune option */
54
+ emptyMessage?: string
55
+ /** Mode multiple */
56
+ multiple?: boolean
57
+ /** Permettre la création de nouvelles options */
58
+ creatable?: boolean
59
+ /** Callback lors de la création */
60
+ onCreate?: (inputValue: string) => ComboboxOption<T> | Promise<ComboboxOption<T>>
61
+ /** Texte du bouton de création */
62
+ createLabel?: string
63
+ /** Recherche asynchrone */
64
+ onSearch?: (query: string) => Promise<ComboboxOption<T>[]>
65
+ /** Délai de debounce pour la recherche (ms) */
66
+ searchDebounce?: number
67
+ /** Désactivé */
68
+ disabled?: boolean
69
+ /** Erreur */
70
+ error?: boolean
71
+ /** Chargement */
72
+ loading?: boolean
73
+ /** Taille */
74
+ size?: "sm" | "md" | "lg"
75
+ /** Permettre de vider la sélection */
76
+ clearable?: boolean
77
+ /** Nombre maximum de sélections (multiple) */
78
+ maxSelections?: number
79
+ /** Classes CSS additionnelles */
80
+ className?: string
81
+ /** ID pour l'accessibilité */
82
+ id?: string
83
+ /** Nom du champ */
84
+ name?: string
85
+ }
86
+
87
+ // ============================================
88
+ // HELPER FUNCTIONS
89
+ // ============================================
90
+
91
+ function groupOptions<T>(options: ComboboxOption<T>[]): Map<string | undefined, ComboboxOption<T>[]> {
92
+ const groups = new Map<string | undefined, ComboboxOption<T>[]>()
93
+
94
+ for (const option of options) {
95
+ const group = option.group
96
+ if (!groups.has(group)) {
97
+ groups.set(group, [])
98
+ }
99
+ groups.get(group)!.push(option)
100
+ }
101
+
102
+ return groups
103
+ }
104
+
105
+ // ============================================
106
+ // MAIN COMPONENT
107
+ // ============================================
108
+
109
+ export function WakaCombobox<T = string>({
110
+ options: initialOptions,
111
+ value,
112
+ onChange,
113
+ placeholder = "Sélectionner...",
114
+ searchPlaceholder = "Rechercher...",
115
+ emptyMessage = "Aucun résultat trouvé.",
116
+ multiple = false,
117
+ creatable = false,
118
+ onCreate,
119
+ createLabel = "Créer",
120
+ onSearch,
121
+ searchDebounce = 300,
122
+ disabled = false,
123
+ error = false,
124
+ loading: externalLoading = false,
125
+ size = "md",
126
+ clearable = true,
127
+ maxSelections,
128
+ className,
129
+ id,
130
+ name,
131
+ }: WakaComboboxProps<T>) {
132
+ const [open, setOpen] = React.useState(false)
133
+ const [searchQuery, setSearchQuery] = React.useState("")
134
+ const [options, setOptions] = React.useState<ComboboxOption<T>[]>(initialOptions)
135
+ const [internalLoading, setInternalLoading] = React.useState(false)
136
+ const [createdOptions, setCreatedOptions] = React.useState<ComboboxOption<T>[]>([])
137
+
138
+ const loading = externalLoading || internalLoading
139
+
140
+ // Update options when initialOptions change
141
+ React.useEffect(() => {
142
+ if (!onSearch) {
143
+ setOptions(initialOptions)
144
+ }
145
+ }, [initialOptions, onSearch])
146
+
147
+ // Debounced search
148
+ React.useEffect(() => {
149
+ if (!onSearch) return
150
+
151
+ const timer = setTimeout(async () => {
152
+ setInternalLoading(true)
153
+ try {
154
+ const results = await onSearch(searchQuery)
155
+ setOptions(results)
156
+ } catch (error) {
157
+ console.error("Search error:", error)
158
+ } finally {
159
+ setInternalLoading(false)
160
+ }
161
+ }, searchDebounce)
162
+
163
+ return () => clearTimeout(timer)
164
+ }, [searchQuery, onSearch, searchDebounce])
165
+
166
+ // Combined options (initial + created)
167
+ const allOptions = React.useMemo(() => {
168
+ return [...options, ...createdOptions]
169
+ }, [options, createdOptions])
170
+
171
+ // Filter options based on search (if not async)
172
+ const filteredOptions = React.useMemo(() => {
173
+ if (onSearch) return allOptions // Async search handles filtering
174
+
175
+ if (!searchQuery) return allOptions
176
+
177
+ const query = searchQuery.toLowerCase()
178
+ return allOptions.filter(
179
+ (option) =>
180
+ option.label.toLowerCase().includes(query) ||
181
+ option.description?.toLowerCase().includes(query)
182
+ )
183
+ }, [allOptions, searchQuery, onSearch])
184
+
185
+ // Selected values as array
186
+ const selectedValues = React.useMemo(() => {
187
+ if (value === undefined) return []
188
+ return Array.isArray(value) ? value : [value]
189
+ }, [value])
190
+
191
+ // Selected options
192
+ const selectedOptions = React.useMemo(() => {
193
+ return allOptions.filter((option) =>
194
+ selectedValues.some((v) => v === option.value)
195
+ )
196
+ }, [allOptions, selectedValues])
197
+
198
+ // Handle selection
199
+ const handleSelect = (option: ComboboxOption<T>) => {
200
+ if (option.disabled) return
201
+
202
+ if (multiple) {
203
+ const isSelected = selectedValues.includes(option.value)
204
+ let newValues: T[]
205
+
206
+ if (isSelected) {
207
+ newValues = selectedValues.filter((v) => v !== option.value)
208
+ } else {
209
+ if (maxSelections && selectedValues.length >= maxSelections) return
210
+ newValues = [...selectedValues, option.value]
211
+ }
212
+
213
+ onChange?.(newValues.length > 0 ? newValues : undefined)
214
+ } else {
215
+ const isSelected = selectedValues.includes(option.value)
216
+ if (isSelected) {
217
+ onChange?.(undefined)
218
+ } else {
219
+ onChange?.(option.value)
220
+ }
221
+ setOpen(false)
222
+ }
223
+ }
224
+
225
+ // Handle creation
226
+ const handleCreate = async () => {
227
+ if (!creatable || !searchQuery.trim()) return
228
+
229
+ setInternalLoading(true)
230
+ try {
231
+ let newOption: ComboboxOption<T>
232
+
233
+ if (onCreate) {
234
+ newOption = await onCreate(searchQuery.trim())
235
+ } else {
236
+ // Default creation (only works for string values)
237
+ newOption = {
238
+ value: searchQuery.trim() as unknown as T,
239
+ label: searchQuery.trim(),
240
+ }
241
+ }
242
+
243
+ setCreatedOptions((prev) => [...prev, newOption])
244
+ handleSelect(newOption)
245
+ setSearchQuery("")
246
+ } catch (error) {
247
+ console.error("Creation error:", error)
248
+ } finally {
249
+ setInternalLoading(false)
250
+ }
251
+ }
252
+
253
+ // Handle clear
254
+ const handleClear = (e: React.MouseEvent) => {
255
+ e.stopPropagation()
256
+ onChange?.(undefined)
257
+ setSearchQuery("")
258
+ }
259
+
260
+ // Remove single value (multiple mode)
261
+ const handleRemove = (valueToRemove: T) => {
262
+ if (multiple) {
263
+ const newValues = selectedValues.filter((v) => v !== valueToRemove)
264
+ onChange?.(newValues.length > 0 ? newValues : undefined)
265
+ }
266
+ }
267
+
268
+ // Check if option matches search for creation
269
+ const canCreate = React.useMemo(() => {
270
+ if (!creatable || !searchQuery.trim()) return false
271
+ const query = searchQuery.trim().toLowerCase()
272
+ return !allOptions.some((option) => option.label.toLowerCase() === query)
273
+ }, [creatable, searchQuery, allOptions])
274
+
275
+ // Group options
276
+ const groupedOptions = React.useMemo(() => {
277
+ return groupOptions(filteredOptions)
278
+ }, [filteredOptions])
279
+
280
+ // Display value
281
+ const displayValue = React.useMemo(() => {
282
+ if (selectedOptions.length === 0) return ""
283
+ if (multiple) {
284
+ return `${selectedOptions.length} sélectionné${selectedOptions.length > 1 ? "s" : ""}`
285
+ }
286
+ return selectedOptions[0]?.label || ""
287
+ }, [selectedOptions, multiple])
288
+
289
+ const sizeClasses = {
290
+ sm: "h-8 text-xs",
291
+ md: "h-9 text-sm",
292
+ lg: "h-10 text-base",
293
+ }
294
+
295
+ const iconSizes = {
296
+ sm: "h-3 w-3",
297
+ md: "h-4 w-4",
298
+ lg: "h-5 w-5",
299
+ }
300
+
301
+ return (
302
+ <Popover open={open} onOpenChange={setOpen}>
303
+ <PopoverTrigger asChild>
304
+ <Button
305
+ id={id}
306
+ variant="outline"
307
+ role="combobox"
308
+ aria-expanded={open}
309
+ disabled={disabled}
310
+ className={cn(
311
+ "w-full justify-between font-normal",
312
+ sizeClasses[size],
313
+ !displayValue && "text-muted-foreground",
314
+ error && "border-destructive focus-visible:ring-destructive",
315
+ className
316
+ )}
317
+ >
318
+ <span className="flex-1 text-left truncate">
319
+ {displayValue || placeholder}
320
+ </span>
321
+ <div className="flex items-center gap-1 ml-2">
322
+ {loading && <Loader2 className={cn(iconSizes[size], "animate-spin")} />}
323
+ {clearable && selectedValues.length > 0 && !disabled && (
324
+ <X
325
+ className={cn(iconSizes[size], "opacity-50 hover:opacity-100")}
326
+ onClick={handleClear}
327
+ />
328
+ )}
329
+ <ChevronsUpDown className={cn(iconSizes[size], "opacity-50")} />
330
+ </div>
331
+ </Button>
332
+ </PopoverTrigger>
333
+ <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
334
+ <Command shouldFilter={false}>
335
+ <CommandInput
336
+ placeholder={searchPlaceholder}
337
+ value={searchQuery}
338
+ onValueChange={setSearchQuery}
339
+ />
340
+ <CommandList>
341
+ {loading && (
342
+ <div className="flex items-center justify-center py-6">
343
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
344
+ </div>
345
+ )}
346
+
347
+ {!loading && filteredOptions.length === 0 && !canCreate && (
348
+ <CommandEmpty>{emptyMessage}</CommandEmpty>
349
+ )}
350
+
351
+ {!loading && Array.from(groupedOptions.entries()).map(([group, groupOptions], groupIndex) => (
352
+ <React.Fragment key={group || "ungrouped"}>
353
+ {groupIndex > 0 && <CommandSeparator />}
354
+ <CommandGroup heading={group}>
355
+ {groupOptions.map((option) => {
356
+ const isSelected = selectedValues.includes(option.value)
357
+
358
+ return (
359
+ <CommandItem
360
+ key={String(option.value)}
361
+ value={String(option.value)}
362
+ onSelect={() => handleSelect(option)}
363
+ disabled={option.disabled || (multiple && !!maxSelections && selectedValues.length >= maxSelections && !isSelected)}
364
+ className={cn(
365
+ "flex items-center gap-2",
366
+ option.disabled && "opacity-50"
367
+ )}
368
+ >
369
+ <div className={cn(
370
+ "flex items-center justify-center w-4 h-4 border rounded",
371
+ isSelected ? "bg-primary border-primary text-primary-foreground" : "border-muted-foreground/30"
372
+ )}>
373
+ {isSelected && <Check className="h-3 w-3" />}
374
+ </div>
375
+ {option.icon && <span className="flex-shrink-0">{option.icon}</span>}
376
+ <div className="flex-1 min-w-0">
377
+ <div className="truncate">{option.label}</div>
378
+ {option.description && (
379
+ <div className="text-xs text-muted-foreground truncate">
380
+ {option.description}
381
+ </div>
382
+ )}
383
+ </div>
384
+ </CommandItem>
385
+ )
386
+ })}
387
+ </CommandGroup>
388
+ </React.Fragment>
389
+ ))}
390
+
391
+ {/* Create option */}
392
+ {canCreate && (
393
+ <>
394
+ <CommandSeparator />
395
+ <CommandGroup>
396
+ <CommandItem onSelect={handleCreate} className="flex items-center gap-2">
397
+ <Plus className={iconSizes[size]} />
398
+ <span>
399
+ {createLabel} "{searchQuery.trim()}"
400
+ </span>
401
+ </CommandItem>
402
+ </CommandGroup>
403
+ </>
404
+ )}
405
+ </CommandList>
406
+ </Command>
407
+ </PopoverContent>
408
+
409
+ {/* Hidden input for form submission */}
410
+ {name && (
411
+ <input
412
+ type="hidden"
413
+ name={name}
414
+ value={multiple ? selectedValues.join(",") : String(selectedValues[0] || "")}
415
+ />
416
+ )}
417
+
418
+ {/* Selected badges (multiple mode) */}
419
+ {multiple && selectedOptions.length > 0 && (
420
+ <div className="flex flex-wrap gap-1 mt-2">
421
+ {selectedOptions.map((option) => (
422
+ <Badge
423
+ key={String(option.value)}
424
+ variant="secondary"
425
+ className="gap-1"
426
+ >
427
+ {option.icon}
428
+ {option.label}
429
+ <X
430
+ className="h-3 w-3 cursor-pointer hover:text-destructive"
431
+ onClick={() => handleRemove(option.value)}
432
+ />
433
+ </Badge>
434
+ ))}
435
+ </div>
436
+ )}
437
+ </Popover>
438
+ )
439
+ }
440
+
441
+ // ============================================
442
+ // HOOK
443
+ // ============================================
444
+
445
+ export interface UseComboboxOptions<T = string> {
446
+ /** Options initiales */
447
+ options?: ComboboxOption<T>[]
448
+ /** Valeur initiale */
449
+ initialValue?: T | T[]
450
+ /** Mode multiple */
451
+ multiple?: boolean
452
+ }
453
+
454
+ export function useCombobox<T = string>(options: UseComboboxOptions<T> = {}) {
455
+ const { options: initialOptions = [], initialValue, multiple = false } = options
456
+ const [value, setValue] = React.useState<T | T[] | undefined>(initialValue)
457
+ const [comboboxOptions, setComboboxOptions] = React.useState<ComboboxOption<T>[]>(initialOptions)
458
+
459
+ const clear = () => setValue(undefined)
460
+
461
+ const addOption = (option: ComboboxOption<T>) => {
462
+ setComboboxOptions((prev) => [...prev, option])
463
+ }
464
+
465
+ const removeOption = (valueToRemove: T) => {
466
+ setComboboxOptions((prev) => prev.filter((o) => o.value !== valueToRemove))
467
+ // Also remove from selection if selected
468
+ if (multiple && Array.isArray(value)) {
469
+ setValue(value.filter((v) => v !== valueToRemove))
470
+ } else if (value === valueToRemove) {
471
+ setValue(undefined)
472
+ }
473
+ }
474
+
475
+ const selectAll = () => {
476
+ if (multiple) {
477
+ setValue(comboboxOptions.filter((o) => !o.disabled).map((o) => o.value))
478
+ }
479
+ }
480
+
481
+ const selectNone = () => setValue(multiple ? [] : undefined)
482
+
483
+ return {
484
+ value,
485
+ onChange: setValue,
486
+ options: comboboxOptions,
487
+ setOptions: setComboboxOptions,
488
+ clear,
489
+ addOption,
490
+ removeOption,
491
+ selectAll,
492
+ selectNone,
493
+ multiple,
494
+ }
495
+ }
496
+
497
+ export default WakaCombobox