@zentauri-ui/zentauri-components 1.9.2 → 2.0.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 (215) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +32 -5
  3. package/cli/registry.json +14 -0
  4. package/dist/{chunk-L4PDJ6IB.mjs → chunk-44NX3DAZ.mjs} +3 -3
  5. package/dist/{chunk-L4PDJ6IB.mjs.map → chunk-44NX3DAZ.mjs.map} +1 -1
  6. package/dist/chunk-FAMHSJTK.js +19 -0
  7. package/dist/{chunk-AQHY4S33.js.map → chunk-FAMHSJTK.js.map} +1 -1
  8. package/dist/chunk-I42UYWYA.mjs +128 -0
  9. package/dist/chunk-I42UYWYA.mjs.map +1 -0
  10. package/dist/{chunk-5J6QMTES.js → chunk-IKXO5SJ4.js} +21 -5
  11. package/dist/chunk-IKXO5SJ4.js.map +1 -0
  12. package/dist/{chunk-OPUO55TO.mjs → chunk-JXSM2EHC.mjs} +3 -3
  13. package/dist/{chunk-OPUO55TO.mjs.map → chunk-JXSM2EHC.mjs.map} +1 -1
  14. package/dist/{chunk-LQPKZ5ZD.js → chunk-LS4GY2ZQ.js} +6 -6
  15. package/dist/{chunk-LQPKZ5ZD.js.map → chunk-LS4GY2ZQ.js.map} +1 -1
  16. package/dist/{chunk-VIKQGO4W.mjs → chunk-VQQHVKEU.mjs} +21 -5
  17. package/dist/{chunk-VIKQGO4W.mjs.map → chunk-VQQHVKEU.mjs.map} +1 -1
  18. package/dist/chunk-ZVRGLG35.js +144 -0
  19. package/dist/chunk-ZVRGLG35.js.map +1 -0
  20. package/dist/design-system/combobox.d.ts +124 -0
  21. package/dist/design-system/combobox.d.ts.map +1 -0
  22. package/dist/design-system/facade.js +6 -5
  23. package/dist/design-system/facade.js.map +1 -1
  24. package/dist/design-system/facade.mjs +5 -4
  25. package/dist/design-system/facade.mjs.map +1 -1
  26. package/dist/design-system/index.d.ts +1 -0
  27. package/dist/design-system/index.d.ts.map +1 -1
  28. package/dist/hooks/index.d.ts +13 -0
  29. package/dist/hooks/index.d.ts.map +1 -1
  30. package/dist/hooks/useCookie/index.d.ts +2 -0
  31. package/dist/hooks/useCookie/index.d.ts.map +1 -0
  32. package/dist/hooks/useCookie/useCookie.d.ts +36 -0
  33. package/dist/hooks/useCookie/useCookie.d.ts.map +1 -0
  34. package/dist/hooks/useCookie.js +82 -0
  35. package/dist/hooks/useCookie.js.map +1 -0
  36. package/dist/hooks/useCookie.mjs +80 -0
  37. package/dist/hooks/useCookie.mjs.map +1 -0
  38. package/dist/hooks/useCountdown/index.d.ts +2 -0
  39. package/dist/hooks/useCountdown/index.d.ts.map +1 -0
  40. package/dist/hooks/useCountdown/useCountdown.d.ts +40 -0
  41. package/dist/hooks/useCountdown/useCountdown.d.ts.map +1 -0
  42. package/dist/hooks/useCountdown.js +60 -0
  43. package/dist/hooks/useCountdown.js.map +1 -0
  44. package/dist/hooks/useCountdown.mjs +58 -0
  45. package/dist/hooks/useCountdown.mjs.map +1 -0
  46. package/dist/hooks/useEventListener/index.d.ts +2 -0
  47. package/dist/hooks/useEventListener/index.d.ts.map +1 -0
  48. package/dist/hooks/useEventListener/useEventListener.d.ts +22 -0
  49. package/dist/hooks/useEventListener/useEventListener.d.ts.map +1 -0
  50. package/dist/hooks/useEventListener.js +45 -0
  51. package/dist/hooks/useEventListener.js.map +1 -0
  52. package/dist/hooks/useEventListener.mjs +43 -0
  53. package/dist/hooks/useEventListener.mjs.map +1 -0
  54. package/dist/hooks/useGeolocation/index.d.ts +2 -0
  55. package/dist/hooks/useGeolocation/index.d.ts.map +1 -0
  56. package/dist/hooks/useGeolocation/useGeolocation.d.ts +48 -0
  57. package/dist/hooks/useGeolocation/useGeolocation.d.ts.map +1 -0
  58. package/dist/hooks/useGeolocation.js +111 -0
  59. package/dist/hooks/useGeolocation.js.map +1 -0
  60. package/dist/hooks/useGeolocation.mjs +109 -0
  61. package/dist/hooks/useGeolocation.mjs.map +1 -0
  62. package/dist/hooks/useHotkeys/index.d.ts +2 -0
  63. package/dist/hooks/useHotkeys/index.d.ts.map +1 -0
  64. package/dist/hooks/useHotkeys/useHotkeys.d.ts +24 -0
  65. package/dist/hooks/useHotkeys/useHotkeys.d.ts.map +1 -0
  66. package/dist/hooks/useHotkeys.js +86 -0
  67. package/dist/hooks/useHotkeys.js.map +1 -0
  68. package/dist/hooks/useHotkeys.mjs +84 -0
  69. package/dist/hooks/useHotkeys.mjs.map +1 -0
  70. package/dist/hooks/useIdleTimeout/index.d.ts +2 -0
  71. package/dist/hooks/useIdleTimeout/index.d.ts.map +1 -0
  72. package/dist/hooks/useIdleTimeout/useIdleTimeout.d.ts +31 -0
  73. package/dist/hooks/useIdleTimeout/useIdleTimeout.d.ts.map +1 -0
  74. package/dist/hooks/useIdleTimeout.js +77 -0
  75. package/dist/hooks/useIdleTimeout.js.map +1 -0
  76. package/dist/hooks/useIdleTimeout.mjs +75 -0
  77. package/dist/hooks/useIdleTimeout.mjs.map +1 -0
  78. package/dist/hooks/useInterval/index.d.ts +2 -0
  79. package/dist/hooks/useInterval/index.d.ts.map +1 -0
  80. package/dist/hooks/useInterval/useInterval.d.ts +12 -0
  81. package/dist/hooks/useInterval/useInterval.d.ts.map +1 -0
  82. package/dist/hooks/useInterval.js +27 -0
  83. package/dist/hooks/useInterval.js.map +1 -0
  84. package/dist/hooks/useInterval.mjs +25 -0
  85. package/dist/hooks/useInterval.mjs.map +1 -0
  86. package/dist/hooks/useKeyPress/index.d.ts +2 -0
  87. package/dist/hooks/useKeyPress/index.d.ts.map +1 -0
  88. package/dist/hooks/useKeyPress/useKeyPress.d.ts +15 -0
  89. package/dist/hooks/useKeyPress/useKeyPress.d.ts.map +1 -0
  90. package/dist/hooks/useKeyPress.js +47 -0
  91. package/dist/hooks/useKeyPress.js.map +1 -0
  92. package/dist/hooks/useKeyPress.mjs +45 -0
  93. package/dist/hooks/useKeyPress.mjs.map +1 -0
  94. package/dist/hooks/useLongPress/index.d.ts +2 -0
  95. package/dist/hooks/useLongPress/index.d.ts.map +1 -0
  96. package/dist/hooks/useLongPress/useLongPress.d.ts +46 -0
  97. package/dist/hooks/useLongPress/useLongPress.d.ts.map +1 -0
  98. package/dist/hooks/useLongPress.js +116 -0
  99. package/dist/hooks/useLongPress.js.map +1 -0
  100. package/dist/hooks/useLongPress.mjs +114 -0
  101. package/dist/hooks/useLongPress.mjs.map +1 -0
  102. package/dist/hooks/usePrevious/index.d.ts +2 -0
  103. package/dist/hooks/usePrevious/index.d.ts.map +1 -0
  104. package/dist/hooks/usePrevious/usePrevious.d.ts +13 -0
  105. package/dist/hooks/usePrevious/usePrevious.d.ts.map +1 -0
  106. package/dist/hooks/usePrevious.js +17 -0
  107. package/dist/hooks/usePrevious.js.map +1 -0
  108. package/dist/hooks/usePrevious.mjs +15 -0
  109. package/dist/hooks/usePrevious.mjs.map +1 -0
  110. package/dist/hooks/useScrollPosition/index.d.ts +2 -0
  111. package/dist/hooks/useScrollPosition/index.d.ts.map +1 -0
  112. package/dist/hooks/useScrollPosition/useScrollPosition.d.ts +37 -0
  113. package/dist/hooks/useScrollPosition/useScrollPosition.d.ts.map +1 -0
  114. package/dist/hooks/useScrollPosition.js +41 -0
  115. package/dist/hooks/useScrollPosition.js.map +1 -0
  116. package/dist/hooks/useScrollPosition.mjs +39 -0
  117. package/dist/hooks/useScrollPosition.mjs.map +1 -0
  118. package/dist/hooks/useTimeout/index.d.ts +2 -0
  119. package/dist/hooks/useTimeout/index.d.ts.map +1 -0
  120. package/dist/hooks/useTimeout/useTimeout.d.ts +19 -0
  121. package/dist/hooks/useTimeout/useTimeout.d.ts.map +1 -0
  122. package/dist/hooks/useTimeout.js +38 -0
  123. package/dist/hooks/useTimeout.js.map +1 -0
  124. package/dist/hooks/useTimeout.mjs +36 -0
  125. package/dist/hooks/useTimeout.mjs.map +1 -0
  126. package/dist/hooks/useVirtualList/index.d.ts +2 -0
  127. package/dist/hooks/useVirtualList/index.d.ts.map +1 -0
  128. package/dist/hooks/useVirtualList/useVirtualList.d.ts +47 -0
  129. package/dist/hooks/useVirtualList/useVirtualList.d.ts.map +1 -0
  130. package/dist/hooks/useVirtualList.js +87 -0
  131. package/dist/hooks/useVirtualList.js.map +1 -0
  132. package/dist/hooks/useVirtualList.mjs +85 -0
  133. package/dist/hooks/useVirtualList.mjs.map +1 -0
  134. package/dist/lib/facade.d.ts.map +1 -1
  135. package/dist/ui/buttons/animated.js +8 -7
  136. package/dist/ui/buttons/animated.js.map +1 -1
  137. package/dist/ui/buttons/animated.mjs +6 -5
  138. package/dist/ui/buttons/animated.mjs.map +1 -1
  139. package/dist/ui/buttons.js +9 -8
  140. package/dist/ui/buttons.mjs +7 -6
  141. package/dist/ui/combobox/combobox-base.d.ts +37 -0
  142. package/dist/ui/combobox/combobox-base.d.ts.map +1 -0
  143. package/dist/ui/combobox/combobox.d.ts +6 -0
  144. package/dist/ui/combobox/combobox.d.ts.map +1 -0
  145. package/dist/ui/combobox/index.d.ts +4 -0
  146. package/dist/ui/combobox/index.d.ts.map +1 -0
  147. package/dist/ui/combobox/types.d.ts +70 -0
  148. package/dist/ui/combobox/types.d.ts.map +1 -0
  149. package/dist/ui/combobox/variants.d.ts +17 -0
  150. package/dist/ui/combobox/variants.d.ts.map +1 -0
  151. package/dist/ui/combobox.js +510 -0
  152. package/dist/ui/combobox.js.map +1 -0
  153. package/dist/ui/combobox.mjs +495 -0
  154. package/dist/ui/combobox.mjs.map +1 -0
  155. package/dist/ui/dynamic-stepper.js +18 -17
  156. package/dist/ui/dynamic-stepper.js.map +1 -1
  157. package/dist/ui/dynamic-stepper.mjs +7 -6
  158. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  159. package/dist/ui/pagination.js +14 -13
  160. package/dist/ui/pagination.js.map +1 -1
  161. package/dist/ui/pagination.mjs +6 -5
  162. package/dist/ui/pagination.mjs.map +1 -1
  163. package/package.json +1 -1
  164. package/src/design-system/combobox.ts +204 -0
  165. package/src/design-system/index.ts +1 -0
  166. package/src/hooks/index.ts +50 -0
  167. package/src/hooks/useCookie/index.ts +5 -0
  168. package/src/hooks/useCookie/useCookie.test.ts +57 -0
  169. package/src/hooks/useCookie/useCookie.ts +133 -0
  170. package/src/hooks/useCountdown/index.ts +5 -0
  171. package/src/hooks/useCountdown/useCountdown.test.ts +113 -0
  172. package/src/hooks/useCountdown/useCountdown.ts +106 -0
  173. package/src/hooks/useEventListener/index.ts +4 -0
  174. package/src/hooks/useEventListener/useEventListener.test.ts +60 -0
  175. package/src/hooks/useEventListener/useEventListener.ts +98 -0
  176. package/src/hooks/useGeolocation/index.ts +6 -0
  177. package/src/hooks/useGeolocation/useGeolocation.test.ts +108 -0
  178. package/src/hooks/useGeolocation/useGeolocation.ts +173 -0
  179. package/src/hooks/useHotkeys/index.ts +5 -0
  180. package/src/hooks/useHotkeys/useHotkeys.test.ts +82 -0
  181. package/src/hooks/useHotkeys/useHotkeys.ts +130 -0
  182. package/src/hooks/useIdleTimeout/index.ts +5 -0
  183. package/src/hooks/useIdleTimeout/useIdleTimeout.test.ts +97 -0
  184. package/src/hooks/useIdleTimeout/useIdleTimeout.ts +111 -0
  185. package/src/hooks/useInterval/index.ts +1 -0
  186. package/src/hooks/useInterval/useInterval.test.ts +56 -0
  187. package/src/hooks/useInterval/useInterval.ts +36 -0
  188. package/src/hooks/useKeyPress/index.ts +1 -0
  189. package/src/hooks/useKeyPress/useKeyPress.test.ts +67 -0
  190. package/src/hooks/useKeyPress/useKeyPress.ts +65 -0
  191. package/src/hooks/useLongPress/index.ts +5 -0
  192. package/src/hooks/useLongPress/useLongPress.test.ts +180 -0
  193. package/src/hooks/useLongPress/useLongPress.ts +177 -0
  194. package/src/hooks/usePrevious/index.ts +1 -0
  195. package/src/hooks/usePrevious/usePrevious.test.ts +33 -0
  196. package/src/hooks/usePrevious/usePrevious.ts +24 -0
  197. package/src/hooks/useScrollPosition/index.ts +5 -0
  198. package/src/hooks/useScrollPosition/useScrollPosition.test.ts +69 -0
  199. package/src/hooks/useScrollPosition/useScrollPosition.ts +88 -0
  200. package/src/hooks/useTimeout/index.ts +1 -0
  201. package/src/hooks/useTimeout/useTimeout.test.ts +63 -0
  202. package/src/hooks/useTimeout/useTimeout.ts +58 -0
  203. package/src/hooks/useVirtualList/index.ts +6 -0
  204. package/src/hooks/useVirtualList/useVirtualList.test.ts +102 -0
  205. package/src/hooks/useVirtualList/useVirtualList.ts +144 -0
  206. package/src/lib/facade.test.ts +7 -7
  207. package/src/lib/facade.ts +6 -2
  208. package/src/ui/combobox/combobox-base.tsx +552 -0
  209. package/src/ui/combobox/combobox.test.tsx +292 -0
  210. package/src/ui/combobox/combobox.tsx +8 -0
  211. package/src/ui/combobox/index.ts +33 -0
  212. package/src/ui/combobox/types.ts +91 -0
  213. package/src/ui/combobox/variants.ts +58 -0
  214. package/dist/chunk-5J6QMTES.js.map +0 -1
  215. package/dist/chunk-AQHY4S33.js +0 -19
@@ -0,0 +1,552 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import type { ReactNode, RefObject } from "react";
14
+
15
+ import {
16
+ zuiComboboxEmptyBase,
17
+ zuiComboboxSearchIconBase,
18
+ zuiComboboxSearchInputBase,
19
+ zuiComboboxSearchRowBase,
20
+ } from "../../design-system/combobox";
21
+ import { cn } from "../../lib/utils";
22
+ import type {
23
+ ComboboxAppearance,
24
+ ComboboxContentProps,
25
+ ComboboxContextType,
26
+ ComboboxEmptyProps,
27
+ ComboboxItemProps,
28
+ ComboboxListProps,
29
+ ComboboxOption,
30
+ ComboboxProps,
31
+ ComboboxSearchProps,
32
+ ComboboxTriggerProps,
33
+ ComboboxValueProps,
34
+ } from "./types";
35
+ import {
36
+ comboboxContentVariants,
37
+ comboboxItemVariants,
38
+ comboboxListVariants,
39
+ comboboxTriggerVariants,
40
+ } from "./variants";
41
+
42
+ const ComboboxContext = createContext<ComboboxContextType | null>(null);
43
+ const ComboboxAppearanceContext = createContext<ComboboxAppearance>("default");
44
+
45
+ export const useCombobox = () => {
46
+ const ctx = useContext(ComboboxContext);
47
+ if (!ctx) throw new Error("Combobox components must be used inside Combobox");
48
+ return ctx;
49
+ };
50
+
51
+ export { ComboboxContext };
52
+
53
+ export const Combobox = ({
54
+ children,
55
+ value,
56
+ defaultValue = [],
57
+ onChange,
58
+ multiple = false,
59
+ className,
60
+ }: ComboboxProps) => {
61
+ const baseId = useId();
62
+ const triggerId = `${baseId}-trigger`;
63
+ const listboxId = `${baseId}-listbox`;
64
+
65
+ const [internal, setInternal] = useState<string[]>(defaultValue);
66
+ const [open, setOpen] = useState(false);
67
+ const [options, setOptions] = useState<ComboboxOption[]>([]);
68
+ const [query, setQuery] = useState("");
69
+ const [activeValue, setActiveValue] = useState<string | null>(null);
70
+ const rootRef = useRef<HTMLDivElement>(null);
71
+ const searchRef = useRef<HTMLInputElement | null>(null);
72
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
73
+
74
+ const selected = value ?? internal;
75
+
76
+ useEffect(() => {
77
+ if (!open) return;
78
+ const handlePointerDown = (event: PointerEvent) => {
79
+ const root = rootRef.current;
80
+ if (!root) return;
81
+ if (event.target instanceof Node && !root.contains(event.target)) {
82
+ setOpen(false);
83
+ }
84
+ };
85
+ document.addEventListener("pointerdown", handlePointerDown);
86
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
87
+ }, [open]);
88
+
89
+ useEffect(() => {
90
+ if (!open) {
91
+ setQuery("");
92
+ setActiveValue(null);
93
+ }
94
+ }, [open]);
95
+
96
+ const visibleValues = useMemo(() => {
97
+ const normalized = query.trim().toLowerCase();
98
+ if (!normalized) return options.map((o) => o.value);
99
+ return options
100
+ .filter((o) => {
101
+ if (o.disabled) return false;
102
+ const labelText =
103
+ typeof o.label === "string" ? o.label.toLowerCase() : "";
104
+ return (
105
+ o.value.toLowerCase().includes(normalized) ||
106
+ labelText.includes(normalized)
107
+ );
108
+ })
109
+ .map((o) => o.value);
110
+ }, [query, options]);
111
+
112
+ // If the current highlight gets filtered out while typing, clear it.
113
+ // We do NOT auto-populate on open so ArrowDown always starts from the top.
114
+ useEffect(() => {
115
+ if (!open) return;
116
+ if (!activeValue) return;
117
+ if (visibleValues.includes(activeValue)) return;
118
+ setActiveValue(null);
119
+ }, [open, activeValue, visibleValues]);
120
+
121
+ const setSelected = (vals: string[]) => {
122
+ if (value !== undefined) {
123
+ onChange?.(vals);
124
+ } else {
125
+ setInternal(vals);
126
+ onChange?.(vals);
127
+ }
128
+ };
129
+
130
+ const toggleValue = useCallback(
131
+ (val: string) => {
132
+ if (multiple) {
133
+ const next = selected.includes(val)
134
+ ? selected.filter((v) => v !== val)
135
+ : [...selected, val];
136
+ setSelected(next);
137
+ } else {
138
+ setSelected([val]);
139
+ setOpen(false);
140
+ }
141
+ },
142
+ // eslint-disable-next-line react-hooks/exhaustive-deps
143
+ [multiple, selected],
144
+ );
145
+
146
+ const isSelected = useCallback(
147
+ (val: string) => selected.includes(val),
148
+ [selected],
149
+ );
150
+
151
+ const isVisible = useCallback(
152
+ (val: string) => visibleValues.includes(val),
153
+ [visibleValues],
154
+ );
155
+
156
+ const registerOption = useCallback((opt: ComboboxOption) => {
157
+ setOptions((prev) => {
158
+ const existing = prev.find((o) => o.value === opt.value);
159
+ if (existing) {
160
+ const labelChanged =
161
+ (typeof opt.label === "string" || typeof opt.label === "number") &&
162
+ existing.label !== opt.label;
163
+ const disabledChanged = existing.disabled !== opt.disabled;
164
+ if (labelChanged || disabledChanged) {
165
+ return prev.map((o) => (o.value === opt.value ? opt : o));
166
+ }
167
+ return prev;
168
+ }
169
+ return [...prev, opt];
170
+ });
171
+ // No cleanup on unmount: options must persist across panel open/close so
172
+ // ComboboxValue can still display the selected label when the panel is closed.
173
+ }, []);
174
+
175
+ return (
176
+ <ComboboxContext.Provider
177
+ value={{
178
+ open,
179
+ setOpen,
180
+ query,
181
+ setQuery,
182
+ selected,
183
+ toggleValue,
184
+ isSelected,
185
+ multiple,
186
+ options,
187
+ registerOption,
188
+ visibleValues,
189
+ isVisible,
190
+ activeValue,
191
+ setActiveValue,
192
+ triggerId,
193
+ listboxId,
194
+ searchRef,
195
+ triggerRef,
196
+ }}
197
+ >
198
+ <div ref={rootRef} className={cn("relative w-full", className)}>
199
+ {children}
200
+ </div>
201
+ </ComboboxContext.Provider>
202
+ );
203
+ };
204
+
205
+ Combobox.displayName = "Combobox";
206
+
207
+ export const ComboboxTrigger = ({
208
+ className,
209
+ variant,
210
+ size,
211
+ onClick,
212
+ ref: refProp,
213
+ ...props
214
+ }: ComboboxTriggerProps) => {
215
+ const { open, setOpen, triggerId, listboxId, triggerRef } = useCombobox();
216
+
217
+ return (
218
+ <button
219
+ ref={(node) => {
220
+ triggerRef.current = node;
221
+ if (typeof refProp === "function") {
222
+ refProp(node);
223
+ } else if (refProp) {
224
+ (refProp as RefObject<HTMLButtonElement | null>).current = node;
225
+ }
226
+ }}
227
+ id={triggerId}
228
+ type="button"
229
+ aria-expanded={open}
230
+ aria-haspopup="listbox"
231
+ aria-controls={listboxId}
232
+ data-slot="combobox-trigger"
233
+ className={cn(comboboxTriggerVariants({ variant, size }), className)}
234
+ {...props}
235
+ onClick={(event) => {
236
+ onClick?.(event);
237
+ if (!event.defaultPrevented) {
238
+ setOpen(!open);
239
+ }
240
+ }}
241
+ />
242
+ );
243
+ };
244
+
245
+ ComboboxTrigger.displayName = "ComboboxTrigger";
246
+
247
+ export const ComboboxValue = ({
248
+ placeholder = "Select...",
249
+ className,
250
+ ...props
251
+ }: ComboboxValueProps) => {
252
+ const { selected, options } = useCombobox();
253
+ const selectedOptions = options.filter((o) => selected.includes(o.value));
254
+
255
+ if (selectedOptions.length === 0) {
256
+ return (
257
+ <span
258
+ data-slot="combobox-placeholder"
259
+ className={cn(
260
+ "text-[color:var(--zui-combobox-value-placeholder-fg,oklch(55.1%_0.027_264.364))] dark:text-white",
261
+ className,
262
+ )}
263
+ {...props}
264
+ >
265
+ {placeholder}
266
+ </span>
267
+ );
268
+ }
269
+
270
+ return (
271
+ <span data-slot="combobox-value" className={cn(className)} {...props}>
272
+ {selectedOptions.map((option, index) => (
273
+ <span key={option.value}>
274
+ {index > 0 ? ", " : null}
275
+ {option.label}
276
+ </span>
277
+ ))}
278
+ </span>
279
+ );
280
+ };
281
+
282
+ ComboboxValue.displayName = "ComboboxValue";
283
+
284
+ export const ComboboxContent = ({
285
+ children,
286
+ className,
287
+ appearance = "default",
288
+ size = "md",
289
+ spacing = "default",
290
+ ...props
291
+ }: ComboboxContentProps) => {
292
+ const { open, triggerId, listboxId, searchRef } = useCombobox();
293
+
294
+ useEffect(() => {
295
+ if (open) {
296
+ searchRef.current?.focus();
297
+ }
298
+ }, [open, searchRef]);
299
+
300
+ if (!open) return null;
301
+
302
+ return (
303
+ <ComboboxAppearanceContext.Provider value={appearance}>
304
+ <div
305
+ aria-labelledby={triggerId}
306
+ data-slot="combobox-content"
307
+ className={cn(
308
+ comboboxContentVariants({ appearance, size, spacing }),
309
+ className,
310
+ )}
311
+ {...props}
312
+ >
313
+ {children}
314
+ </div>
315
+ </ComboboxAppearanceContext.Provider>
316
+ );
317
+ };
318
+
319
+ ComboboxContent.displayName = "ComboboxContent";
320
+
321
+ export const ComboboxSearch = ({
322
+ className,
323
+ placeholder = "Search...",
324
+ ref: refProp,
325
+ ...props
326
+ }: ComboboxSearchProps) => {
327
+ const {
328
+ query,
329
+ setQuery,
330
+ visibleValues,
331
+ activeValue,
332
+ setActiveValue,
333
+ toggleValue,
334
+ setOpen,
335
+ searchRef,
336
+ listboxId,
337
+ options,
338
+ triggerRef,
339
+ } = useCombobox();
340
+
341
+ const optionByValue = useMemo(
342
+ () => new Map(options.map((option) => [option.value, option])),
343
+ [options],
344
+ );
345
+
346
+ const moveActive = (direction: 1 | -1) => {
347
+ if (visibleValues.length === 0) return;
348
+ const currentIndex = activeValue ? visibleValues.indexOf(activeValue) : -1;
349
+ let nextIndex = currentIndex;
350
+ for (let i = 0; i < visibleValues.length; i++) {
351
+ if (nextIndex === -1) {
352
+ nextIndex = direction === 1 ? 0 : visibleValues.length - 1;
353
+ } else {
354
+ nextIndex =
355
+ (nextIndex + direction + visibleValues.length) % visibleValues.length;
356
+ }
357
+ const candidate = optionByValue.get(visibleValues[nextIndex] ?? "");
358
+ if (candidate && !candidate.disabled) {
359
+ setActiveValue(visibleValues[nextIndex] ?? null);
360
+ return;
361
+ }
362
+ }
363
+ };
364
+
365
+ return (
366
+ <div data-slot="combobox-search-row" className={zuiComboboxSearchRowBase}>
367
+ <svg
368
+ aria-hidden
369
+ viewBox="0 0 24 24"
370
+ className={zuiComboboxSearchIconBase}
371
+ fill="none"
372
+ stroke="currentColor"
373
+ strokeWidth="2"
374
+ strokeLinecap="round"
375
+ strokeLinejoin="round"
376
+ >
377
+ <circle cx="11" cy="11" r="8" />
378
+ <path d="m21 21-4.3-4.3" />
379
+ </svg>
380
+ <input
381
+ ref={(node) => {
382
+ searchRef.current = node;
383
+ if (typeof refProp === "function") {
384
+ refProp(node);
385
+ } else if (refProp) {
386
+ (refProp as RefObject<HTMLInputElement | null>).current = node;
387
+ }
388
+ }}
389
+ type="text"
390
+ role="combobox"
391
+ aria-expanded
392
+ aria-controls={listboxId}
393
+ aria-autocomplete="list"
394
+ aria-activedescendant={
395
+ activeValue ? `${listboxId}-option-${activeValue}` : undefined
396
+ }
397
+ data-slot="combobox-search"
398
+ className={cn(zuiComboboxSearchInputBase, className)}
399
+ placeholder={placeholder}
400
+ value={query}
401
+ onChange={(event) => setQuery(event.target.value)}
402
+ onKeyDown={(event) => {
403
+ if (event.key === "ArrowDown") {
404
+ event.preventDefault();
405
+ moveActive(1);
406
+ } else if (event.key === "ArrowUp") {
407
+ event.preventDefault();
408
+ moveActive(-1);
409
+ } else if (event.key === "Enter") {
410
+ event.preventDefault();
411
+ if (activeValue && visibleValues.includes(activeValue)) {
412
+ const option = optionByValue.get(activeValue);
413
+ if (option && !option.disabled) {
414
+ toggleValue(activeValue);
415
+ }
416
+ }
417
+ } else if (event.key === "Escape") {
418
+ event.preventDefault();
419
+ setOpen(false);
420
+ triggerRef.current?.focus();
421
+ }
422
+ }}
423
+ {...props}
424
+ />
425
+ </div>
426
+ );
427
+ };
428
+
429
+ ComboboxSearch.displayName = "ComboboxSearch";
430
+
431
+ export const ComboboxList = ({
432
+ children,
433
+ className,
434
+ ...props
435
+ }: ComboboxListProps) => {
436
+ const { listboxId, triggerId } = useCombobox();
437
+ const appearance = useContext(ComboboxAppearanceContext);
438
+
439
+ return (
440
+ <div
441
+ id={listboxId}
442
+ role="listbox"
443
+ aria-labelledby={triggerId}
444
+ data-slot="combobox-list"
445
+ className={cn(comboboxListVariants({ appearance }), className)}
446
+ {...props}
447
+ >
448
+ {children}
449
+ </div>
450
+ );
451
+ };
452
+
453
+ ComboboxList.displayName = "ComboboxList";
454
+
455
+ export const ComboboxItem = ({
456
+ value,
457
+ children,
458
+ disabled,
459
+ appearance = "default",
460
+ className,
461
+ ...props
462
+ }: ComboboxItemProps) => {
463
+ const {
464
+ toggleValue,
465
+ isSelected,
466
+ registerOption,
467
+ isVisible,
468
+ activeValue,
469
+ setActiveValue,
470
+ listboxId,
471
+ } = useCombobox();
472
+
473
+ useEffect(() => {
474
+ registerOption({ label: children, value, disabled });
475
+ }, [children, disabled, registerOption, value]);
476
+
477
+ const visible = isVisible(value);
478
+ const isActive = isSelected(value);
479
+ const isHighlighted = activeValue === value;
480
+
481
+ if (!visible) return null;
482
+
483
+ return (
484
+ <div
485
+ id={`${listboxId}-option-${value}`}
486
+ role="option"
487
+ aria-selected={isActive}
488
+ aria-disabled={disabled ? true : undefined}
489
+ tabIndex={-1}
490
+ data-slot="combobox-item"
491
+ data-selected={isActive ? "true" : "false"}
492
+ data-active={isHighlighted ? "true" : undefined}
493
+ onMouseEnter={() => !disabled && setActiveValue(value)}
494
+ onClick={() => !disabled && toggleValue(value)}
495
+ onKeyDown={(e) => {
496
+ if (disabled) return;
497
+ if (e.key === "Enter" || e.key === " ") {
498
+ e.preventDefault();
499
+ toggleValue(value);
500
+ }
501
+ }}
502
+ className={cn(
503
+ comboboxItemVariants({ disabled, appearance }),
504
+ "flex justify-between",
505
+ isHighlighted &&
506
+ "bg-[var(--zui-combobox-item-active-bg,oklch(93%_0.006_264.531))] dark:bg-[var(--zui-combobox-item-active-bg-dark,oklch(22%_0.006_264.531))]",
507
+ className,
508
+ )}
509
+ {...props}
510
+ >
511
+ {children}
512
+ {isActive && (
513
+ <svg
514
+ aria-hidden
515
+ viewBox="0 0 24 24"
516
+ className="size-4 shrink-0"
517
+ fill="none"
518
+ stroke="currentColor"
519
+ strokeWidth="2.5"
520
+ strokeLinecap="round"
521
+ strokeLinejoin="round"
522
+ >
523
+ <path d="M20 6 9 17l-5-5" />
524
+ </svg>
525
+ )}
526
+ </div>
527
+ );
528
+ };
529
+
530
+ ComboboxItem.displayName = "ComboboxItem";
531
+
532
+ export const ComboboxEmpty = ({
533
+ className,
534
+ children,
535
+ ...props
536
+ }: ComboboxEmptyProps) => {
537
+ const { visibleValues } = useCombobox();
538
+
539
+ if (visibleValues.length > 0) return null;
540
+
541
+ return (
542
+ <div
543
+ data-slot="combobox-empty"
544
+ className={cn(zuiComboboxEmptyBase, className)}
545
+ {...props}
546
+ >
547
+ {children}
548
+ </div>
549
+ );
550
+ };
551
+
552
+ ComboboxEmpty.displayName = "ComboboxEmpty";