@altimateai/ui-components 0.0.49 → 0.0.51-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { Meta, StoryFn } from "@storybook/react";
2
2
  import { Combobox, Button } from "../shadcn";
3
+ import type { ComboboxOption } from "./Combobox";
3
4
  import { DatabaseIcon } from "@ac-assets/icons";
4
- import { useState } from "react";
5
+ import { useCallback, useState } from "react";
5
6
 
6
7
  export default {
7
8
  title: "Shadcn/Components/Combobox",
@@ -164,7 +165,7 @@ const ComboboxColumnsExample = () => {
164
165
  placeholder="Columns"
165
166
  multiSelect={true}
166
167
  showApplyButton={true}
167
- buttonProps={{ className: "al-w-[200px]" }}
168
+ buttonProps={{ className: "al-w-auto" }}
168
169
  />
169
170
  );
170
171
  };
@@ -436,81 +437,6 @@ export const ComboboxInfiniteScrollExample: StoryFn = () => {
436
437
  );
437
438
  };
438
439
 
439
- export const ComboboxSearchExample: StoryFn = () => {
440
- const [options] = useState([
441
- { value: "react", label: "React" },
442
- { value: "vue", label: "Vue" },
443
- { value: "angular", label: "Angular" },
444
- { value: "svelte", label: "Svelte" },
445
- { value: "nextjs", label: "Next.js" },
446
- ]);
447
-
448
- const [selectedValue, setSelectedValue] = useState("");
449
- const [searchValue, setSearchValue] = useState("");
450
- const [searchHistory, setSearchHistory] = useState<string[]>([]);
451
-
452
- const handleSearch = (searchTerm: string) => {
453
- setSearchValue(searchTerm);
454
- if (searchTerm.trim() && !searchHistory.includes(searchTerm.trim())) {
455
- setSearchHistory(prev => [searchTerm.trim(), ...prev.slice(0, 4)]); // Keep last 5 searches
456
- }
457
- };
458
-
459
- const clearHistory = () => {
460
- setSearchHistory([]);
461
- setSearchValue("");
462
- };
463
-
464
- return (
465
- <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
466
- <div className="al-flex al-gap-4 al-items-center">
467
- <h3 className="al-text-lg al-font-medium">Search Callback Example</h3>
468
- <Button onClick={clearHistory} size="sm" variant="outline">
469
- Clear History
470
- </Button>
471
- </div>
472
-
473
- <div className="al-flex al-flex-col al-gap-4">
474
- <div className="al-flex al-flex-col al-gap-2">
475
- <label className="al-text-sm al-font-medium">Combobox with Search Callback</label>
476
- <p className="al-text-xs al-text-muted-foreground">
477
- Type in the search box to see the search values being emitted
478
- </p>
479
- <Combobox
480
- options={options}
481
- value={selectedValue}
482
- onChange={value => setSelectedValue(value as string)}
483
- placeholder="Search frameworks..."
484
- searchPlaceholder="Type to search..."
485
- onSearch={handleSearch}
486
- buttonProps={{ className: "al-w-[250px]" }}
487
- />
488
- </div>
489
-
490
- <div className="al-flex al-flex-col al-gap-2">
491
- <label className="al-text-sm al-font-medium">Current Search Value:</label>
492
- <code className="al-text-sm al-bg-muted al-p-2 al-rounded">
493
- {searchValue || "No search performed yet"}
494
- </code>
495
- </div>
496
-
497
- {searchHistory.length > 0 && (
498
- <div className="al-flex al-flex-col al-gap-2">
499
- <label className="al-text-sm al-font-medium">Search History:</label>
500
- <ul className="al-text-sm al-bg-muted al-p-2 al-rounded al-space-y-1">
501
- {searchHistory.map((search, index) => (
502
- <li key={index} className="al-font-mono">
503
- {index + 1}. &quot;{search}&quot;
504
- </li>
505
- ))}
506
- </ul>
507
- </div>
508
- )}
509
- </div>
510
- </div>
511
- );
512
- };
513
-
514
440
  export const ComboboxCallbacksExample: StoryFn = () => {
515
441
  const options = [
516
442
  { value: "react", label: "React" },
@@ -592,7 +518,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
592
518
  {
593
519
  value: "react",
594
520
  label: "React",
595
- node: (
521
+ labelNode: (
596
522
  <div className="al-flex al-items-center al-justify-between al-w-full">
597
523
  <div className="al-flex al-items-center al-gap-2">
598
524
  <div className="al-w-4 al-h-4 al-bg-blue-500 al-rounded"></div>
@@ -605,7 +531,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
605
531
  {
606
532
  value: "vue",
607
533
  label: "Vue",
608
- node: (
534
+ labelNode: (
609
535
  <div className="al-flex al-items-center al-justify-between al-w-full">
610
536
  <div className="al-flex al-items-center al-gap-2">
611
537
  <div className="al-w-4 al-h-4 al-bg-green-500 al-rounded"></div>
@@ -618,7 +544,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
618
544
  {
619
545
  value: "angular",
620
546
  label: "Angular",
621
- node: (
547
+ labelNode: (
622
548
  <div className="al-flex al-items-center al-justify-between al-w-full">
623
549
  <div className="al-flex al-items-center al-gap-2">
624
550
  <div className="al-w-4 al-h-4 al-bg-red-500 al-rounded"></div>
@@ -631,7 +557,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
631
557
  {
632
558
  value: "svelte",
633
559
  label: "Svelte",
634
- node: (
560
+ labelNode: (
635
561
  <div className="al-flex al-items-center al-justify-between al-w-full">
636
562
  <div className="al-flex al-items-center al-gap-2">
637
563
  <div className="al-w-4 al-h-4 al-bg-orange-500 al-rounded"></div>
@@ -648,7 +574,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
648
574
  {
649
575
  value: "typescript",
650
576
  label: "TypeScript",
651
- node: (
577
+ labelNode: (
652
578
  <div className="al-flex al-items-center al-justify-between al-w-full">
653
579
  <div className="al-flex al-items-center al-gap-2">
654
580
  <div className="al-w-4 al-h-4 al-bg-blue-600 al-rounded"></div>
@@ -672,7 +598,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
672
598
  {
673
599
  value: "javascript",
674
600
  label: "JavaScript",
675
- node: (
601
+ labelNode: (
676
602
  <div className="al-flex al-items-center al-justify-between al-w-full">
677
603
  <div className="al-flex al-items-center al-gap-2">
678
604
  <div className="al-w-4 al-h-4 al-bg-yellow-500 al-rounded"></div>
@@ -696,7 +622,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
696
622
  {
697
623
  value: "python",
698
624
  label: "Python",
699
- node: (
625
+ labelNode: (
700
626
  <div className="al-flex al-items-center al-justify-between al-w-full">
701
627
  <div className="al-flex al-items-center al-gap-2">
702
628
  <div className="al-w-4 al-h-4 al-bg-green-600 al-rounded"></div>
@@ -727,7 +653,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
727
653
  {
728
654
  value: "custom1",
729
655
  label: "Custom Option 1",
730
- node: (
656
+ labelNode: (
731
657
  <div className="al-flex al-items-center al-gap-2">
732
658
  <div className="al-w-2 al-h-2 al-bg-purple-500 al-rounded-full"></div>
733
659
  <span className="al-italic">Custom with node</span>
@@ -738,7 +664,7 @@ export const ComboboxCustomNodesExample: StoryFn = () => {
738
664
  {
739
665
  value: "custom2",
740
666
  label: "Custom Option 2",
741
- node: (
667
+ labelNode: (
742
668
  <div className="al-flex al-items-center al-gap-2">
743
669
  <div className="al-w-2 al-h-2 al-bg-pink-500 al-rounded-full"></div>
744
670
  <span className="al-font-bold">Another custom node</span>
@@ -900,3 +826,729 @@ export const ComboboxPopoverCustomizationExample: StoryFn = () => {
900
826
  </div>
901
827
  );
902
828
  };
829
+
830
+ export const ComboboxValidationExample: StoryFn = () => {
831
+ const options = [
832
+ { value: "databases", label: "Databases" },
833
+ { value: "api", label: "API" },
834
+ { value: "frontend", label: "Frontend" },
835
+ { value: "backend", label: "Backend" },
836
+ { value: "devops", label: "DevOps" },
837
+ { value: "mobile", label: "Mobile" },
838
+ ];
839
+
840
+ const [selectedValue, setSelectedValue] = useState<string[]>(["databases"]);
841
+ const [selectedValueWithoutValidation, setSelectedValueWithoutValidation] = useState<string[]>(
842
+ []
843
+ );
844
+
845
+ return (
846
+ <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
847
+ <div className="al-flex al-flex-col al-gap-2">
848
+ <h3 className="al-text-lg al-font-medium">Validation Examples</h3>
849
+ <p className="al-text-sm al-text-muted-foreground">
850
+ Demonstrating the disableAllDeselect prop for preventing empty selections
851
+ </p>
852
+ </div>
853
+
854
+ <div className="al-flex al-flex-col al-gap-6">
855
+ <div className="al-flex al-flex-col al-gap-2">
856
+ <label className="al-text-sm al-font-medium">
857
+ With Validation (disableAllDeselect=true)
858
+ </label>
859
+ <p className="al-text-xs al-text-muted-foreground">
860
+ Try to deselect all items and click Apply - you&apos;ll see a warning message
861
+ </p>
862
+ <Combobox
863
+ options={options}
864
+ value={selectedValue}
865
+ onChange={value => setSelectedValue(value as string[])}
866
+ placeholder="Select at least one skill..."
867
+ multiSelect={true}
868
+ showApplyButton={true}
869
+ showClearButton={true}
870
+ disableAllDeselect={true}
871
+ buttonProps={{ className: "al-w-[300px]" }}
872
+ />
873
+ <div className="al-text-sm">
874
+ Selected: {selectedValue.length > 0 ? selectedValue.join(", ") : "None"}
875
+ </div>
876
+ </div>
877
+
878
+ <div className="al-flex al-flex-col al-gap-2">
879
+ <label className="al-text-sm al-font-medium">
880
+ Without Validation (disableAllDeselect=false)
881
+ </label>
882
+ <p className="al-text-xs al-text-muted-foreground">
883
+ You can deselect all items and apply with an empty selection
884
+ </p>
885
+ <Combobox
886
+ options={options}
887
+ value={selectedValueWithoutValidation}
888
+ onChange={value => setSelectedValueWithoutValidation(value as string[])}
889
+ placeholder="Select skills (optional)..."
890
+ multiSelect={true}
891
+ showApplyButton={true}
892
+ showClearButton={true}
893
+ disableAllDeselect={false}
894
+ buttonProps={{ className: "al-w-[300px]" }}
895
+ />
896
+ <div className="al-text-sm">
897
+ Selected:{" "}
898
+ {selectedValueWithoutValidation.length > 0
899
+ ? selectedValueWithoutValidation.join(", ")
900
+ : "None"}
901
+ </div>
902
+ </div>
903
+ </div>
904
+ </div>
905
+ );
906
+ };
907
+
908
+ export const ComboboxDebouncedSearchExample: StoryFn = () => {
909
+ const allOptions = [
910
+ { value: "javascript", label: "JavaScript" },
911
+ { value: "typescript", label: "TypeScript" },
912
+ { value: "python", label: "Python" },
913
+ { value: "java", label: "Java" },
914
+ { value: "csharp", label: "C#" },
915
+ { value: "cpp", label: "C++" },
916
+ { value: "go", label: "Go" },
917
+ { value: "rust", label: "Rust" },
918
+ { value: "php", label: "PHP" },
919
+ { value: "ruby", label: "Ruby" },
920
+ { value: "swift", label: "Swift" },
921
+ { value: "kotlin", label: "Kotlin" },
922
+ ];
923
+
924
+ const [selectedValue, setSelectedValue] = useState("");
925
+ const [filteredOptions, setFilteredOptions] = useState(allOptions);
926
+ const [searchHistory, setSearchHistory] = useState<string[]>([]);
927
+
928
+ const clearHistory = () => {
929
+ setSearchHistory([]);
930
+ setFilteredOptions(allOptions);
931
+ };
932
+
933
+ return (
934
+ <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
935
+ <div className="al-flex al-gap-4 al-items-center">
936
+ <h3 className="al-text-lg al-font-medium">Debounced Search Example</h3>
937
+ <Button onClick={clearHistory} size="sm" variant="outline">
938
+ Clear History
939
+ </Button>
940
+ </div>
941
+
942
+ <div className="al-flex al-flex-col al-gap-4">
943
+ <div className="al-flex al-flex-col al-gap-2">
944
+ <label className="al-text-sm al-font-medium">Search with Default Debounce (300ms)</label>
945
+ <p className="al-text-xs al-text-muted-foreground">
946
+ Type quickly to see how the search is debounced. The search function will only be called
947
+ 300ms after you stop typing.
948
+ </p>
949
+ <Combobox
950
+ options={filteredOptions}
951
+ value={selectedValue}
952
+ onChange={value => setSelectedValue(value as string)}
953
+ placeholder="Search programming languages..."
954
+ searchPlaceholder="Type to search languages..."
955
+ buttonProps={{ className: "al-w-[300px]" }}
956
+ />
957
+ <div className="al-text-sm">
958
+ Showing {filteredOptions.length} of {allOptions.length} languages
959
+ </div>
960
+ </div>
961
+
962
+ {searchHistory.length > 0 && (
963
+ <div className="al-flex al-flex-col al-gap-2">
964
+ <label className="al-text-sm al-font-medium">Search History (Last 10 calls):</label>
965
+ <ul className="al-text-xs al-bg-muted al-p-2 al-rounded al-space-y-1 al-max-h-40 al-overflow-y-auto">
966
+ {searchHistory.map((entry, index) => (
967
+ <li key={index} className="al-font-mono">
968
+ {entry}
969
+ </li>
970
+ ))}
971
+ </ul>
972
+ </div>
973
+ )}
974
+ </div>
975
+ </div>
976
+ );
977
+ };
978
+
979
+ export const ComboboxFilterDropdownCompatibilityExample: StoryFn = () => {
980
+ const tagOptions = [
981
+ { value: "urgent", label: "Urgent" },
982
+ { value: "bug", label: "Bug" },
983
+ { value: "feature", label: "Feature" },
984
+ { value: "enhancement", label: "Enhancement" },
985
+ { value: "documentation", label: "Documentation" },
986
+ { value: "question", label: "Question" },
987
+ { value: "duplicate", label: "Duplicate" },
988
+ { value: "wontfix", label: "Won't Fix" },
989
+ ];
990
+
991
+ const [selectedTags, setSelectedTags] = useState<string[]>(["urgent", "bug"]);
992
+ const [requiredTags, setRequiredTags] = useState<string[]>(["urgent"]);
993
+
994
+ return (
995
+ <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
996
+ <div className="al-flex al-flex-col al-gap-2">
997
+ <h3 className="al-text-lg al-font-medium">FilterDropdown Compatibility</h3>
998
+ <p className="al-text-sm al-text-muted-foreground">
999
+ Demonstrating all FilterDropdown features: debounced search, validation, apply button, and
1000
+ clear functionality
1001
+ </p>
1002
+ </div>
1003
+
1004
+ <div className="al-flex al-flex-col al-gap-6">
1005
+ <div className="al-flex al-flex-col al-gap-2">
1006
+ <label className="al-text-sm al-font-medium">Full Feature Set</label>
1007
+ <p className="al-text-xs al-text-muted-foreground">
1008
+ Multi-select with apply button, clear button, debounced search, and validation
1009
+ </p>
1010
+ <Combobox
1011
+ options={tagOptions}
1012
+ value={selectedTags}
1013
+ onChange={value => setSelectedTags(value as string[])}
1014
+ placeholder="Select tags..."
1015
+ searchPlaceholder="Search tags..."
1016
+ multiSelect={true}
1017
+ showApplyButton={true}
1018
+ showClearButton={true}
1019
+ disableAllDeselect={false}
1020
+ buttonProps={{ className: "al-w-[300px]" }}
1021
+ />
1022
+ <div className="al-text-sm">
1023
+ Selected tags: {selectedTags.length > 0 ? selectedTags.join(", ") : "None"}
1024
+ </div>
1025
+ </div>
1026
+
1027
+ <div className="al-flex al-flex-col al-gap-2">
1028
+ <label className="al-text-sm al-font-medium">
1029
+ Required Tags (disableAllDeselect=true)
1030
+ </label>
1031
+ <p className="al-text-xs al-text-muted-foreground">
1032
+ At least one tag must always be selected. Try to deselect all and apply.
1033
+ </p>
1034
+ <Combobox
1035
+ options={tagOptions}
1036
+ value={requiredTags}
1037
+ onChange={value => setRequiredTags(value as string[])}
1038
+ placeholder="Select required tags..."
1039
+ searchPlaceholder="Find tags..."
1040
+ multiSelect={true}
1041
+ showApplyButton={true}
1042
+ showClearButton={true}
1043
+ disableAllDeselect={true}
1044
+ buttonProps={{ className: "al-w-[300px]" }}
1045
+ />
1046
+ <div className="al-text-sm">
1047
+ Required tags: {requiredTags.length > 0 ? requiredTags.join(", ") : "None"}
1048
+ </div>
1049
+ </div>
1050
+ </div>
1051
+ </div>
1052
+ );
1053
+ };
1054
+
1055
+ export const ComboboxHierarchicalExample: StoryFn = () => {
1056
+ const [selectedDepartment, setSelectedDepartment] = useState("");
1057
+ const [selectedMultiDepartment, setSelectedMultiDepartment] = useState<string[]>([]);
1058
+ const [hierarchicalSelection, setHierarchicalSelection] = useState<{
1059
+ value: string;
1060
+ label: string;
1061
+ parent?: { value: string; label: string };
1062
+ } | null>(null);
1063
+
1064
+ // Helper function to handle hierarchical selection
1065
+ const handleHierarchicalSelect = (value: string, parentValue: string, parentLabel: string) => {
1066
+ // Update single select state
1067
+ setSelectedDepartment(value);
1068
+ setHierarchicalSelection({
1069
+ value,
1070
+ label:
1071
+ departmentOptions.find(opt => opt.nestedLabels?.[value])?.nestedLabels?.[value] || value,
1072
+ parent: { value: parentValue, label: parentLabel },
1073
+ });
1074
+
1075
+ // Also update multi-select state (toggle the value) since no apply button
1076
+ setSelectedMultiDepartment(prev => {
1077
+ if (prev.includes(value)) {
1078
+ return prev.filter(v => v !== value);
1079
+ } else {
1080
+ return [...prev, value];
1081
+ }
1082
+ });
1083
+ };
1084
+
1085
+ // Helper function to get the selected value for a nested combobox
1086
+ const getNestedValue = (parentValue: string): string => {
1087
+ const parentOption = departmentOptions.find(opt => opt.value === parentValue);
1088
+ if (parentOption?.nestedLabels && selectedDepartment in parentOption.nestedLabels) {
1089
+ return selectedDepartment;
1090
+ }
1091
+ return "";
1092
+ };
1093
+
1094
+ // Hierarchical options with subcomponents for single select
1095
+ const departmentOptions: ComboboxOption[] = [
1096
+ {
1097
+ value: "engineering",
1098
+ label: "Engineering",
1099
+ nestedLabels: {
1100
+ frontend: "Frontend",
1101
+ backend: "Backend",
1102
+ devops: "DevOps",
1103
+ mobile: "Mobile",
1104
+ },
1105
+ children: (close: () => void) => (
1106
+ <div className="al-flex al-flex-col al-gap-2">
1107
+ <h4 className="al-font-medium al-text-sm">Engineering Teams</h4>
1108
+ <div className="al-grid al-grid-cols-2 al-gap-2">
1109
+ <Button
1110
+ size="sm"
1111
+ variant="outline"
1112
+ onClick={() => {
1113
+ handleHierarchicalSelect("frontend", "engineering", "Engineering");
1114
+ close();
1115
+ }}
1116
+ >
1117
+ Frontend
1118
+ </Button>
1119
+ <Button
1120
+ size="sm"
1121
+ variant="outline"
1122
+ onClick={() => {
1123
+ handleHierarchicalSelect("backend", "engineering", "Engineering");
1124
+ close();
1125
+ }}
1126
+ >
1127
+ Backend
1128
+ </Button>
1129
+ <Button
1130
+ size="sm"
1131
+ variant="outline"
1132
+ onClick={() => {
1133
+ handleHierarchicalSelect("devops", "engineering", "Engineering");
1134
+ close();
1135
+ }}
1136
+ >
1137
+ DevOps
1138
+ </Button>
1139
+ <Button
1140
+ size="sm"
1141
+ variant="outline"
1142
+ onClick={() => {
1143
+ handleHierarchicalSelect("mobile", "engineering", "Engineering");
1144
+ close();
1145
+ }}
1146
+ >
1147
+ Mobile
1148
+ </Button>
1149
+ </div>
1150
+ <div className="al-text-xs al-text-muted-foreground al-mt-2">
1151
+ Click a team to select and close
1152
+ </div>
1153
+ </div>
1154
+ ),
1155
+ },
1156
+ {
1157
+ value: "design",
1158
+ label: "Design",
1159
+ nestedLabels: {
1160
+ "ux-design": "UX Designer",
1161
+ "ui-design": "UI Designer",
1162
+ "product-design": "Product Designer",
1163
+ },
1164
+ children: (close: () => void) => (
1165
+ <div className="al-flex al-flex-col al-gap-2">
1166
+ <h4 className="al-font-medium al-text-sm">Design Roles</h4>
1167
+ <div className="al-space-y-1">
1168
+ <Button
1169
+ size="sm"
1170
+ variant="ghost"
1171
+ className="al-w-full al-justify-start"
1172
+ onClick={() => {
1173
+ handleHierarchicalSelect("ux-design", "design", "Design");
1174
+ close();
1175
+ }}
1176
+ >
1177
+ UX Designer
1178
+ </Button>
1179
+ <Button
1180
+ size="sm"
1181
+ variant="ghost"
1182
+ className="al-w-full al-justify-start"
1183
+ onClick={() => {
1184
+ handleHierarchicalSelect("ui-design", "design", "Design");
1185
+ close();
1186
+ }}
1187
+ >
1188
+ UI Designer
1189
+ </Button>
1190
+ <Button
1191
+ size="sm"
1192
+ variant="ghost"
1193
+ className="al-w-full al-justify-start"
1194
+ onClick={() => {
1195
+ handleHierarchicalSelect("product-design", "design", "Design");
1196
+ close();
1197
+ }}
1198
+ >
1199
+ Product Designer
1200
+ </Button>
1201
+ </div>
1202
+ </div>
1203
+ ),
1204
+ },
1205
+ {
1206
+ value: "marketing",
1207
+ label: "Marketing",
1208
+ nestedLabels: {
1209
+ content: "Content Marketing",
1210
+ social: "Social Media",
1211
+ email: "Email Marketing",
1212
+ seo: "SEO/SEM",
1213
+ },
1214
+ children: (close: () => void) => (
1215
+ <div className="al-flex al-flex-col al-gap-2">
1216
+ <h4 className="al-font-medium al-text-sm">Marketing Channels</h4>
1217
+ <Combobox
1218
+ options={[
1219
+ { value: "content", label: "Content Marketing" },
1220
+ { value: "social", label: "Social Media" },
1221
+ { value: "email", label: "Email Marketing" },
1222
+ { value: "seo", label: "SEO/SEM" },
1223
+ ]}
1224
+ value={getNestedValue("marketing")}
1225
+ onChange={value => {
1226
+ handleHierarchicalSelect(value as string, "marketing", "Marketing");
1227
+ close();
1228
+ }}
1229
+ placeholder="Select marketing area..."
1230
+ buttonProps={{ className: "al-w-full" }}
1231
+ />
1232
+ <div className="al-text-xs al-text-muted-foreground">
1233
+ Nested combobox for marketing specializations
1234
+ </div>
1235
+ </div>
1236
+ ),
1237
+ },
1238
+ {
1239
+ value: "sales",
1240
+ label: "Sales",
1241
+ // No subComponent - regular option
1242
+ },
1243
+ {
1244
+ value: "hr",
1245
+ label: "Human Resources",
1246
+ // No subComponent - regular option
1247
+ },
1248
+ ];
1249
+
1250
+ return (
1251
+ <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
1252
+ <div className="al-flex al-flex-col al-gap-2">
1253
+ <h3 className="al-text-lg al-font-medium">Hierarchical Options</h3>
1254
+ <p className="al-text-sm al-text-muted-foreground">
1255
+ NEW FEATURE: Options with subComponent show a chevron and display nested content on hover.
1256
+ Mix hierarchical and regular options freely.
1257
+ </p>
1258
+ </div>
1259
+
1260
+ <div className="al-flex al-flex-col al-gap-6">
1261
+ <div className="al-flex al-flex-col al-gap-2">
1262
+ <label className="al-text-sm al-font-medium">Department Selection (Single)</label>
1263
+ <p className="al-text-xs al-text-muted-foreground">
1264
+ Hover over Engineering, Design, or Marketing to see subcomponents. Sales and HR are
1265
+ regular options.
1266
+ </p>
1267
+ <Combobox
1268
+ options={departmentOptions}
1269
+ value={selectedDepartment}
1270
+ onChange={value => setSelectedDepartment(value as string)}
1271
+ placeholder="Select department..."
1272
+ searchPlaceholder="Search departments..."
1273
+ hoverDelayMs={200}
1274
+ buttonProps={{ className: "al-w-[300px]" }}
1275
+ />
1276
+ <div className="al-text-sm">Selected: {selectedDepartment || "None"}</div>
1277
+ {hierarchicalSelection && (
1278
+ <div className="al-text-sm al-bg-blue-50 al-p-2 al-rounded">
1279
+ <strong>Hierarchical Selection:</strong>
1280
+ <br />
1281
+ Value: {hierarchicalSelection.value}
1282
+ <br />
1283
+ Label: {hierarchicalSelection.label}
1284
+ <br />
1285
+ {hierarchicalSelection.parent && (
1286
+ <>
1287
+ Parent: {hierarchicalSelection.parent.label} ({hierarchicalSelection.parent.value}
1288
+ )
1289
+ </>
1290
+ )}
1291
+ </div>
1292
+ )}
1293
+ </div>
1294
+
1295
+ <div className="al-flex al-flex-col al-gap-2">
1296
+ <label className="al-text-sm al-font-medium">Department Selection (Multi-Select)</label>
1297
+ <p className="al-text-xs al-text-muted-foreground">
1298
+ Multi-select mode also supports hierarchical options. Regular options (Sales/HR) can be
1299
+ selected from dropdown, hierarchical options (Engineering/Design/Marketing) show nested
1300
+ selections on hover.
1301
+ </p>
1302
+ <Combobox
1303
+ options={departmentOptions}
1304
+ value={selectedMultiDepartment}
1305
+ onChange={value => setSelectedMultiDepartment(value as string[])}
1306
+ placeholder="Select departments..."
1307
+ searchPlaceholder="Search departments..."
1308
+ multiSelect={true}
1309
+ showApplyButton={false}
1310
+ hoverDelayMs={300}
1311
+ buttonProps={{ className: "al-w-[300px]" }}
1312
+ />
1313
+ <div className="al-text-sm">
1314
+ Selected:{" "}
1315
+ {selectedMultiDepartment.length > 0 ? selectedMultiDepartment.join(", ") : "None"}
1316
+ </div>
1317
+ </div>
1318
+ </div>
1319
+ </div>
1320
+ );
1321
+ };
1322
+
1323
+ export const ComboboxEnhancedAsyncExample: StoryFn = () => {
1324
+ const [selectedValue, setSelectedValue] = useState("");
1325
+ const [multiSelectedValue, setMultiSelectedValue] = useState<string[]>([]);
1326
+ const [displayedOptions, setDisplayedOptions] = useState<Array<{ value: string; label: string }>>(
1327
+ []
1328
+ );
1329
+ const [hasMore, setHasMore] = useState(true);
1330
+ const [currentPage, setCurrentPage] = useState(1);
1331
+ const [currentSearch, setCurrentSearch] = useState("");
1332
+
1333
+ // Simulate API data
1334
+ const generateApiData = (search: string = "", page: number = 1) => {
1335
+ const allItems = Array.from({ length: 100 }, (_, i) => ({
1336
+ value: `item-${i + 1}`,
1337
+ label: `${search ? "Filtered " : ""}API Item ${i + 1}`,
1338
+ }));
1339
+
1340
+ const filteredItems = search
1341
+ ? allItems.filter(item => item.label.toLowerCase().includes(search.toLowerCase()))
1342
+ : allItems;
1343
+
1344
+ const startIndex = (page - 1) * 20;
1345
+ const endIndex = startIndex + 20;
1346
+ const pageItems = filteredItems.slice(startIndex, endIndex);
1347
+
1348
+ return {
1349
+ items: pageItems,
1350
+ hasMore: endIndex < filteredItems.length,
1351
+ total: filteredItems.length,
1352
+ page,
1353
+ };
1354
+ };
1355
+
1356
+ const handleLoadMore = useCallback(async (searchValue?: string, page?: number) => {
1357
+ // Simulate API delay
1358
+ await new Promise(resolve => setTimeout(resolve, 800));
1359
+
1360
+ const isNewSearch = searchValue !== currentSearch;
1361
+ const targetPage = page || (isNewSearch ? 1 : currentPage + 1);
1362
+
1363
+ const result = generateApiData(searchValue || "", targetPage);
1364
+
1365
+ if (isNewSearch || targetPage === 1) {
1366
+ setDisplayedOptions(result.items);
1367
+ } else {
1368
+ setDisplayedOptions(prev => [...prev, ...result.items]);
1369
+ }
1370
+
1371
+ setHasMore(result.hasMore);
1372
+ setCurrentPage(targetPage);
1373
+ setCurrentSearch(searchValue || "");
1374
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1375
+ }, []);
1376
+
1377
+ const handlePageReset = () => {
1378
+ setDisplayedOptions([]);
1379
+ setCurrentPage(1);
1380
+ setHasMore(true);
1381
+ };
1382
+
1383
+ const resetDemo = () => {
1384
+ setSelectedValue("");
1385
+ setMultiSelectedValue([]);
1386
+ setDisplayedOptions([]);
1387
+ setCurrentPage(1);
1388
+ setCurrentSearch("");
1389
+ setHasMore(true);
1390
+ };
1391
+
1392
+ return (
1393
+ <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
1394
+ <div className="al-flex al-gap-4 al-items-center">
1395
+ <h3 className="al-text-lg al-font-medium">Enhanced Async Loading</h3>
1396
+ <Button onClick={resetDemo} size="sm" variant="outline">
1397
+ Reset Demo
1398
+ </Button>
1399
+ </div>
1400
+
1401
+ <div className="al-flex al-flex-col al-gap-2">
1402
+ <p className="al-text-sm al-text-muted-foreground">
1403
+ NEW FEATURES: Enhanced onLoadMore with search and page parameters, automatic page
1404
+ management, debounced search with page reset, and improved loading states.
1405
+ </p>
1406
+ <div className="al-text-xs al-bg-blue-50 al-p-2 al-rounded">
1407
+ <strong>Try this:</strong> Open dropdown → scroll to load more → search → see how
1408
+ pagination resets → search again
1409
+ </div>
1410
+ </div>
1411
+
1412
+ <div className="al-flex al-flex-col al-gap-6">
1413
+ <div className="al-flex al-flex-col al-gap-2">
1414
+ <label className="al-text-sm al-font-medium">Single Select with Enhanced Async</label>
1415
+ <p className="al-text-xs al-text-muted-foreground">
1416
+ Page: {currentPage} | Showing: {displayedOptions.length} items | Search: &quot;
1417
+ {currentSearch}&quot; | Has more: {hasMore ? "Yes" : "No"}
1418
+ </p>
1419
+ <Combobox
1420
+ options={displayedOptions}
1421
+ value={selectedValue}
1422
+ onChange={value => setSelectedValue(value as string)}
1423
+ placeholder="Select item..."
1424
+ searchPlaceholder="Search items (debounced)..."
1425
+ onLoadMore={handleLoadMore}
1426
+ hasMore={hasMore}
1427
+ onPageReset={handlePageReset}
1428
+ buttonProps={{ className: "al-w-[300px]" }}
1429
+ />
1430
+ <div className="al-text-sm">Selected: {selectedValue || "None"}</div>
1431
+ </div>
1432
+
1433
+ <div className="al-flex al-flex-col al-gap-2">
1434
+ <label className="al-text-sm al-font-medium">Multi-Select with Enhanced Async</label>
1435
+ <p className="al-text-xs al-text-muted-foreground">
1436
+ Multi-select mode with apply button and async loading
1437
+ </p>
1438
+ <Combobox
1439
+ options={displayedOptions}
1440
+ value={multiSelectedValue}
1441
+ onChange={value => setMultiSelectedValue(value as string[])}
1442
+ placeholder="Select multiple items..."
1443
+ searchPlaceholder="Search and select..."
1444
+ multiSelect={true}
1445
+ showApplyButton={true}
1446
+ onLoadMore={handleLoadMore}
1447
+ hasMore={hasMore}
1448
+ onPageReset={handlePageReset}
1449
+ buttonProps={{ className: "al-w-[300px]" }}
1450
+ />
1451
+ <div className="al-text-sm">
1452
+ Selected: {multiSelectedValue.length} items{" "}
1453
+ {multiSelectedValue.length > 0 &&
1454
+ `(${multiSelectedValue.slice(0, 3).join(", ")}${multiSelectedValue.length > 3 ? "..." : ""})`}
1455
+ </div>
1456
+ </div>
1457
+ </div>
1458
+ </div>
1459
+ );
1460
+ };
1461
+
1462
+ export const ComboboxSheetIntegrationExample: StoryFn = () => {
1463
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
1464
+ const [selectedValue, setSelectedValue] = useState("");
1465
+ const [multiSelectedValue, setSelectedMultiValue] = useState<string[]>([]);
1466
+
1467
+ const options = [
1468
+ { value: "react", label: "React" },
1469
+ { value: "vue", label: "Vue" },
1470
+ { value: "angular", label: "Angular" },
1471
+ { value: "svelte", label: "Svelte" },
1472
+ { value: "nextjs", label: "Next.js" },
1473
+ { value: "remix", label: "Remix" },
1474
+ { value: "gatsby", label: "Gatsby" },
1475
+ { value: "ember", label: "Ember.js" },
1476
+ { value: "solid", label: "Solid.js" },
1477
+ { value: "qwik", label: "Qwik" },
1478
+ { value: "alpine", label: "Alpine.js" },
1479
+ { value: "lit", label: "Lit" },
1480
+ ];
1481
+
1482
+ return (
1483
+ <div className="al-flex al-flex-col al-gap-6 al-justify-start al-items-start">
1484
+ <div className="al-flex al-flex-col al-gap-2">
1485
+ <h3 className="al-text-lg al-font-medium">Sheet Integration</h3>
1486
+ <p className="al-text-sm al-text-muted-foreground">
1487
+ BUG FIX: Fixed scrolling issues when Combobox is used inside Sheet components. Added
1488
+ onWheel stopPropagation to prevent scroll conflicts.
1489
+ </p>
1490
+ </div>
1491
+
1492
+ <Button onClick={() => setIsSheetOpen(true)}>Open Sheet with Combobox</Button>
1493
+
1494
+ {/* Note: This is a simulated sheet for the story. In real usage, you'd use the actual Sheet component */}
1495
+ {isSheetOpen && (
1496
+ <div className="al-fixed al-inset-0 al-bg-black/50 al-z-50 al-flex al-items-center al-justify-center">
1497
+ <div className="al-bg-white al-rounded-lg al-p-6 al-w-[500px] al-max-h-[80vh] al-overflow-y-auto">
1498
+ <div className="al-flex al-justify-between al-items-center al-mb-4">
1499
+ <h4 className="al-text-lg al-font-medium">Sheet with Combobox</h4>
1500
+ <Button variant="ghost" size="sm" onClick={() => setIsSheetOpen(false)}>
1501
+
1502
+ </Button>
1503
+ </div>
1504
+
1505
+ <div className="al-space-y-4">
1506
+ <div className="al-text-sm al-text-muted-foreground">
1507
+ Try scrolling in the combobox dropdown - it should work properly now!
1508
+ </div>
1509
+
1510
+ <div className="al-flex al-flex-col al-gap-2">
1511
+ <label className="al-text-sm al-font-medium">Single Select in Sheet</label>
1512
+ <Combobox
1513
+ options={options}
1514
+ value={selectedValue}
1515
+ onChange={value => setSelectedValue(value as string)}
1516
+ placeholder="Select framework..."
1517
+ searchPlaceholder="Search frameworks..."
1518
+ buttonProps={{ className: "al-w-full" }}
1519
+ />
1520
+ <div className="al-text-xs">Selected: {selectedValue || "None"}</div>
1521
+ </div>
1522
+
1523
+ <div className="al-flex al-flex-col al-gap-2">
1524
+ <label className="al-text-sm al-font-medium">Multi-Select in Sheet</label>
1525
+ <Combobox
1526
+ options={options}
1527
+ value={multiSelectedValue}
1528
+ onChange={value => setSelectedMultiValue(value as string[])}
1529
+ placeholder="Select frameworks..."
1530
+ searchPlaceholder="Search frameworks..."
1531
+ multiSelect={true}
1532
+ showApplyButton={true}
1533
+ buttonProps={{ className: "al-w-full" }}
1534
+ />
1535
+ <div className="al-text-xs">
1536
+ Selected: {multiSelectedValue.length > 0 ? multiSelectedValue.join(", ") : "None"}
1537
+ </div>
1538
+ </div>
1539
+
1540
+ <div className="al-text-xs al-text-muted-foreground al-bg-green-50 al-p-2 al-rounded">
1541
+ ✅ Mouse wheel scrolling now works properly in both comboboxes!
1542
+ </div>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+ )}
1547
+
1548
+ <div className="al-text-sm">
1549
+ Selected in sheet: {selectedValue || "None"} | Multi:{" "}
1550
+ {multiSelectedValue.join(", ") || "None"}
1551
+ </div>
1552
+ </div>
1553
+ );
1554
+ };