diff --git a/web/messages/de.json b/web/messages/de.json index 353ecfe..a2ffc91 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -4,6 +4,7 @@ "app_tagline": "Verwaltungskonsole", "nav_dashboard": "Übersicht", "nav_host": "Host", + "nav_displays": "Virtuelle Anzeigen", "nav_clients": "Gekoppelte Geräte", "nav_pairing": "Kopplung", "nav_library": "Bibliothek", @@ -49,6 +50,7 @@ "gpu_env_note": "PUNKTFUNK_RENDER_ADAPTER={value} bindet die GPU im Automatikmodus.", "host_displays": "Virtuelle Displays", "host_displays_help": "Wie virtuelle Displays erstellt, aktiv gehalten und angeordnet werden. Wähle eine Voreinstellung oder „Benutzerdefiniert“, um Optionen direkt zu setzen. Eine Änderung gilt ab der nächsten Sitzung.", + "display_config_title": "Konfiguration", "display_preset": "Voreinstellung", "display_preset_custom": "Benutzerdefiniert", "display_preset_default": "Standard", diff --git a/web/messages/en.json b/web/messages/en.json index f7b1442..0db8281 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -4,6 +4,7 @@ "app_tagline": "management console", "nav_dashboard": "Dashboard", "nav_host": "Host", + "nav_displays": "Virtual displays", "nav_clients": "Paired clients", "nav_pairing": "Pairing", "nav_library": "Library", @@ -49,6 +50,7 @@ "gpu_env_note": "PUNKTFUNK_RENDER_ADAPTER={value} pins the GPU while in automatic mode.", "host_displays": "Virtual displays", "host_displays_help": "How virtual displays are created, kept alive, and arranged. Pick a preset, or choose Custom to set options directly. A change applies to the next session.", + "display_config_title": "Configuration", "display_preset": "Preset", "display_preset_custom": "Custom", "display_preset_default": "Default", diff --git a/web/src/components/app-shell.tsx b/web/src/components/app-shell.tsx index 546733b..4499f5f 100644 --- a/web/src/components/app-shell.tsx +++ b/web/src/components/app-shell.tsx @@ -4,6 +4,7 @@ import { GaugeCircle, KeyRound, LibraryBig, + MonitorPlay, ScrollText, Server, Settings, @@ -21,6 +22,7 @@ const MLink = motion(Link); const NAV = [ { to: "/", icon: Activity, label: () => m.nav_dashboard() }, { to: "/host", icon: Server, label: () => m.nav_host() }, + { to: "/displays", icon: MonitorPlay, label: () => m.nav_displays() }, { to: "/library", icon: LibraryBig, label: () => m.nav_library() }, { to: "/stats", icon: GaugeCircle, label: () => m.nav_stats() }, { to: "/logs", icon: ScrollText, label: () => m.nav_logs() }, diff --git a/web/src/routes/displays.tsx b/web/src/routes/displays.tsx new file mode 100644 index 0000000..959d331 --- /dev/null +++ b/web/src/routes/displays.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { SectionDisplays } from "@/sections/Displays"; + +export const Route = createFileRoute("/displays")({ component: SectionDisplays }); diff --git a/web/src/sections/Host/DisplayCard.tsx b/web/src/sections/Displays/DisplayCard.tsx similarity index 91% rename from web/src/sections/Host/DisplayCard.tsx rename to web/src/sections/Displays/DisplayCard.tsx index 35e034a..cf99afa 100644 --- a/web/src/sections/Host/DisplayCard.tsx +++ b/web/src/sections/Displays/DisplayCard.tsx @@ -60,27 +60,36 @@ export const DisplaySection: FC = () => { ); return ( - - - {m.host_displays()} - - -

{m.host_displays_help()}

- - {q.data && draft && ( - - )} - - -
-
+
+ + + {m.display_config_title()} + + +

{m.host_displays_help()}

+ + {q.data && draft && ( + + )} + +
+
+ + + {m.display_live()} + + + + + +
); }; @@ -142,10 +151,10 @@ const DisplayForm: FC<{ return (
- {/* One-click presets */} -
- -
+ {/* One-click presets — a 2-up grid so each has room to breathe */} +
+ +
{PRESET_ORDER.map((id) => { const p = presets.find((x) => x.id === id); const fields = id === "custom" ? undefined : p?.fields; @@ -153,8 +162,10 @@ const DisplayForm: FC<{ const selected = preset === id; const soon = DISABLED_PRESETS.has(id); const cls = [ - "w-full rounded-md border p-3 text-left transition-colors", - selected ? "border-primary ring-1 ring-primary" : "hover:bg-muted/50", + "flex h-full flex-col rounded-lg border p-4 text-left transition-colors", + selected + ? "border-primary ring-1 ring-primary" + : "hover:border-primary/40 hover:bg-muted/50", soon ? "opacity-60" : "", ].join(" "); return ( @@ -166,7 +177,7 @@ const DisplayForm: FC<{ className={cls} >
- + {(PRESET_LABEL[id] ?? (() => id))()} {soon && ( @@ -178,9 +189,11 @@ const DisplayForm: FC<{ {m.display_preset_current()} )}
- {summary &&

{summary}

} + {summary && ( +

{summary}

+ )} {fields && ( -
+
{fmtKeepAlive(fields.keep_alive)} {tr(TOPOLOGY_LABEL, fields.topology)} {tr(CONFLICT_LABEL, fields.mode_conflict)} @@ -361,10 +374,9 @@ const LiveDisplays: FC = () => { ); return ( -
-
-

{m.display_live()}

- {kept.length > 0 && ( +
+ {kept.length > 0 && ( +
- )} -
+
+ )} {displays.length === 0 ? (

{m.display_none_live()}

) : ( diff --git a/web/src/sections/Displays/index.tsx b/web/src/sections/Displays/index.tsx new file mode 100644 index 0000000..b707a95 --- /dev/null +++ b/web/src/sections/Displays/index.tsx @@ -0,0 +1,22 @@ +import Section from "@unom/ui/section"; +import type { FC } from "react"; +import { useLocale } from "@/lib/i18n"; +import { m } from "@/paraglide/messages"; +import { DisplaySection } from "./DisplayCard"; + +/** + * The **Virtual displays** page (design/display-management.md): the host's virtual-display policy + * (presets + every axis) plus the live-display list + multi-monitor arrangement. Its own nav + * section — the config surface is large enough to warrant the room, and it kept the Host page busy. + */ +export const SectionDisplays: FC = () => { + useLocale(); + return ( +
+
+

{m.nav_displays()}

+ +
+
+ ); +}; diff --git a/web/src/sections/Host/index.tsx b/web/src/sections/Host/index.tsx index bfd2e7a..322518d 100644 --- a/web/src/sections/Host/index.tsx +++ b/web/src/sections/Host/index.tsx @@ -1,7 +1,6 @@ import type { FC } from "react"; import { useGetHostInfo, useListCompositors } from "@/api/gen/host/host"; import { useLocale } from "@/lib/i18n"; -import { DisplaySection } from "./DisplayCard"; import { GpuSection } from "./GpuCard"; import { HostView } from "./view"; @@ -10,12 +9,5 @@ export const SectionHost: FC = () => { const host = useGetHostInfo(); const compositors = useListCompositors(); - return ( - } - displays={} - /> - ); + return } />; }; diff --git a/web/src/sections/Host/view.tsx b/web/src/sections/Host/view.tsx index 0f54545..487ce41 100644 --- a/web/src/sections/Host/view.tsx +++ b/web/src/sections/Host/view.tsx @@ -13,9 +13,7 @@ export const HostView: FC<{ compositors: Loadable; /** The GPU inventory/selection card (a self-contained container — see `GpuCard.tsx`). */ gpu?: ReactNode; - /** The virtual-display management card (self-contained container — see `DisplayCard.tsx`). */ - displays?: ReactNode; -}> = ({ host, compositors, gpu, displays }) => { +}> = ({ host, compositors, gpu }) => { const h = host.data; return (
@@ -83,8 +81,6 @@ export const HostView: FC<{ {gpu} - {displays} - {m.host_compositors()}