@thangnqdev/ui 0.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 (253) hide show
  1. package/README.md +196 -0
  2. package/dist/Label-DWOlgSNW.d.cts +14 -0
  3. package/dist/Label-DWOlgSNW.d.ts +14 -0
  4. package/dist/bento/index.cjs +140 -0
  5. package/dist/bento/index.cjs.map +1 -0
  6. package/dist/bento/index.d.cts +4 -0
  7. package/dist/bento/index.d.ts +4 -0
  8. package/dist/bento/index.js +4 -0
  9. package/dist/bento/index.js.map +1 -0
  10. package/dist/bento/styles.css +46 -0
  11. package/dist/buttons/index.cjs +365 -0
  12. package/dist/buttons/index.cjs.map +1 -0
  13. package/dist/buttons/index.d.cts +4 -0
  14. package/dist/buttons/index.d.ts +4 -0
  15. package/dist/buttons/index.js +5 -0
  16. package/dist/buttons/index.js.map +1 -0
  17. package/dist/buttons/styles.css +6 -0
  18. package/dist/chunk-2OXZK6IP.js +514 -0
  19. package/dist/chunk-2OXZK6IP.js.map +1 -0
  20. package/dist/chunk-4S6L5V7X.js +15 -0
  21. package/dist/chunk-4S6L5V7X.js.map +1 -0
  22. package/dist/chunk-5FZ6ZFII.js +728 -0
  23. package/dist/chunk-5FZ6ZFII.js.map +1 -0
  24. package/dist/chunk-66VWUJEI.js +86 -0
  25. package/dist/chunk-66VWUJEI.js.map +1 -0
  26. package/dist/chunk-A7P3O5Z5.js +163 -0
  27. package/dist/chunk-A7P3O5Z5.js.map +1 -0
  28. package/dist/chunk-ATYGDTMK.js +257 -0
  29. package/dist/chunk-ATYGDTMK.js.map +1 -0
  30. package/dist/chunk-C4SUXMI3.js +138 -0
  31. package/dist/chunk-C4SUXMI3.js.map +1 -0
  32. package/dist/chunk-EU2LPUYP.js +25 -0
  33. package/dist/chunk-EU2LPUYP.js.map +1 -0
  34. package/dist/chunk-F5Q7BTMH.js +1991 -0
  35. package/dist/chunk-F5Q7BTMH.js.map +1 -0
  36. package/dist/chunk-GXI2H5XA.js +46 -0
  37. package/dist/chunk-GXI2H5XA.js.map +1 -0
  38. package/dist/chunk-H3LCNPMC.js +101 -0
  39. package/dist/chunk-H3LCNPMC.js.map +1 -0
  40. package/dist/chunk-IABPUCAQ.js +19 -0
  41. package/dist/chunk-IABPUCAQ.js.map +1 -0
  42. package/dist/chunk-KJRBRTQE.js +101 -0
  43. package/dist/chunk-KJRBRTQE.js.map +1 -0
  44. package/dist/chunk-LBUBXUYQ.js +40 -0
  45. package/dist/chunk-LBUBXUYQ.js.map +1 -0
  46. package/dist/chunk-LWFRUJRU.js +537 -0
  47. package/dist/chunk-LWFRUJRU.js.map +1 -0
  48. package/dist/chunk-NFVONJUK.js +882 -0
  49. package/dist/chunk-NFVONJUK.js.map +1 -0
  50. package/dist/chunk-PF4ZHGVW.js +284 -0
  51. package/dist/chunk-PF4ZHGVW.js.map +1 -0
  52. package/dist/chunk-WAFHC4DQ.js +366 -0
  53. package/dist/chunk-WAFHC4DQ.js.map +1 -0
  54. package/dist/chunk-X76KBH4Y.js +71 -0
  55. package/dist/chunk-X76KBH4Y.js.map +1 -0
  56. package/dist/core/index.cjs +103 -0
  57. package/dist/core/index.cjs.map +1 -0
  58. package/dist/core/index.d.cts +3 -0
  59. package/dist/core/index.d.ts +3 -0
  60. package/dist/core/index.js +4 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/core/styles.css +128 -0
  63. package/dist/data-display/index.cjs +1023 -0
  64. package/dist/data-display/index.cjs.map +1 -0
  65. package/dist/data-display/index.d.cts +4 -0
  66. package/dist/data-display/index.d.ts +4 -0
  67. package/dist/data-display/index.js +6 -0
  68. package/dist/data-display/index.js.map +1 -0
  69. package/dist/data-display/styles.css +4 -0
  70. package/dist/feedback/index.cjs +368 -0
  71. package/dist/feedback/index.cjs.map +1 -0
  72. package/dist/feedback/index.d.cts +4 -0
  73. package/dist/feedback/index.d.ts +4 -0
  74. package/dist/feedback/index.js +4 -0
  75. package/dist/feedback/index.js.map +1 -0
  76. package/dist/feedback/styles.css +4 -0
  77. package/dist/forms/index.cjs +2287 -0
  78. package/dist/forms/index.cjs.map +1 -0
  79. package/dist/forms/index.d.cts +5 -0
  80. package/dist/forms/index.d.ts +5 -0
  81. package/dist/forms/index.js +10 -0
  82. package/dist/forms/index.js.map +1 -0
  83. package/dist/forms/styles.css +6 -0
  84. package/dist/glass/index.cjs +165 -0
  85. package/dist/glass/index.cjs.map +1 -0
  86. package/dist/glass/index.d.cts +4 -0
  87. package/dist/glass/index.d.ts +4 -0
  88. package/dist/glass/index.js +4 -0
  89. package/dist/glass/index.js.map +1 -0
  90. package/dist/glass/styles.css +81 -0
  91. package/dist/index-BTvkbCIj.d.cts +146 -0
  92. package/dist/index-BTvkbCIj.d.ts +146 -0
  93. package/dist/index-BkRqGNG1.d.cts +201 -0
  94. package/dist/index-BkRqGNG1.d.ts +201 -0
  95. package/dist/index-BlPcGeW-.d.cts +45 -0
  96. package/dist/index-BlPcGeW-.d.ts +45 -0
  97. package/dist/index-BzOCqvC6.d.cts +32 -0
  98. package/dist/index-BzOCqvC6.d.ts +32 -0
  99. package/dist/index-CCeWYAvn.d.cts +83 -0
  100. package/dist/index-COQ7uwt9.d.cts +44 -0
  101. package/dist/index-COQ7uwt9.d.ts +44 -0
  102. package/dist/index-CenuF_P2.d.cts +221 -0
  103. package/dist/index-CenuF_P2.d.ts +221 -0
  104. package/dist/index-Cle6QzQl.d.ts +371 -0
  105. package/dist/index-D6fE7w-b.d.cts +90 -0
  106. package/dist/index-D6fE7w-b.d.ts +90 -0
  107. package/dist/index-DR8cfJUG.d.ts +83 -0
  108. package/dist/index-DzcWh1Hs.d.cts +131 -0
  109. package/dist/index-DzcWh1Hs.d.ts +131 -0
  110. package/dist/index-TyK-Ldpy.d.cts +371 -0
  111. package/dist/index-ugj5s6a3.d.cts +95 -0
  112. package/dist/index-ugj5s6a3.d.ts +95 -0
  113. package/dist/index.cjs +6400 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +16 -0
  116. package/dist/index.d.ts +16 -0
  117. package/dist/index.js +21 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/layout/index.cjs +516 -0
  120. package/dist/layout/index.cjs.map +1 -0
  121. package/dist/layout/index.d.cts +4 -0
  122. package/dist/layout/index.d.ts +4 -0
  123. package/dist/layout/index.js +4 -0
  124. package/dist/layout/index.js.map +1 -0
  125. package/dist/layout/styles.css +8 -0
  126. package/dist/navigation/index.cjs +673 -0
  127. package/dist/navigation/index.cjs.map +1 -0
  128. package/dist/navigation/index.d.cts +2 -0
  129. package/dist/navigation/index.d.ts +2 -0
  130. package/dist/navigation/index.js +8 -0
  131. package/dist/navigation/index.js.map +1 -0
  132. package/dist/navigation/styles.css +4 -0
  133. package/dist/overlays/index.cjs +954 -0
  134. package/dist/overlays/index.cjs.map +1 -0
  135. package/dist/overlays/index.d.cts +4 -0
  136. package/dist/overlays/index.d.ts +4 -0
  137. package/dist/overlays/index.js +9 -0
  138. package/dist/overlays/index.js.map +1 -0
  139. package/dist/overlays/styles.css +8 -0
  140. package/dist/styles.css +333 -0
  141. package/dist/typography/index.cjs +311 -0
  142. package/dist/typography/index.cjs.map +1 -0
  143. package/dist/typography/index.d.cts +5 -0
  144. package/dist/typography/index.d.ts +5 -0
  145. package/dist/typography/index.js +6 -0
  146. package/dist/typography/index.js.map +1 -0
  147. package/dist/typography/styles.css +5 -0
  148. package/llms.txt +113 -0
  149. package/package.json +116 -0
  150. package/src/bento/BentoCard.tsx +140 -0
  151. package/src/bento/BentoGrid.tsx +50 -0
  152. package/src/bento/index.ts +2 -0
  153. package/src/bento/tokens.css +46 -0
  154. package/src/buttons/Button.tsx +104 -0
  155. package/src/buttons/ButtonGroup.tsx +43 -0
  156. package/src/buttons/IconButton.tsx +79 -0
  157. package/src/buttons/SplitButton.tsx +86 -0
  158. package/src/buttons/ToggleButton.tsx +157 -0
  159. package/src/buttons/index.ts +5 -0
  160. package/src/buttons/tokens.css +6 -0
  161. package/src/core/ThemeProvider.tsx +123 -0
  162. package/src/core/index.ts +2 -0
  163. package/src/core/tokens.css +128 -0
  164. package/src/data-display/Accordion.tsx +147 -0
  165. package/src/data-display/Avatar.tsx +134 -0
  166. package/src/data-display/Card.tsx +112 -0
  167. package/src/data-display/Carousel.tsx +130 -0
  168. package/src/data-display/Chip.tsx +74 -0
  169. package/src/data-display/DataTablePro.tsx +356 -0
  170. package/src/data-display/StatCard.tsx +60 -0
  171. package/src/data-display/Table.tsx +74 -0
  172. package/src/data-display/Timeline.tsx +62 -0
  173. package/src/data-display/TreeView.tsx +140 -0
  174. package/src/data-display/index.ts +10 -0
  175. package/src/data-display/tokens.css +4 -0
  176. package/src/feedback/Alert.tsx +80 -0
  177. package/src/feedback/EmptyState.tsx +45 -0
  178. package/src/feedback/ProgressBar.tsx +174 -0
  179. package/src/feedback/Skeleton.tsx +66 -0
  180. package/src/feedback/Spinner.tsx +65 -0
  181. package/src/feedback/index.ts +5 -0
  182. package/src/feedback/tokens.css +4 -0
  183. package/src/forms/Checkbox.tsx +69 -0
  184. package/src/forms/CheckboxGroup.tsx +29 -0
  185. package/src/forms/Combobox.tsx +368 -0
  186. package/src/forms/DatePicker.tsx +480 -0
  187. package/src/forms/FileUpload.tsx +222 -0
  188. package/src/forms/FormField.tsx +115 -0
  189. package/src/forms/Input.tsx +151 -0
  190. package/src/forms/MaskedInput.tsx +97 -0
  191. package/src/forms/NumberInput.tsx +248 -0
  192. package/src/forms/PasswordInput.tsx +35 -0
  193. package/src/forms/PinInput.tsx +125 -0
  194. package/src/forms/Radio.tsx +142 -0
  195. package/src/forms/RangeSlider.tsx +111 -0
  196. package/src/forms/SearchInput.tsx +34 -0
  197. package/src/forms/Select.tsx +103 -0
  198. package/src/forms/Slider.tsx +85 -0
  199. package/src/forms/Switch.tsx +95 -0
  200. package/src/forms/Textarea.tsx +114 -0
  201. package/src/forms/index.ts +18 -0
  202. package/src/forms/tokens.css +6 -0
  203. package/src/glass/GlassBadge.tsx +61 -0
  204. package/src/glass/GlassButton.tsx +59 -0
  205. package/src/glass/GlassCard.tsx +71 -0
  206. package/src/glass/index.ts +3 -0
  207. package/src/glass/tokens.css +81 -0
  208. package/src/index.ts +26 -0
  209. package/src/layout/AspectRatio.tsx +39 -0
  210. package/src/layout/Container.tsx +53 -0
  211. package/src/layout/Divider.tsx +61 -0
  212. package/src/layout/Flex.tsx +79 -0
  213. package/src/layout/Grid.tsx +124 -0
  214. package/src/layout/PageLayout.tsx +59 -0
  215. package/src/layout/ScrollArea.tsx +43 -0
  216. package/src/layout/Section.tsx +47 -0
  217. package/src/layout/Spacer.tsx +30 -0
  218. package/src/layout/Stack.tsx +88 -0
  219. package/src/layout/index.ts +10 -0
  220. package/src/layout/tokens.css +8 -0
  221. package/src/navigation/Breadcrumb.tsx +73 -0
  222. package/src/navigation/CommandPalette.tsx +142 -0
  223. package/src/navigation/DropdownMenu.tsx +160 -0
  224. package/src/navigation/Pagination.tsx +114 -0
  225. package/src/navigation/Stepper.tsx +93 -0
  226. package/src/navigation/Tabs.tsx +133 -0
  227. package/src/navigation/index.ts +6 -0
  228. package/src/navigation/tokens.css +4 -0
  229. package/src/overlays/AlertDialog.tsx +54 -0
  230. package/src/overlays/BottomSheet.tsx +123 -0
  231. package/src/overlays/ContextMenu.tsx +119 -0
  232. package/src/overlays/Drawer.tsx +145 -0
  233. package/src/overlays/FocusTrap.tsx +49 -0
  234. package/src/overlays/HoverCard.tsx +72 -0
  235. package/src/overlays/Modal.tsx +219 -0
  236. package/src/overlays/Popover.tsx +126 -0
  237. package/src/overlays/Portal.tsx +23 -0
  238. package/src/overlays/Toast.tsx +137 -0
  239. package/src/overlays/Tooltip.tsx +100 -0
  240. package/src/overlays/index.ts +11 -0
  241. package/src/overlays/tokens.css +8 -0
  242. package/src/styles/tokens.css +2 -0
  243. package/src/typography/Blockquote.tsx +32 -0
  244. package/src/typography/Code.tsx +89 -0
  245. package/src/typography/Heading.tsx +56 -0
  246. package/src/typography/Kbd.tsx +25 -0
  247. package/src/typography/Label.tsx +47 -0
  248. package/src/typography/Link.tsx +56 -0
  249. package/src/typography/Text.tsx +62 -0
  250. package/src/typography/index.ts +7 -0
  251. package/src/typography/tokens.css +5 -0
  252. package/src/utils/cn.ts +6 -0
  253. package/ui-contract.json +62 -0
@@ -0,0 +1,368 @@
1
+ import React, { forwardRef, useState, useRef, useEffect, useId } from 'react';
2
+ import { Check, ChevronsUpDown, Search, X } from 'lucide-react';
3
+ import { cn } from '../utils/cn';
4
+ import { Popover } from '../overlays/Popover';
5
+ import { Chip } from '../data-display/Chip';
6
+
7
+ export interface ComboboxOption {
8
+ value: string;
9
+ label: string;
10
+ description?: string;
11
+ icon?: React.ReactNode;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface ComboboxProps {
16
+ options: ComboboxOption[];
17
+ value?: string;
18
+ defaultValue?: string;
19
+ onChange?: (val: string) => void;
20
+ placeholder?: string;
21
+ searchPlaceholder?: string;
22
+ emptyText?: string;
23
+ disabled?: boolean;
24
+ clearable?: boolean;
25
+ className?: string;
26
+ }
27
+
28
+ export const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
29
+ (
30
+ {
31
+ options,
32
+ value: controlledVal,
33
+ defaultValue = '',
34
+ onChange,
35
+ placeholder = 'Select option...',
36
+ searchPlaceholder = 'Search items...',
37
+ emptyText = 'No items found.',
38
+ disabled = false,
39
+ clearable = true,
40
+ className,
41
+ },
42
+ ref
43
+ ) => {
44
+ const [internalVal, setInternalVal] = useState(defaultValue);
45
+ const selectedValue = controlledVal !== undefined ? controlledVal : internalVal;
46
+
47
+ const [search, setSearch] = useState('');
48
+ const [isOpen, setIsOpen] = useState(false);
49
+ const [highlightIndex, setHighlightIndex] = useState(0);
50
+
51
+ const filteredOptions = options.filter(
52
+ (opt) =>
53
+ opt.label.toLowerCase().includes(search.toLowerCase()) ||
54
+ opt.value.toLowerCase().includes(search.toLowerCase()) ||
55
+ (opt.description && opt.description.toLowerCase().includes(search.toLowerCase()))
56
+ );
57
+
58
+ const selectedOption = options.find((opt) => opt.value === selectedValue);
59
+
60
+ const handleSelect = (val: string) => {
61
+ setInternalVal(val);
62
+ onChange?.(val);
63
+ setIsOpen(false);
64
+ setSearch('');
65
+ };
66
+
67
+ const handleClear = (e: React.MouseEvent) => {
68
+ e.stopPropagation();
69
+ setInternalVal('');
70
+ onChange?.('');
71
+ };
72
+
73
+ const handleKeyDown = (e: React.KeyboardEvent) => {
74
+ if (!isOpen) {
75
+ if (e.key === 'ArrowDown' || e.key === 'Enter') {
76
+ setIsOpen(true);
77
+ }
78
+ return;
79
+ }
80
+
81
+ if (e.key === 'ArrowDown') {
82
+ e.preventDefault();
83
+ setHighlightIndex((prev) => (prev + 1) % Math.max(1, filteredOptions.length));
84
+ } else if (e.key === 'ArrowUp') {
85
+ e.preventDefault();
86
+ setHighlightIndex((prev) => (prev - 1 + filteredOptions.length) % Math.max(1, filteredOptions.length));
87
+ } else if (e.key === 'Enter') {
88
+ e.preventDefault();
89
+ if (filteredOptions[highlightIndex] && !filteredOptions[highlightIndex].disabled) {
90
+ handleSelect(filteredOptions[highlightIndex].value);
91
+ }
92
+ } else if (e.key === 'Escape') {
93
+ setIsOpen(false);
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div ref={ref} className={cn('relative w-full', className)}>
99
+ <Popover
100
+ isOpen={isOpen}
101
+ onClose={() => setIsOpen(false)}
102
+ trigger={
103
+ <button
104
+ type="button"
105
+ disabled={disabled}
106
+ onClick={() => !disabled && setIsOpen(!isOpen)}
107
+ onKeyDown={handleKeyDown}
108
+ className={cn(
109
+ 'w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm transition-all shadow-inner cursor-pointer',
110
+ disabled
111
+ ? 'opacity-50 cursor-not-allowed border-[var(--ui-border-subtle)]'
112
+ : 'border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] focus:ring-2 focus:ring-[var(--ui-primary)]/50'
113
+ )}
114
+ >
115
+ <div className="flex items-center gap-2.5 truncate">
116
+ {selectedOption?.icon}
117
+ <span className={selectedOption ? 'text-[var(--ui-text-primary)] font-medium' : 'text-[var(--ui-text-muted)]'}>
118
+ {selectedOption ? selectedOption.label : placeholder}
119
+ </span>
120
+ </div>
121
+
122
+ <div className="flex items-center gap-1 text-[var(--ui-text-muted)]">
123
+ {clearable && selectedValue && !disabled && (
124
+ <span
125
+ onClick={handleClear}
126
+ className="p-1 rounded-full hover:bg-[var(--ui-bg-tertiary)] hover:text-[var(--ui-text-primary)] transition-colors"
127
+ >
128
+ <X className="w-3.5 h-3.5" />
129
+ </span>
130
+ )}
131
+ <ChevronsUpDown className="w-4 h-4 opacity-50 shrink-0" />
132
+ </div>
133
+ </button>
134
+ }
135
+ >
136
+ <div className="p-2 w-72 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-2">
137
+ <div className="relative flex items-center">
138
+ <Search className="absolute left-2.5 w-3.5 h-3.5 text-[var(--ui-text-muted)] pointer-events-none" />
139
+ <input
140
+ type="text"
141
+ autoFocus
142
+ placeholder={searchPlaceholder}
143
+ value={search}
144
+ onChange={(e) => {
145
+ setSearch(e.target.value);
146
+ setHighlightIndex(0);
147
+ }}
148
+ className="w-full h-8 pl-8 pr-3 text-xs rounded-xl bg-[var(--ui-bg-secondary)] border border-[var(--ui-border-subtle)] focus:outline-none focus:border-[var(--ui-primary)] text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)]"
149
+ />
150
+ </div>
151
+
152
+ <div className="max-h-56 overflow-y-auto space-y-0.5" role="listbox">
153
+ {filteredOptions.length === 0 ? (
154
+ <div className="py-4 text-center text-xs text-[var(--ui-text-muted)]">{emptyText}</div>
155
+ ) : (
156
+ filteredOptions.map((opt, idx) => {
157
+ const isSelected = opt.value === selectedValue;
158
+ const isHighlighted = idx === highlightIndex;
159
+
160
+ return (
161
+ <div
162
+ key={opt.value}
163
+ role="option"
164
+ aria-selected={isSelected}
165
+ onClick={() => !opt.disabled && handleSelect(opt.value)}
166
+ onMouseEnter={() => setHighlightIndex(idx)}
167
+ className={cn(
168
+ 'flex items-center justify-between px-3 py-2 rounded-xl text-xs font-medium cursor-pointer transition-colors',
169
+ opt.disabled && 'opacity-40 cursor-not-allowed pointer-events-none',
170
+ isSelected
171
+ ? 'bg-[var(--ui-primary)] text-white font-semibold shadow-xs'
172
+ : isHighlighted
173
+ ? 'bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
174
+ : 'text-[var(--ui-text-primary)]'
175
+ )}
176
+ >
177
+ <div className="flex items-center gap-2 truncate">
178
+ {opt.icon}
179
+ <div>
180
+ <span className="block truncate">{opt.label}</span>
181
+ {opt.description && (
182
+ <span className={cn('text-[10px] block truncate', isSelected ? 'text-white/80' : 'text-[var(--ui-text-muted)]')}>
183
+ {opt.description}
184
+ </span>
185
+ )}
186
+ </div>
187
+ </div>
188
+
189
+ {isSelected && <Check className="w-3.5 h-3.5 shrink-0" />}
190
+ </div>
191
+ );
192
+ })
193
+ )}
194
+ </div>
195
+ </div>
196
+ </Popover>
197
+ </div>
198
+ );
199
+ }
200
+ );
201
+ Combobox.displayName = 'Combobox';
202
+
203
+ /* -------------------------------------------------------------------------------------------------
204
+ * MultiSelect Component
205
+ * -----------------------------------------------------------------------------------------------*/
206
+ export interface MultiSelectProps {
207
+ options: ComboboxOption[];
208
+ value?: string[];
209
+ defaultValue?: string[];
210
+ onChange?: (val: string[]) => void;
211
+ placeholder?: string;
212
+ searchPlaceholder?: string;
213
+ maxCount?: number;
214
+ disabled?: boolean;
215
+ className?: string;
216
+ }
217
+
218
+ export const MultiSelect = forwardRef<HTMLDivElement, MultiSelectProps>(
219
+ (
220
+ {
221
+ options,
222
+ value: controlledVal,
223
+ defaultValue = [],
224
+ onChange,
225
+ placeholder = 'Select items...',
226
+ searchPlaceholder = 'Search options...',
227
+ maxCount = 3,
228
+ disabled = false,
229
+ className,
230
+ },
231
+ ref
232
+ ) => {
233
+ const [internalVal, setInternalVal] = useState<string[]>(defaultValue);
234
+ const selectedValues = controlledVal !== undefined ? controlledVal : internalVal;
235
+
236
+ const [search, setSearch] = useState('');
237
+ const [isOpen, setIsOpen] = useState(false);
238
+
239
+ const filteredOptions = options.filter(
240
+ (opt) =>
241
+ opt.label.toLowerCase().includes(search.toLowerCase()) ||
242
+ opt.value.toLowerCase().includes(search.toLowerCase())
243
+ );
244
+
245
+ const toggleOption = (val: string) => {
246
+ const next = selectedValues.includes(val)
247
+ ? selectedValues.filter((v) => v !== val)
248
+ : [...selectedValues, val];
249
+ setInternalVal(next);
250
+ onChange?.(next);
251
+ };
252
+
253
+ const removeTag = (val: string, e?: React.MouseEvent) => {
254
+ e?.stopPropagation?.();
255
+ const next = selectedValues.filter((v) => v !== val);
256
+ setInternalVal(next);
257
+ onChange?.(next);
258
+ };
259
+
260
+ const clearAll = (e?: React.MouseEvent) => {
261
+ e?.stopPropagation?.();
262
+ setInternalVal([]);
263
+ onChange?.([]);
264
+ };
265
+
266
+ return (
267
+ <div ref={ref} className={cn('relative w-full', className)}>
268
+ <Popover
269
+ isOpen={isOpen}
270
+ onClose={() => setIsOpen(false)}
271
+ trigger={
272
+ <div
273
+ onClick={() => !disabled && setIsOpen(!isOpen)}
274
+ className={cn(
275
+ 'min-h-10 py-1.5 px-3 rounded-[var(--ui-radius-md)] border bg-[var(--ui-bg-surface)] flex flex-wrap items-center justify-between gap-1.5 text-sm transition-all shadow-inner cursor-pointer',
276
+ disabled
277
+ ? 'opacity-50 cursor-not-allowed border-[var(--ui-border-subtle)]'
278
+ : 'border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)]'
279
+ )}
280
+ >
281
+ <div className="flex flex-wrap items-center gap-1.5 flex-1">
282
+ {selectedValues.length === 0 ? (
283
+ <span className="text-[var(--ui-text-muted)] text-sm">{placeholder}</span>
284
+ ) : (
285
+ <>
286
+ {selectedValues.slice(0, maxCount).map((val) => {
287
+ const opt = options.find((o) => o.value === val);
288
+ return (
289
+ <Chip
290
+ key={val}
291
+ size="sm"
292
+ variant="primary"
293
+ onRemove={() => removeTag(val, {} as any)}
294
+ >
295
+ {opt?.label || val}
296
+ </Chip>
297
+ );
298
+ })}
299
+ {selectedValues.length > maxCount && (
300
+ <Chip size="sm" variant="secondary">
301
+ +{selectedValues.length - maxCount} more
302
+ </Chip>
303
+ )}
304
+ </>
305
+ )}
306
+ </div>
307
+
308
+ <div className="flex items-center gap-1 text-[var(--ui-text-muted)]">
309
+ {selectedValues.length > 0 && !disabled && (
310
+ <span
311
+ onClick={clearAll}
312
+ className="p-1 rounded-full hover:bg-[var(--ui-bg-tertiary)] hover:text-[var(--ui-text-primary)] transition-colors"
313
+ >
314
+ <X className="w-3.5 h-3.5" />
315
+ </span>
316
+ )}
317
+ <ChevronsUpDown className="w-4 h-4 opacity-50 shrink-0" />
318
+ </div>
319
+ </div>
320
+ }
321
+ >
322
+ <div className="p-2 w-76 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-2">
323
+ <div className="relative flex items-center">
324
+ <Search className="absolute left-2.5 w-3.5 h-3.5 text-[var(--ui-text-muted)] pointer-events-none" />
325
+ <input
326
+ type="text"
327
+ autoFocus
328
+ placeholder={searchPlaceholder}
329
+ value={search}
330
+ onChange={(e) => setSearch(e.target.value)}
331
+ className="w-full h-8 pl-8 pr-3 text-xs rounded-xl bg-[var(--ui-bg-secondary)] border border-[var(--ui-border-subtle)] focus:outline-none focus:border-[var(--ui-primary)] text-[var(--ui-text-primary)]"
332
+ />
333
+ </div>
334
+
335
+ <div className="max-h-56 overflow-y-auto space-y-0.5">
336
+ {filteredOptions.length === 0 ? (
337
+ <div className="py-4 text-center text-xs text-[var(--ui-text-muted)]">No options found.</div>
338
+ ) : (
339
+ filteredOptions.map((opt) => {
340
+ const isChecked = selectedValues.includes(opt.value);
341
+ return (
342
+ <div
343
+ key={opt.value}
344
+ onClick={() => toggleOption(opt.value)}
345
+ className={cn(
346
+ 'flex items-center justify-between px-3 py-2 rounded-xl text-xs font-medium cursor-pointer transition-colors',
347
+ isChecked
348
+ ? 'bg-[var(--ui-primary)]/15 text-[var(--ui-primary)] font-semibold'
349
+ : 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
350
+ )}
351
+ >
352
+ <div className="flex items-center gap-2 truncate">
353
+ {opt.icon}
354
+ <span>{opt.label}</span>
355
+ </div>
356
+ {isChecked && <Check className="w-3.5 h-3.5 text-[var(--ui-primary)] shrink-0" />}
357
+ </div>
358
+ );
359
+ })
360
+ )}
361
+ </div>
362
+ </div>
363
+ </Popover>
364
+ </div>
365
+ );
366
+ }
367
+ );
368
+ MultiSelect.displayName = 'MultiSelect';