From b84c7618e7fd0b78453c60854ca50173df6450ea Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Wed, 27 May 2026 16:40:57 +0200 Subject: [PATCH] fix viewmode height, update other ui stuff --- client/ui/frontend/CLAUDE.md | 4 +-- client/ui/frontend/package.json | 2 +- client/ui/frontend/pnpm-lock.yaml | 8 ++--- .../ui/frontend/src/components/EmptyState.tsx | 15 +++------ .../src/components/NotConnectedState.tsx | 6 ++-- .../src/layouts/ConnectionStatusSwitch.tsx | 2 +- client/ui/frontend/src/layouts/Header.tsx | 6 ++-- client/ui/frontend/src/layouts/Main.tsx | 32 ++++++++++++++++--- client/ui/frontend/src/layouts/Navigation.tsx | 12 ++++--- client/ui/frontend/src/lib/viewMode.tsx | 27 +++++++++++----- .../src/modules/exit-nodes/ExitNodes.tsx | 18 ++++------- .../src/modules/networks/Networks.tsx | 13 ++------ .../ui/frontend/src/modules/peers/Peers.tsx | 13 ++------ .../src/modules/settings/SettingsAbout.tsx | 2 +- .../src/modules/settings/SettingsSection.tsx | 2 +- client/ui/i18n/locales/de/common.json | 4 +-- client/ui/i18n/locales/en/common.json | 6 ++-- client/ui/i18n/locales/hu/common.json | 4 +-- 18 files changed, 92 insertions(+), 84 deletions(-) diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index c2a755e9a..e8e7d7c9b 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -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, )`, 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) diff --git a/client/ui/frontend/package.json b/client/ui/frontend/package.json index beb10cd6b..7cf271ac1 100644 --- a/client/ui/frontend/package.json +++ b/client/ui/frontend/package.json @@ -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", diff --git a/client/ui/frontend/pnpm-lock.yaml b/client/ui/frontend/pnpm-lock.yaml index cdca9b0fa..55a3c6d1c 100644 --- a/client/ui/frontend/pnpm-lock.yaml +++ b/client/ui/frontend/pnpm-lock.yaml @@ -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: diff --git a/client/ui/frontend/src/components/EmptyState.tsx b/client/ui/frontend/src/components/EmptyState.tsx index ceecc98be..c79b1a82b 100644 --- a/client/ui/frontend/src/components/EmptyState.tsx +++ b/client/ui/frontend/src/components/EmptyState.tsx @@ -31,16 +31,12 @@ export const EmptyState = ({
-

- {title} -

- {description && ( -

{description}

- )} +

{title}

+ {description &&

{description}

} {learnMoreUrl && learnMoreTopic && (

{t("common.learnMoreAbout")}{" "} @@ -57,10 +53,7 @@ export const EmptyState = ({ )} > {learnMoreTopic} - +

)} diff --git a/client/ui/frontend/src/components/NotConnectedState.tsx b/client/ui/frontend/src/components/NotConnectedState.tsx index 168d3fe7c..486ff0b2c 100644 --- a/client/ui/frontend/src/components/NotConnectedState.tsx +++ b/client/ui/frontend/src/components/NotConnectedState.tsx @@ -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 (
diff --git a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx index 87e134421..cf642a081 100644 --- a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx +++ b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx @@ -319,7 +319,7 @@ export const ConnectionStatusSwitch = () => {

{t(STATUS_KEY[connState])} diff --git a/client/ui/frontend/src/layouts/Header.tsx b/client/ui/frontend/src/layouts/Header.tsx index be7d269c3..0fa253d32 100644 --- a/client/ui/frontend/src/layouts/Header.tsx +++ b/client/ui/frontend/src/layouts/Header.tsx @@ -143,7 +143,7 @@ export const Header = () => {
@@ -151,7 +151,9 @@ export const Header = () => {
{profileSlot}
-
{settingsSlot}
+
+ {settingsSlot} +
); }; diff --git a/client/ui/frontend/src/layouts/Main.tsx b/client/ui/frontend/src/layouts/Main.tsx index 4dda83fc5..1f5b3b76f 100644 --- a/client/ui/frontend/src/layouts/Main.tsx +++ b/client/ui/frontend/src/layouts/Main.tsx @@ -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 (
@@ -43,12 +48,29 @@ const MainBody = () => { overlay={} overlayOpen={selected !== null} > - -
- {section === "peers" && } - {section === "networks" && } - {section === "exitNode" && } +
+ +
+ {section === "peers" && } + {section === "networks" && } + {section === "exitNode" && } +
+ {!isConnected && ( +
+ +
+ )} )}
diff --git a/client/ui/frontend/src/layouts/Navigation.tsx b/client/ui/frontend/src/layouts/Navigation.tsx index 2e274a41f..d3760adb5 100644 --- a/client/ui/frontend/src/layouts/Navigation.tsx +++ b/client/ui/frontend/src/layouts/Navigation.tsx @@ -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", )} /> @@ -72,8 +72,12 @@ export const Navigation = () => { ); }; -const ExitNodeIcon = (props: LucideProps) => ( - +const ExitNodeIcon = ({ size, ...props }: LucideProps) => ( + ); export type { NavSection } from "@/lib/navSection"; diff --git a/client/ui/frontend/src/lib/viewMode.tsx b/client/ui/frontend/src/lib/viewMode.tsx index c80aeae43..bfd20804c 100644 --- a/client/ui/frontend/src/lib/viewMode.tsx +++ b/client/ui/frontend/src/lib/viewMode.tsx @@ -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 = { - 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 = { + 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; }); diff --git a/client/ui/frontend/src/modules/exit-nodes/ExitNodes.tsx b/client/ui/frontend/src/modules/exit-nodes/ExitNodes.tsx index 9ee265b11..7ae62602e 100644 --- a/client/ui/frontend/src/modules/exit-nodes/ExitNodes.tsx +++ b/client/ui/frontend/src/modules/exit-nodes/ExitNodes.tsx @@ -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 ( -
- -
- ); - } - - if (exitNodes.length === 0) { + if (isConnected && exitNodes.length === 0) { return (
{ {filtered.length === 0 ? ( ) : ( - + )} { }); }, [networkRoutes, search, filter, overlapById]); - if (!isConnected) { - return ( -
- -
- ); - } - - if (networkRoutes.length === 0) { + if (isConnected && networkRoutes.length === 0) { return (
{ }); }, [peers, search, statusFilter]); - if (!isConnected) { - return ( -
- -
- ); - } - - if (peers.length === 0) { + if (isConnected && peers.length === 0) { return (
-

+

{t("settings.about.gui", { version: guiVersion })}

diff --git a/client/ui/frontend/src/modules/settings/SettingsSection.tsx b/client/ui/frontend/src/modules/settings/SettingsSection.tsx index 1d672ab81..3379baf0f 100644 --- a/client/ui/frontend/src/modules/settings/SettingsSection.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsSection.tsx @@ -28,7 +28,7 @@ export const SettingsBottomBar = ({ children }: { children: ReactNode }) => (
{children} diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index 2d68a2426..c5d622e02 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -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…", diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index 4f0754382..f0f7e82f5 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -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", diff --git a/client/ui/i18n/locales/hu/common.json b/client/ui/i18n/locales/hu/common.json index 3f5166eac..292bbaff1 100644 --- a/client/ui/i18n/locales/hu/common.json +++ b/client/ui/i18n/locales/hu/common.json @@ -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…",