Getting Started
Components
- Agent Status
- Agent Template Card
- API Key Card
- Audio Player
- Bar Visualizer
- Channel Status Card
- Chat Composer
- Code Editor
- Condition Builder
- Confirm Dialog
- Conversation
- Conversation Bar
- Dataset Card
- Document Status Badge
- Extraction Strategy Select
- File Icon
- File Upload
- Indexing Progress
- Live Waveform
- Matrix
- Message
- Mic Selector
- Model Diff View
- Model Selector
- Model Type Filter
- Node Catalog
- Option Editor
- Orb
- Output Variables View
- Package Card
- Prompt Editor
- Provider Status Card
- Radio Card
- Resource Card
- Resource Sidebar
- Response
- Run Node List
- Scrub Bar
- Segment Card
- Shimmering Text
- Speech Input
- Token Trend Chart
- Tool Call Card
- Transcript Viewer
- Usage Stat Card
- Variable Binding Editor
- Variable Selector
- Voice Button
- Voice Picker
- Waveform
- Workflow Node Card
- Workspace Selector
"use client"
import { useState } from "react"
import {
OptionEditor,
type OptionEditorItem,
} from "@/components/ui/option-editor"
const initialOptions: OptionEditorItem[] = [
{ id: "approved", label: "Approved", value: "approved" },
{ id: "needs-review", label: "Needs review", value: "needs_review" },
{ id: "blocked", label: "Blocked", value: "blocked" },
]
export function OptionEditorDemo() {
const [options, setOptions] = useState(initialOptions)
return (
<OptionEditor
value={options}
onValueChange={setOptions}
title="Review states"
className="w-full max-w-xl"
/>
)
}
Installation
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/option-editor.jsonpnpm dlx shadcn@latest add https://ui.zgi.ai/r/option-editor.json
yarn dlx shadcn@latest add https://ui.zgi.ai/r/option-editor.jsonComponent
"use client"
import * as React from "react"
import { ArrowDownIcon, ArrowUpIcon, PlusIcon, Trash2Icon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export interface OptionEditorItem {
id: string
label: string
value?: string
}
export interface OptionEditorProps
extends Omit<React.ComponentProps<"div">, "onChange" | "title"> {
value: OptionEditorItem[]
onValueChange?: (value: OptionEditorItem[]) => void
title?: React.ReactNode
addText?: string
labelPlaceholder?: string
valuePlaceholder?: string
showValue?: boolean
disabled?: boolean
}
function createOption(): OptionEditorItem {
return {
id: crypto.randomUUID(),
label: "",
value: "",
}
}
function moveItem<T>(items: T[], from: number, to: number) {
const next = [...items]
const [item] = next.splice(from, 1)
next.splice(to, 0, item)
return next
}
export function OptionEditor({
value,
onValueChange,
title = "Options",
addText = "Add option",
labelPlaceholder = "Label",
valuePlaceholder = "Value",
showValue = true,
disabled = false,
className,
...props
}: OptionEditorProps) {
function updateItem(index: number, patch: Partial<OptionEditorItem>) {
onValueChange?.(
value.map((item, itemIndex) =>
itemIndex === index ? { ...item, ...patch } : item
)
)
}
return (
<div className={cn("space-y-3", className)} {...props}>
<div className="flex items-center justify-between gap-3">
<Label className="text-sm font-medium">{title}</Label>
<Button
type="button"
variant="outline"
size="sm"
className="h-8 rounded-md"
disabled={disabled}
onClick={() => onValueChange?.([...value, createOption()])}
>
<PlusIcon className="size-4" />
{addText}
</Button>
</div>
<div className="space-y-2">
{value.map((item, index) => (
<div
key={item.id}
className="border-border bg-card grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-lg border p-2"
>
<div className="text-muted-foreground flex flex-col">
<Button
type="button"
variant="ghost"
size="icon"
className="size-7 rounded-md"
disabled={disabled || index === 0}
onClick={() =>
onValueChange?.(moveItem(value, index, index - 1))
}
aria-label="Move option up"
>
<ArrowUpIcon className="size-3.5" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="size-7 rounded-md"
disabled={disabled || index === value.length - 1}
onClick={() =>
onValueChange?.(moveItem(value, index, index + 1))
}
aria-label="Move option down"
>
<ArrowDownIcon className="size-3.5" />
</Button>
</div>
<div className={cn("grid gap-2", showValue && "sm:grid-cols-2")}>
<Input
value={item.label}
onChange={(event) =>
updateItem(index, { label: event.target.value })
}
placeholder={labelPlaceholder}
disabled={disabled}
className="h-9"
/>
{showValue ? (
<Input
value={item.value ?? ""}
onChange={(event) =>
updateItem(index, { value: event.target.value })
}
placeholder={valuePlaceholder}
disabled={disabled}
className="h-9 font-mono text-sm"
/>
) : null}
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="size-8 rounded-md"
disabled={disabled}
onClick={() =>
onValueChange?.(
value.filter((_, itemIndex) => itemIndex !== index)
)
}
aria-label="Remove option"
>
<Trash2Icon className="size-4" />
</Button>
</div>
))}
</div>
</div>
)
}
Usage
import { OptionEditor } from "@/components/ui/option-editor"
export function Example() {
return <OptionEditor value={[]} onValueChange={console.log} />
}Props
| Prop | Type | Default |
|---|---|---|
| value | OptionEditorItem[] | required |
| onValueChange | (value: OptionEditorItem[]) => void | - |
| title | ReactNode | "Options" |
| showValue | boolean | true |
| disabled | boolean | false |
Deploy and Scale Agents with ZGI
ZGI delivers the infrastructure and developer experience you need to ship reliable audio & agent applications at scale.
Talk to an expert