TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { Plus, X } from "lucide-react";4import { Button } from "@/components/ui/button";5import { Input } from "@/components/ui/input";6import { newKv, type KV } from "./types";78/** Editable key/value rows (headers, query params, cookies). */9export function KvRows({10 rows,11 onChange,12 keyPlaceholder = "Name",13 valuePlaceholder = "Value",14 addLabel = "Add",15 label,16 secret,17}: {18 rows: KV[];19 onChange: (rows: KV[]) => void;20 keyPlaceholder?: string;21 valuePlaceholder?: string;22 addLabel?: string;23 /** Accessible label prefix for inputs. */24 label: string;25 /** Mask values (e.g. cookies). */26 secret?: boolean;27}) {28 const update = (id: string, patch: Partial<KV>) => onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));29 const remove = (id: string) => onChange(rows.filter((r) => r.id !== id));30 const add = () => onChange([...rows, newKv()]);31 const lastKeyRef = React.useRef<HTMLInputElement | null>(null);32 const [focusNew, setFocusNew] = React.useState(false);3334 React.useEffect(() => {35 if (focusNew && lastKeyRef.current) {36 lastKeyRef.current.focus();37 setFocusNew(false);38 }39 }, [focusNew, rows.length]);4041 return (42 <div className="grid gap-1.5">43 {rows.length === 0 ? <p className="text-xs text-fg-subtle">None.</p> : null}44 {rows.map((r, i) => (45 <div key={r.id} className="grid grid-cols-[minmax(0,2fr)_minmax(0,3fr)_auto] items-center gap-1.5">46 <Input47 ref={i === rows.length - 1 ? lastKeyRef : undefined}48 value={r.key}49 onChange={(e) => update(r.id, { key: e.target.value })}50 placeholder={keyPlaceholder}51 aria-label={`${label} ${i + 1} name`}52 className="h-8 font-mono text-[12.5px]"53 autoCapitalize="off"54 autoCorrect="off"55 spellCheck={false}56 />57 <Input58 value={r.value}59 type={secret ? "password" : "text"}60 onChange={(e) => update(r.id, { value: e.target.value })}61 placeholder={valuePlaceholder}62 aria-label={`${label} ${i + 1} value`}63 className="h-8 font-mono text-[12.5px]"64 autoCapitalize="off"65 autoCorrect="off"66 spellCheck={false}67 onKeyDown={(e) => {68 if (e.key === "Enter" && i === rows.length - 1 && r.key.trim()) {69 e.preventDefault();70 add();71 setFocusNew(true);72 }73 }}74 />75 <Button type="button" variant="ghost" size="icon-sm" className="size-8 text-fg-subtle hover:text-danger" onClick={() => remove(r.id)} aria-label={`Remove ${label.toLowerCase()} ${i + 1}`}>76 <X />77 </Button>78 </div>79 ))}80 <div>81 <Button82 type="button"83 variant="ghost"84 size="xs"85 className="-ml-1 text-fg-muted"86 onClick={() => {87 add();88 setFocusNew(true);89 }}90 >91 <Plus className="size-3.5" /> {addLabel}92 </Button>93 </div>94 </div>95 );96}97