import { useState, type ReactNode } from 'react' import { Check, ChevronDown } from 'lucide-react' import ToggleSwitch from '../Settings/ToggleSwitch' /** * The inspector's vocabulary. * * ── Why this file exists ───────────────────────────────────────────────── * * The panel had grown four ways of saying "this one is on" — a filled pill, a * ring, a tinted border, ink on a tint — and used the *same* pill for a choice * between four things and for a switch that turns one thing off. A map showed * seventy controls in ten sections that were all open at once, all labelled in * the same small capitals, so nothing on the panel looked more important than * anything else. None of that was a decision; it was what happens when a panel * grows one control at a time. * * So there are now exactly three ways to answer a question here, and each looks * like what it is: * * **Choose one** is a row of chips, and the chosen one is filled. * **Choose several** is a row of chips that carry a tick when they are on, * because a set with three of seven picked has to be readable at a glance. * **On or off** is the app's switch, on its own line beside the words — * the one control in TREK that already means exactly that. * * A section can be folded away, which is the other half of the answer: a * property panel is a reference, and a reference nobody can collapse is a wall. */ /** * One group of properties. * * `defaultOpen={false}` is for the groups you set once and rarely return to — * the frame's numbers, mostly. The state is per mounted section rather than * remembered: coming back to an element and finding its panel folded up * differently from last time is worse than opening one group again. */ export function Section({ label, children, defaultOpen = true, hint, badge }: { label: string children: ReactNode defaultOpen?: boolean /** A line under the label, for a group whose name cannot carry the whole idea. */ hint?: string /** * A figure beside the label, for a group worth reading while it is folded. * The stops list is the case: how many of them count is the thing you came * to check, and opening a section to read one number is a section that * cannot usefully be closed. */ badge?: string }) { const [open, setOpen] = useState(defaultOpen) return (
{open && (
{hint &&

{hint}

} {children}
)}
) } /** A labelled line. The label sits left, the control fills what is left. */ export function Line({ label, children, wide = false }: { label: string children: ReactNode /** Put the control under the label rather than beside it, when it needs the width. */ wide?: boolean }) { return (
{label}
{children}
) } /** Choose one of a few. The chosen one is filled in. */ export function Choice({ value, options, onPick, ariaLabel }: { value: T options: { value: T; label: ReactNode; title?: string; disabled?: boolean }[] onPick: (v: T) => void ariaLabel?: string }) { return (
{options.map(o => ( ))}
) } /** * Choose several. A tick marks what is in the set. * * Not switches: seven metrics as seven switched lines is a column of switches * taller than the element it configures, and what matters about a set is which * of it is in — a question a row answers and a list does not. */ export function Picks({ value, options, onToggle }: { value: readonly T[] options: { value: T; label: ReactNode; disabled?: boolean }[] onToggle: (v: T) => void }) { return (
{options.map(o => { const on = value.includes(o.value) return ( ) })}
) } /** On or off, said the way the rest of TREK says it. */ export function Switch({ label, on, onToggle }: { label: string; on: boolean; onToggle: () => void }) { return (
{label}
) } /** * A number, with its unit. * * Held as text while it is being typed and only read back on blur or Enter, * which fixes three things at once: "0." no longer snaps to "0" the moment the * point is typed, an emptied field no longer collapses the element to zero, * and a value is one undo step rather than one per keystroke. */ export function NumField({ label, value, onChange, min, max, step = 0.5, unit, }: { label: string value: number onChange: (v: number) => void min?: number max?: number step?: number unit?: string }) { const shown = String(Math.round(value * 100) / 100) const [draft, setDraft] = useState(null) const commit = (raw: string) => { setDraft(null) const v = Number(raw) if (raw.trim() === '' || !Number.isFinite(v)) return const clamped = Math.min(max ?? Infinity, Math.max(min ?? -Infinity, v)) if (clamped !== value) onChange(clamped) } return ( ) }