[client] Add light mode with system, light, and dark theme options (#7344)

* desktop UI light mode

* Theme review fixes plus macOS window outline fix

* Windows runtime chrome re-theming plus apply serialization

* Windows chrome threading and theme event ordering fixes

* Darken toggle and setting sidebar text

* resolve theme appearance, apply on UI thread

* read theme once per window

* Re-assert Windows dark opt-in after SetTheme

* split app-wide GTK theming from per-window chrome

* Update Wails dependency and checksums

* KDE tray icon panel fix

* Five review fixes: theme ordering, cgo dedup, KDE panel resolution

* Path guard hardening, toggle contrast, windows comment

* non-vacuous escape tests

* Default view edits

* Polish settings nav, controls, borders, and disc

* Profiles settings boarder, modals, and buttons

* Additional edits based on feedback

* Switch colors away from slight blue hue

* Update missing lang

* Fix vertical tab active view
This commit is contained in:
Brandon Hopkins
2026-09-11 08:25:10 -07:00
committed by GitHub
parent f422c41654
commit ec0c36b0e7
68 changed files with 1853 additions and 259 deletions
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { Browser } from "@wailsio/runtime";
import { BookOpen, MessageSquareText, MessagesSquare } from "lucide-react";
import netbirdFull from "@/assets/logos/netbird-full.svg";
import netbirdFullLight from "@/assets/logos/netbird-full-light.svg";
// Brand glyphs from simpleicons.org (lucide deprecated its brand icons).
const GithubIcon = (props: SVGProps<SVGSVGElement>) => (
@@ -90,7 +91,16 @@ export function SettingsAbout() {
"mx-auto flex min-h-[calc(100vh-12rem)] max-w-2xl flex-col items-center justify-center gap-4"
}
>
<img src={netbirdFull} alt={t("common.netbird")} className={"h-7 w-auto"} />
<img
src={netbirdFullLight}
alt={t("common.netbird")}
className={"h-7 w-auto dark:hidden"}
/>
<img
src={netbirdFull}
alt={t("common.netbird")}
className={"hidden h-7 w-auto dark:block"}
/>
<div className={"flex flex-col items-center gap-0.5 text-center"}>
<button
type={"button"}
@@ -139,7 +149,7 @@ export function SettingsAbout() {
tabIndex={0}
onClick={() => openUrl(url)}
className={
"inline-flex items-center gap-1.5 rounded-sm decoration-[0.5px] underline-offset-4 outline-none transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
"inline-flex items-center gap-1.5 rounded-sm decoration-[0.5px] underline-offset-4 outline-none transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
}
>
<Icon aria-hidden={"true"} className={iconClassName ?? "h-3.5 w-3.5"} />
@@ -157,7 +167,7 @@ export function SettingsAbout() {
tabIndex={0}
onClick={() => openUrl(link.url)}
className={
"rounded-sm decoration-[0.5px] underline-offset-4 outline-none transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-white/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
"rounded-sm decoration-[0.5px] underline-offset-4 outline-none transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940"
}
>
{link.label}
@@ -10,6 +10,7 @@ import { useAutostartSetting, useSettings } from "@/contexts/SettingsContext.tsx
import { ManagementServerSwitch } from "@/components/ManagementServerSwitch.tsx";
import { ManagementMode, useManagementUrl } from "@/hooks/useManagementUrl.ts";
import { LanguagePicker } from "@/components/LanguagePicker.tsx";
import { ThemePicker } from "@/components/ThemePicker.tsx";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
import { useKeepConnectedOnQuit } from "@/hooks/useKeepConnectedOnQuit.ts";
@@ -36,6 +37,7 @@ export function SettingsGeneral() {
<>
<SectionGroup title={t("settings.general.section.general")}>
<LanguagePicker />
<ThemePicker />
<FancyToggleSwitch
value={!config.disableNotifications}
onChange={(v) => setField("disableNotifications", !v)}
@@ -16,7 +16,7 @@ export const SectionGroup = ({
{...(disabled ? { inert: "" } : {})}
className={cn(
"mb-8 rounded-md px-1 outline-none last:mb-1",
"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 && "pointer-events-none opacity-30",
)}
>
@@ -33,7 +33,7 @@ export const SettingsBottomBar = ({ children }: { children: ReactNode }) => (
<div className={"absolute bottom-0 left-0 w-full"}>
<div
className={
"flex w-full justify-end gap-3 border-t border-nb-gray-920 bg-nb-gray-940 px-8 py-5"
"flex w-full justify-end gap-3 border-t border-nb-gray-800 bg-nb-gray-940 px-8 py-5 dark:border-nb-gray-920"
}
>
{children}
@@ -99,7 +99,7 @@ export function SettingsTroubleshooting() {
"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",
"hover:border-nb-gray-700 data-[state=open]:border-nb-gray-700 dark:hover:border-nb-gray-600 dark:data-[state=open]:border-nb-gray-600",
)}
>
{t(`settings.troubleshooting.anonymize.${anonymizeLevel}`)}
@@ -277,7 +277,10 @@ function DoneResult({
};
return (
<CenteredPanel>
<SquareIcon icon={CircleCheckBig} className={"[&_svg]:text-green-500"} />
<SquareIcon
icon={CircleCheckBig}
className={"bg-white dark:bg-nb-gray-920 [&_svg]:text-green-500"}
/>
<div className={"flex max-w-sm flex-col items-center gap-2"}>
<DialogHeading className={"text-balance"}>
@@ -326,7 +329,9 @@ function DoneResult({
<button
type={"button"}
onClick={onRevealPath}
className={"pointer-events-auto transition-all hover:text-white"}
className={
"pointer-events-auto transition-all hover:text-nb-gray-50"
}
aria-label={t("settings.troubleshooting.done.openFileLocation")}
>
<FolderOpen size={16} aria-hidden={"true"} />
@@ -339,7 +344,7 @@ function DoneResult({
<div
role={"alert"}
className={
"rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300"
"rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-700 dark:text-red-300"
}
>
{result.uploadFailureReason