Skip to content

Commit e012a2f

Browse files
committed
feat: search dynamic parameter dropdowns
1 parent 0b82f41 commit e012a2f

File tree

7 files changed

+899
-21
lines changed

7 files changed

+899
-21
lines changed

docs/about/contributing/frontend.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ All UI-related code is in the `site` folder. Key directories include:
6666
- **util** - Helper functions that can be used across the application
6767
- **static** - Static assets like images, fonts, icons, etc
6868

69+
Do not use barrel files. Imports should be directly from the file that defines
70+
the value.
71+
6972
## Routing
7073

7174
We use [react-router](https://reactrouter.com/en/main) as our routing engine.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { useState } from "react";
3+
import {
4+
SearchableSelect,
5+
SearchableSelectContent,
6+
SearchableSelectItem,
7+
SearchableSelectTrigger,
8+
SearchableSelectValue,
9+
} from "./SearchableSelect";
10+
import { GitBranch, Globe, Lock, Users } from "lucide-react";
11+
12+
const meta: Meta<typeof SearchableSelect> = {
13+
title: "components/SearchableSelect",
14+
component: SearchableSelect,
15+
args: {
16+
placeholder: "Select an option",
17+
},
18+
};
19+
20+
export default meta;
21+
type Story = StoryObj<typeof SearchableSelect>;
22+
23+
const SimpleOptions = () => {
24+
const [value, setValue] = useState("");
25+
26+
return (
27+
<SearchableSelect value={value} onValueChange={setValue}>
28+
<SearchableSelectTrigger>
29+
<SearchableSelectValue />
30+
</SearchableSelectTrigger>
31+
<SearchableSelectContent>
32+
<SearchableSelectItem value="option1">Option 1</SearchableSelectItem>
33+
<SearchableSelectItem value="option2">Option 2</SearchableSelectItem>
34+
<SearchableSelectItem value="option3">Option 3</SearchableSelectItem>
35+
<SearchableSelectItem value="option4">Option 4</SearchableSelectItem>
36+
</SearchableSelectContent>
37+
</SearchableSelect>
38+
);
39+
};
40+
41+
export const Default: Story = {
42+
render: () => <SimpleOptions />,
43+
};
44+
45+
const ManyOptionsExample = () => {
46+
const [value, setValue] = useState("");
47+
const options = Array.from({ length: 50 }, (_, i) => ({
48+
value: `option-${i + 1}`,
49+
label: `Option ${i + 1}`,
50+
}));
51+
52+
return (
53+
<SearchableSelect
54+
value={value}
55+
onValueChange={setValue}
56+
placeholder="Search from many options..."
57+
>
58+
<SearchableSelectTrigger>
59+
<SearchableSelectValue />
60+
</SearchableSelectTrigger>
61+
<SearchableSelectContent>
62+
{options.map((option) => (
63+
<SearchableSelectItem key={option.value} value={option.value}>
64+
{option.label}
65+
</SearchableSelectItem>
66+
))}
67+
</SearchableSelectContent>
68+
</SearchableSelect>
69+
);
70+
};
71+
72+
export const WithManyOptions: Story = {
73+
render: () => <ManyOptionsExample />,
74+
};
75+
76+
const WithIconsExample = () => {
77+
const [value, setValue] = useState("");
78+
79+
return (
80+
<SearchableSelect value={value} onValueChange={setValue}>
81+
<SearchableSelectTrigger>
82+
<SearchableSelectValue placeholder="Select visibility" />
83+
</SearchableSelectTrigger>
84+
<SearchableSelectContent>
85+
<SearchableSelectItem value="public">
86+
<div className="flex items-center gap-2">
87+
<Globe className="size-icon-sm" />
88+
<span>Public</span>
89+
</div>
90+
</SearchableSelectItem>
91+
<SearchableSelectItem value="private">
92+
<div className="flex items-center gap-2">
93+
<Lock className="size-icon-sm" />
94+
<span>Private</span>
95+
</div>
96+
</SearchableSelectItem>
97+
<SearchableSelectItem value="team">
98+
<div className="flex items-center gap-2">
99+
<Users className="size-icon-sm" />
100+
<span>Team only</span>
101+
</div>
102+
</SearchableSelectItem>
103+
</SearchableSelectContent>
104+
</SearchableSelect>
105+
);
106+
};
107+
108+
export const WithIcons: Story = {
109+
render: () => <WithIconsExample />,
110+
};
111+
112+
const ProgrammingLanguagesExample = () => {
113+
const [value, setValue] = useState("");
114+
const languages = [
115+
"JavaScript", "TypeScript", "Python", "Java", "C++", "C#", "Ruby",
116+
"Go", "Rust", "Swift", "Kotlin", "Scala", "PHP", "Perl", "R",
117+
"MATLAB", "Julia", "Dart", "Lua", "Haskell", "Clojure", "Elixir",
118+
"F#", "OCaml", "Erlang", "Nim", "Crystal", "Zig", "V", "Racket"
119+
];
120+
121+
return (
122+
<SearchableSelect
123+
value={value}
124+
onValueChange={setValue}
125+
placeholder="Select a programming language"
126+
>
127+
<SearchableSelectTrigger>
128+
<SearchableSelectValue />
129+
</SearchableSelectTrigger>
130+
<SearchableSelectContent>
131+
{languages.map((lang) => (
132+
<SearchableSelectItem key={lang} value={lang.toLowerCase()}>
133+
{lang}
134+
</SearchableSelectItem>
135+
))}
136+
</SearchableSelectContent>
137+
</SearchableSelect>
138+
);
139+
};
140+
141+
export const ProgrammingLanguages: Story = {
142+
render: () => <ProgrammingLanguagesExample />,
143+
};
144+
145+
const DisabledExample = () => {
146+
return (
147+
<SearchableSelect value="disabled" disabled>
148+
<SearchableSelectTrigger>
149+
<SearchableSelectValue />
150+
</SearchableSelectTrigger>
151+
<SearchableSelectContent>
152+
<SearchableSelectItem value="disabled">Disabled Option</SearchableSelectItem>
153+
</SearchableSelectContent>
154+
</SearchableSelect>
155+
);
156+
};
157+
158+
export const Disabled: Story = {
159+
render: () => <DisabledExample />,
160+
};
161+
162+
const RequiredExample = () => {
163+
const [value, setValue] = useState("");
164+
165+
return (
166+
<form onSubmit={(e) => { e.preventDefault(); alert(`Selected: ${value}`); }}>
167+
<div className="space-y-4">
168+
<SearchableSelect
169+
value={value}
170+
onValueChange={setValue}
171+
required
172+
placeholder="This field is required"
173+
>
174+
<SearchableSelectTrigger>
175+
<SearchableSelectValue />
176+
</SearchableSelectTrigger>
177+
<SearchableSelectContent>
178+
<SearchableSelectItem value="option1">Option 1</SearchableSelectItem>
179+
<SearchableSelectItem value="option2">Option 2</SearchableSelectItem>
180+
<SearchableSelectItem value="option3">Option 3</SearchableSelectItem>
181+
</SearchableSelectContent>
182+
</SearchableSelect>
183+
<button type="submit" className="px-4 py-2 bg-content-link text-white rounded">
184+
Submit
185+
</button>
186+
</div>
187+
</form>
188+
);
189+
};
190+
191+
export const Required: Story = {
192+
render: () => <RequiredExample />,
193+
};
194+
195+
const EmptyStateExample = () => {
196+
const [value, setValue] = useState("");
197+
198+
return (
199+
<SearchableSelect
200+
value={value}
201+
onValueChange={setValue}
202+
emptyMessage="No matching options found. Try a different search term."
203+
>
204+
<SearchableSelectTrigger>
205+
<SearchableSelectValue placeholder="Type to search..." />
206+
</SearchableSelectTrigger>
207+
<SearchableSelectContent>
208+
{/* Intentionally empty to show empty state */}
209+
</SearchableSelectContent>
210+
</SearchableSelect>
211+
);
212+
};
213+
214+
export const EmptyState: Story = {
215+
render: () => <EmptyStateExample />,
216+
};
217+
218+
const GitBranchesExample = () => {
219+
const [value, setValue] = useState("main");
220+
const branches = [
221+
{ name: "main", isDefault: true },
222+
{ name: "develop", isDefault: false },
223+
{ name: "feature/user-authentication", isDefault: false },
224+
{ name: "feature/payment-integration", isDefault: false },
225+
{ name: "bugfix/header-alignment", isDefault: false },
226+
{ name: "hotfix/security-patch", isDefault: false },
227+
{ name: "release/v2.0.0", isDefault: false },
228+
{ name: "chore/update-dependencies", isDefault: false },
229+
];
230+
231+
return (
232+
<SearchableSelect
233+
value={value}
234+
onValueChange={setValue}
235+
placeholder="Select a branch"
236+
>
237+
<SearchableSelectTrigger className="w-72">
238+
<SearchableSelectValue />
239+
</SearchableSelectTrigger>
240+
<SearchableSelectContent>
241+
{branches.map((branch) => (
242+
<SearchableSelectItem key={branch.name} value={branch.name}>
243+
<div className="flex items-center gap-2">
244+
<GitBranch className="size-icon-sm" />
245+
<span>{branch.name}</span>
246+
{branch.isDefault && (
247+
<span className="ml-auto text-xs text-content-secondary">default</span>
248+
)}
249+
</div>
250+
</SearchableSelectItem>
251+
))}
252+
</SearchableSelectContent>
253+
</SearchableSelect>
254+
);
255+
};
256+
257+
export const GitBranches: Story = {
258+
render: () => <GitBranchesExample />,
259+
};

0 commit comments

Comments
 (0)