Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions client/ui/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,25 @@
<title>NetBird</title>
<style>
html, body { background: #181A1D; }
html:not(.dark), html:not(.dark) body { background: #E9ECEF; }
</style>
<script>
// Pre-paint theme guard: apply the last-known theme before first render
// to avoid a flash of the wrong theme. ThemeContext keeps the mirror
// fresh from the persisted preference and the Go-reported OS appearance.
(function () {
try {
var pref = localStorage.getItem("nb-theme-pref") || "system";
var dark;
if (pref === "dark") dark = true;
else if (pref === "light") dark = false;
else dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", dark);
} catch (e) {
/* keep the default dark class */
}
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
57 changes: 33 additions & 24 deletions client/ui/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SkeletonTheme } from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { welcome } from "@/lib/welcome";
import LoginWaitingForBrowserDialog from "@/modules/login/LoginWaitingForBrowserDialog.tsx";
import { ThemeProvider } from "@/contexts/ThemeContext.tsx";
import { initI18n } from "@/lib/i18n";
import { initPlatform } from "@/lib/platform";
import { initLogForwarding } from "@/lib/logs";
Expand All @@ -35,30 +36,38 @@ Promise.all([
]).finally(() => {
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
<HashRouter>
<Routes>
<Route path={"dialog"}>
<Route
path={"browser-login"}
element={<LoginWaitingForBrowserDialog />}
/>
<Route path={"install-progress"} element={<UpdateInProgressDialog />} />
<Route
path={"session-expiration"}
element={<SessionExpirationDialog />}
/>
<Route path={"welcome"} element={<WelcomeDialog />} />
<Route path={"error"} element={<ErrorDialog />} />
</Route>
<Route element={<AppLayout />}>
<Route index element={<MainPage />} />
<Route path={"settings"} element={<SettingsPage />} />
<Route path={"*"} element={<Navigate to={"/"} replace />} />
</Route>
</Routes>
</HashRouter>
</SkeletonTheme>
<ThemeProvider>
<SkeletonTheme
baseColor={"rgb(var(--skeleton-base))"}
highlightColor={"rgb(var(--skeleton-highlight))"}
>
<HashRouter>
<Routes>
<Route path={"dialog"}>
<Route
path={"browser-login"}
element={<LoginWaitingForBrowserDialog />}
/>
<Route
path={"install-progress"}
element={<UpdateInProgressDialog />}
/>
<Route
path={"session-expiration"}
element={<SessionExpirationDialog />}
/>
<Route path={"welcome"} element={<WelcomeDialog />} />
<Route path={"error"} element={<ErrorDialog />} />
</Route>
<Route element={<AppLayout />}>
<Route index element={<MainPage />} />
<Route path={"settings"} element={<SettingsPage />} />
<Route path={"*"} element={<Navigate to={"/"} replace />} />
</Route>
</Routes>
</HashRouter>
</SkeletonTheme>
</ThemeProvider>
</React.StrictMode>,
);
});
19 changes: 19 additions & 0 deletions client/ui/frontend/src/assets/logos/netbird-full-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 7 additions & 5 deletions client/ui/frontend/src/components/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ type Props = HTMLAttributes<HTMLSpanElement> & {
};

const VARIANT_CLASSES: Record<BadgeVariant, string> = {
info: "bg-sky-900 border border-sky-700 text-sky-200",
info: "bg-sky-100 border border-sky-300 text-sky-800 dark:bg-sky-900 dark:border-sky-700 dark:text-sky-200",
neutral: "bg-nb-gray-900 border border-nb-gray-850 text-nb-gray-200",
brand: "bg-netbird/15 border border-netbird/30 text-netbird",
success: "bg-green-900 border border-green-700 text-green-200",
warning: "bg-yellow-900 border border-yellow-700 text-yellow-200",
danger: "bg-red-900 border border-red-700 text-red-200",
brand: "bg-netbird/15 border border-netbird/30 text-netbird-700 dark:text-netbird",
success:
"bg-green-100 border border-green-300 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-200",
warning:
"bg-yellow-100 border border-yellow-300 text-yellow-800 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200",
danger: "bg-red-100 border border-red-300 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-200",
};

export const Badge = forwardRef<HTMLSpanElement, Props>(function Badge(
Expand Down
2 changes: 1 addition & 1 deletion client/ui/frontend/src/components/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const CopyToClipboard = ({
aria-live={"polite"}
className={cn(
"group/copy wails-no-draggable pointer-events-auto inline-flex cursor-default items-center gap-2 rounded-sm text-left outline-none",
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
className,
)}
>
Expand Down
2 changes: 1 addition & 1 deletion client/ui/frontend/src/components/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const menuItemVariants = cva("", {
variant: {
default:
"text-nb-gray-200 hover:bg-nb-gray-900 hover:text-nb-gray-50 focus-visible:bg-nb-gray-900 focus-visible:text-nb-gray-50 data-[state=open]:bg-nb-gray-900 data-[state=open]:text-nb-gray-50",
danger: "text-red-500 hover:bg-red-900/20 hover:text-red-500 focus-visible:bg-red-900/20 focus-visible:text-red-500",
danger: "text-red-500 hover:bg-red-500/10 hover:text-red-500 focus-visible:bg-red-500/10 focus-visible:text-red-500 dark:hover:bg-red-900/20 dark:focus-visible:bg-red-900/20",
},
},
defaultVariants: { variant: "default" },
Expand Down
2 changes: 1 addition & 1 deletion client/ui/frontend/src/components/LanguagePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function LanguagePicker() {
"cursor-default text-xs font-semibold text-nb-gray-100 outline-none",
"hover:border-nb-gray-600 data-[state=open]:border-nb-gray-600",
isFocusVisible &&
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"disabled:opacity-50",
)}
>
Expand Down
2 changes: 1 addition & 1 deletion client/ui/frontend/src/components/SquareIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cn } from "@/lib/cn";
export type SquareIconVariant = "default" | "info" | "warning" | "danger";

const variantClass: Record<SquareIconVariant, string> = {
default: "text-white",
default: "text-nb-gray-50",
info: "text-sky-400",
warning: "text-netbird",
danger: "text-red-500",
Expand Down
109 changes: 109 additions & 0 deletions client/ui/frontend/src/components/ThemePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, MonitorIcon, MoonIcon, SunMediumIcon, type LucideIcon } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/DropdownMenu";
import { HelpText } from "@/components/typography/HelpText";
import { Label } from "@/components/typography/Label";
import { useTheme, type ThemePreference } from "@/contexts/ThemeContext";
import { useFocusVisible } from "@/hooks/useFocusVisible";
import { cn } from "@/lib/cn";
import { errorDialog, formatErrorMessage } from "@/lib/errors";

const OPTIONS: { value: ThemePreference; icon: LucideIcon; labelKey: string }[] = [
{ value: "system", icon: MonitorIcon, labelKey: "settings.general.theme.system" },
{ value: "light", icon: SunMediumIcon, labelKey: "settings.general.theme.light" },
{ value: "dark", icon: MoonIcon, labelKey: "settings.general.theme.dark" },
];

export function ThemePicker() {
const { t } = useTranslation();
const { theme, setTheme } = useTheme();
const [busy, setBusy] = useState(false);
const isFocusVisible = useFocusVisible();

const current = OPTIONS.find((o) => o.value === theme) ?? OPTIONS[0];
const CurrentIcon = current.icon;

const select = async (value: string) => {
if (busy || value === theme) return;
setBusy(true);
try {
await setTheme(value as ThemePreference);
} catch (e) {
await errorDialog({
Title: t("settings.error.saveTitle"),
Message: formatErrorMessage(e),
});
} finally {
setBusy(false);
}
};

return (
<div className={"flex items-center justify-between gap-6"}>
<div className={"max-w-md flex-1"}>
<Label as={"div"}>{t("settings.general.theme.label")}</Label>
<HelpText margin={false}>{t("settings.general.theme.help")}</HelpText>
</div>
<div className={"shrink-0"}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type={"button"}
tabIndex={0}
disabled={busy}
aria-label={t("settings.general.theme.label")}
className={cn(
"inline-flex h-[40px] min-w-[160px] items-center gap-2 px-3",
"rounded-md border bg-white dark:bg-nb-gray-900",
"border-neutral-200 dark:border-nb-gray-700",
"cursor-default text-xs font-semibold text-nb-gray-100 outline-none",
"hover:border-nb-gray-600 data-[state=open]:border-nb-gray-600",
isFocusVisible &&
"focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"disabled:opacity-50",
)}
>
<CurrentIcon
size={16}
aria-hidden={"true"}
className={"shrink-0 text-nb-gray-200"}
/>
<span className={"flex-1 truncate text-left"}>
{t(current.labelKey)}
</span>
<ChevronDown
size={12}
aria-hidden={"true"}
className={"shrink-0 text-nb-gray-400"}
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align={"end"}
className={"w-[var(--radix-dropdown-menu-trigger-width)]"}
>
<DropdownMenuRadioGroup value={theme} onValueChange={(v) => void select(v)}>
{OPTIONS.map(({ value, icon: Icon, labelKey }) => (
<DropdownMenuRadioItem key={value} value={value}>
<Icon
size={14}
aria-hidden={"true"}
className={"mr-2 shrink-0 text-nb-gray-300"}
/>
{t(labelKey)}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion client/ui/frontend/src/components/VerticalTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function VerticalTab
"data-[state=active]:bg-nb-gray-930",
"data-[state=inactive]:hover:bg-nb-gray-935",
isFocusVisible &&
"focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
"focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940",
className,
)}
{...props}
Expand Down
Loading
Loading