Skip to content

Commit 14aabf6

Browse files
committed
Added combobox, redesigned select
1 parent 2b620c7 commit 14aabf6

6 files changed

Lines changed: 467 additions & 203 deletions

File tree

packages/react-components/src/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
.lucide * {
1212
/* vector-effect: non-scaling-stroke; */
1313
}
14+
1415
@theme {
1516
--color-selection: var(--color-blue-300);
1617
--color-ring-red: var(--color-red-200);
@@ -20,7 +21,7 @@
2021
--color-ring-orange: var(--color-orange-200);
2122
--color-ring-pink: var(--color-pink-200);
2223
--color-ring-blue: var(--color-blue-200);
23-
--color-ring-red: var(--color-red-200);
24+
--color-ring-neutral: var(--color-neutral-200);
2425
--color-text-primary: var(--color-neutral-900);
2526
--color-text-secondary: var(--color-neutral-500);
2627
--color-text-secondary_hover: var(--color-neutral-600);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"use client";
2+
import * as React from "react";
3+
import { Check, ChevronsUpDown } from "lucide-react";
4+
import { cn } from "@/lib/utils";
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
Command,
8+
CommandEmpty,
9+
CommandGroup,
10+
CommandInput,
11+
CommandItem,
12+
CommandList,
13+
} from "@/components/ui/command";
14+
import {
15+
Popover,
16+
PopoverContent,
17+
PopoverTrigger,
18+
} from "@/components/ui/popover";
19+
const frameworks = [
20+
{
21+
value: "next.js",
22+
label: "Next.js",
23+
},
24+
{
25+
value: "sveltekit",
26+
label: "SvelteKit",
27+
},
28+
{
29+
value: "nuxt.js",
30+
label: "Nuxt.js",
31+
},
32+
{
33+
value: "remix",
34+
label: "Remix",
35+
},
36+
{
37+
value: "astro",
38+
label: "Astro",
39+
},
40+
];
41+
export function ComboboxDemo() {
42+
const [open, setOpen] = React.useState(false);
43+
const [value, setValue] = React.useState("");
44+
return (
45+
<Popover open={open} onOpenChange={setOpen}>
46+
<PopoverTrigger asChild>
47+
<Button
48+
variant="outline"
49+
role="combobox"
50+
aria-expanded={open}
51+
className="w-[200px] justify-between"
52+
>
53+
{value
54+
? frameworks.find((framework) => framework.value === value)?.label
55+
: "Select framework..."}
56+
<ChevronsUpDown className="opacity-50" />
57+
</Button>
58+
</PopoverTrigger>
59+
<PopoverContent className="w-[200px] p-0">
60+
<Command>
61+
<CommandInput placeholder="Search framework..." className="h-9" />
62+
<CommandList>
63+
<CommandEmpty>No framework found.</CommandEmpty>
64+
<CommandGroup>
65+
{frameworks.map((framework) => (
66+
<CommandItem
67+
key={framework.value}
68+
value={framework.value}
69+
onSelect={(currentValue) => {
70+
setValue(currentValue === value ? "" : currentValue);
71+
setOpen(false);
72+
}}
73+
>
74+
{framework.label}
75+
<Check
76+
className={cn(
77+
"ml-auto",
78+
value === framework.value ? "opacity-100" : "opacity-0"
79+
)}
80+
/>
81+
</CommandItem>
82+
))}
83+
</CommandGroup>
84+
</CommandList>
85+
</Command>
86+
</PopoverContent>
87+
</Popover>
88+
);
89+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { ComboboxDemo } from "./combobox";
3+
4+
const meta = {
5+
title: "Component/Combobox",
6+
component: ComboboxDemo,
7+
argTypes: {},
8+
} satisfies Meta;
9+
export default meta;
10+
11+
type Story = StoryObj<typeof meta>;
12+
13+
export const Default = {
14+
render: () => (
15+
<div className="w-[560px]">
16+
<ComboboxDemo />
17+
</div>
18+
),
19+
} satisfies Story;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
5+
import { cn } from "#shadcn/lib/utils";
6+
import { Button } from "#shadcn/components/ui/button";
7+
import {
8+
Command,
9+
CommandEmpty,
10+
CommandGroup,
11+
CommandInput,
12+
CommandItem,
13+
CommandList,
14+
} from "#shadcn/components/ui/command";
15+
import {
16+
Select,
17+
SelectContent,
18+
SelectItem,
19+
SelectTrigger,
20+
SelectValue,
21+
} from "#shadcn/components/ui/select";
22+
23+
export interface ComboboxOption {
24+
value: string;
25+
label: string;
26+
}
27+
28+
interface ComboboxProps {
29+
options: ComboboxOption[];
30+
value?: string;
31+
onValueChange?: (value: string) => void;
32+
placeholder?: string;
33+
searchPlaceholder?: string;
34+
emptyText?: string;
35+
disabled?: boolean;
36+
className?: string;
37+
}
38+
39+
export function Combobox({
40+
options,
41+
value,
42+
onValueChange,
43+
placeholder = "Select option...",
44+
searchPlaceholder = "Search...",
45+
emptyText = "No options found.",
46+
disabled = false,
47+
className,
48+
}: ComboboxProps) {
49+
const [open, setOpen] = React.useState(false);
50+
const [searchValue, setSearchValue] = React.useState("");
51+
const inputRef = React.useRef<HTMLInputElement>(null);
52+
53+
const filteredOptions = React.useMemo(() => {
54+
if (!searchValue) return options;
55+
return options.filter((option) =>
56+
option.label.toLowerCase().includes(searchValue.toLowerCase())
57+
);
58+
}, [options, searchValue]);
59+
60+
const selectedOption = options.find((option) => option.value === value);
61+
62+
// Сброс поиска при закрытии и автофокус при открытии
63+
React.useEffect(() => {
64+
if (!open) {
65+
setSearchValue("");
66+
} else {
67+
// Автофокус на поисковую строку при открытии
68+
setTimeout(() => {
69+
inputRef.current?.focus();
70+
}, 0);
71+
}
72+
}, [open]);
73+
74+
return (
75+
<Select
76+
value={value || ""}
77+
{...(onValueChange && { onValueChange })}
78+
disabled={disabled}
79+
open={open}
80+
onOpenChange={setOpen}
81+
>
82+
<SelectTrigger className={cn("w-full", className)}>
83+
<SelectValue placeholder={placeholder}>
84+
{selectedOption?.label}
85+
</SelectValue>
86+
</SelectTrigger>
87+
<SelectContent>
88+
<Command>
89+
<CommandInput
90+
ref={inputRef}
91+
placeholder={searchPlaceholder}
92+
value={searchValue}
93+
onValueChange={setSearchValue}
94+
/>
95+
<CommandList>
96+
<CommandEmpty>{emptyText}</CommandEmpty>
97+
<CommandGroup>
98+
{filteredOptions.map((option) => (
99+
<CommandItem
100+
key={option.value}
101+
value={option.value}
102+
onSelect={(currentValue) => {
103+
onValueChange?.(currentValue);
104+
setOpen(false);
105+
}}
106+
>
107+
{option.label}
108+
<CheckIcon
109+
className={cn(
110+
"ml-auto size-4",
111+
value === option.value ? "opacity-100" : "opacity-0"
112+
)}
113+
/>
114+
</CommandItem>
115+
))}
116+
</CommandGroup>
117+
</CommandList>
118+
</Command>
119+
</SelectContent>
120+
</Select>
121+
);
122+
}
123+
124+
// Demo компонент для Storybook
125+
const demoOptions = [
126+
{ value: "next.js", label: "Next.js" },
127+
{ value: "sveltekit", label: "SvelteKit" },
128+
{ value: "nuxt.js", label: "Nuxt.js" },
129+
{ value: "remix", label: "Remix" },
130+
{ value: "astro", label: "Astro" },
131+
];
132+
133+
export function ComboboxDemo() {
134+
const [value, setValue] = React.useState("");
135+
136+
return (
137+
<Combobox
138+
options={demoOptions}
139+
value={value}
140+
onValueChange={setValue}
141+
placeholder="Select framework..."
142+
searchPlaceholder="Search framework..."
143+
/>
144+
);
145+
}
Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,60 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
22
import { Label } from "#shadcn/components/ui/label";
33
import {
4-
Select,
5-
SelectContent,
6-
SelectGroup,
7-
SelectItem,
8-
SelectLabel,
9-
SelectTrigger,
10-
SelectValue,
4+
Select,
5+
SelectContent,
6+
SelectGroup,
7+
SelectItem,
8+
SelectLabel,
9+
SelectTrigger,
10+
SelectValue,
1111
} from "#shadcn/components/ui/select";
1212

1313
const meta = {
14-
title: "Component/Select",
14+
title: "Component/Select",
1515
} satisfies Meta;
1616
export default meta;
1717

1818
type Story = StoryObj<typeof meta>;
1919

2020
export const Demo = {
21-
render: () => (
22-
<Select>
23-
<SelectTrigger className="w-[180px]">
24-
<SelectValue placeholder="Select a fruit" />
25-
</SelectTrigger>
26-
<SelectContent>
27-
<SelectGroup>
28-
<SelectLabel>Fruits</SelectLabel>
29-
<SelectItem value="apple">Apple</SelectItem>
30-
<SelectItem value="banana">Banana</SelectItem>
31-
<SelectItem value="blueberry">Blueberry</SelectItem>
32-
<SelectItem value="grapes">Grapes</SelectItem>
33-
<SelectItem value="pineapple">Pineapple</SelectItem>
34-
</SelectGroup>
35-
</SelectContent>
36-
</Select>
37-
),
21+
render: () => (
22+
<Select>
23+
<SelectTrigger className="w-[180px]">
24+
<SelectValue placeholder="Select a fruit" />
25+
</SelectTrigger>
26+
<SelectContent>
27+
<SelectGroup>
28+
<SelectItem value="apple">Apple</SelectItem>
29+
<SelectItem value="banana">Banana</SelectItem>
30+
<SelectItem value="blueberry">Blueberry</SelectItem>
31+
<SelectItem value="grapes">Grapes</SelectItem>
32+
<SelectItem value="pineapple">Pineapple</SelectItem>
33+
</SelectGroup>
34+
</SelectContent>
35+
</Select>
36+
),
3837
} satisfies Story;
3938

4039
export const CompoundSelectWrite: Story = {
41-
render: () => (
42-
<div className="grid w-full max-w-sm items-center gap-1.5">
43-
<Label htmlFor="compound-select">Compound Select Write</Label>
44-
<Select>
45-
<SelectTrigger
46-
id="compound-select"
47-
variant="compound"
48-
className="w-[180px]"
49-
>
50-
<SelectValue placeholder="Select option" />
51-
</SelectTrigger>
52-
<SelectContent>
53-
<SelectItem value="option1">Option 1</SelectItem>
54-
<SelectItem value="option2">Option 2</SelectItem>
55-
<SelectItem value="option3">Option 3</SelectItem>
56-
<SelectItem value="option4">Option 4</SelectItem>
57-
</SelectContent>
58-
</Select>
59-
</div>
60-
),
40+
render: () => (
41+
<div className="grid w-full max-w-sm items-center gap-1.5">
42+
<Label htmlFor="compound-select">Compound Select Write</Label>
43+
<Select>
44+
<SelectTrigger
45+
id="compound-select"
46+
variant="compound"
47+
className="w-[180px]"
48+
>
49+
<SelectValue placeholder="Select option" />
50+
</SelectTrigger>
51+
<SelectContent>
52+
<SelectItem value="option1">Option 1</SelectItem>
53+
<SelectItem value="option2">Option 2</SelectItem>
54+
<SelectItem value="option3">Option 3</SelectItem>
55+
<SelectItem value="option4">Option 4</SelectItem>
56+
</SelectContent>
57+
</Select>
58+
</div>
59+
),
6160
};

0 commit comments

Comments
 (0)