Files
silo-server/web/src/components/settings/SettingSlider.tsx
T
QuickandClaude Fable 5 a28960819d fix(settings): make device setting controls work and fit the device
The device settings screen shipped with three defects:

- Sliders (audio sync, playback speed, subtitle sync, next-up prompt,
  sleep timer) were rendered controlled with only onValueCommit, so the
  thumb never moved and no gesture could change the value. A shared
  SettingSlider now keeps a draft during the drag and commits once on
  release; the admin RegistrySettingControl had the same bug and uses it
  too.
- "Change how they look" called an onOpenPanel the page never passed.
  The screen now opens the shared SubtitleAppearancePanelView scoped to
  the selected device and profile, with a reset that clears the row.
- Every setting was shown for every device, so a browser offered
  "Screen orientation" and native-only toggles. settingsgen now emits
  the manifest's advisory platforms field into the TypeScript contract,
  and the screen hides settings whose platforms exclude the target
  device — unless the device stores a value, which must stay clearable.
  audio_sync_ms, dolby_vision_enabled and dv_profile7_hdr10_fallback
  gain native-only tags (no web consumer exists); platforms is advisory
  UI metadata, so no revision bump.

Kotlin/Swift generators deliberately unchanged: the native clients
hardcode their own applicability today, and their vendored bindings are
regenerated from their own repos.

Part of #215

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:15:51 +00:00

68 lines
2.0 KiB
TypeScript

import { useState } from "react";
import { Slider } from "@/components/ui/slider";
interface SettingSliderProps {
/** The persisted value. The thumb returns here if a save is rejected. */
value: number;
min?: number;
max?: number;
step?: number;
unit?: string;
disabled?: boolean;
"aria-label"?: string;
/** Fired once per gesture, when the thumb is released or a key is pressed. */
onCommit: (value: number) => void;
className?: string;
}
/**
* A slider bound to a saved setting.
*
* Radix's Slider is controlled here, so the thumb only moves if something
* re-renders it with a new value. Persisting on every pointer move would flood
* the API, but rendering only the saved value makes the control inert — the
* thumb snaps back under the pointer and onValueCommit never sees a change,
* which is exactly the bug this component exists to prevent. While a drag is
* in flight the draft value drives the thumb and the readout; on release the
* draft is committed and cleared, and the saved value takes over again.
*/
export function SettingSlider({
value,
min,
max,
step,
unit,
disabled = false,
"aria-label": ariaLabel,
onCommit,
className,
}: SettingSliderProps) {
const [draft, setDraft] = useState<number | null>(null);
const shown = draft ?? value;
return (
<div className={className ?? "flex w-full items-center gap-3 sm:max-w-[260px]"}>
<Slider
value={[shown]}
min={min}
max={max}
step={step}
disabled={disabled}
aria-label={ariaLabel}
thumbLabels={ariaLabel ? [ariaLabel] : undefined}
onValueChange={(values) => setDraft(values[0] ?? shown)}
onValueCommit={(values) => {
setDraft(null);
const next = values[0];
if (next !== undefined && next !== value) onCommit(next);
}}
/>
<span className="text-muted-foreground min-w-16 text-right text-xs font-medium tabular-nums">
{shown}
{unit ? ` ${unit}` : ""}
</span>
</div>
);
}