pejay-ui 1.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.
@@ -0,0 +1,687 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import {
3
+ useFloating,
4
+ autoUpdate,
5
+ flip,
6
+ shift,
7
+ size,
8
+ offset,
9
+ FloatingPortal,
10
+ } from "@floating-ui/react";
11
+ import { ChevronDown, Check, X, Search, Loader2 } from "lucide-react";
12
+ import { cn } from "@/utils/cn";
13
+ import { Tooltip } from "../button/tooltip";
14
+ import { type SelectOption } from "./select-input";
15
+
16
+ function getNextSelectableIndex(
17
+ currentIndex: number,
18
+ direction: "up" | "down",
19
+ list: SelectOption[],
20
+ ) {
21
+ if (list.length === 0) return -1;
22
+ let nextIndex = currentIndex;
23
+
24
+ for (let i = 0; i < list.length; i++) {
25
+ if (direction === "down") {
26
+ nextIndex = nextIndex < list.length - 1 ? nextIndex + 1 : 0;
27
+ } else {
28
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : list.length - 1;
29
+ }
30
+ if (!list[nextIndex]?.isHeader) {
31
+ return nextIndex;
32
+ }
33
+ }
34
+ return currentIndex;
35
+ }
36
+
37
+ export interface MultiSelectInputProps {
38
+ /** Array of option objects: { id, label, key } */
39
+ options: SelectOption[];
40
+ /** Controlled array of selected keys */
41
+ value?: string[];
42
+ /** Callback triggered when the selection changes */
43
+ onChange?: (keys: string[], selectedOptions: SelectOption[]) => void;
44
+ /** Optional placeholder if no options are selected */
45
+ placeholder?: string;
46
+ /** Optional icon rendered on the left of the button trigger */
47
+ prefixIcon?: React.ReactNode;
48
+ /** Display mode: 'count' (e.g. "3 selected") or 'chips' (individual tags) */
49
+ displayMode?: "count" | "chips";
50
+ /** Custom class name for the wrapper */
51
+ className?: string;
52
+ /** Custom width for the button trigger (e.g. 'w-64', 'w-full') */
53
+ width?: string;
54
+ /** Disabled state */
55
+ disabled?: boolean;
56
+ /** Loading state */
57
+ loading?: boolean;
58
+ /** Show full selected value in a tooltip on hover */
59
+ showTooltip?: boolean;
60
+ /** Enable filtering search functionality */
61
+ searchable?: boolean;
62
+ /** Position of the search input: 'trigger' (direct typing in trigger) or 'dropdown' (input inside overlay) */
63
+ searchPosition?: "trigger" | "dropdown";
64
+ /** Error state message or boolean flag */
65
+ error?: string | boolean;
66
+ }
67
+
68
+ // A helper component to conditionally display a tooltip only if the wrapped child is truncated
69
+ function TruncatedTooltip({
70
+ content,
71
+ children,
72
+ enabled,
73
+ }: {
74
+ content: string;
75
+ children: React.ReactElement<any>;
76
+ enabled: boolean;
77
+ }) {
78
+ const [isTruncated, setIsTruncated] = useState(false);
79
+ const ref = useRef<HTMLElement>(null);
80
+
81
+ const checkTruncation = () => {
82
+ if (!enabled || !ref.current) return;
83
+ const el = ref.current;
84
+ setIsTruncated(el.scrollWidth > el.clientWidth);
85
+ };
86
+
87
+ const trigger = React.cloneElement(children, {
88
+ ref,
89
+ onMouseEnter: (e: React.MouseEvent) => {
90
+ checkTruncation();
91
+ children.props.onMouseEnter?.(e);
92
+ },
93
+ });
94
+
95
+ return (
96
+ <Tooltip content={content} disabled={!enabled || !isTruncated} fullWidth>
97
+ {trigger}
98
+ </Tooltip>
99
+ );
100
+ }
101
+
102
+ export function MultiSelectInput({
103
+ options = [],
104
+ value: controlledValue,
105
+ onChange,
106
+ placeholder = "Select...",
107
+ prefixIcon,
108
+ displayMode = "count",
109
+ className,
110
+ width = "w-full",
111
+ disabled = false,
112
+ loading = false,
113
+ showTooltip = false,
114
+ searchable = false,
115
+ searchPosition = "trigger",
116
+ error,
117
+ }: MultiSelectInputProps) {
118
+ const [isOpen, setIsOpen] = useState(false);
119
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
120
+ const [searchQuery, setSearchQuery] = useState("");
121
+
122
+ // Uncontrolled fallback state
123
+ const [localValue, setLocalValue] = useState<string[]>([]);
124
+
125
+ const activeValue =
126
+ controlledValue !== undefined ? controlledValue : localValue;
127
+
128
+ // Selected option objects
129
+ const selectedOptions = options.filter(opt => activeValue.includes(opt.key));
130
+
131
+ // Filter options based on search query, keeping headers only if their group contains matches
132
+ const filteredOptions = React.useMemo(() => {
133
+ if (!searchQuery) return options;
134
+
135
+ const result: SelectOption[] = [];
136
+ let currentHeader: SelectOption | null = null;
137
+ let hasItemsInCurrentGroup = false;
138
+
139
+ for (const opt of options) {
140
+ if (opt.isHeader) {
141
+ currentHeader = opt;
142
+ hasItemsInCurrentGroup = false;
143
+ } else {
144
+ const matches = opt.label
145
+ .toLowerCase()
146
+ .includes(searchQuery.toLowerCase());
147
+ if (matches) {
148
+ if (currentHeader && !hasItemsInCurrentGroup) {
149
+ result.push(currentHeader);
150
+ hasItemsInCurrentGroup = true;
151
+ }
152
+ result.push(opt);
153
+ }
154
+ }
155
+ }
156
+ return result;
157
+ }, [options, searchQuery]);
158
+
159
+ const containerRef = useRef<HTMLDivElement>(null);
160
+ const listRef = useRef<HTMLDivElement>(null);
161
+ const inputRef = useRef<HTMLInputElement>(null);
162
+ const dropdownInputRef = useRef<HTMLInputElement>(null);
163
+
164
+ // 1. Setup Floating UI
165
+ const { refs, floatingStyles } = useFloating({
166
+ open: isOpen,
167
+ onOpenChange: open => {
168
+ setIsOpen(open);
169
+ if (!open) {
170
+ setSearchQuery("");
171
+ }
172
+ },
173
+ placement: "bottom-start",
174
+ whileElementsMounted: autoUpdate,
175
+ middleware: [
176
+ offset(4),
177
+ flip({ padding: 8 }),
178
+ shift({ padding: 8 }),
179
+ size({
180
+ apply({ rects, elements }) {
181
+ Object.assign(elements.floating.style, {
182
+ width: `${rects.reference.width}px`,
183
+ });
184
+ },
185
+ padding: 8,
186
+ }),
187
+ ],
188
+ });
189
+
190
+ // Highlight first selected item or index 0 on open/search
191
+ useEffect(() => {
192
+ if (isOpen) {
193
+ if (searchQuery !== "") {
194
+ const firstIdx = filteredOptions.findIndex(opt => !opt.isHeader);
195
+ setHighlightedIndex(firstIdx >= 0 ? firstIdx : 0);
196
+ } else if (activeValue.length > 0) {
197
+ const lastSelectedKey = activeValue[activeValue.length - 1];
198
+ const lastSelectedIdx = filteredOptions.findIndex(
199
+ opt => opt.key === lastSelectedKey && !opt.isHeader,
200
+ );
201
+ if (lastSelectedIdx >= 0) {
202
+ setHighlightedIndex(lastSelectedIdx);
203
+ } else {
204
+ const firstIdx = filteredOptions.findIndex(opt => !opt.isHeader);
205
+ setHighlightedIndex(firstIdx >= 0 ? firstIdx : 0);
206
+ }
207
+ } else {
208
+ const firstIdx = filteredOptions.findIndex(opt => !opt.isHeader);
209
+ setHighlightedIndex(firstIdx >= 0 ? firstIdx : 0);
210
+ }
211
+
212
+ // Auto-focus search input inside dropdown if searchPosition is 'dropdown'
213
+ if (searchable && searchPosition === "dropdown") {
214
+ setTimeout(() => dropdownInputRef.current?.focus(), 50);
215
+ }
216
+ }
217
+ }, [
218
+ isOpen,
219
+ searchQuery,
220
+ activeValue,
221
+ filteredOptions,
222
+ searchable,
223
+ searchPosition,
224
+ ]);
225
+
226
+ // Scroll highlighted item into view
227
+ useEffect(() => {
228
+ if (!isOpen || highlightedIndex < 0 || !listRef.current) return;
229
+ const itemEl = listRef.current.children[highlightedIndex] as HTMLElement;
230
+ if (itemEl) {
231
+ itemEl.scrollIntoView({ block: "nearest", behavior: "smooth" });
232
+ }
233
+ }, [highlightedIndex, isOpen]);
234
+
235
+ // Click outside to close dropdown
236
+ useEffect(() => {
237
+ const handleClickOutside = (e: MouseEvent) => {
238
+ const target = e.target as Node;
239
+ if (
240
+ containerRef.current?.contains(target) ||
241
+ refs.floating.current?.contains(target)
242
+ ) {
243
+ return;
244
+ }
245
+ setIsOpen(false);
246
+ };
247
+
248
+ document.addEventListener("mousedown", handleClickOutside);
249
+ return () => document.removeEventListener("mousedown", handleClickOutside);
250
+ }, [refs.floating]);
251
+
252
+ const toggleOption = (opt: SelectOption) => {
253
+ let nextValue: string[];
254
+ if (activeValue.includes(opt.key)) {
255
+ nextValue = activeValue.filter(val => val !== opt.key);
256
+ } else {
257
+ nextValue = [...activeValue, opt.key];
258
+ }
259
+
260
+ if (controlledValue === undefined) {
261
+ setLocalValue(nextValue);
262
+ }
263
+ const nextOptions = options.filter(o => nextValue.includes(o.key));
264
+ onChange?.(nextValue, nextOptions);
265
+ // Clear search on selection to be user friendly
266
+ setSearchQuery("");
267
+ };
268
+
269
+ const removeOptionKey = (keyToRemove: string, e?: React.MouseEvent) => {
270
+ e?.stopPropagation();
271
+ const nextValue = activeValue.filter(val => val !== keyToRemove);
272
+ if (controlledValue === undefined) {
273
+ setLocalValue(nextValue);
274
+ }
275
+ const nextOptions = options.filter(o => nextValue.includes(o.key));
276
+ onChange?.(nextValue, nextOptions);
277
+ };
278
+
279
+ const typeBufferRef = useRef("");
280
+ const typeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
281
+
282
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
283
+ if (disabled || loading) return;
284
+
285
+ if (!isOpen) {
286
+ if (
287
+ e.key === "Enter" ||
288
+ e.key === " " ||
289
+ e.key === "ArrowDown" ||
290
+ e.key === "ArrowUp"
291
+ ) {
292
+ e.preventDefault();
293
+ setIsOpen(true);
294
+ if (searchable && searchPosition === "trigger") {
295
+ setTimeout(() => inputRef.current?.focus(), 50);
296
+ }
297
+ }
298
+ return;
299
+ }
300
+
301
+ // Typeahead matching: only enabled when NOT searching
302
+ if (!searchable && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
303
+ e.preventDefault();
304
+ typeBufferRef.current += e.key.toLowerCase();
305
+
306
+ const matchIndex = filteredOptions.findIndex(opt =>
307
+ opt.label.toLowerCase().startsWith(typeBufferRef.current),
308
+ );
309
+
310
+ if (matchIndex !== -1) {
311
+ setHighlightedIndex(matchIndex);
312
+ }
313
+
314
+ if (typeTimeoutRef.current) {
315
+ clearTimeout(typeTimeoutRef.current);
316
+ }
317
+
318
+ typeTimeoutRef.current = setTimeout(() => {
319
+ typeBufferRef.current = "";
320
+ }, 500);
321
+
322
+ return;
323
+ }
324
+
325
+ switch (e.key) {
326
+ case "ArrowDown":
327
+ e.preventDefault();
328
+ setHighlightedIndex(prev =>
329
+ getNextSelectableIndex(prev, "down", filteredOptions),
330
+ );
331
+ break;
332
+ case "ArrowUp":
333
+ e.preventDefault();
334
+ setHighlightedIndex(prev =>
335
+ getNextSelectableIndex(prev, "up", filteredOptions),
336
+ );
337
+ break;
338
+ case "Enter":
339
+ e.preventDefault();
340
+ if (
341
+ highlightedIndex >= 0 &&
342
+ highlightedIndex < filteredOptions.length
343
+ ) {
344
+ toggleOption(filteredOptions[highlightedIndex]);
345
+ }
346
+ break;
347
+ case " ":
348
+ // Only toggle option on Space when not actively typing inside the search input
349
+ if (
350
+ !searchable ||
351
+ (searchable &&
352
+ e.target !== inputRef.current &&
353
+ e.target !== dropdownInputRef.current)
354
+ ) {
355
+ e.preventDefault();
356
+ if (
357
+ highlightedIndex >= 0 &&
358
+ highlightedIndex < filteredOptions.length
359
+ ) {
360
+ toggleOption(filteredOptions[highlightedIndex]);
361
+ }
362
+ }
363
+ break;
364
+ case "Backspace":
365
+ // Delete last selected chip if searchQuery is empty
366
+ if (
367
+ displayMode === "chips" &&
368
+ activeValue.length > 0 &&
369
+ searchQuery === ""
370
+ ) {
371
+ e.preventDefault();
372
+ removeOptionKey(activeValue[activeValue.length - 1]);
373
+ }
374
+ break;
375
+ case "Escape":
376
+ case "Tab":
377
+ setIsOpen(false);
378
+ break;
379
+ default:
380
+ break;
381
+ }
382
+ };
383
+
384
+ const isTriggerSearchActive = searchable && searchPosition === "trigger";
385
+
386
+ return (
387
+ <div
388
+ ref={containerRef}
389
+ className={cn("relative inline-block", width, className)}
390
+ >
391
+ <div
392
+ ref={refs.setReference}
393
+ onClick={() => {
394
+ if (!disabled && !loading) {
395
+ setIsOpen(prev => !prev);
396
+ if (!isOpen && isTriggerSearchActive) {
397
+ setTimeout(() => inputRef.current?.focus(), 50);
398
+ }
399
+ }
400
+ }}
401
+ onKeyDown={handleKeyDown}
402
+ tabIndex={disabled || loading || isTriggerSearchActive ? -1 : 0}
403
+ className={cn(
404
+ "flex items-center justify-between rounded-lg border-[1.5px] border-black text-sm select-none cursor-pointer outline-none transition-all duration-150 min-h-[36px] w-full",
405
+ "bg-white text-black hover:border-gray-800 focus-within:border-sky-500 focus-within:ring-4 focus-within:ring-sky-500/10",
406
+ error &&
407
+ "border-red-500 focus-within:border-red-500 focus-within:ring-red-500/10",
408
+ (disabled || loading) &&
409
+ "opacity-50 pointer-events-none cursor-not-allowed border-gray-300",
410
+ )}
411
+ >
412
+ {/* Left Prefix Icon Area */}
413
+ {prefixIcon &&
414
+ !(
415
+ displayMode === "chips" &&
416
+ selectedOptions.length > 0 &&
417
+ !loading
418
+ ) && (
419
+ <div className="flex items-center pl-3 text-black shrink-0 self-stretch min-h-[36px]">
420
+ {prefixIcon}
421
+ </div>
422
+ )}
423
+
424
+ {/* Central Content Area */}
425
+ <div
426
+ className={cn(
427
+ "flex min-w-0 flex-1 px-3 py-1.5 items-center",
428
+ !loading && displayMode === "chips" && selectedOptions.length > 0
429
+ ? "flex-col gap-1.5 items-start"
430
+ : "flex-row gap-2",
431
+ )}
432
+ >
433
+ {loading ? (
434
+ <span className="text-black/50 animate-pulse flex-1 text-left text-sm font-medium">
435
+ Loading...
436
+ </span>
437
+ ) : (
438
+ <>
439
+ {/* Header container for icon & count */}
440
+ {selectedOptions.length > 0 && displayMode === "chips" && (
441
+ <div className="flex items-center justify-between w-full select-none text-black">
442
+ <div className="flex items-center gap-1.5 shrink-0">
443
+ {prefixIcon}
444
+ <span className="text-black/50 text-xs font-semibold">
445
+ selected ({selectedOptions.length})
446
+ </span>
447
+ </div>
448
+ <button
449
+ onClick={e => {
450
+ e.stopPropagation();
451
+ if (controlledValue === undefined) {
452
+ setLocalValue([]);
453
+ }
454
+ onChange?.([], []);
455
+ setSearchQuery("");
456
+ }}
457
+ className="shrink-0 text-[10px] font-semibold text-black bg-black/10 rounded px-1.5 py-0.5 outline-none transition-all duration-150 active:scale-95 cursor-pointer"
458
+ tabIndex={-1}
459
+ >
460
+ Clear Selection
461
+ </button>
462
+ </div>
463
+ )}
464
+
465
+ {/* Chips Display Mode */}
466
+ {displayMode === "chips" && selectedOptions.length > 0 ? (
467
+ <div
468
+ className="flex flex-wrap gap-1 flex-1 w-full items-center max-h-[82px] overflow-y-auto pr-1"
469
+ onClick={e => {
470
+ if (isOpen) {
471
+ e.stopPropagation();
472
+ }
473
+ }}
474
+ >
475
+ {selectedOptions.map(opt => (
476
+ <div
477
+ key={opt.key}
478
+ className="flex items-center justify-between gap-1 w-[120px] bg-gray-155 bg-sky-500/10 text-sky-500 px-2 py-0.5 rounded-md text-xs shrink-0 font-medium"
479
+ >
480
+ <TruncatedTooltip
481
+ content={opt.label}
482
+ enabled={showTooltip}
483
+ >
484
+ <span className="truncate block text-left flex-1 min-w-0 pr-1">
485
+ {opt.label}
486
+ </span>
487
+ </TruncatedTooltip>
488
+ <button
489
+ onClick={e => removeOptionKey(opt.key, e)}
490
+ className="text-sky-500 hover:text-red-500 shrink-0 outline-none cursor-pointer"
491
+ tabIndex={-1}
492
+ >
493
+ <X size={10} />
494
+ </button>
495
+ </div>
496
+ ))}
497
+
498
+ {isTriggerSearchActive && isOpen && (
499
+ <input
500
+ ref={inputRef}
501
+ type="text"
502
+ className="bg-transparent border-none text-black placeholder:text-gray-400 outline-none text-left p-0 text-xs flex-1 min-w-[60px] h-5 font-medium"
503
+ placeholder={
504
+ selectedOptions.length === 0
505
+ ? options.length === 0
506
+ ? "No options available"
507
+ : placeholder
508
+ : ""
509
+ }
510
+ value={searchQuery}
511
+ onChange={e => {
512
+ setSearchQuery(e.target.value);
513
+ setIsOpen(true);
514
+ }}
515
+ onClick={e => {
516
+ e.stopPropagation();
517
+ setIsOpen(true);
518
+ }}
519
+ />
520
+ )}
521
+ </div>
522
+ ) : (
523
+ <>
524
+ {isTriggerSearchActive ? (
525
+ <input
526
+ ref={inputRef}
527
+ type="text"
528
+ className="w-full bg-transparent border-none text-black placeholder:text-gray-400 outline-none text-left p-0 text-sm font-medium"
529
+ placeholder={
530
+ selectedOptions.length > 0
531
+ ? `${selectedOptions.length} selected`
532
+ : options.length === 0
533
+ ? "No options available"
534
+ : placeholder
535
+ }
536
+ value={
537
+ isOpen
538
+ ? searchQuery
539
+ : selectedOptions.length > 0
540
+ ? `${selectedOptions.length} selected`
541
+ : ""
542
+ }
543
+ onChange={e => {
544
+ setSearchQuery(e.target.value);
545
+ setIsOpen(true);
546
+ }}
547
+ onClick={e => {
548
+ e.stopPropagation();
549
+ setIsOpen(true);
550
+ }}
551
+ />
552
+ ) : (
553
+ <TruncatedTooltip
554
+ content={selectedOptions.map(o => o.label).join(", ")}
555
+ enabled={showTooltip && selectedOptions.length > 0}
556
+ >
557
+ <span className="truncate text-black flex-1 text-left font-medium">
558
+ {selectedOptions.length > 0
559
+ ? `${selectedOptions.length} selected`
560
+ : options.length === 0
561
+ ? "No options available"
562
+ : placeholder}
563
+ </span>
564
+ </TruncatedTooltip>
565
+ )}
566
+ </>
567
+ )}
568
+ </>
569
+ )}
570
+ </div>
571
+
572
+ {/* Right Caret Icon Area */}
573
+ <div className="flex items-center pr-3 pl-1 text-black shrink-0 self-stretch min-h-[36px]">
574
+ {loading ? (
575
+ <Loader2 size={16} className="animate-spin text-sky-500" />
576
+ ) : (
577
+ <ChevronDown
578
+ size={16}
579
+ className={cn(
580
+ "transition-transform duration-200",
581
+ isOpen && "rotate-180",
582
+ )}
583
+ />
584
+ )}
585
+ </div>
586
+ </div>
587
+
588
+ {/* Portal Dropdown Menu */}
589
+ {isOpen && (
590
+ <FloatingPortal>
591
+ <div
592
+ ref={refs.setFloating}
593
+ style={floatingStyles}
594
+ onKeyDown={handleKeyDown}
595
+ className="z-[9999] overflow-hidden rounded-lg border-[1.5px] border-black bg-white p-1 shadow-2xl flex flex-col"
596
+ >
597
+ {/* Search Position: Dropdown layout input header */}
598
+ {searchable && searchPosition === "dropdown" && (
599
+ <div className="flex items-center gap-2 px-2 py-1.5 border-b border-gray-200 mb-1 select-none">
600
+ <Search size={14} className="text-black shrink-0" />
601
+ <input
602
+ ref={dropdownInputRef}
603
+ type="text"
604
+ placeholder="Search options..."
605
+ className="w-full bg-transparent border-none text-xs text-black placeholder:text-gray-400 outline-none p-0 flex-1 font-medium"
606
+ value={searchQuery}
607
+ onChange={e => setSearchQuery(e.target.value)}
608
+ onClick={e => e.stopPropagation()}
609
+ />
610
+ {searchQuery && (
611
+ <button
612
+ onClick={e => {
613
+ e.stopPropagation();
614
+ setSearchQuery("");
615
+ dropdownInputRef.current?.focus();
616
+ }}
617
+ className="text-gray-500 hover:text-black p-0.5 rounded outline-none shrink-0"
618
+ >
619
+ <X size={12} />
620
+ </button>
621
+ )}
622
+ </div>
623
+ )}
624
+
625
+ <div
626
+ ref={listRef}
627
+ className="max-h-60 overflow-y-auto flex flex-col gap-0.5 no-scrollbar"
628
+ >
629
+ {options.length === 0 ? (
630
+ <div className="px-3 py-2 text-gray-500 text-xs text-left select-none">
631
+ No options available
632
+ </div>
633
+ ) : filteredOptions.length === 0 ? (
634
+ <div className="px-3 py-2 text-gray-500 text-xs text-left select-none">
635
+ No results found
636
+ </div>
637
+ ) : (
638
+ filteredOptions.map((item, index) => {
639
+ if (item.isHeader) {
640
+ return (
641
+ <div
642
+ key={item.id}
643
+ className="px-3 py-1.5 text-[10px] font-bold text-black tracking-wider uppercase text-left select-none pointer-events-none mt-1 border-t border-gray-200/50 first:mt-0 first:border-none"
644
+ >
645
+ {item.label}
646
+ </div>
647
+ );
648
+ }
649
+
650
+ const isSelected = activeValue.includes(item.key);
651
+ const isHighlighted = index === highlightedIndex;
652
+
653
+ return (
654
+ <div
655
+ key={item.id}
656
+ onClick={() => toggleOption(item)}
657
+ className={cn(
658
+ "flex items-center justify-between gap-3 px-3 py-2 rounded-md text-sm select-none cursor-pointer transition-colors duration-75",
659
+ isSelected &&
660
+ "text-sky-600 font-semibold bg-sky-500/10",
661
+ isHighlighted &&
662
+ !isSelected &&
663
+ "bg-black/10 text-black",
664
+ !isHighlighted &&
665
+ !isSelected &&
666
+ "text-black hover:bg-gray-50",
667
+ )}
668
+ >
669
+ <span className="flex-1 whitespace-normal break-words text-left">
670
+ {item.label}
671
+ </span>
672
+
673
+ {/* Check icon for selected value */}
674
+ {isSelected && (
675
+ <Check size={14} className="text-sky-600 shrink-0" />
676
+ )}
677
+ </div>
678
+ );
679
+ })
680
+ )}
681
+ </div>
682
+ </div>
683
+ </FloatingPortal>
684
+ )}
685
+ </div>
686
+ );
687
+ }