fix(web): unify display-field spacing (shared Field) + clearer layout help

- Every option in the custom form now renders through one `Field` wrapper (label →
  control → help at a consistent `space-y-3`), so the label→input gap is roomier and
  identical across keep-alive, the button groups, and max-displays — the first field no
  longer spaces differently from the rest.
- Reworded the multi-monitor layout help: it now says Auto is side-by-side and Manual
  gives a per-display X/Y editor "in the Live displays section below once two or more are
  streaming" — instead of pointing at an "arrangement table" that isn't visible until
  clients connect.

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:07:45 +00:00
parent 2aa7ac8c7e
commit 2e43fcc27c
3 changed files with 24 additions and 16 deletions
+1 -1
View File
@@ -100,7 +100,7 @@
"display_identity_per_client": "Pro Client", "display_identity_per_client": "Pro Client",
"display_identity_per_client_mode": "Pro Client + Auflösung", "display_identity_per_client_mode": "Pro Client + Auflösung",
"display_layout_mode": "Multi-Monitor-Anordnung", "display_layout_mode": "Multi-Monitor-Anordnung",
"display_layout_help": "Wie mehrere Anzeigen auf dem Desktop angeordnet werden. Manuell nutzt die Anordnungstabelle unten (ab 2 Anzeigen).", "display_layout_help": "Automatisch ordnet die Anzeigen nebeneinander an (links nach rechts). Manuell: Du platzierst jede selbst — ein X/Y-Editor pro Anzeige erscheint im Abschnitt „Aktive Displays“ unten, sobald zwei oder mehr streamen.",
"display_layout_auto_row": "Automatisch (nebeneinander)", "display_layout_auto_row": "Automatisch (nebeneinander)",
"display_layout_manual": "Manuell", "display_layout_manual": "Manuell",
"clients_title": "Gekoppelte Geräte", "clients_title": "Gekoppelte Geräte",
+1 -1
View File
@@ -100,7 +100,7 @@
"display_identity_per_client": "Per client", "display_identity_per_client": "Per client",
"display_identity_per_client_mode": "Per client + resolution", "display_identity_per_client_mode": "Per client + resolution",
"display_layout_mode": "Multi-monitor layout", "display_layout_mode": "Multi-monitor layout",
"display_layout_help": "How several displays are arranged on the desktop. Manual uses the arrangement table below (with 2+ displays).", "display_layout_help": "Auto lays displays out side by side, left to right. Manual: you position each one yourself — a per-display X/Y editor appears in the Live displays section below once two or more are streaming.",
"display_layout_auto_row": "Auto (side by side)", "display_layout_auto_row": "Auto (side by side)",
"display_layout_manual": "Manual", "display_layout_manual": "Manual",
"clients_title": "Paired clients", "clients_title": "Paired clients",
+22 -14
View File
@@ -1,6 +1,6 @@
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@unom/ui/button"; import { Button } from "@unom/ui/button";
import { type FC, useEffect, useState } from "react"; import { type FC, type ReactNode, useEffect, useState } from "react";
import { import {
getGetDisplayStateQueryKey, getGetDisplayStateQueryKey,
getGetDisplaySettingsQueryKey, getGetDisplaySettingsQueryKey,
@@ -210,8 +210,7 @@ const DisplayForm: FC<{
{/* Custom: every option by hand */} {/* Custom: every option by hand */}
{isCustom && ( {isCustom && (
<div className="space-y-6 rounded-lg border p-5"> <div className="space-y-6 rounded-lg border p-5">
<div className="space-y-2.5"> <Field label={m.display_keep_alive()} help={m.display_keep_alive_help()}>
<Label>{m.display_keep_alive()}</Label>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Button <Button
size="sm" size="sm"
@@ -251,8 +250,7 @@ const DisplayForm: FC<{
</div> </div>
)} )}
</div> </div>
<p className="text-xs text-muted-foreground">{m.display_keep_alive_help()}</p> </Field>
</div>
<Choice <Choice
label={m.display_topology()} label={m.display_topology()}
@@ -296,10 +294,8 @@ const DisplayForm: FC<{
} }
/> />
<div className="space-y-2.5"> <Field label={m.display_max()}>
<Label htmlFor="disp-max">{m.display_max()}</Label>
<Input <Input
id="disp-max"
type="number" type="number"
min={1} min={1}
max={16} max={16}
@@ -313,7 +309,7 @@ const DisplayForm: FC<{
}) })
} }
/> />
</div> </Field>
<div className="border-t pt-4"> <div className="border-t pt-4">
<Button onClick={() => apply(draft)} disabled={busy}> <Button onClick={() => apply(draft)} disabled={busy}>
@@ -340,7 +336,21 @@ const DisplayForm: FC<{
); );
}; };
/** A labeled row of mutually-exclusive option buttons (topology / conflict / identity / layout). */ /** A labeled config field — label, then the control, then optional help. The single source of the
* label→control→help spacing so every field (keep-alive, the button groups, max-displays) lines up. */
const Field: FC<{ label: string; help?: string; children: ReactNode }> = ({
label,
help,
children,
}) => (
<div className="space-y-3">
<Label className="block">{label}</Label>
{children}
{help && <p className="text-xs text-muted-foreground">{help}</p>}
</div>
);
/** A [`Field`] whose control is a row of mutually-exclusive option buttons (topology / conflict / …). */
const Choice: FC<{ const Choice: FC<{
label: string; label: string;
help?: string; help?: string;
@@ -350,8 +360,7 @@ const Choice: FC<{
disabled: boolean; disabled: boolean;
onPick: (v: string) => void; onPick: (v: string) => void;
}> = ({ label, help, value, options, labels, disabled, onPick }) => ( }> = ({ label, help, value, options, labels, disabled, onPick }) => (
<div className="space-y-2.5"> <Field label={label} help={help}>
<Label>{label}</Label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{options.map((o) => ( {options.map((o) => (
<Button <Button
@@ -365,8 +374,7 @@ const Choice: FC<{
</Button> </Button>
))} ))}
</div> </div>
{help && <p className="text-xs text-muted-foreground">{help}</p>} </Field>
</div>
); );
/** /**