/** * @file SettingsRow — the settings page's row primitives: a titled card * (`SettingsSection`), a label/description | control row (`SettingsRow`), a * switch row (`SettingsSwitchRow`) and a segmented radio group * (`SettingsSegmented`, arrow keys + one roving tab stop). Rows divide * themselves with a hairline, none after the last, so a section is just its * rows in order. * @author Nguyễn Ngọc Trí Vĩ */ import { Switch } from "../Switch"; import type { KeyboardEvent, ReactNode } from "react"; export function SettingsSection({ id, title, description, children, }: { /** `
` — a deep-link target. */ id?: string; title: ReactNode; description?: ReactNode; children: ReactNode; }) { return (

{title}

{description &&

{description}

}
{children}
); } export function SettingsRow({ label, description, control, alignTop = false, id, }: { label: ReactNode; description?: ReactNode; control: ReactNode; alignTop?: boolean; /** A deep-link anchor (`#id`) for one row inside a grouped section. */ id?: string; }) { return (

{label}

{description &&

{description}

}
{control}
); } export function SettingsSwitchRow({ label, description, checked, onChange, disabled = false, id, }: { label: string; description?: ReactNode; checked: boolean; onChange: (v: boolean) => void; disabled?: boolean; id?: string; }) { return ( } /> ); } export function SettingsSegmented({ options, value, onChange, ariaLabel, }: { options: { value: T; label: ReactNode }[]; value: T; onChange: (v: T) => void; ariaLabel?: string; }) { // A radio group's keyboard contract: one tab stop (the checked option), and // the arrow keys move the selection — wrapping — and the focus with it. const onKeyDown = (e: KeyboardEvent) => { const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key]; if (!step || options.length === 0) return; e.preventDefault(); const at = Math.max( 0, options.findIndex((o) => o.value === value) ); const next = (at + step + options.length) % options.length; onChange(options[next]!.value); (e.currentTarget.children[next] as HTMLElement | undefined)?.focus(); }; return (
{options.map((o, i) => ( ))}
); }