Files
silo-server/web/src/components/settings/RegistrySettingControl.tsx
T

100 lines
3.0 KiB
TypeScript
Raw Normal View History

2026-05-22 20:26:11 -04:00
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { controlKindFor, optionsFor, type SettingDisplay } from "@/lib/settingsDisplay";
2026-05-22 20:26:11 -04:00
const EMPTY_SELECT_VALUE = "__empty__";
interface RegistrySettingControlProps {
/**
* The generated definition this control edits, as
* {@link SettingDisplay} resolves it from SETTING_DEFINITIONS.
*/
definition: SettingDisplay;
2026-05-22 20:26:11 -04:00
value: string;
disabled?: boolean;
onChange: (value: string) => void;
}
/**
* Renders one admin-facing setting control from the generated contract.
*
* The control shape, bounds, and option list all come from the manifest, so a
* definition the server knows and this build does not is impossible to edit
* with the wrong widget — the hand-written registry this used to read could
* (and did) disagree with the server about a setting's type and range.
*
* Values are strings here on purpose: the admin surface edits the string form
* and re-types on save through the contract (see
* hooks/queries/admin/users.ts), which keeps this component free of the typed
* JSON round trip.
*/
2026-05-22 20:26:11 -04:00
export function RegistrySettingControl({
definition,
value,
disabled = false,
onChange,
}: RegistrySettingControlProps) {
const control = controlKindFor(definition);
if (control === "switch") {
2026-05-22 20:26:11 -04:00
return (
<Switch
checked={value === "true"}
disabled={disabled}
onCheckedChange={(checked) => onChange(checked ? "true" : "false")}
/>
);
}
if (control === "slider" || control === "stepper") {
const fallback = Number(definition.defaultValue ?? 0);
const parsed = Number(value);
const numericValue = Number.isFinite(parsed) && value !== "" ? parsed : fallback;
2026-05-22 20:26:11 -04:00
return (
<div className="flex w-full max-w-[260px] items-center gap-3">
<Slider
value={[numericValue]}
min={definition.minimum}
max={definition.maximum}
2026-05-22 20:26:11 -04:00
step={definition.step}
disabled={disabled}
onValueCommit={(values) => onChange(String(values[0] ?? numericValue))}
/>
<span className="text-muted-foreground min-w-16 text-right text-xs font-medium">
{numericValue}
{definition.unit ? ` ${definition.unit}` : ""}
</span>
</div>
);
}
return (
<Select
value={value === "" ? EMPTY_SELECT_VALUE : value}
onValueChange={(nextValue) => onChange(nextValue === EMPTY_SELECT_VALUE ? "" : nextValue)}
disabled={disabled}
>
<SelectTrigger className="w-full min-w-[180px] sm:w-[220px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{optionsFor(definition).map((option) => (
2026-05-22 20:26:11 -04:00
<SelectItem
key={option.value || EMPTY_SELECT_VALUE}
value={option.value === "" ? EMPTY_SELECT_VALUE : option.value}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}