fix(web): explicit keep-alive Off/Keep toggle + roomier custom display form

Two UX fixes on the Virtual displays Configuration card:

- Keep-alive is no longer implicitly "on" by typing in the seconds field. It's an
  explicit two-button toggle — **Off** (tear down at disconnect) vs. **Keep for** [N]
  seconds — and the seconds input only appears when "Keep for" is selected. The
  duration is remembered across toggles, and the help text explains both modes.
- Opened up the cramped custom form: the fields container is `space-y-6` with more
  padding (`p-5`, rounded-lg), each option group is `space-y-2.5`, and the Save button
  sits below a divider — so it reads as sections with room instead of a pressed stack.

web tsc + vite build + biome-lint green; deployed on .21.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 15:00:57 +00:00
parent 6b4f9f86ed
commit 2aa7ac8c7e
3 changed files with 44 additions and 29 deletions
+2 -1
View File
@@ -60,6 +60,7 @@
"display_preset_workstation": "Workstation",
"display_keep_alive": "Nach Trennung aktiv halten",
"display_keep_alive_off": "Aus",
"display_keep_alive_keep": "Behalten für",
"display_keep_alive_seconds": "Sekunden",
"display_topology": "Topologie",
"display_topology_auto": "Automatisch",
@@ -85,7 +86,7 @@
"display_custom_desc": "Jede Option selbst festlegen.",
"display_preset_current": "Aktiv",
"display_preset_soon": "in Kürze",
"display_keep_alive_help": "Wie lange eine Anzeige (und bei gamescope ihr Spiel) nach dem Trennen bestehen bleibt. 0 = sofort abbauen.",
"display_keep_alive_help": "„Aus“ baut die Anzeige sofort beim Trennen ab. Halte sie (und bei gamescope ihr Spiel) am Leben, damit ein schnelles Wiederverbinden sofort fortsetzt, statt neu aufzubauen.",
"display_topology_help": "Was mit den physischen Monitoren des Hosts während des Streamings geschieht.",
"display_conflict": "Wenn ein weiterer Client verbindet",
"display_conflict_help": "Was passiert, wenn ein zweiter Client verbindet, während bereits gestreamt wird, und eine andere Auflösung anfragt.",
+2 -1
View File
@@ -60,6 +60,7 @@
"display_preset_workstation": "Workstation",
"display_keep_alive": "Keep alive after disconnect",
"display_keep_alive_off": "Off",
"display_keep_alive_keep": "Keep for",
"display_keep_alive_seconds": "seconds",
"display_topology": "Topology",
"display_topology_auto": "Automatic",
@@ -85,7 +86,7 @@
"display_custom_desc": "Set every option yourself.",
"display_preset_current": "Active",
"display_preset_soon": "coming soon",
"display_keep_alive_help": "How long a display (and, on gamescope, its game) survives after the client disconnects. 0 = tear down immediately.",
"display_keep_alive_help": "Off tears the display down as soon as the client disconnects. Keep it alive (and, on gamescope, its game) so a quick reconnect resumes instantly instead of rebuilding.",
"display_topology_help": "What happens to the host's physical monitors while streaming.",
"display_conflict": "When another client connects",
"display_conflict_help": "What happens if a second client connects while one is already streaming and asks for a different resolution.",
+40 -27
View File
@@ -147,10 +147,11 @@ const DisplayForm: FC<{
};
const ka = customFields.keep_alive;
const secondsValue = ka.mode === "duration" ? ka.seconds : 300;
// The duration value, remembered across the Off/Keep toggle so switching back restores it.
const [keepSecs, setKeepSecs] = useState(ka.mode === "duration" ? ka.seconds : 300);
return (
<div className="space-y-4">
<div className="space-y-6">
{/* One-click presets — a 2-up grid so each has room to breathe */}
<div className="space-y-3">
<Label className="text-base font-semibold">{m.display_preset()}</Label>
@@ -208,10 +209,10 @@ const DisplayForm: FC<{
{/* Custom: every option by hand */}
{isCustom && (
<div className="space-y-4 rounded-md border p-4">
<div className="space-y-2">
<div className="space-y-6 rounded-lg border p-5">
<div className="space-y-2.5">
<Label>{m.display_keep_alive()}</Label>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
variant={ka.mode === "off" ? "default" : "outline"}
@@ -220,25 +221,35 @@ const DisplayForm: FC<{
>
{m.display_keep_alive_off()}
</Button>
<Input
type="number"
min={0}
className="w-24"
value={secondsValue}
<Button
size="sm"
variant={ka.mode === "duration" ? "default" : "outline"}
disabled={busy}
onChange={(e) =>
setDraft({
...draft,
keep_alive: {
mode: "duration",
seconds: Math.max(0, Number(e.target.value) || 0),
},
})
onClick={() =>
setDraft({ ...draft, keep_alive: { mode: "duration", seconds: keepSecs } })
}
/>
<span className="text-sm text-muted-foreground">
{m.display_keep_alive_seconds()}
</span>
>
{m.display_keep_alive_keep()}
</Button>
{ka.mode === "duration" && (
<div className="flex items-center gap-2">
<Input
type="number"
min={0}
className="w-24"
value={ka.seconds}
disabled={busy}
onChange={(e) => {
const n = Math.max(0, Number(e.target.value) || 0);
setKeepSecs(n);
setDraft({ ...draft, keep_alive: { mode: "duration", seconds: n } });
}}
/>
<span className="text-sm text-muted-foreground">
{m.display_keep_alive_seconds()}
</span>
</div>
)}
</div>
<p className="text-xs text-muted-foreground">{m.display_keep_alive_help()}</p>
</div>
@@ -285,7 +296,7 @@ const DisplayForm: FC<{
}
/>
<div className="space-y-2">
<div className="space-y-2.5">
<Label htmlFor="disp-max">{m.display_max()}</Label>
<Input
id="disp-max"
@@ -304,9 +315,11 @@ const DisplayForm: FC<{
/>
</div>
<Button onClick={() => apply(draft)} disabled={busy}>
{m.display_save()}
</Button>
<div className="border-t pt-4">
<Button onClick={() => apply(draft)} disabled={busy}>
{m.display_save()}
</Button>
</div>
</div>
)}
@@ -337,7 +350,7 @@ const Choice: FC<{
disabled: boolean;
onPick: (v: string) => void;
}> = ({ label, help, value, options, labels, disabled, onPick }) => (
<div className="space-y-2">
<div className="space-y-2.5">
<Label>{label}</Label>
<div className="flex flex-wrap gap-2">
{options.map((o) => (