@turtleclub/ui 0.7.0-beta.3 → 0.7.0-beta.30
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.
- package/.turbo/turbo-build.log +143 -132
- package/CHANGELOG.md +152 -0
- package/dist/index.cjs +76 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +36068 -17674
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/components/charts/area-chart.d.ts +108 -0
- package/dist/types/components/charts/area-chart.d.ts.map +1 -0
- package/dist/types/components/charts/bar-chart.d.ts +110 -0
- package/dist/types/components/charts/bar-chart.d.ts.map +1 -0
- package/dist/types/components/charts/index.d.ts +5 -0
- package/dist/types/components/charts/index.d.ts.map +1 -0
- package/dist/types/components/charts/pie-chart.d.ts +94 -0
- package/dist/types/components/charts/pie-chart.d.ts.map +1 -0
- package/dist/types/components/charts/radial-chart.d.ts +151 -0
- package/dist/types/components/charts/radial-chart.d.ts.map +1 -0
- package/dist/types/components/features/data-table/data-table.d.ts +7 -4
- package/dist/types/components/features/data-table/data-table.d.ts.map +1 -1
- package/dist/types/components/features/data-table/sort-dropdown.d.ts.map +1 -1
- package/dist/types/components/features/search-bar.d.ts +1 -0
- package/dist/types/components/features/search-bar.d.ts.map +1 -1
- package/dist/types/components/molecules/swap-input.d.ts +3 -0
- package/dist/types/components/molecules/swap-input.d.ts.map +1 -1
- package/dist/types/components/molecules/token-selector.d.ts +2 -1
- package/dist/types/components/molecules/token-selector.d.ts.map +1 -1
- package/dist/types/components/ui/avatar.d.ts +2 -2
- package/dist/types/components/ui/avatar.d.ts.map +1 -1
- package/dist/types/components/ui/chart.d.ts +18 -4
- package/dist/types/components/ui/chart.d.ts.map +1 -1
- package/dist/types/components/ui/combobox.d.ts +21 -0
- package/dist/types/components/ui/combobox.d.ts.map +1 -1
- package/dist/types/components/ui/dialog.d.ts.map +1 -1
- package/dist/types/components/ui/dropdown.d.ts +2 -1
- package/dist/types/components/ui/dropdown.d.ts.map +1 -1
- package/dist/types/components/ui/index.d.ts +1 -0
- package/dist/types/components/ui/index.d.ts.map +1 -1
- package/dist/types/components/ui/multi-select.d.ts.map +1 -1
- package/dist/types/components/ui/segment-control.d.ts +1 -0
- package/dist/types/components/ui/segment-control.d.ts.map +1 -1
- package/dist/types/components/ui/slider.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/charts/QUICK_REFERENCE.md +323 -0
- package/src/components/charts/README.md +658 -0
- package/src/components/charts/RECHARTS_FEATURES.md +458 -0
- package/src/components/charts/area-chart.tsx +248 -0
- package/src/components/charts/bar-chart.tsx +362 -0
- package/src/components/charts/index.ts +4 -0
- package/src/components/charts/pie-chart.tsx +277 -0
- package/src/components/charts/radial-chart.tsx +312 -0
- package/src/components/features/data-table/data-table.tsx +136 -125
- package/src/components/features/data-table/sort-dropdown.tsx +8 -11
- package/src/components/features/search-bar.tsx +6 -1
- package/src/components/molecules/swap-input.tsx +44 -30
- package/src/components/molecules/token-selector.tsx +10 -1
- package/src/components/ui/avatar.tsx +8 -15
- package/src/components/ui/chart.tsx +100 -109
- package/src/components/ui/combobox.tsx +150 -137
- package/src/components/ui/dialog.tsx +9 -23
- package/src/components/ui/dropdown.tsx +3 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/multi-select.tsx +325 -307
- package/src/components/ui/segment-control.tsx +7 -2
- package/src/components/ui/slider.tsx +6 -11
- package/src/index.ts +1 -0
- package/src/styles/globals.css +4 -0
- package/src/styles/themes/semantic.css +26 -56
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { CheckIcon, XIcon, ChevronDown, WandSparkles } from "lucide-react";
|
|
2
|
+
import { CheckIcon, XIcon, ChevronDown, ChevronRight, WandSparkles } from "lucide-react";
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
4
|
import { Separator } from "@/components/ui/separator";
|
|
5
|
-
import {
|
|
5
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
6
6
|
import { Badge } from "@/components/ui/badge";
|
|
7
|
-
import {
|
|
8
|
-
Popover,
|
|
9
|
-
PopoverContent,
|
|
10
|
-
PopoverTrigger,
|
|
11
|
-
} from "@/components/ui/popover";
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
12
8
|
import {
|
|
13
9
|
Command,
|
|
14
10
|
CommandEmpty,
|
|
@@ -48,10 +44,7 @@ interface MultiSelectGroup {
|
|
|
48
44
|
* Props for MultiSelect component
|
|
49
45
|
*/
|
|
50
46
|
interface MultiSelectProps
|
|
51
|
-
extends Omit<
|
|
52
|
-
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
53
|
-
"animationConfig"
|
|
54
|
-
> {
|
|
47
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "animationConfig"> {
|
|
55
48
|
/**
|
|
56
49
|
* An array of option objects or groups to be displayed in the multi-select component.
|
|
57
50
|
*/
|
|
@@ -263,13 +256,13 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
263
256
|
closeOnSelect = false,
|
|
264
257
|
...props
|
|
265
258
|
},
|
|
266
|
-
ref
|
|
259
|
+
ref
|
|
267
260
|
) => {
|
|
268
|
-
const [selectedValues, setSelectedValues] =
|
|
269
|
-
React.useState<string[]>(defaultValue);
|
|
261
|
+
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);
|
|
270
262
|
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
|
271
263
|
const [isAnimating, setIsAnimating] = React.useState(false);
|
|
272
264
|
const [searchValue, setSearchValue] = React.useState("");
|
|
265
|
+
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
|
|
273
266
|
|
|
274
267
|
const [politeMessage, setPoliteMessage] = React.useState("");
|
|
275
268
|
const [assertiveMessage, setAssertiveMessage] = React.useState("");
|
|
@@ -287,7 +280,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
287
280
|
setTimeout(() => setPoliteMessage(""), 100);
|
|
288
281
|
}
|
|
289
282
|
},
|
|
290
|
-
[]
|
|
283
|
+
[]
|
|
291
284
|
);
|
|
292
285
|
|
|
293
286
|
const multiSelectId = React.useId();
|
|
@@ -298,23 +291,18 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
298
291
|
const prevDefaultValueRef = React.useRef<string[]>(defaultValue);
|
|
299
292
|
|
|
300
293
|
const isGroupedOptions = React.useCallback(
|
|
301
|
-
(
|
|
302
|
-
opts: MultiSelectOption[] | MultiSelectGroup[],
|
|
303
|
-
): opts is MultiSelectGroup[] => {
|
|
294
|
+
(opts: MultiSelectOption[] | MultiSelectGroup[]): opts is MultiSelectGroup[] => {
|
|
304
295
|
return opts.length > 0 && "heading" in opts[0];
|
|
305
296
|
},
|
|
306
|
-
[]
|
|
297
|
+
[]
|
|
307
298
|
);
|
|
308
299
|
|
|
309
|
-
const arraysEqual = React.useCallback(
|
|
310
|
-
(a
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
},
|
|
316
|
-
[],
|
|
317
|
-
);
|
|
300
|
+
const arraysEqual = React.useCallback((a: string[], b: string[]): boolean => {
|
|
301
|
+
if (a.length !== b.length) return false;
|
|
302
|
+
const sortedA = [...a].sort();
|
|
303
|
+
const sortedB = [...b].sort();
|
|
304
|
+
return sortedA.every((val, index) => val === sortedB[index]);
|
|
305
|
+
}, []);
|
|
318
306
|
|
|
319
307
|
const resetToDefault = React.useCallback(() => {
|
|
320
308
|
setSelectedValues(defaultValue);
|
|
@@ -354,12 +342,10 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
354
342
|
}
|
|
355
343
|
},
|
|
356
344
|
}),
|
|
357
|
-
[resetToDefault, selectedValues, onValueChange]
|
|
345
|
+
[resetToDefault, selectedValues, onValueChange]
|
|
358
346
|
);
|
|
359
347
|
|
|
360
|
-
const [screenSize, setScreenSize] = React.useState<
|
|
361
|
-
"mobile" | "tablet" | "desktop"
|
|
362
|
-
>("desktop");
|
|
348
|
+
const [screenSize, setScreenSize] = React.useState<"mobile" | "tablet" | "desktop">("desktop");
|
|
363
349
|
|
|
364
350
|
React.useEffect(() => {
|
|
365
351
|
if (typeof window === "undefined") return;
|
|
@@ -436,16 +422,14 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
436
422
|
}
|
|
437
423
|
});
|
|
438
424
|
if (process.env.NODE_ENV === "development" && duplicates.length > 0) {
|
|
439
|
-
const action = deduplicateOptions
|
|
440
|
-
? "automatically removed"
|
|
441
|
-
: "detected";
|
|
425
|
+
const action = deduplicateOptions ? "automatically removed" : "detected";
|
|
442
426
|
console.warn(
|
|
443
427
|
`MultiSelect: Duplicate option values ${action}: ${duplicates.join(", ")}. ` +
|
|
444
428
|
`${
|
|
445
429
|
deduplicateOptions
|
|
446
430
|
? "Duplicates have been removed automatically."
|
|
447
431
|
: "This may cause unexpected behavior. Consider setting 'deduplicateOptions={true}' or ensure all option values are unique."
|
|
448
|
-
}
|
|
432
|
+
}`
|
|
449
433
|
);
|
|
450
434
|
}
|
|
451
435
|
return deduplicateOptions ? uniqueOptions : allOptions;
|
|
@@ -455,13 +439,11 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
455
439
|
(value: string): MultiSelectOption | undefined => {
|
|
456
440
|
const option = getAllOptions().find((option) => option.value === value);
|
|
457
441
|
if (!option && process.env.NODE_ENV === "development") {
|
|
458
|
-
console.warn(
|
|
459
|
-
`MultiSelect: Option with value "${value}" not found in options list`,
|
|
460
|
-
);
|
|
442
|
+
console.warn(`MultiSelect: Option with value "${value}" not found in options list`);
|
|
461
443
|
}
|
|
462
444
|
return option;
|
|
463
445
|
},
|
|
464
|
-
[getAllOptions]
|
|
446
|
+
[getAllOptions]
|
|
465
447
|
);
|
|
466
448
|
|
|
467
449
|
const filteredOptions = React.useMemo(() => {
|
|
@@ -473,10 +455,8 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
473
455
|
...group,
|
|
474
456
|
options: group.options.filter(
|
|
475
457
|
(option) =>
|
|
476
|
-
option.label
|
|
477
|
-
|
|
478
|
-
.includes(searchValue.toLowerCase()) ||
|
|
479
|
-
option.value.toLowerCase().includes(searchValue.toLowerCase()),
|
|
458
|
+
option.label.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
459
|
+
option.value.toLowerCase().includes(searchValue.toLowerCase())
|
|
480
460
|
),
|
|
481
461
|
}))
|
|
482
462
|
.filter((group) => group.options.length > 0);
|
|
@@ -484,13 +464,11 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
484
464
|
return options.filter(
|
|
485
465
|
(option) =>
|
|
486
466
|
option.label.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
487
|
-
option.value.toLowerCase().includes(searchValue.toLowerCase())
|
|
467
|
+
option.value.toLowerCase().includes(searchValue.toLowerCase())
|
|
488
468
|
);
|
|
489
469
|
}, [options, searchValue, searchable, isGroupedOptions]);
|
|
490
470
|
|
|
491
|
-
const handleInputKeyDown = (
|
|
492
|
-
event: React.KeyboardEvent<HTMLInputElement>,
|
|
493
|
-
) => {
|
|
471
|
+
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
494
472
|
if (event.key === "Enter") {
|
|
495
473
|
setIsPopoverOpen(true);
|
|
496
474
|
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
|
@@ -528,10 +506,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
528
506
|
|
|
529
507
|
const clearExtraOptions = () => {
|
|
530
508
|
if (disabled) return;
|
|
531
|
-
const newSelectedValues = selectedValues.slice(
|
|
532
|
-
0,
|
|
533
|
-
responsiveSettings.maxCount,
|
|
534
|
-
);
|
|
509
|
+
const newSelectedValues = selectedValues.slice(0, responsiveSettings.maxCount);
|
|
535
510
|
setSelectedValues(newSelectedValues);
|
|
536
511
|
onValueChange(newSelectedValues);
|
|
537
512
|
};
|
|
@@ -552,6 +527,42 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
552
527
|
}
|
|
553
528
|
};
|
|
554
529
|
|
|
530
|
+
const toggleGroup = (groupHeading: string) => {
|
|
531
|
+
setCollapsedGroups((prev) => {
|
|
532
|
+
const newSet = new Set(prev);
|
|
533
|
+
if (newSet.has(groupHeading)) {
|
|
534
|
+
newSet.delete(groupHeading);
|
|
535
|
+
} else {
|
|
536
|
+
newSet.add(groupHeading);
|
|
537
|
+
}
|
|
538
|
+
return newSet;
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const toggleGroupSelectAll = (groupOptions: MultiSelectOption[]) => {
|
|
543
|
+
if (disabled) return;
|
|
544
|
+
const selectableOptions = groupOptions.filter((opt) => !opt.disabled);
|
|
545
|
+
const groupValues = selectableOptions.map((opt) => opt.value);
|
|
546
|
+
const allGroupSelected = groupValues.every((val) => selectedValues.includes(val));
|
|
547
|
+
|
|
548
|
+
let newSelectedValues: string[];
|
|
549
|
+
if (allGroupSelected) {
|
|
550
|
+
// Deselect all in this group
|
|
551
|
+
newSelectedValues = selectedValues.filter((val) => !groupValues.includes(val));
|
|
552
|
+
} else {
|
|
553
|
+
// Select all in this group
|
|
554
|
+
const valuesToAdd = groupValues.filter((val) => !selectedValues.includes(val));
|
|
555
|
+
newSelectedValues = [...selectedValues, ...valuesToAdd];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
setSelectedValues(newSelectedValues);
|
|
559
|
+
onValueChange(newSelectedValues);
|
|
560
|
+
|
|
561
|
+
if (closeOnSelect) {
|
|
562
|
+
setIsPopoverOpen(false);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
555
566
|
React.useEffect(() => {
|
|
556
567
|
if (!resetOnDefaultValueChange) return;
|
|
557
568
|
const prevDefaultValue = prevDefaultValueRef.current;
|
|
@@ -591,24 +602,20 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
591
602
|
if (diff > 0) {
|
|
592
603
|
const addedItems = selectedValues.slice(-diff);
|
|
593
604
|
const addedLabels = addedItems
|
|
594
|
-
.map(
|
|
595
|
-
(value) => allOptions.find((opt) => opt.value === value)?.label,
|
|
596
|
-
)
|
|
605
|
+
.map((value) => allOptions.find((opt) => opt.value === value)?.label)
|
|
597
606
|
.filter(Boolean);
|
|
598
607
|
|
|
599
608
|
if (addedLabels.length === 1) {
|
|
600
609
|
announce(
|
|
601
|
-
`${addedLabels[0]} selected. ${selectedCount} of ${totalOptions} options selected
|
|
610
|
+
`${addedLabels[0]} selected. ${selectedCount} of ${totalOptions} options selected.`
|
|
602
611
|
);
|
|
603
612
|
} else {
|
|
604
613
|
announce(
|
|
605
|
-
`${addedLabels.length} options selected. ${selectedCount} of ${totalOptions} total selected
|
|
614
|
+
`${addedLabels.length} options selected. ${selectedCount} of ${totalOptions} total selected.`
|
|
606
615
|
);
|
|
607
616
|
}
|
|
608
617
|
} else if (diff < 0) {
|
|
609
|
-
announce(
|
|
610
|
-
`Option removed. ${selectedCount} of ${totalOptions} options selected.`,
|
|
611
|
-
);
|
|
618
|
+
announce(`Option removed. ${selectedCount} of ${totalOptions} options selected.`);
|
|
612
619
|
}
|
|
613
620
|
prevSelectedCount.current = selectedCount;
|
|
614
621
|
}
|
|
@@ -616,7 +623,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
616
623
|
if (isPopoverOpen !== prevIsOpen.current) {
|
|
617
624
|
if (isPopoverOpen) {
|
|
618
625
|
announce(
|
|
619
|
-
`Dropdown opened. ${totalOptions} options available. Use arrow keys to navigate
|
|
626
|
+
`Dropdown opened. ${totalOptions} options available. Use arrow keys to navigate.`
|
|
620
627
|
);
|
|
621
628
|
} else {
|
|
622
629
|
announce("Dropdown closed.");
|
|
@@ -624,19 +631,16 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
624
631
|
prevIsOpen.current = isPopoverOpen;
|
|
625
632
|
}
|
|
626
633
|
|
|
627
|
-
if (
|
|
628
|
-
searchValue !== prevSearchValue.current &&
|
|
629
|
-
searchValue !== undefined
|
|
630
|
-
) {
|
|
634
|
+
if (searchValue !== prevSearchValue.current && searchValue !== undefined) {
|
|
631
635
|
if (searchValue && isPopoverOpen) {
|
|
632
636
|
const filteredCount = allOptions.filter(
|
|
633
637
|
(opt) =>
|
|
634
638
|
opt.label.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
635
|
-
opt.value.toLowerCase().includes(searchValue.toLowerCase())
|
|
639
|
+
opt.value.toLowerCase().includes(searchValue.toLowerCase())
|
|
636
640
|
).length;
|
|
637
641
|
|
|
638
642
|
announce(
|
|
639
|
-
`${filteredCount} option${filteredCount === 1 ? "" : "s"} found for "${searchValue}"
|
|
643
|
+
`${filteredCount} option${filteredCount === 1 ? "" : "s"} found for "${searchValue}"`
|
|
640
644
|
);
|
|
641
645
|
}
|
|
642
646
|
prevSearchValue.current = searchValue;
|
|
@@ -654,14 +658,9 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
654
658
|
</div>
|
|
655
659
|
</div>
|
|
656
660
|
|
|
657
|
-
<Popover
|
|
658
|
-
open={isPopoverOpen}
|
|
659
|
-
onOpenChange={setIsPopoverOpen}
|
|
660
|
-
modal={modalPopover}
|
|
661
|
-
>
|
|
661
|
+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
|
|
662
662
|
<div id={triggerDescriptionId} className="sr-only">
|
|
663
|
-
Multi-select dropdown. Use arrow keys to navigate, Enter to select,
|
|
664
|
-
and Escape to close.
|
|
663
|
+
Multi-select dropdown. Use arrow keys to navigate, Enter to select, and Escape to close.
|
|
665
664
|
</div>
|
|
666
665
|
<div id={selectedCountId} className="sr-only" aria-live="polite">
|
|
667
666
|
{selectedValues.length === 0
|
|
@@ -674,184 +673,168 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
674
673
|
.join(", ")}`}
|
|
675
674
|
</div>
|
|
676
675
|
|
|
677
|
-
<PopoverTrigger
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
{
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
: {}
|
|
722
|
-
}
|
|
723
|
-
>
|
|
724
|
-
{selectedValues
|
|
725
|
-
.slice(0, responsiveSettings.maxCount)
|
|
726
|
-
.map((value) => {
|
|
727
|
-
const option = getOptionByValue(value);
|
|
728
|
-
const IconComponent = option?.icon;
|
|
729
|
-
if (!option) {
|
|
730
|
-
return null;
|
|
676
|
+
<PopoverTrigger
|
|
677
|
+
ref={buttonRef}
|
|
678
|
+
{...props}
|
|
679
|
+
onClick={handleTogglePopover}
|
|
680
|
+
disabled={disabled}
|
|
681
|
+
role="combobox"
|
|
682
|
+
aria-expanded={isPopoverOpen}
|
|
683
|
+
aria-haspopup="listbox"
|
|
684
|
+
aria-controls={isPopoverOpen ? listboxId : undefined}
|
|
685
|
+
aria-describedby={`${triggerDescriptionId} ${selectedCountId}`}
|
|
686
|
+
aria-label={`Multi-select: ${selectedValues.length} of ${
|
|
687
|
+
getAllOptions().length
|
|
688
|
+
} options selected. ${placeholder}`}
|
|
689
|
+
className={cn(
|
|
690
|
+
buttonVariants({
|
|
691
|
+
variant: "default",
|
|
692
|
+
size: "default",
|
|
693
|
+
border: "plain",
|
|
694
|
+
}),
|
|
695
|
+
"flex h-10 items-center justify-between [&_svg]:pointer-events-auto",
|
|
696
|
+
"!bg-neutral-alpha-2 text-base !font-normal shadow md:text-sm",
|
|
697
|
+
autoSize ? "w-auto" : "w-full",
|
|
698
|
+
responsiveSettings.compactMode && "h-8 text-sm",
|
|
699
|
+
screenSize === "mobile" && "h-12 text-base",
|
|
700
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
701
|
+
className
|
|
702
|
+
)}
|
|
703
|
+
style={{
|
|
704
|
+
...widthConstraints,
|
|
705
|
+
maxWidth: `min(${widthConstraints.maxWidth}, 100%)`,
|
|
706
|
+
}}
|
|
707
|
+
>
|
|
708
|
+
{selectedValues.length > 0 ? (
|
|
709
|
+
<div className="flex w-full items-center justify-between">
|
|
710
|
+
<div
|
|
711
|
+
className={cn(
|
|
712
|
+
"flex items-center gap-1",
|
|
713
|
+
singleLine ? "multiselect-singleline-scroll overflow-x-auto" : "flex-wrap",
|
|
714
|
+
responsiveSettings.compactMode && "gap-0.5"
|
|
715
|
+
)}
|
|
716
|
+
style={
|
|
717
|
+
singleLine
|
|
718
|
+
? {
|
|
719
|
+
paddingBottom: "4px",
|
|
731
720
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
721
|
+
: {}
|
|
722
|
+
}
|
|
723
|
+
>
|
|
724
|
+
{selectedValues
|
|
725
|
+
.slice(0, responsiveSettings.maxCount)
|
|
726
|
+
.map((value) => {
|
|
727
|
+
const option = getOptionByValue(value);
|
|
728
|
+
const IconComponent = option?.icon;
|
|
729
|
+
if (!option) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const badgeStyle: React.CSSProperties = {
|
|
733
|
+
animationDuration: `${animation}s`,
|
|
734
|
+
};
|
|
735
|
+
return (
|
|
736
|
+
<Badge
|
|
737
|
+
key={value}
|
|
738
|
+
className={cn(
|
|
739
|
+
"!pr-1.5 pl-3.5",
|
|
740
|
+
responsiveSettings.compactMode && "px-1.5 py-0.5 text-xs",
|
|
741
|
+
screenSize === "mobile" && "max-w-[120px] truncate",
|
|
742
|
+
singleLine && "flex-shrink-0 whitespace-nowrap",
|
|
743
|
+
"[&>svg]:pointer-events-auto"
|
|
744
|
+
)}
|
|
745
|
+
style={badgeStyle}
|
|
746
|
+
>
|
|
747
|
+
{IconComponent && !responsiveSettings.hideIcons && (
|
|
748
|
+
<IconComponent
|
|
759
749
|
className={cn(
|
|
760
|
-
|
|
750
|
+
"mr-2 size-3.5",
|
|
751
|
+
responsiveSettings.compactMode && "mr-1 h-3 w-3"
|
|
761
752
|
)}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
753
|
+
/>
|
|
754
|
+
)}
|
|
755
|
+
<span className={cn(screenSize === "mobile" && "truncate")}>
|
|
756
|
+
{option.label}
|
|
757
|
+
</span>
|
|
758
|
+
<div
|
|
759
|
+
role="button"
|
|
760
|
+
tabIndex={0}
|
|
761
|
+
onClick={(event) => {
|
|
762
|
+
event.stopPropagation();
|
|
763
|
+
toggleOption(value);
|
|
764
|
+
}}
|
|
765
|
+
onKeyDown={(event) => {
|
|
766
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
767
|
+
event.preventDefault();
|
|
769
768
|
event.stopPropagation();
|
|
770
769
|
toggleOption(value);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
<
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
770
|
+
}
|
|
771
|
+
}}
|
|
772
|
+
aria-label={`Remove ${option.label} from selection`}
|
|
773
|
+
className="text-muted-foreground hover:text-foreground size-4 cursor-pointer rounded-sm p-0.5 hover:bg-white/20 focus:ring-1 focus:ring-white/50 focus:outline-none"
|
|
774
|
+
>
|
|
775
|
+
<XIcon className="size-3" />
|
|
776
|
+
</div>
|
|
777
|
+
</Badge>
|
|
778
|
+
);
|
|
779
|
+
})
|
|
780
|
+
.filter(Boolean)}
|
|
781
|
+
{selectedValues.length > responsiveSettings.maxCount && (
|
|
782
|
+
<Badge
|
|
783
|
+
className={cn(
|
|
784
|
+
"!pr-1.5 pl-3.5",
|
|
785
|
+
responsiveSettings.compactMode && "px-1.5 py-0.5 text-xs",
|
|
786
|
+
singleLine && "flex-shrink-0 whitespace-nowrap",
|
|
787
|
+
"[&>svg]:pointer-events-auto"
|
|
788
|
+
)}
|
|
789
|
+
>
|
|
790
|
+
{`+ ${selectedValues.length - responsiveSettings.maxCount} more`}
|
|
791
|
+
<div
|
|
792
|
+
role="button"
|
|
793
|
+
tabIndex={0}
|
|
794
|
+
onClick={(event) => {
|
|
795
|
+
event.stopPropagation();
|
|
796
|
+
clearExtraOptions();
|
|
797
|
+
}}
|
|
798
|
+
className="text-muted-foreground hover:text-foreground size-4 cursor-pointer rounded-sm p-0.5 hover:bg-white/20 focus:ring-1 focus:ring-white/50 focus:outline-none"
|
|
800
799
|
>
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
)
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
<div
|
|
818
|
-
role="button"
|
|
819
|
-
tabIndex={0}
|
|
820
|
-
onClick={(event) => {
|
|
800
|
+
<XIcon className="size-3" />
|
|
801
|
+
</div>
|
|
802
|
+
</Badge>
|
|
803
|
+
)}
|
|
804
|
+
</div>
|
|
805
|
+
<div className="flex items-center justify-between">
|
|
806
|
+
<div
|
|
807
|
+
role="button"
|
|
808
|
+
tabIndex={0}
|
|
809
|
+
onClick={(event) => {
|
|
810
|
+
event.stopPropagation();
|
|
811
|
+
handleClear();
|
|
812
|
+
}}
|
|
813
|
+
onKeyDown={(event) => {
|
|
814
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
815
|
+
event.preventDefault();
|
|
821
816
|
event.stopPropagation();
|
|
822
817
|
handleClear();
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
}}
|
|
831
|
-
aria-label={`Clear all ${selectedValues.length} selected options`}
|
|
832
|
-
className="text-muted-foreground hover:text-foreground focus:ring-ring mx-2 flex size-3.5 cursor-pointer items-center justify-center rounded-sm focus:ring-2 focus:ring-offset-1 focus:outline-none"
|
|
833
|
-
>
|
|
834
|
-
<XIcon className="size-3.5" />
|
|
835
|
-
</div>
|
|
836
|
-
<Separator
|
|
837
|
-
orientation="vertical"
|
|
838
|
-
className="flex h-full min-h-6"
|
|
839
|
-
/>
|
|
840
|
-
<ChevronDown
|
|
841
|
-
className="text-muted-foreground mx-2 h-4 cursor-pointer"
|
|
842
|
-
aria-hidden="true"
|
|
843
|
-
/>
|
|
818
|
+
}
|
|
819
|
+
}}
|
|
820
|
+
aria-label={`Clear all ${selectedValues.length} selected options`}
|
|
821
|
+
className="text-muted-foreground hover:text-foreground focus:ring-ring mx-2 flex size-3.5 cursor-pointer items-center justify-center rounded-sm focus:ring-2 focus:ring-offset-1 focus:outline-none"
|
|
822
|
+
>
|
|
823
|
+
<XIcon className="size-3.5" />
|
|
844
824
|
</div>
|
|
825
|
+
<Separator orientation="vertical" className="flex h-full min-h-6" />
|
|
826
|
+
<ChevronDown
|
|
827
|
+
className="text-muted-foreground mx-2 h-4 cursor-pointer"
|
|
828
|
+
aria-hidden="true"
|
|
829
|
+
/>
|
|
845
830
|
</div>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
)}
|
|
854
|
-
</Button>
|
|
831
|
+
</div>
|
|
832
|
+
) : (
|
|
833
|
+
<div className="mx-auto flex w-full items-center justify-between">
|
|
834
|
+
<span className="text-muted-foreground text-sm">{placeholder}</span>
|
|
835
|
+
<ChevronDown className="text-muted-foreground mx-2 h-4 cursor-pointer" />
|
|
836
|
+
</div>
|
|
837
|
+
)}
|
|
855
838
|
</PopoverTrigger>
|
|
856
839
|
<PopoverContent
|
|
857
840
|
id={listboxId}
|
|
@@ -863,7 +846,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
863
846
|
screenSize === "mobile" && "w-[85vw] max-w-[280px]",
|
|
864
847
|
screenSize === "tablet" && "w-[70vw] max-w-md",
|
|
865
848
|
screenSize === "desktop" && "min-w-[300px]",
|
|
866
|
-
popoverClassName
|
|
849
|
+
popoverClassName
|
|
867
850
|
)}
|
|
868
851
|
style={{
|
|
869
852
|
maxWidth: `min(${widthConstraints.maxWidth}, 85vw)`,
|
|
@@ -893,12 +876,10 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
893
876
|
className={cn(
|
|
894
877
|
"multiselect-scrollbar max-h-[40vh] overflow-y-auto",
|
|
895
878
|
screenSize === "mobile" && "max-h-[50vh]",
|
|
896
|
-
"overscroll-behavior-y-contain"
|
|
879
|
+
"overscroll-behavior-y-contain"
|
|
897
880
|
)}
|
|
898
881
|
>
|
|
899
|
-
<CommandEmpty>
|
|
900
|
-
{emptyIndicator || "No results found."}
|
|
901
|
-
</CommandEmpty>{" "}
|
|
882
|
+
<CommandEmpty>{emptyIndicator || "No results found."}</CommandEmpty>{" "}
|
|
902
883
|
{!hideSelectAll && !searchValue && (
|
|
903
884
|
<CommandGroup>
|
|
904
885
|
<CommandItem
|
|
@@ -916,10 +897,9 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
916
897
|
className={cn(
|
|
917
898
|
"border-border mr-2 flex size-3.5 items-center justify-center rounded-xs border",
|
|
918
899
|
selectedValues.length ===
|
|
919
|
-
getAllOptions().filter((opt) => !opt.disabled)
|
|
920
|
-
.length
|
|
900
|
+
getAllOptions().filter((opt) => !opt.disabled).length
|
|
921
901
|
? "bg-primary"
|
|
922
|
-
: "opacity-50 [&_svg]:invisible"
|
|
902
|
+
: "opacity-50 [&_svg]:invisible"
|
|
923
903
|
)}
|
|
924
904
|
aria-hidden="true"
|
|
925
905
|
>
|
|
@@ -927,61 +907,104 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
927
907
|
</div>
|
|
928
908
|
<span>
|
|
929
909
|
(Select All
|
|
930
|
-
{getAllOptions().length > 20
|
|
931
|
-
? ` - ${getAllOptions().length} options`
|
|
932
|
-
: ""}
|
|
933
|
-
)
|
|
910
|
+
{getAllOptions().length > 20 ? ` - ${getAllOptions().length} options` : ""})
|
|
934
911
|
</span>
|
|
935
912
|
</CommandItem>
|
|
936
913
|
</CommandGroup>
|
|
937
914
|
)}
|
|
938
915
|
{isGroupedOptions(filteredOptions) ? (
|
|
939
|
-
filteredOptions.map((group) =>
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
"
|
|
957
|
-
option.disabled &&
|
|
958
|
-
"cursor-not-allowed opacity-50",
|
|
959
|
-
)}
|
|
960
|
-
disabled={option.disabled}
|
|
961
|
-
>
|
|
962
|
-
<div
|
|
963
|
-
className={cn(
|
|
964
|
-
"border-border mr-2 flex size-3.5 items-center justify-center rounded-xs border",
|
|
965
|
-
isSelected
|
|
966
|
-
? "bg-primary"
|
|
967
|
-
: "opacity-50 [&_svg]:invisible",
|
|
968
|
-
)}
|
|
969
|
-
aria-hidden="true"
|
|
916
|
+
filteredOptions.map((group) => {
|
|
917
|
+
const isCollapsed = collapsedGroups.has(group.heading);
|
|
918
|
+
const selectableGroupOptions = group.options.filter((opt) => !opt.disabled);
|
|
919
|
+
const groupValues = selectableGroupOptions.map((opt) => opt.value);
|
|
920
|
+
const allGroupSelected =
|
|
921
|
+
groupValues.length > 0 &&
|
|
922
|
+
groupValues.every((val) => selectedValues.includes(val));
|
|
923
|
+
const someGroupSelected =
|
|
924
|
+
groupValues.some((val) => selectedValues.includes(val)) && !allGroupSelected;
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
<CommandGroup key={group.heading}>
|
|
928
|
+
<div className="px-2 py-1.5">
|
|
929
|
+
<div className="flex items-center gap-2">
|
|
930
|
+
<button
|
|
931
|
+
type="button"
|
|
932
|
+
onClick={() => toggleGroup(group.heading)}
|
|
933
|
+
className="text-muted-foreground hover:text-foreground flex flex-1 items-center gap-1 text-xs font-medium transition-colors"
|
|
970
934
|
>
|
|
971
|
-
<
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
aria-hidden="true"
|
|
935
|
+
<ChevronRight
|
|
936
|
+
className={cn(
|
|
937
|
+
"h-3.5 w-3.5 transition-transform",
|
|
938
|
+
!isCollapsed && "rotate-90"
|
|
939
|
+
)}
|
|
977
940
|
/>
|
|
941
|
+
{group.heading}
|
|
942
|
+
</button>
|
|
943
|
+
{!hideSelectAll && (
|
|
944
|
+
<button
|
|
945
|
+
type="button"
|
|
946
|
+
onClick={() => toggleGroupSelectAll(group.options)}
|
|
947
|
+
className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-xs transition-colors"
|
|
948
|
+
disabled={disabled || selectableGroupOptions.length === 0}
|
|
949
|
+
>
|
|
950
|
+
<div
|
|
951
|
+
className={cn(
|
|
952
|
+
"border-border flex size-3.5 items-center justify-center rounded-xs border",
|
|
953
|
+
allGroupSelected && "bg-primary",
|
|
954
|
+
someGroupSelected && "bg-primary/50",
|
|
955
|
+
!allGroupSelected &&
|
|
956
|
+
!someGroupSelected &&
|
|
957
|
+
"opacity-50 [&_svg]:invisible"
|
|
958
|
+
)}
|
|
959
|
+
>
|
|
960
|
+
<CheckIcon className="text-primary-foreground size-3.5" />
|
|
961
|
+
</div>
|
|
962
|
+
<span className="text-[10px]">All</span>
|
|
963
|
+
</button>
|
|
978
964
|
)}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
{!isCollapsed &&
|
|
968
|
+
group.options.map((option) => {
|
|
969
|
+
const isSelected = selectedValues.includes(option.value);
|
|
970
|
+
return (
|
|
971
|
+
<CommandItem
|
|
972
|
+
key={option.value}
|
|
973
|
+
onSelect={() => toggleOption(option.value)}
|
|
974
|
+
role="option"
|
|
975
|
+
aria-selected={isSelected}
|
|
976
|
+
aria-disabled={option.disabled}
|
|
977
|
+
aria-label={`${option.label}${
|
|
978
|
+
isSelected ? ", selected" : ", not selected"
|
|
979
|
+
}${option.disabled ? ", disabled" : ""}`}
|
|
980
|
+
className={cn(
|
|
981
|
+
"cursor-pointer",
|
|
982
|
+
option.disabled && "cursor-not-allowed opacity-50"
|
|
983
|
+
)}
|
|
984
|
+
disabled={option.disabled}
|
|
985
|
+
>
|
|
986
|
+
<div
|
|
987
|
+
className={cn(
|
|
988
|
+
"border-border mr-2 flex size-3.5 items-center justify-center rounded-xs border",
|
|
989
|
+
isSelected ? "bg-primary" : "opacity-50 [&_svg]:invisible"
|
|
990
|
+
)}
|
|
991
|
+
aria-hidden="true"
|
|
992
|
+
>
|
|
993
|
+
<CheckIcon className="text-primary-foreground size-3.5" />
|
|
994
|
+
</div>
|
|
995
|
+
{option.icon && (
|
|
996
|
+
<option.icon
|
|
997
|
+
className="text-muted-foreground mr-2 size-3.5"
|
|
998
|
+
aria-hidden="true"
|
|
999
|
+
/>
|
|
1000
|
+
)}
|
|
1001
|
+
<span>{option.label}</span>
|
|
1002
|
+
</CommandItem>
|
|
1003
|
+
);
|
|
1004
|
+
})}
|
|
1005
|
+
</CommandGroup>
|
|
1006
|
+
);
|
|
1007
|
+
})
|
|
985
1008
|
) : (
|
|
986
1009
|
<CommandGroup>
|
|
987
1010
|
{filteredOptions.map((option) => {
|
|
@@ -998,16 +1021,14 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
998
1021
|
}${option.disabled ? ", disabled" : ""}`}
|
|
999
1022
|
className={cn(
|
|
1000
1023
|
"cursor-pointer",
|
|
1001
|
-
option.disabled && "cursor-not-allowed opacity-50"
|
|
1024
|
+
option.disabled && "cursor-not-allowed opacity-50"
|
|
1002
1025
|
)}
|
|
1003
1026
|
disabled={option.disabled}
|
|
1004
1027
|
>
|
|
1005
1028
|
<div
|
|
1006
1029
|
className={cn(
|
|
1007
1030
|
"border-border mr-2 flex size-3.5 items-center justify-center rounded-xs border",
|
|
1008
|
-
isSelected
|
|
1009
|
-
? "bg-primary"
|
|
1010
|
-
: "opacity-50 [&_svg]:invisible",
|
|
1031
|
+
isSelected ? "bg-primary" : "opacity-50 [&_svg]:invisible"
|
|
1011
1032
|
)}
|
|
1012
1033
|
aria-hidden="true"
|
|
1013
1034
|
>
|
|
@@ -1036,10 +1057,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
1036
1057
|
>
|
|
1037
1058
|
Clear
|
|
1038
1059
|
</CommandItem>
|
|
1039
|
-
<Separator
|
|
1040
|
-
orientation="vertical"
|
|
1041
|
-
className="mx-1 flex h-full min-h-6"
|
|
1042
|
-
/>
|
|
1060
|
+
<Separator orientation="vertical" className="mx-1 flex h-full min-h-6" />
|
|
1043
1061
|
</>
|
|
1044
1062
|
)}
|
|
1045
1063
|
<CommandItem
|
|
@@ -1057,7 +1075,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
1057
1075
|
<WandSparkles
|
|
1058
1076
|
className={cn(
|
|
1059
1077
|
"text-foreground bg-background my-2 h-3 w-3 cursor-pointer",
|
|
1060
|
-
isAnimating ? "" : "text-muted-foreground"
|
|
1078
|
+
isAnimating ? "" : "text-muted-foreground"
|
|
1061
1079
|
)}
|
|
1062
1080
|
onClick={() => setIsAnimating(!isAnimating)}
|
|
1063
1081
|
/>
|
|
@@ -1065,7 +1083,7 @@ export const MultiSelect = React.forwardRef<MultiSelectRef, MultiSelectProps>(
|
|
|
1065
1083
|
</Popover>
|
|
1066
1084
|
</>
|
|
1067
1085
|
);
|
|
1068
|
-
}
|
|
1086
|
+
}
|
|
1069
1087
|
);
|
|
1070
1088
|
|
|
1071
1089
|
MultiSelect.displayName = "MultiSelect";
|