feat: closed-set admin knobs are pickers, not free text
log_level, search_source, download_layout and the transcode hw/preset were typed by hand, and every consumer carried its own guess at what a typo meant. The value set is now a Literal in config.py: pydantic rejects a bad env value at boot (any letter case accepted), sysconfig refuses a bad override and derives the admin picker's options from the same Literal, and the consumers read the value as-is. The Configuration page renders a choice key as a segmented bar, or a dropdown past six options, with translated option names. Its tabs now follow the registry's order instead of a hand-kept copy that had already dropped the explore group. A single-choice FilterSelect now opens on the option in use: in the browser it could land on the first row instead.
This commit is contained in:
@@ -21,6 +21,9 @@ export interface FilterSelectOption {
|
||||
export interface FilterSelectProps {
|
||||
/** Visible group name shown as the trigger prefix (`Label: value`) and the accessible name. */
|
||||
label: string;
|
||||
/** Show only the value on the trigger — for a form row whose own caption already names the field.
|
||||
* The label still names the control and its menu for assistive tech. */
|
||||
hideLabel?: boolean;
|
||||
options: readonly FilterSelectOption[];
|
||||
selected: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
@@ -39,6 +42,7 @@ export default function FilterSelect({
|
||||
onChange,
|
||||
multiple = false,
|
||||
requireOne = true,
|
||||
hideLabel = false,
|
||||
testId,
|
||||
}: FilterSelectProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -47,6 +51,14 @@ export default function FilterSelect({
|
||||
onOpenChange: setOpen,
|
||||
mode: "menu",
|
||||
placement: "bottom-start",
|
||||
// Single choice: opening lands on the option in use, not the first row. A multi-select toggle
|
||||
// list has no one "current" row, so it keeps the default.
|
||||
selectedIndex: multiple
|
||||
? null
|
||||
: Math.max(
|
||||
0,
|
||||
options.findIndex((o) => o.value === selected[0]),
|
||||
),
|
||||
});
|
||||
|
||||
const labelOf = (v: string) => options.find((o) => o.value === v)?.label ?? v;
|
||||
@@ -88,7 +100,7 @@ export default function FilterSelect({
|
||||
open ? "border-accent" : "border-border"
|
||||
}`}
|
||||
>
|
||||
<span className="text-muted">{label}:</span>
|
||||
{!hideLabel && <span className="text-muted">{label}:</span>}
|
||||
<span className="text-fg">{summary}</span>
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user