{ "name": "request-builder", "type": "registry:ui", "title": "Request Builder", "description": "Method, URL, headers, query and body. Query parameters are edited as rows and the assembled URL is shown beneath — hand-editing a query string is where encoding bugs come from.", "category": "API", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "button", "collapsible", "input", "lib-motion", "lib-styles", "lib-utils", "select", "switch", "textarea" ], "files": [ { "path": "components/ui/request-builder.tsx", "type": "registry:ui", "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Plus, Send, Trash2 } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Select } from '@/components/ui/select'\nimport { Switch } from '@/components/ui/switch'\nimport { Badge } from '@/components/ui/badge'\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'\nimport { Textarea } from '@/components/ui/textarea'\nimport { enterFade } from '@/lib/motion'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Compose an HTTP request: method, URL, headers, query and body.\n *\n * Query parameters are edited as rows, not as a query string. Hand-editing a\n * URL is where encoding bugs come from — a `&` inside a value silently becomes\n * a parameter boundary — and the assembled URL is shown read-only underneath so\n * you can still see exactly what will be sent.\n *\n * Query, headers and body are stacked disclosures rather than tabs. Their\n * heights differ a lot, so a shared tab panel resizes the card on every switch\n * and the strip moves out from under the pointer. Stacking also lets you see\n * the parameters and the body at once, which is what composing a request takes.\n *\n * Rows can be disabled rather than only deleted. Toggling a header off to test\n * something and back on again is the actual workflow; deleting it means\n * retyping it.\n *\n * The body tab is disabled for methods that do not carry one. GET with a body\n * is legal in the RFC, widely unsupported in practice, and offering it invites\n * an afternoon debugging a proxy that stripped it.\n */\nexport type RequestRow = { id: string; name: string; value: string; enabled?: boolean }\n\nconst METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']\nconst BODYLESS = new Set(['GET', 'HEAD'])\n\nfunction RequestBuilder({\n method = 'GET',\n onMethodChange,\n url = '',\n onUrlChange,\n headers = [],\n onHeadersChange,\n params = [],\n onParamsChange,\n body = '',\n onBodyChange,\n onSend,\n sending = false,\n urlPlaceholder = 'https://api.example.com/v1/users',\n urlLabel = 'Request URL',\n sendLabel = 'Send',\n headersLabel = 'Headers',\n paramsLabel = 'Query',\n bodyLabel = 'Body',\n addLabel = 'Add row',\n removeLabel = 'Remove row',\n enableLabel = 'Include this row',\n nameePlaceholder = 'name',\n valuePlaceholder = 'value',\n className,\n ...props\n}: Omit, 'onChange' | 'children'> & {\n method?: string\n onMethodChange?: (method: string) => void\n url?: string\n onUrlChange?: (url: string) => void\n headers?: RequestRow[]\n onHeadersChange?: (rows: RequestRow[]) => void\n params?: RequestRow[]\n onParamsChange?: (rows: RequestRow[]) => void\n body?: string\n onBodyChange?: (body: string) => void\n onSend?: () => void\n sending?: boolean\n urlPlaceholder?: string\n urlLabel?: string\n sendLabel?: ReactNode\n headersLabel?: ReactNode\n paramsLabel?: ReactNode\n bodyLabel?: ReactNode\n addLabel?: ReactNode\n removeLabel?: string\n enableLabel?: string\n nameePlaceholder?: string\n valuePlaceholder?: string\n}) {\n const bodyless = BODYLESS.has(method.toUpperCase())\n\n // Built with URL so values are encoded properly, not concatenated.\n const assembled = (() => {\n const active = params.filter((row) => row.enabled !== false && row.name)\n if (!url) return ''\n try {\n const parsed = new URL(url.includes('://') ? url : `https://${url}`)\n for (const row of active) parsed.searchParams.set(row.name, row.value)\n return parsed.toString()\n } catch {\n return url\n }\n })()\n\n const rowEditor = (\n rows: RequestRow[],\n onChange: ((rows: RequestRow[]) => void) | undefined,\n ) => (\n
\n {rows.map((row, index) => (\n
\n {/* Disable, don't delete — toggling off and on is the real workflow. */}\n \n onChange?.(\n rows.map((r, i) => (i === index ? { ...r, enabled: event.target.checked } : r)),\n )\n }\n />\n \n onChange?.(rows.map((r, i) => (i === index ? { ...r, name: event.target.value } : r)))\n }\n />\n \n onChange?.(rows.map((r, i) => (i === index ? { ...r, value: event.target.value } : r)))\n }\n />\n onChange?.(rows.filter((_, i) => i !== index))}\n >\n \n \n
\n ))}\n \n onChange?.([...rows, { id: `row-${Date.now()}`, name: '', value: '', enabled: true }])\n }\n >\n \n {addLabel}\n \n
\n )\n\n return (\n \n
\n onMethodChange?.(next)}\n options={METHODS.map((m) => ({ value: m, label: m }))}\n />\n onUrlChange?.(event.target.value)}\n />\n {onSend && (\n \n )}\n
\n\n
\n {rowEditor(params, onParamsChange)}\n
\n\n
\n {rowEditor(headers, onHeadersChange)}\n
\n\n {/* A GET body is legal in the RFC and widely stripped by proxies.\n Offering it invites an afternoon of debugging. */}\n {!bodyless && (\n
\n onBodyChange?.(event.target.value)}\n />\n
\n )}\n\n {assembled && (\n

\n {assembled}\n

\n )}\n \n )\n}\n\n/** One disclosure row, at module scope so it is a stable component type. */\nfunction Section({\n title,\n count,\n defaultOpen = false,\n children,\n}: {\n title: ReactNode\n count?: number\n defaultOpen?: boolean\n children: ReactNode\n}) {\n return (\n \n \n {title}\n {count !== undefined && count > 0 && (\n \n {count}\n \n )}\n \n {/* Padding goes on the inner content, not on CollapsibleContent itself:\n that element's height is animated from its child's measured height,\n so padding on the wrapper is excluded from the measurement and the\n panel opens to the padding alone. */}\n \n
{children}
\n
\n
\n )\n}\n\nexport { RequestBuilder }\n" } ] }