"use client"; /** * Thin settings primitives (settings-app wave): the control kit the * settings dialog needs. Sections → groups → rows; everything SSRs clean * (render tests pin the sections' markup). * * The dependency graying (owner, 2026-09-20) follows the graph viewer's * render-panel convention: the WHOLE row dims (opacity-40, label and * control together) and the control itself locks — the row owns the dim. */ import { useState } from "react"; import { cn } from "@/lib/utils"; /** A section block: pronounced mono heading + content. */ export function SettingsSection({ title, hint, children, testid, }: { title: string; /** One quiet line under the header — only where it earns its place. */ hint?: string; children: React.ReactNode; testid?: string; }) { return (

{title}

{hint &&

{hint}

}
{children}
); } /** A labeled sub-group inside a section (owner, 2026-09-20: clear * separation — Models / Tools / Prompts read as three blocks). */ export function SettingsGroup({ label, children, testid, }: { label: string; children: React.ReactNode; testid?: string; }) { return (

{label}

{children}
); } /** One label + one control. Label left (with optional muted hint), control * right — the settings row shape. */ export function SettingsRow({ label, hint, children, testid, dimmed, }: { label: string; hint?: string; children: React.ReactNode; testid?: string; /** Dependency graying, graph-view style: the whole row dims when the * control is inert (parent off, no file yet, …). */ dimmed?: boolean; }) { return (
{label} {hint && {hint}} {children}
); } /** The on/off switch — a plain button in the switch role. */ export function Toggle({ checked, disabled, onChange, label, }: { checked: boolean; disabled?: boolean; onChange: (next: boolean) => void; /** Accessible name for the switch role. */ label: string; }) { return ( ); } /** A thin select — native element, options as given. */ export function ThinSelect({ value, onChange, children, ariaLabel, testid, disabled, }: { value: string; onChange: (next: string) => void; children: React.ReactNode; ariaLabel: string; testid?: string; disabled?: boolean; }) { return ( ); } /** A thin hex/text input committing on blur + Enter (settings pace, not * typing pace — every commit is a section PUT). */ export function CommitInput({ value, placeholder, onCommit, ariaLabel, testid, type = "text", }: { /** "" = unset (the factory default). Mirrors the STORED record — * after a clamped/normalized commit this flows back and the field * snaps to the stored truth (owner repro: a negative size kept * displaying "-5" while 12 was stored). */ value: string; placeholder?: string; onCommit: (next: string) => void; ariaLabel: string; testid?: string; type?: "text" | "number"; }) { // The draft exists only while editing; a blur shows the record's value // again — commit-on-blur/Enter (owner ruling), the field honest about // what landed. const [draft, setDraft] = useState(value); const [editing, setEditing] = useState(false); return ( { setEditing(true); setDraft(e.target.value); }} onBlur={() => { if (!editing) return; setEditing(false); const next = draft.trim(); if (next !== value) onCommit(next); }} onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); if (e.key === "Escape") { setEditing(false); e.currentTarget.blur(); } }} className="h-7 w-40 rounded-sm border border-border bg-background px-2 font-ui text-xs text-foreground outline-none placeholder:text-muted-foreground/60" /> ); }