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_mode": "Pro Client + Auflösung",
"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_manual": "Manuell",
"clients_title": "Gekoppelte Geräte",
+1 -1
View File
@@ -100,7 +100,7 @@
"display_identity_per_client": "Per client",
"display_identity_per_client_mode": "Per client + resolution",
"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_manual": "Manual",
"clients_title": "Paired clients",
+22 -14
View File
@@ -1,6 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@unom/ui/button";
import { type FC, useEffect, useState } from "react";
import { type FC, type ReactNode, useEffect, useState } from "react";
import {
getGetDisplayStateQueryKey,
getGetDisplaySettingsQueryKey,
@@ -210,8 +210,7 @@ const DisplayForm: FC<{
{/* Custom: every option by hand */}
{isCustom && (
<div className="space-y-6 rounded-lg border p-5">
<div className="space-y-2.5">
<Label>{m.display_keep_alive()}</Label>
<Field label={m.display_keep_alive()} help={m.display_keep_alive_help()}>
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
@@ -251,8 +250,7 @@ const DisplayForm: FC<{
</div>
)}
</div>
<p className="text-xs text-muted-foreground">{m.display_keep_alive_help()}</p>
</div>
</Field>
<Choice
label={m.display_topology()}
@@ -296,10 +294,8 @@ const DisplayForm: FC<{
}
/>
<div className="space-y-2.5">
<Label htmlFor="disp-max">{m.display_max()}</Label>
<Field label={m.display_max()}>
<Input
id="disp-max"
type="number"
min={1}
max={16}
@@ -313,7 +309,7 @@ const DisplayForm: FC<{
})
}
/>
</div>
</Field>
<div className="border-t pt-4">
<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<{
label: string;
help?: string;
@@ -350,8 +360,7 @@ const Choice: FC<{
disabled: boolean;
onPick: (v: string) => void;
}> = ({ label, help, value, options, labels, disabled, onPick }) => (
<div className="space-y-2.5">
<Label>{label}</Label>
<Field label={label} help={help}>
<div className="flex flex-wrap gap-2">
{options.map((o) => (
<Button
@@ -365,8 +374,7 @@ const Choice: FC<{
</Button>
))}
</div>
{help && <p className="text-xs text-muted-foreground">{help}</p>}
</div>
</Field>
);
/**