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 (
)
}
/** 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 (
)
}