diff --git a/docs/CONFIG_REFERENCE.md b/docs/CONFIG_REFERENCE.md
index 213114c..1602aad 100644
--- a/docs/CONFIG_REFERENCE.md
+++ b/docs/CONFIG_REFERENCE.md
@@ -101,7 +101,7 @@ Type-specific fields:
| `string` | `text` (or `password` if `secret: true`) | `textarea` (needs `multiline: true`), `password` |
| `number` | `number` | `slider` (requires both `min` and `max`) |
| `boolean` | `checkbox` | `switch` |
-| `enum` | `select` | `radio` |
+| `enum` | `select` | `radio`, `combobox` (searchable, single-select) |
| `multiselect` | `multiselect` (combobox) | `checkboxGroup` |
### `passAs` semantics
diff --git a/src/components/forms/controls/combobox.tsx b/src/components/forms/controls/combobox.tsx
new file mode 100644
index 0000000..0bc423a
--- /dev/null
+++ b/src/components/forms/controls/combobox.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { useState } from "react";
+import { Check, ChevronsUpDown } from "lucide-react";
+import { buttonVariants } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+
+interface ComboboxProps {
+ choices: string[];
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export function Combobox({
+ choices,
+ value,
+ onChange,
+ placeholder = "Select...",
+}: ComboboxProps) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+
+ {value || placeholder}
+
+
+
+
+
+
+
+ No matches.
+
+ {choices.map((choice) => (
+ {
+ onChange(choice);
+ setOpen(false);
+ }}
+ >
+
+ {choice}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/components/forms/field-renderer.tsx b/src/components/forms/field-renderer.tsx
index c39c002..ab8562a 100644
--- a/src/components/forms/field-renderer.tsx
+++ b/src/components/forms/field-renderer.tsx
@@ -24,6 +24,7 @@ import {
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { MultiSelect } from "./controls/multi-select";
+import { Combobox } from "./controls/combobox";
import type { ClientVariable } from "@/lib/config/serialize";
export function FieldRenderer({ variable }: { variable: ClientVariable }) {
@@ -211,6 +212,31 @@ export function FieldRenderer({ variable }: { variable: ClientVariable }) {
);
+ case "combobox":
+ return (
+
+
+ {label}
+ {variable.required && (
+ *
+ )}
+
+
+ field.onChange(value)}
+ />
+
+ {variable.description && (
+ {variable.description}
+ )}
+
+
+ );
+
case "radio":
return (
diff --git a/src/lib/config/schema.ts b/src/lib/config/schema.ts
index ab4b7a2..1fe994e 100644
--- a/src/lib/config/schema.ts
+++ b/src/lib/config/schema.ts
@@ -18,6 +18,7 @@ const controlSchema = z.enum([
"switch",
"select",
"radio",
+ "combobox",
"multiselect",
"checkboxGroup",
]);
@@ -115,6 +116,13 @@ const variableWithChecks = variableSchema.superRefine((variable, ctx) => {
path: ["control"],
});
}
+ if (variable.control === "combobox" && variable.type !== "enum") {
+ ctx.addIssue({
+ code: "custom",
+ message: "control 'combobox' requires type 'enum'",
+ path: ["control"],
+ });
+ }
if (
variable.control === "slider" &&
variable.type === "number" &&