mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-20 12:29:09 +02:00
add default and advanced resize
This commit is contained in:
+1
-1
@@ -88,7 +88,7 @@ Also: `ProfileSwitcher.SwitchActive` mirrors the daemon switch into the user-sid
|
|||||||
|
|
||||||
The main window is created up front in `main.go`. Auxiliary windows are created on demand by `services.WindowManager`:
|
The main window is created up front in `main.go`. Auxiliary windows are created on demand by `services.WindowManager`:
|
||||||
|
|
||||||
- **Settings** (`/#/settings`) — opened from the header gear icon (`layouts/Header.tsx → WindowManager.OpenSettings("")`), the tray's Settings menu entry (`tray.go openSettings`), and the profile dropdown's "Manage Profiles" entry (`WindowManager.OpenSettings("profiles")`, which sets `?tab=profiles` in the start URL — `Settings.tsx` reads it via `useSearchParams`). The window hosts every settings tab — including **Profiles** (`SettingsProfiles.tsx`, `UserCircle` icon, sits between Security and SSH), which lists profiles in a table with Deregister/Delete in a per-row kebab and an Add Profile button. Both call sites go through `WindowManager` so the user sees the same dedicated frameless window from either trigger — the tray used to repurpose the main window via `SetURL("/#/settings")`, which replaced the main UI in place. Frameless-look (translucent macOS backdrop, hidden inset title bar), fixed 900×640, no resize, no minimise/maximise.
|
- **Settings** (`/#/settings`) — opened from the header gear icon (`layouts/Header.tsx → WindowManager.OpenSettings("")`), the tray's Settings menu entry (`tray.go openSettings`), and the profile dropdown's "Manage Profiles" entry (`WindowManager.OpenSettings("profiles")`, which sets `?tab=profiles` in the start URL — `Settings.tsx` reads it via `useSearchParams`). The window hosts every settings tab — including **Profiles** (`SettingsProfiles.tsx`, `UserCircle` icon, sits between Security and SSH), which lists profiles in a table with Deregister/Delete in a per-row kebab and an Add Profile button. Both call sites go through `WindowManager` so the user sees the same dedicated frameless window from either trigger — the tray used to repurpose the main window via `SetURL("/#/settings")`, which replaced the main UI in place. Frameless-look (opaque macOS backdrop, hidden inset title bar), fixed 900×640, no resize, no minimise/maximise.
|
||||||
- **BrowserLogin** (`/#/browser-login?uri=…`) — opened by the connection toggle's SSO flow (`layouts/ConnectionStatusSwitch.tsx`). 460×440, fixed size. The close button (red X) fires `EventBrowserLoginCancel` so the JS-side `startLogin()` can tear down the daemon's pending `WaitSSOLogin`. `WindowManager.CloseBrowserLogin` closes it programmatically when the flow completes.
|
- **BrowserLogin** (`/#/browser-login?uri=…`) — opened by the connection toggle's SSO flow (`layouts/ConnectionStatusSwitch.tsx`). 460×440, fixed size. The close button (red X) fires `EventBrowserLoginCancel` so the JS-side `startLogin()` can tear down the daemon's pending `WaitSSOLogin`. `WindowManager.CloseBrowserLogin` closes it programmatically when the flow completes.
|
||||||
- **SessionExpired** (`/#/session-expired`) and **SessionAboutToExpire** (`/#/session-about-to-expire?seconds=<n>`) — opened by `WindowManager.OpenSessionExpired` / `OpenSessionAboutToExpire(seconds)`. 460×380, fixed size, `AlwaysOnTop: true` (the user can't miss them). The React-side buttons close the window via `WindowManager.CloseSession*` and (for Sign-in / Stay-connected) emit `EventTriggerLogin` so the main window's `startLogin()` orchestrator handles the SSO flow. Currently triggered only by the DEV-only "Development" Settings tab; daemon-status integration is a follow-up.
|
- **SessionExpired** (`/#/session-expired`) and **SessionAboutToExpire** (`/#/session-about-to-expire?seconds=<n>`) — opened by `WindowManager.OpenSessionExpired` / `OpenSessionAboutToExpire(seconds)`. 460×380, fixed size, `AlwaysOnTop: true` (the user can't miss them). The React-side buttons close the window via `WindowManager.CloseSession*` and (for Sign-in / Stay-connected) emit `EventTriggerLogin` so the main window's `startLogin()` orchestrator handles the SSO flow. Currently triggered only by the DEV-only "Development" Settings tab; daemon-status integration is a follow-up.
|
||||||
- **InstallProgress** (`/#/install-progress?version=<v>`) — opened by `WindowManager.OpenInstallProgress(version)` from `ClientVersionContext` (force-install branch on `installing` flip, user-driven enforced branch from `triggerUpdate`). 360-wide auto-sized via `useAutoSizeWindow`, `AlwaysOnTop`. Owns its own polling loop against `Update.GetInstallerResult` with the 5-second daemon-down-grace (sustained gRPC failure = success → call `Update.Quit()`). Hides every other visible window on open (restored on close). The DEV-only "Development" tab has a "Show updating dialog" button that opens this window directly for preview.
|
- **InstallProgress** (`/#/install-progress?version=<v>`) — opened by `WindowManager.OpenInstallProgress(version)` from `ClientVersionContext` (force-install branch on `installing` flip, user-driven enforced branch from `triggerUpdate`). 360-wide auto-sized via `useAutoSizeWindow`, `AlwaysOnTop`. Owns its own polling loop against `Update.GetInstallerResult` with the 5-second daemon-down-grace (sustained gRPC failure = success → call `Update.Quit()`). Hides every other visible window on open (restored on close). The DEV-only "Development" tab has a "Show updating dialog" button that opens this window directly for preview.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ React 18 + TS 5.7 (`strict`, `noImplicitAny: false`) + Vite 6 + Tailwind 3 (`dar
|
|||||||
| `/settings` | `Settings` | `SettingsLayout` | Auxiliary window (Go `WindowManager.OpenSettings(tab)`). The `Profiles` tab (`modules/settings/SettingsProfiles.tsx`, `UserCircle` icon, between Security and SSH) lists profiles in a table with Deregister/Delete in a per-row kebab and an Add Profile button. The header `ProfileDropdown`'s "Manage Profiles" entry calls `OpenSettings("profiles")` — `Settings.tsx` reads `?tab=` via `useSearchParams` so the window opens at that tab. |
|
| `/settings` | `Settings` | `SettingsLayout` | Auxiliary window (Go `WindowManager.OpenSettings(tab)`). The `Profiles` tab (`modules/settings/SettingsProfiles.tsx`, `UserCircle` icon, between Security and SSH) lists profiles in a table with Deregister/Delete in a per-row kebab and an Add Profile button. The header `ProfileDropdown`'s "Manage Profiles" entry calls `OpenSettings("profiles")` — `Settings.tsx` reads `?tab=` via `useSearchParams` so the window opens at that tab. |
|
||||||
| `*` | `<Navigate to="/">` | `AppLayout` | Catch-all |
|
| `*` | `<Navigate to="/">` | `AppLayout` | Catch-all |
|
||||||
|
|
||||||
`AppLayout` wraps `Header + <Outlet/>` in this provider order: `StatusProvider → ProfileProvider → DebugBundleProvider → ClientVersionProvider`. `StatusProvider` (in `modules/daemon-status/StatusContext.tsx`) owns the single `Peers.Get` + `netbird:status` subscription, exposes `{ status, error, refresh, isReady, isDaemonAvailable, isDaemonUnavailable }`, **and only renders its children when the daemon is reachable** — until the first `Peers.Get` resolves and on `DaemonUnavailable` it short-circuits to just the `<DaemonUnavailableOverlay/>` (also owned by the provider). The consequence: every context downstream (`ProfileProvider`, `DebugBundleProvider`, `ClientVersionProvider`) can assume the daemon is reachable at mount time — no per-context `useStatus` gating. When the daemon flips back to unavailable the whole downstream subtree unmounts and remounts fresh once it returns. `ClientVersionProvider` no longer paints any inline overlay; install progress lives in its own auxiliary window (see `/install-progress` route). `AppLayout` also owns the wide/narrow `expanded` state as plain `useState` (no persistence) and passes it to `Header` via props and to `Main` via Outlet context (`MainOutletContext`).
|
`AppLayout` wraps `Header + <Outlet/>` in this provider order: `StatusProvider → ProfileProvider → DebugBundleProvider → ClientVersionProvider`. `StatusProvider` (in `modules/daemon-status/StatusContext.tsx`) owns the single `Peers.Get` + `netbird:status` subscription, exposes `{ status, error, refresh, isReady, isDaemonAvailable, isDaemonUnavailable }`, **and only renders its children when the daemon is reachable** — until the first `Peers.Get` resolves and on `DaemonUnavailable` it short-circuits to just the `<DaemonUnavailableOverlay/>` (also owned by the provider). The consequence: every context downstream (`ProfileProvider`, `DebugBundleProvider`, `ClientVersionProvider`) can assume the daemon is reachable at mount time — no per-context `useStatus` gating. When the daemon flips back to unavailable the whole downstream subtree unmounts and remounts fresh once it returns. `ClientVersionProvider` no longer paints any inline overlay; install progress lives in its own auxiliary window (see `/install-progress` route). The view-mode (Default 380×640 vs Advanced 900×640) lives as `useState` inside `Header.tsx`, which calls `Window.SetSize` directly on change — no shared shell context for it.
|
||||||
|
|
||||||
`SettingsLayout` uses the same provider stack minus the `Header`. It also reserves a 38px `wails-draggable` strip at the top so the macOS traffic-light buttons (the window uses `MacTitleBarHiddenInset`) don't overlap content.
|
`SettingsLayout` uses the same provider stack minus the `Header`. It also reserves a 38px `wails-draggable` strip at the top so the macOS traffic-light buttons (the window uses `MacTitleBarHiddenInset`) don't overlap content.
|
||||||
|
|
||||||
@@ -75,9 +75,9 @@ State that crosses screens / windows lives in context. Each provider is mounted
|
|||||||
3. `available && enforced && installing` — daemon already installing (force-install). The `installing` flip auto-opens `/install-progress` via `WindowManager.OpenInstallProgress`.
|
3. `available && enforced && installing` — daemon already installing (force-install). The `installing` flip auto-opens `/install-progress` via `WindowManager.OpenInstallProgress`.
|
||||||
Dev preview: `SettingsDevelopment` toggles emit `netbird:dev:overrides`, which this provider listens for and overrides `available / enforced / version`. No more module-level `FORCE_*` constants.
|
Dev preview: `SettingsDevelopment` toggles emit `netbird:dev:overrides`, which this provider listens for and overrides `available / enforced / version`. No more module-level `FORCE_*` constants.
|
||||||
|
|
||||||
### Wide/narrow panel + no client-side persistence
|
### Default/Advanced view + no client-side persistence
|
||||||
|
|
||||||
The `expanded` flag (380px ↔ 925px) lives in `AppLayout` as plain `useState(false)` — the only shell-layout knob. `Header.tsx` reads it via props and calls `Window.SetSize(w, 615)`; `Main.tsx` reads it via `MainOutletContext` to mount/unmount the right-side panel. Every app launch starts small. **No `localStorage` / `sessionStorage` / cookies anywhere in the frontend** — persistence is the Go side's job (settings → `SetConfig`, language → `Preferences.SetLanguage`). Nav-item visibility and header buttons are hardcoded to always-render (the old Appearance toggles are gone).
|
The Header's "more" dropdown owns a `viewMode: "default" | "advanced"` `useState` and calls `Window.SetSize(width, 640)` directly on change. Sizes live in `VIEW_SIZE` at the top of `Header.tsx`: Default = 380×640, Advanced = 900×640 — the 640 height matches the Settings window so chrome height is consistent across surfaces. Every app launch starts in Default (the Go-side main window is created 380×640 in `main.go`). **No `localStorage` / `sessionStorage` / cookies anywhere in the frontend** — persistence is the Go side's job (settings → `SetConfig`, language → `Preferences.SetLanguage`).
|
||||||
|
|
||||||
## Localisation (i18n)
|
## Localisation (i18n)
|
||||||
|
|
||||||
@@ -158,7 +158,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.
|
- **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.
|
- **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` to switch between 380-wide and 925-wide layouts. There's a one-time initial sync on mount so localStorage's `expanded` flag wins over the Go-side default of 925.
|
- **`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.
|
||||||
- **`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).
|
- **`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)
|
## Things in flight (don't be surprised by)
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { forwardRef, type ComponentType, type HTMLAttributes } from "react";
|
|||||||
import type { LucideProps } from "lucide-react";
|
import type { LucideProps } from "lucide-react";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
export type BadgeVariant =
|
export type BadgeVariant = "info" | "neutral" | "brand" | "success" | "warning" | "danger";
|
||||||
| "info"
|
|
||||||
| "neutral"
|
|
||||||
| "brand"
|
|
||||||
| "success"
|
|
||||||
| "warning"
|
|
||||||
| "danger";
|
|
||||||
|
|
||||||
type Props = HTMLAttributes<HTMLSpanElement> & {
|
type Props = HTMLAttributes<HTMLSpanElement> & {
|
||||||
/** Visual color scheme. Defaults to `info` (sky), used as the
|
/** Visual color scheme. Defaults to `info` (sky), used as the
|
||||||
@@ -34,22 +28,15 @@ const VARIANT_CLASSES: Record<BadgeVariant, string> = {
|
|||||||
// lets the small text sit flush in the pill without the line-height padding
|
// lets the small text sit flush in the pill without the line-height padding
|
||||||
// inflating it.
|
// inflating it.
|
||||||
export const Badge = forwardRef<HTMLSpanElement, Props>(function Badge(
|
export const Badge = forwardRef<HTMLSpanElement, Props>(function Badge(
|
||||||
{
|
{ variant = "info", icon: Icon, iconSize = 10, className, children, ...rest },
|
||||||
variant = "info",
|
|
||||||
icon: Icon,
|
|
||||||
iconSize = 10,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
},
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative top-px inline-flex items-center gap-1 rounded-full px-2 py-[0.2rem]",
|
"relative top-px inline-flex items-center gap-1 rounded-full px-1.5 py-[0.15rem]",
|
||||||
"text-[0.65rem] leading-none font-semibold shrink-0",
|
"text-[0.64rem] leading-none font-semibold shrink-0",
|
||||||
VARIANT_CLASSES[variant],
|
VARIANT_CLASSES[variant],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
|||||||
className={cn(
|
className={cn(
|
||||||
"h-10 w-10 flex items-center justify-center rounded-lg cursor-default outline-none",
|
"h-10 w-10 flex items-center justify-center rounded-lg cursor-default outline-none",
|
||||||
"text-nb-gray-400 hover:text-nb-gray-300 hover:bg-nb-gray-900",
|
"text-nb-gray-400 hover:text-nb-gray-300 hover:bg-nb-gray-900",
|
||||||
"transition-colors duration-150",
|
"transition-colors duration-150 wails-no-draggable",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild className={"wails-no-draggable"}>
|
||||||
<ProfileTriggerButton name={displayName} />
|
<ProfileTriggerButton name={displayName} />
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
@@ -91,7 +91,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => {
|
|||||||
collisionPadding={12}
|
collisionPadding={12}
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-64 overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg",
|
"z-50 min-w-64 overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg wails-no-draggable",
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||||
@@ -186,19 +186,19 @@ const ProfileTriggerButton = forwardRef<HTMLButtonElement, ProfileTriggerButtonP
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default",
|
"h-10 flex items-center gap-2 px-3 rounded-lg outline-none cursor-default wails-no-draggable",
|
||||||
"text-nb-gray-200 hover:bg-nb-gray-900",
|
"text-nb-gray-200 hover:bg-nb-gray-900",
|
||||||
"data-[state=open]:bg-nb-gray-900",
|
"data-[state=open]:bg-nb-gray-900",
|
||||||
"transition-colors duration-150",
|
"transition-colors duration-150 wails-no-draggable",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon size={16} className={"text-nb-gray-200 shrink-0"} />
|
<Icon size={16} className={"text-nb-gray-200 shrink-0 wails-no-draggable"} />
|
||||||
<span className={"text-sm font-medium truncate max-w-[140px]"}>
|
<span className={"text-sm font-medium truncate max-w-[140px] wails-no-draggable"}>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown size={14} className={"text-nb-gray-200 shrink-0"} />
|
<ChevronDown size={14} className={"text-nb-gray-200 shrink-0 wails-no-draggable"} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,72 +8,67 @@ import { cn } from "@/lib/cn";
|
|||||||
type SwitchVariants = VariantProps<typeof switchVariants>;
|
type SwitchVariants = VariantProps<typeof switchVariants>;
|
||||||
|
|
||||||
const switchVariants = cva("", {
|
const switchVariants = cva("", {
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
default: "h-[24px] w-[44px]",
|
default: "h-[24px] w-[44px]",
|
||||||
small: "h-[18px] w-[36px]",
|
small: "h-[18px] w-[36px]",
|
||||||
large: "h-[36px] w-[66px]",
|
large: "h-[36px] w-[66px]",
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
default: [
|
||||||
|
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||||
|
"dark:data-[state=checked]:hover:bg-netbird-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
|
||||||
|
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
|
||||||
|
"data-[state=checked]:hover:bg-neutral-800 data-[state=unchecked]:hover:bg-neutral-300",
|
||||||
|
],
|
||||||
|
"red-green": [
|
||||||
|
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||||
|
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
|
||||||
|
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||||
|
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
|
||||||
|
],
|
||||||
|
red: [
|
||||||
|
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||||
|
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
|
||||||
|
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||||
|
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"thumb-size": {
|
||||||
|
default: "h-5 w-5 data-[state=checked]:translate-x-5",
|
||||||
|
small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px]",
|
||||||
|
large: "h-[28px] w-[28px] data-[state=checked]:translate-x-[34px]",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
variant: {
|
|
||||||
default: [
|
|
||||||
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
|
|
||||||
"dark:data-[state=checked]:hover:bg-netbird-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
|
|
||||||
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
|
|
||||||
"data-[state=checked]:hover:bg-neutral-800 data-[state=unchecked]:hover:bg-neutral-300",
|
|
||||||
],
|
|
||||||
"red-green": [
|
|
||||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
|
||||||
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
|
|
||||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
|
||||||
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
|
|
||||||
],
|
|
||||||
red: [
|
|
||||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
|
||||||
"dark:data-[state=checked]:hover:bg-red-500 dark:data-[state=unchecked]:hover:bg-nb-gray-600",
|
|
||||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
|
||||||
"data-[state=checked]:hover:bg-red-400 data-[state=unchecked]:hover:bg-red-300",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"thumb-size": {
|
|
||||||
default: "h-5 w-5 data-[state=checked]:translate-x-5",
|
|
||||||
small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px]",
|
|
||||||
large: "h-[28px] w-[28px] data-[state=checked]:translate-x-[34px]",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ToggleSwitch = React.forwardRef<
|
const ToggleSwitch = React.forwardRef<
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
|
||||||
SwitchVariants & { dataCy?: string }
|
SwitchVariants & { dataCy?: string }
|
||||||
>(
|
>(({ className, size = "default", variant = "default", dataCy, ...props }, ref) => (
|
||||||
(
|
|
||||||
{ className, size = "default", variant = "default", dataCy, ...props },
|
|
||||||
ref,
|
|
||||||
) => (
|
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
|
||||||
"peer inline-flex shrink-0 cursor-default items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
|
|
||||||
className,
|
|
||||||
switchVariants({ size, variant }),
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
data-cy={dataCy}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
props.onClick?.(e);
|
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<SwitchPrimitives.Thumb
|
|
||||||
className={cn(
|
className={cn(
|
||||||
switchVariants({ "thumb-size": size }),
|
"wails-no-draggable peer inline-flex shrink-0 cursor-default items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
|
||||||
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
|
className,
|
||||||
|
switchVariants({ size, variant }),
|
||||||
)}
|
)}
|
||||||
/>
|
{...props}
|
||||||
|
data-cy={dataCy}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onClick?.(e);
|
||||||
|
}}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
switchVariants({ "thumb-size": size }),
|
||||||
|
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
),
|
));
|
||||||
);
|
|
||||||
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;
|
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
|
|
||||||
export { ToggleSwitch };
|
export { ToggleSwitch };
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ const STATUS_KEY: Record<ConnectionState, string> = {
|
|||||||
const EVENT_BROWSER_LOGIN_CANCEL = "browser-login:cancel";
|
const EVENT_BROWSER_LOGIN_CANCEL = "browser-login:cancel";
|
||||||
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
const EVENT_TRIGGER_LOGIN = "trigger-login";
|
||||||
|
|
||||||
const NEEDS_LOGIN_STATES = new Set([
|
const NEEDS_LOGIN_STATES = new Set(["NeedsLogin", "SessionExpired", "LoginFailed"]);
|
||||||
"NeedsLogin",
|
|
||||||
"SessionExpired",
|
|
||||||
"LoginFailed",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorMessage = formatErrorMessage;
|
const errorMessage = formatErrorMessage;
|
||||||
|
|
||||||
@@ -164,10 +160,7 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
if (action === "disconnect" && daemonState === "Connected") {
|
if (action === "disconnect" && daemonState === "Connected") {
|
||||||
return ConnectionState.Disconnecting;
|
return ConnectionState.Disconnecting;
|
||||||
}
|
}
|
||||||
if (
|
if ((action === "connect" || action === "logging-in") && daemonState !== "Connected") {
|
||||||
(action === "connect" || action === "logging-in") &&
|
|
||||||
daemonState !== "Connected"
|
|
||||||
) {
|
|
||||||
return ConnectionState.Connecting;
|
return ConnectionState.Connecting;
|
||||||
}
|
}
|
||||||
switch (daemonState) {
|
switch (daemonState) {
|
||||||
@@ -271,11 +264,7 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === "disconnect") {
|
if (action === "disconnect") {
|
||||||
if (
|
if (daemonState === "Idle" || daemonState === "Disconnected" || unreachable) {
|
||||||
daemonState === "Idle" ||
|
|
||||||
daemonState === "Disconnected" ||
|
|
||||||
unreachable
|
|
||||||
) {
|
|
||||||
setAction(null);
|
setAction(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,11 +296,9 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isTransitioning =
|
const isTransitioning =
|
||||||
connState === ConnectionState.Connecting ||
|
connState === ConnectionState.Connecting || connState === ConnectionState.Disconnecting;
|
||||||
connState === ConnectionState.Disconnecting;
|
|
||||||
const isOn =
|
const isOn =
|
||||||
connState === ConnectionState.Connected ||
|
connState === ConnectionState.Connected || connState === ConnectionState.Connecting;
|
||||||
connState === ConnectionState.Connecting;
|
|
||||||
const showLocal = connState === ConnectionState.Connected;
|
const showLocal = connState === ConnectionState.Connected;
|
||||||
const fqdn = status?.local.fqdn || "";
|
const fqdn = status?.local.fqdn || "";
|
||||||
const ip = status?.local.ip || "";
|
const ip = status?.local.ip || "";
|
||||||
@@ -321,7 +308,7 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
<img
|
<img
|
||||||
src={netbirdFullLogo}
|
src={netbirdFullLogo}
|
||||||
alt={"NetBird"}
|
alt={"NetBird"}
|
||||||
className={"h-7 w-auto select-none mb-4"}
|
className={"h-7 w-auto select-none mb-4 wails-no-draggable"}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -330,23 +317,20 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
checked={isOn}
|
checked={isOn}
|
||||||
onCheckedChange={handleSwitch}
|
onCheckedChange={handleSwitch}
|
||||||
disabled={isTransitioning || unreachable}
|
disabled={isTransitioning || unreachable}
|
||||||
className={cn(
|
className={cn(unreachable && "opacity-80", isTransitioning && "animate-pulse")}
|
||||||
unreachable && "opacity-80",
|
|
||||||
isTransitioning && "animate-pulse",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={"flex flex-col items-center"}>
|
<div className={"flex flex-col items-center"}>
|
||||||
<h1
|
<h1
|
||||||
className={
|
className={
|
||||||
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300"
|
"text-sm font-medium text-nb-gray-200 tracking-wide transition-colors duration-300 wails-no-draggable"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(STATUS_KEY[connState])}
|
{t(STATUS_KEY[connState])}
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-2 transition-opacity duration-300",
|
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-2 transition-opacity duration-300 wails-no-draggable",
|
||||||
showLocal && fqdn ? "opacity-100" : "opacity-0",
|
showLocal && fqdn ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -354,7 +338,7 @@ export const ConnectionStatusSwitch = () => {
|
|||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-0.5 transition-opacity duration-300",
|
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-0.5 transition-opacity duration-300 wails-no-draggable",
|
||||||
showLocal && ip ? "opacity-100" : "opacity-0",
|
showLocal && ip ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Window } from "@wailsio/runtime";
|
||||||
import { WindowManager } from "@bindings/services";
|
import { WindowManager } from "@bindings/services";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -22,6 +23,14 @@ import { cn } from "@/lib/cn";
|
|||||||
|
|
||||||
type ViewMode = "default" | "advanced";
|
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.
|
||||||
|
const VIEW_SIZE: Record<ViewMode, { width: number; height: number }> = {
|
||||||
|
default: { width: 380, height: 640 },
|
||||||
|
advanced: { width: 900, height: 640 },
|
||||||
|
};
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
@@ -38,7 +47,10 @@ export const Header = () => {
|
|||||||
|
|
||||||
const selectMode = (mode: ViewMode) => {
|
const selectMode = (mode: ViewMode) => {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
|
if (mode === viewMode) return;
|
||||||
setViewMode(mode);
|
setViewMode(mode);
|
||||||
|
const { width, height } = VIEW_SIZE[mode];
|
||||||
|
void Window.SetSize(width, height).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,12 +66,19 @@ export const Header = () => {
|
|||||||
<div className={"flex justify-center ml-4"}>
|
<div className={"flex justify-center ml-4"}>
|
||||||
<ProfileDropdown onManageProfiles={openManageProfiles} />
|
<ProfileDropdown onManageProfiles={openManageProfiles} />
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex justify-end"}>
|
<div className={"flex justify-end wails-no-draggable"}>
|
||||||
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
|
<DropdownMenu modal={false} open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild className={"wails-no-draggable"}>
|
||||||
<IconButton icon={MoreVertical} iconClassName={"text-nb-gray-200"} />
|
<IconButton
|
||||||
|
icon={MoreVertical}
|
||||||
|
iconClassName={"text-nb-gray-200 wails-no-draggable"}
|
||||||
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" sideOffset={8} className="min-w-52">
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
sideOffset={8}
|
||||||
|
className="min-w-52 data-[state=closed]:!animate-none data-[state=closed]:!duration-0"
|
||||||
|
>
|
||||||
<DropdownMenuItem onClick={openSettings}>
|
<DropdownMenuItem onClick={openSettings}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings size={14} />
|
<Settings size={14} />
|
||||||
|
|||||||
+1
-1
@@ -181,7 +181,7 @@ func main() {
|
|||||||
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
Title: "NetBird",
|
Title: "NetBird",
|
||||||
Width: 380,
|
Width: 380,
|
||||||
Height: 590,
|
Height: 640,
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
BackgroundColour: application.NewRGB(24, 26, 29),
|
BackgroundColour: application.NewRGB(24, 26, 29),
|
||||||
URL: "/",
|
URL: "/",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (s *WindowManager) OpenSettings(tab string) {
|
|||||||
URL: startURL,
|
URL: startURL,
|
||||||
Mac: application.MacWindow{
|
Mac: application.MacWindow{
|
||||||
InvisibleTitleBarHeight: 38,
|
InvisibleTitleBarHeight: 38,
|
||||||
Backdrop: application.MacBackdropTranslucent,
|
Backdrop: application.MacBackdropNormal,
|
||||||
TitleBar: application.MacTitleBarHiddenInset,
|
TitleBar: application.MacTitleBarHiddenInset,
|
||||||
CollectionBehavior: application.MacWindowCollectionBehaviorFullScreenNone,
|
CollectionBehavior: application.MacWindowCollectionBehaviorFullScreenNone,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user