fix viewmode height, update other ui stuff

This commit is contained in:
Eduard Gert
2026-05-27 16:40:57 +02:00
parent ec5da43d73
commit b84c7618e7
18 changed files with 92 additions and 84 deletions

View File

@@ -76,7 +76,7 @@ State that crosses screens / windows lives in context. Each provider is mounted
### Default/Advanced view + no client-side persistence
The `ViewModeProvider` (`src/lib/viewMode.tsx`, mounted in `AppLayout`) owns a `viewMode: "default" | "advanced"` state and is consumed by `Header.tsx`'s "more" dropdown via `useViewMode()`. `setViewMode` does three things: updates state, calls `Window.SetSize(width, 640)`, and persists via `Preferences.SetViewMode`. Sizes live in `VIEW_SIZE` at the top of `viewMode.tsx`: Default = 380×640, Advanced = 900×640 — the 640 height matches the Settings window so chrome height is consistent across surfaces. The view is persisted user-side (see Go-side `preferences.Store`): `main.go` opens the main window at the saved width so the user never sees a 380→900 flash on launch, and the provider hydrates its React state from `Preferences.Get()` in a mount effect (no resize triggered there — Go already sized it). **No `localStorage` / `sessionStorage` / cookies anywhere in the frontend** — persistence is the Go side's job (settings → `SetConfig`, language → `Preferences.SetLanguage`, view mode → `Preferences.SetViewMode`).
The `ViewModeProvider` (`src/lib/viewMode.tsx`, mounted in `AppLayout`) owns a `viewMode: "default" | "advanced"` state and is consumed by `Header.tsx`'s "more" dropdown via `useViewMode()`. `setViewMode` updates state, calls `Window.SetSize(width, <live frame height>)`, and persists via `Preferences.SetViewMode`. Widths live in `VIEW_WIDTH` at the top of `viewMode.tsx`: Default = 380, Advanced = 900. **The height is intentionally not asserted** — we read the current frame height via `Window.Size()` and pass it back, because Wails' macOS `windowSetSize` is implemented as `setFrame:` (frame, incl. ~28px title bar) while the initial `windowNew` uses `initWithContentRect:` (content). Passing a constant 640 would chop ~28px off the content area on the first switch and visually shift everything inside (the connect toggle is `justify-center` in a column that depends on the parent's height). Reusing the live height keeps content area stable across all switches. The view is persisted user-side (see Go-side `preferences.Store`): `main.go` opens the main window at the saved width so the user never sees a 380→900 flash on launch, and the provider hydrates its React state from `Preferences.Get()` in a mount effect (no resize triggered there — Go already sized it). **No `localStorage` / `sessionStorage` / cookies anywhere in the frontend** — persistence is the Go side's job (settings → `SetConfig`, language → `Preferences.SetLanguage`, view mode → `Preferences.SetViewMode`).
## Localisation (i18n)
@@ -157,7 +157,7 @@ Defined in `tailwind.config.ts`. `nb-gray` is the neutral palette (background =
- **Window dragging.** Use class `wails-draggable` on regions that should drag the OS window (the Header, the SettingsLayout title strip, dialog wrappers like `ConfirmDialog`). Use `wails-no-draggable` on interactive children inside a draggable region (buttons, inputs) — otherwise the drag swallows their click.
- **Webview asset access.** Background images / fonts go through Vite at build time, so reference them with `import url from "@/assets/.../foo.svg"`. The Wails dev server proxies `/` to Vite, but absolute filesystem paths won't work in either dev or prod.
- **`Window.SetSize(w, h)`.** Called from `Header.tsx` when the user picks Default (380×640) or Advanced (900×640) in the view-mode dropdown. Height stays 640 in both, matching the Settings window.
- **`Window.SetSize(w, h)`.** Called from `viewMode.tsx`'s `setViewMode` when the user flips the view-mode dropdown. Width comes from `VIEW_WIDTH` (380 / 900); height is read fresh from `Window.Size()` and re-passed, because Wails' macOS `windowSetSize` treats height as the frame (including title bar) while initial window creation treats it as content — re-asserting a constant would shrink the content area by one title-bar height. See the "Default/Advanced view" section above.
- **`Browser.OpenURL(url)`.** Used by `SettingsAbout` for legal links and by the `BrowserLogin` page's "Try again". Has a `window.open` fallback in `SettingsAbout` for the case where Wails refuses (non-http schemes are rejected by Wails).
## Things in flight (don't be surprised by)

View File

@@ -33,7 +33,7 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.38.0",
"i18next": "^26.2.0",
"lucide-react": "^0.535.0",
"lucide-react": "^0.566.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^17.0.8",

View File

@@ -63,8 +63,8 @@ dependencies:
specifier: ^26.2.0
version: 26.2.0(typescript@5.9.3)
lucide-react:
specifier: ^0.535.0
version: 0.535.0(react@18.3.1)
specifier: ^0.566.0
version: 0.566.0(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
@@ -2127,8 +2127,8 @@ packages:
yallist: 3.1.1
dev: true
/lucide-react@0.535.0(react@18.3.1):
resolution: {integrity: sha512-2E3+YWGLpjZ8ejIYrdqxVjWMSMiRQHmU6xZYE9xA2SC5j2m0NeB4/acjhRdhxbfniBKoNEukDDQnmShTxwOQ4g==}
/lucide-react@0.566.0(react@18.3.1):
resolution: {integrity: sha512-b18qC/JAh1X9rVKlF5EtSIyumdIYuh78b0JShynZnHbcaWR4AW4oZyi8Ms/aQYVSnLPlAnMhug2hSr19BgVZAw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
dependencies:

View File

@@ -31,16 +31,12 @@ export const EmptyState = ({
<div className={cn("py-12 text-center", className)}>
<div
className={
"flex flex-col items-center justify-center max-w-sm mx-auto"
"flex flex-col items-center justify-center max-w-sm mx-auto relative top-7"
}
>
<SquareIcon icon={icon} className={"mb-3"} />
<p className={"text-base font-semibold text-nb-gray-200 mb-1"}>
{title}
</p>
{description && (
<p className={"text-sm text-nb-gray-350"}>{description}</p>
)}
<p className={"text-base font-semibold text-nb-gray-200 mb-1"}>{title}</p>
{description && <p className={"text-sm text-nb-gray-350"}>{description}</p>}
{learnMoreUrl && learnMoreTopic && (
<p className={"text-sm text-nb-gray-350"}>
{t("common.learnMoreAbout")}{" "}
@@ -57,10 +53,7 @@ export const EmptyState = ({
)}
>
{learnMoreTopic}
<ExternalLinkIcon
size={12}
className={"shrink-0"}
/>
<ExternalLinkIcon size={12} className={"shrink-0"} />
</a>
</p>
)}

View File

@@ -1,4 +1,4 @@
import { UnplugIcon } from "lucide-react";
import { GlobeOffIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { EmptyState } from "./EmptyState";
@@ -7,11 +7,11 @@ export const NotConnectedState = () => {
return (
<div
className={
"h-full min-h-[260px] flex items-center justify-center px-6"
"h-full min-h-[260px] flex-1 flex items-center justify-center px-6 pb-20 top-1 relative"
}
>
<EmptyState
icon={UnplugIcon}
icon={GlobeOffIcon}
title={t("notConnected.title")}
description={t("notConnected.description")}
/>

View File

@@ -319,7 +319,7 @@ export const ConnectionStatusSwitch = () => {
<div className={"flex flex-col items-center"}>
<h1
className={
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300 select-none wails-no-draggable mb-1"
"text-sm font-semibold text-nb-gray-200 tracking-wide transition-colors duration-300 select-none wails-no-draggable mb-1"
}
>
{t(STATUS_KEY[connState])}

View File

@@ -143,7 +143,7 @@ export const Header = () => {
<div
className={cn(
"shrink-0 cursor-default wails-draggable relative",
"flex items-center h-12 px-3 top-2",
"flex items-center h-12 px-3 top-2.5",
)}
>
<div className={"grid grid-cols-3 items-center w-[356px] shrink-0"}>
@@ -151,7 +151,9 @@ export const Header = () => {
<div className={"flex justify-center ml-4"}>{profileSlot}</div>
<div />
</div>
<div className={"absolute right-3 top-1/2 -translate-y-1/2"}>{settingsSlot}</div>
<div className={"absolute right-[0.98rem] top-1/2 -translate-y-1/2"}>
{settingsSlot}
</div>
</div>
);
};

View File

@@ -1,8 +1,11 @@
import { ConnectionStatusSwitch } from "@/layouts/ConnectionStatusSwitch.tsx";
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
import { Navigation } from "@/layouts/Navigation.tsx";
import { cn } from "@/lib/cn";
import { useNavSection } from "@/lib/navSection";
import { useViewMode } from "@/lib/viewMode";
import { NotConnectedState } from "@/components/NotConnectedState";
import { useStatus } from "@/modules/daemon-status/StatusContext";
import { Peers } from "@/modules/peers/Peers";
import { Networks } from "@/modules/networks/Networks";
import { ExitNodes } from "@/modules/exit-nodes/ExitNodes";
@@ -28,6 +31,8 @@ const MainBody = () => {
const isAdvanced = viewMode === "advanced";
const { section } = useNavSection();
const { selected } = usePeerDetail();
const { status } = useStatus();
const isConnected = status?.status === "Connected";
return (
<div className={"wails-draggable flex flex-1 min-h-0 p-4 gap-4"}>
@@ -43,12 +48,29 @@ const MainBody = () => {
overlay={<PeerDetailPanel />}
overlayOpen={selected !== null}
>
<Navigation />
<div className={"flex-1 min-h-0 flex flex-col"}>
{section === "peers" && <Peers />}
{section === "networks" && <Networks />}
{section === "exitNode" && <ExitNodes />}
<div
className={cn(
"flex-1 min-h-0 min-w-0 flex flex-col",
!isConnected && "pointer-events-none select-none",
)}
aria-hidden={!isConnected}
>
<Navigation />
<div className={"flex-1 min-h-0 flex flex-col"}>
{section === "peers" && <Peers />}
{section === "networks" && <Networks />}
{section === "exitNode" && <ExitNodes />}
</div>
</div>
{!isConnected && (
<div
className={
"absolute inset-0 z-20 flex pointer-events-auto bg-nb-gray-940"
}
>
<NotConnectedState />
</div>
)}
</MainRightSide>
)}
</div>

View File

@@ -51,7 +51,7 @@ export const Navigation = () => {
"group relative flex flex-1 items-center justify-center",
"gap-2.5 px-5 py-3",
"outline-none transition-all",
isActive ? "text-netbird" : "text-nb-gray-500 hover:text-nb-gray-400",
isActive ? "text-netbird" : "text-nb-gray-400 hover:text-nb-gray-300",
isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
)}
>
@@ -62,7 +62,7 @@ export const Navigation = () => {
"absolute inset-x-0 bottom-0 h-px transition-all",
isActive
? "bg-netbird"
: "bg-nb-gray-900 group-hover:bg-nb-gray-600",
: "bg-nb-gray-910 group-hover:bg-nb-gray-700",
)}
/>
</button>
@@ -72,8 +72,12 @@ export const Navigation = () => {
);
};
const ExitNodeIcon = (props: LucideProps) => (
<SquareArrowUpRight {...props} className={cn("rotate-45", props.className)} />
const ExitNodeIcon = ({ size, ...props }: LucideProps) => (
<SquareArrowUpRight
{...props}
size={typeof size === "number" ? size - 2 : size}
className={cn("rotate-45", props.className)}
/>
);
export type { NavSection } from "@/lib/navSection";

View File

@@ -5,12 +5,16 @@ import { ViewMode as ViewModePref } from "@bindings/preferences/models.js";
export type ViewMode = "default" | "advanced";
// Window dimensions per view. Height matches the Settings window (640) so
// the chrome height is identical across surfaces; width grows from the
// compact 380 default to 900 in advanced.
export const VIEW_SIZE: Record<ViewMode, { width: number; height: number }> = {
default: { width: 380, height: 640 },
advanced: { width: 900, height: 640 },
// Window widths per view. Height stays at whatever the window was first
// created with — we deliberately don't pass a fixed height to
// Window.SetSize because Wails' macOS implementation interprets it as the
// outer frame (windowSetSize → setFrame:), while the initial creation
// uses initWithContentRect:. The two differ by one title-bar height
// (~28px), so re-asserting 640 here would chop ~28px off the content
// area on the first switch and visually shift everything inside.
export const VIEW_WIDTH: Record<ViewMode, number> = {
default: 380,
advanced: 900,
};
type ViewModeContextValue = {
@@ -46,8 +50,15 @@ export const ViewModeProvider = ({ children }: { children: ReactNode }) => {
(mode: ViewMode) => {
setMode((prev) => {
if (prev === mode) return prev;
const { width, height } = VIEW_SIZE[mode];
void Window.SetSize(width, height).catch(() => {});
void (async () => {
// Reuse the live frame height instead of asserting a
// constant — keeps content area stable across switches
// (see VIEW_WIDTH comment above).
const size = await Window.Size().catch(() => null);
const width = VIEW_WIDTH[mode];
const height = size?.height ?? 640;
await Window.SetSize(width, height).catch(() => {});
})();
void Preferences.SetViewMode(mode as unknown as ViewModePref).catch(() => {});
return mode;
});

View File

@@ -6,7 +6,6 @@ import { cn } from "@/lib/cn";
import { SearchInput } from "@/components/SearchInput";
import { EmptyState } from "@/components/EmptyState";
import { NoResults } from "@/components/NoResults";
import { NotConnectedState } from "@/components/NotConnectedState";
import { useStatus } from "@/modules/daemon-status/StatusContext";
import {
formatShortcut,
@@ -46,19 +45,11 @@ export const ExitNodes = () => {
});
}, [exitNodes, search]);
if (!isConnected) {
return (
<div className={"flex flex-col w-full h-full min-h-0"}>
<NotConnectedState />
</div>
);
}
if (exitNodes.length === 0) {
if (isConnected && exitNodes.length === 0) {
return (
<div
className={
"flex-1 flex items-center justify-center px-6 w-full h-full min-h-0"
"flex-1 flex items-center justify-center px-6 pb-20 w-full h-full min-h-0"
}
>
<EmptyState
@@ -91,7 +82,10 @@ export const ExitNodes = () => {
{filtered.length === 0 ? (
<NoResults />
) : (
<ExitNodesList data={filtered} onToggle={toggleExitNode} />
<ExitNodesList
data={filtered}
onToggle={toggleExitNode}
/>
)}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar

View File

@@ -6,7 +6,6 @@ import { cn } from "@/lib/cn";
import { SearchInput } from "@/components/SearchInput";
import { EmptyState } from "@/components/EmptyState";
import { NoResults } from "@/components/NoResults";
import { NotConnectedState } from "@/components/NotConnectedState";
import { useStatus } from "@/modules/daemon-status/StatusContext";
import {
formatShortcut,
@@ -97,19 +96,11 @@ export const Networks = () => {
});
}, [networkRoutes, search, filter, overlapById]);
if (!isConnected) {
return (
<div className={"flex flex-col w-full h-full min-h-0"}>
<NotConnectedState />
</div>
);
}
if (networkRoutes.length === 0) {
if (isConnected && networkRoutes.length === 0) {
return (
<div
className={
"flex-1 flex items-center justify-center px-6 w-full h-full min-h-0"
"flex-1 flex items-center justify-center px-6 pb-20 w-full h-full min-h-0"
}
>
<EmptyState

View File

@@ -6,7 +6,6 @@ import { cn } from "@/lib/cn";
import { SearchInput } from "@/components/SearchInput";
import { EmptyState } from "@/components/EmptyState";
import { NoResults } from "@/components/NoResults";
import { NotConnectedState } from "@/components/NotConnectedState";
import { useStatus } from "@/modules/daemon-status/StatusContext";
import {
formatShortcut,
@@ -70,19 +69,11 @@ export const Peers = () => {
});
}, [peers, search, statusFilter]);
if (!isConnected) {
return (
<div className={"flex flex-col w-full h-full min-h-0"}>
<NotConnectedState />
</div>
);
}
if (peers.length === 0) {
if (isConnected && peers.length === 0) {
return (
<div
className={
"flex-1 flex items-center justify-center px-6 w-full h-full min-h-0"
"flex-1 flex items-center justify-center px-6 pb-20 w-full h-full min-h-0"
}
>
<EmptyState

View File

@@ -78,7 +78,7 @@ export function SettingsAbout() {
t("settings.about.client", { version: daemonVersion })
)}
</p>
<p className={"text-sm text-nb-gray-300 cursor-text select-text"}>
<p className={"text-sm text-nb-gray-250 cursor-text select-text font-medium"}>
{t("settings.about.gui", { version: guiVersion })}
</p>
</div>

View File

@@ -28,7 +28,7 @@ export const SettingsBottomBar = ({ children }: { children: ReactNode }) => (
<div className={"absolute bottom-0 left-0 w-full"}>
<div
className={
"w-full flex justify-end gap-3 px-8 py-5 border-t border-nb-gray-900 bg-nb-gray-935"
"w-full flex justify-end gap-3 px-8 py-5 border-t border-nb-gray-920 bg-nb-gray-940"
}
>
{children}

View File

@@ -64,8 +64,8 @@
"common.learnMoreAbout": "Mehr erfahren über",
"common.noResults.title": "Keine Ergebnisse gefunden",
"common.noResults.description": "Es konnten keine Ergebnisse gefunden werden. Bitte versuchen Sie es mit einem anderen Suchbegriff oder ändern Sie Ihre Filter.",
"notConnected.title": "Nicht verbunden",
"notConnected.description": "Verbinden Sie sich mit NetBird, um Peers, Netzwerke und Exit Nodes anzuzeigen.",
"notConnected.title": "Getrennt",
"notConnected.description": "Verbinden Sie sich zuerst mit NetBird, um detaillierte Informationen zu Ihren Peers, Netzwerkressourcen und Exit Nodes einzusehen.",
"connect.status.disconnected": "Getrennt",
"connect.status.connecting": "Verbindet…",

View File

@@ -64,8 +64,8 @@
"common.learnMoreAbout": "Learn more about",
"common.noResults.title": "Could not find any results",
"common.noResults.description": "We couldn't find any results. Please try a different search term or change your filters.",
"notConnected.title": "Not connected",
"notConnected.description": "Connect to NetBird to view peers, networks, and exit nodes.",
"notConnected.title": "Disconnected",
"notConnected.description": "Connect to NetBird first to view detailed information about your peers, network resources, and exit nodes.",
"connect.status.disconnected": "Disconnected",
"connect.status.connecting": "Connecting...",
@@ -145,7 +145,7 @@
"settings.tabs.profiles": "Profiles",
"settings.tabs.ssh": "SSH",
"settings.tabs.advanced": "Advanced",
"settings.tabs.troubleshooting": "Troubleshooting",
"settings.tabs.troubleshooting": "Troubleshoot",
"settings.tabs.about": "About",
"settings.tabs.updateAvailable": "Update Available",

View File

@@ -64,8 +64,8 @@
"common.learnMoreAbout": "Tudjon meg többet erről:",
"common.noResults.title": "Nincs találat",
"common.noResults.description": "Nem találtunk eredményt. Próbáljon meg másik keresési kifejezést, vagy módosítsa a szűrőket.",
"notConnected.title": "Nincs csatlakozva",
"notConnected.description": "Csatlakozzon a NetBirdhöz a társak, hálózatok és kilépő csomópontok megtekintéséhez.",
"notConnected.title": "Lecsatlakozva",
"notConnected.description": "Csatlakozz először a NetBirdhöz, hogy részletes információkat láthass a társakról, hálózati erőforrásokról és kilépő csomópontokról.",
"connect.status.disconnected": "Lekapcsolva",
"connect.status.connecting": "Csatlakozás…",