diff --git a/client/ui/assets/netbird-menu-16.png b/client/ui/assets/netbird-menu-16.png
new file mode 100644
index 000000000..d5dcab446
Binary files /dev/null and b/client/ui/assets/netbird-menu-16.png differ
diff --git a/client/ui/assets/netbird-menu-22.png b/client/ui/assets/netbird-menu-22.png
new file mode 100644
index 000000000..87c6b5995
Binary files /dev/null and b/client/ui/assets/netbird-menu-22.png differ
diff --git a/client/ui/assets/netbird-menu-24.png b/client/ui/assets/netbird-menu-24.png
new file mode 100644
index 000000000..087c1c2ae
Binary files /dev/null and b/client/ui/assets/netbird-menu-24.png differ
diff --git a/client/ui/assets/netbird-menu-dot-connected-16.png b/client/ui/assets/netbird-menu-dot-connected-16.png
new file mode 100644
index 000000000..3a7fa31a4
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connected-16.png differ
diff --git a/client/ui/assets/netbird-menu-dot-connected-22.png b/client/ui/assets/netbird-menu-dot-connected-22.png
new file mode 100644
index 000000000..78b068748
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connected-22.png differ
diff --git a/client/ui/assets/netbird-menu-dot-connecting-16.png b/client/ui/assets/netbird-menu-dot-connecting-16.png
new file mode 100644
index 000000000..f874706b5
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connecting-16.png differ
diff --git a/client/ui/assets/netbird-menu-dot-connecting-22.png b/client/ui/assets/netbird-menu-dot-connecting-22.png
new file mode 100644
index 000000000..d8e5970f5
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connecting-22.png differ
diff --git a/client/ui/assets/netbird-menu-dot-error-16.png b/client/ui/assets/netbird-menu-dot-error-16.png
new file mode 100644
index 000000000..cdc6254da
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-error-16.png differ
diff --git a/client/ui/assets/netbird-menu-dot-error-22.png b/client/ui/assets/netbird-menu-dot-error-22.png
new file mode 100644
index 000000000..d9bd013d6
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-error-22.png differ
diff --git a/client/ui/assets/netbird-menu-dot-idle-16.png b/client/ui/assets/netbird-menu-dot-idle-16.png
new file mode 100644
index 000000000..354b5b860
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-idle-16.png differ
diff --git a/client/ui/assets/netbird-menu-dot-idle-22.png b/client/ui/assets/netbird-menu-dot-idle-22.png
new file mode 100644
index 000000000..675cf1ffe
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-idle-22.png differ
diff --git a/client/ui/assets/netbird-menu-dot-login-16.png b/client/ui/assets/netbird-menu-dot-login-16.png
new file mode 100644
index 000000000..6d65365a2
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-login-16.png differ
diff --git a/client/ui/assets/netbird-menu-dot-login-22.png b/client/ui/assets/netbird-menu-dot-login-22.png
new file mode 100644
index 000000000..563de48ed
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-login-22.png differ
diff --git a/client/ui/assets/netbird-menu-dot-offline-16.png b/client/ui/assets/netbird-menu-dot-offline-16.png
new file mode 100644
index 000000000..f9aa5c3e9
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-offline-16.png differ
diff --git a/client/ui/assets/netbird-menu-dot-offline-22.png b/client/ui/assets/netbird-menu-dot-offline-22.png
new file mode 100644
index 000000000..5202c8baa
Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-offline-22.png differ
diff --git a/client/ui/assets/svg/netbird-menu.svg b/client/ui/assets/svg/netbird-menu.svg
new file mode 100644
index 000000000..bd4e9d65d
--- /dev/null
+++ b/client/ui/assets/svg/netbird-menu.svg
@@ -0,0 +1,7 @@
+
diff --git a/client/ui/icons.go b/client/ui/icons.go
index 3e8215b73..d5759cee7 100644
--- a/client/ui/icons.go
+++ b/client/ui/icons.go
@@ -59,24 +59,9 @@ var iconUpdateDisconnectedMacOS []byte
//go:embed assets/netbird.png
var iconWindow []byte
-// Small colored dots shown next to the status menu entry. Rendered as
-// regular NSImage/HBITMAP/GTK menu-item icons (not template), so the
-// colours stay intact on every platform.
-
-//go:embed assets/netbird-menu-dot-connected.png
-var iconMenuDotConnected []byte
-
-//go:embed assets/netbird-menu-dot-connecting.png
-var iconMenuDotConnecting []byte
-
-//go:embed assets/netbird-menu-dot-login.png
-var iconMenuDotLogin []byte
-
-//go:embed assets/netbird-menu-dot-error.png
-var iconMenuDotError []byte
-
-//go:embed assets/netbird-menu-dot-idle.png
-var iconMenuDotIdle []byte
-
-//go:embed assets/netbird-menu-dot-offline.png
-var iconMenuDotOffline []byte
+// Per-platform menu-row icons (status dots + NetBird brand mark) live in
+// icons_menu_windows.go and icons_menu_other.go. Windows installs them
+// into the Win32 check-mark slot, which expects SM_CXMENUCHECK-sized
+// bitmaps (~16x16 at 100% DPI) — anything bigger gets cropped, anything
+// smaller leaves blank space — so Windows ships its own 16x16 set
+// while macOS/Linux keep the larger 24x24 assets that fit their menus.
diff --git a/client/ui/icons_menu_darwin.go b/client/ui/icons_menu_darwin.go
new file mode 100644
index 000000000..464e67096
--- /dev/null
+++ b/client/ui/icons_menu_darwin.go
@@ -0,0 +1,39 @@
+//go:build darwin
+
+package main
+
+import _ "embed"
+
+// 22x22 menu-row icons used on macOS. Apple's HIG recommends an 18–22 px
+// glyph for NSMenuItem leading images; 22 sits at the top of that range
+// and matches the visual weight of the surrounding row text. Windows
+// ships a 16x16 variant (Win32 SM_CXMENUCHECK slot) and Linux a 24x24
+// variant (GTK menu row supports the larger range) — see the sibling
+// icons_menu_*.go files.
+//
+// Regenerate the brand mark from assets/svg/netbird-menu.svg (vector
+// source — re-rendering keeps the strokes crisp at every target size):
+// inkscape assets/svg/netbird-menu.svg -o netbird-menu-22.png -w 22 -h 22 \
+// --export-background-opacity=0
+// Status dots are downscaled from the 24x24 originals with ImageMagick.
+
+//go:embed assets/netbird-menu-22.png
+var iconMenuNetbird []byte
+
+//go:embed assets/netbird-menu-dot-connected-22.png
+var iconMenuDotConnected []byte
+
+//go:embed assets/netbird-menu-dot-connecting-22.png
+var iconMenuDotConnecting []byte
+
+//go:embed assets/netbird-menu-dot-login-22.png
+var iconMenuDotLogin []byte
+
+//go:embed assets/netbird-menu-dot-error-22.png
+var iconMenuDotError []byte
+
+//go:embed assets/netbird-menu-dot-idle-22.png
+var iconMenuDotIdle []byte
+
+//go:embed assets/netbird-menu-dot-offline-22.png
+var iconMenuDotOffline []byte
diff --git a/client/ui/icons_menu_linux.go b/client/ui/icons_menu_linux.go
new file mode 100644
index 000000000..8a562c639
--- /dev/null
+++ b/client/ui/icons_menu_linux.go
@@ -0,0 +1,40 @@
+//go:build linux
+
+package main
+
+import _ "embed"
+
+// 24x24 menu-row icons used on Linux. GTK4 menu rows accept icons in the
+// 22–48 px range with no automatic downscaling at this size; 24 reads
+// cleanly next to the row text across the GNOME / KDE / minimal-WM
+// flavours we ship to. Windows ships a 16x16 variant (Win32
+// SM_CXMENUCHECK slot) and macOS a 22x22 variant — see the sibling
+// icons_menu_*.go files.
+//
+// Regenerate the brand mark from assets/svg/netbird-menu.svg (vector
+// source — re-rendering keeps the strokes crisp at every target size):
+// inkscape assets/svg/netbird-menu.svg -o netbird-menu-24.png -w 24 -h 24 \
+// --export-background-opacity=0
+// Status dots are the canonical 24x24 originals used everywhere else
+// in the legacy Fyne tray.
+
+//go:embed assets/netbird-menu-24.png
+var iconMenuNetbird []byte
+
+//go:embed assets/netbird-menu-dot-connected.png
+var iconMenuDotConnected []byte
+
+//go:embed assets/netbird-menu-dot-connecting.png
+var iconMenuDotConnecting []byte
+
+//go:embed assets/netbird-menu-dot-login.png
+var iconMenuDotLogin []byte
+
+//go:embed assets/netbird-menu-dot-error.png
+var iconMenuDotError []byte
+
+//go:embed assets/netbird-menu-dot-idle.png
+var iconMenuDotIdle []byte
+
+//go:embed assets/netbird-menu-dot-offline.png
+var iconMenuDotOffline []byte
diff --git a/client/ui/icons_menu_windows.go b/client/ui/icons_menu_windows.go
new file mode 100644
index 000000000..d2b44af60
--- /dev/null
+++ b/client/ui/icons_menu_windows.go
@@ -0,0 +1,42 @@
+//go:build windows
+
+package main
+
+import _ "embed"
+
+// 16x16 menu-row icons used on Windows. The Win32 SetMenuItemBitmaps API
+// paints the HBITMAP into the check-mark slot, sized to SM_CXMENUCHECK /
+// SM_CYMENUCHECK (typically 16x16 at 100% DPI). Larger bitmaps overflow
+// the row visually, so Windows ships its own scaled set instead of the
+// 24x24 assets used on macOS/Linux. Regenerate the brand mark from
+// assets/svg/netbird-menu.svg (vector source — re-rendering keeps the
+// strokes crisp at every target size):
+// inkscape assets/svg/netbird-menu.svg -o netbird-menu-16.png -w 16 -h 16 \
+// --export-background-opacity=0
+// The status dots are downscaled from the 24x24 originals with
+// ImageMagick — simple solid-fill circles survive the bicubic resize
+// without visible quality loss:
+// magick netbird-menu-dot-.png -resize 16x16 \
+// -background none -gravity center -extent 16x16 \
+// netbird-menu-dot--16.png
+
+//go:embed assets/netbird-menu-16.png
+var iconMenuNetbird []byte
+
+//go:embed assets/netbird-menu-dot-connected-16.png
+var iconMenuDotConnected []byte
+
+//go:embed assets/netbird-menu-dot-connecting-16.png
+var iconMenuDotConnecting []byte
+
+//go:embed assets/netbird-menu-dot-login-16.png
+var iconMenuDotLogin []byte
+
+//go:embed assets/netbird-menu-dot-error-16.png
+var iconMenuDotError []byte
+
+//go:embed assets/netbird-menu-dot-idle-16.png
+var iconMenuDotIdle []byte
+
+//go:embed assets/netbird-menu-dot-offline-16.png
+var iconMenuDotOffline []byte
diff --git a/client/ui/tray.go b/client/ui/tray.go
index eb1a4f227..f28aa4a83 100644
--- a/client/ui/tray.go
+++ b/client/ui/tray.go
@@ -242,7 +242,7 @@ func (t *Tray) reapplyMenuState() {
if t.statusItem != nil && lastStatus != "" {
t.statusItem.SetLabel(t.loc.StatusLabel(lastStatus))
- t.statusItem.SetEnabled(false)
+ t.statusItem.SetEnabled(statusRowEnabled())
t.applyStatusIndicator(lastStatus)
}
if t.sessionExpiresItem != nil {
@@ -314,13 +314,18 @@ func (t *Tray) ShowWindow() {
func (t *Tray) buildMenu() *application.Menu {
menu := application.NewMenu()
- // statusItem shows the daemon's current status. Disabled (and no
- // OnClick handler) so clicks are no-ops — the row is informational
- // only. The Connect entry below drives every actionable transition,
- // including the SSO re-auth flow for NeedsLogin/SessionExpired
- // (the daemon's Up RPC returns NeedsSSOLogin when applicable).
+ // statusItem shows the daemon's current status. Informational row
+ // with no OnClick handler — clicks are no-ops. Whether the row is
+ // kept enabled is platform-dependent (see statusRowEnabled): on
+ // Windows the disabled-state mask would desaturate the coloured
+ // status dot painted into the check-mark slot, so the row stays
+ // enabled there; macOS/Linux disable it so the greyed-out label
+ // signals that it is not clickable. The Connect entry below drives
+ // every actionable transition, including the SSO re-auth flow for
+ // NeedsLogin/SessionExpired (the daemon's Up RPC returns
+ // NeedsSSOLogin when applicable).
t.statusItem = menu.Add(t.loc.T("tray.status.disconnected")).
- SetEnabled(false).
+ SetEnabled(statusRowEnabled()).
SetBitmap(iconMenuDotIdle)
// sessionExpiresItem sits directly below the status row so the
@@ -376,7 +381,11 @@ func (t *Tray) buildMenu() *application.Menu {
menu.AddSeparator()
- about := menu.AddSubmenu(t.loc.T("tray.menu.about"))
+ aboutLabel := t.loc.T("tray.menu.about")
+ about := menu.AddSubmenu(aboutLabel)
+ if aboutItem := menu.FindByLabel(aboutLabel); aboutItem != nil {
+ aboutItem.SetBitmap(iconMenuNetbird)
+ }
about.Add(t.loc.T("tray.menu.github")).OnClick(func(*application.Context) {
_ = t.app.Browser.OpenURL(urlGitHubRepo)
})
@@ -621,11 +630,14 @@ func (t *Tray) applyStatus(st services.Status) {
daemonUnavailable := strings.EqualFold(st.Status, services.StatusDaemonUnavailable)
connecting := strings.EqualFold(st.Status, services.StatusConnecting)
if t.statusItem != nil {
- // Label-only: kept disabled (informational row). Swap the
- // displayed text so the user sees a familiar phrase instead
- // of the raw daemon enum.
+ // Label-only: row is informational (no OnClick). Enablement
+ // is platform-dependent via statusRowEnabled — Windows
+ // keeps it enabled so the Win32 disabled-state mask does
+ // not desaturate the coloured dot; macOS/Linux disable it.
+ // Swap the displayed text so the user sees a familiar
+ // phrase instead of the raw daemon enum.
t.statusItem.SetLabel(t.loc.StatusLabel(st.Status))
- t.statusItem.SetEnabled(false)
+ t.statusItem.SetEnabled(statusRowEnabled())
t.applyStatusIndicator(st.Status)
}
if t.upItem != nil {
diff --git a/client/ui/tray_status_enabled_other.go b/client/ui/tray_status_enabled_other.go
new file mode 100644
index 000000000..1e4c8a13c
--- /dev/null
+++ b/client/ui/tray_status_enabled_other.go
@@ -0,0 +1,12 @@
+//go:build !windows && !android && !ios && !freebsd && !js
+
+package main
+
+// statusRowEnabled reports whether the informational status row at the
+// top of the tray menu should stay enabled. False on macOS and Linux:
+// both platforms paint disabled menu rows at slightly reduced opacity
+// without desaturating the leading bitmap, so the coloured status dot
+// stays visible while the greyed-out label still signals to the user
+// that the row is informational and not clickable. Windows opts in via
+// the sibling tray_status_enabled_windows.go file.
+func statusRowEnabled() bool { return false }
diff --git a/client/ui/tray_status_enabled_windows.go b/client/ui/tray_status_enabled_windows.go
new file mode 100644
index 000000000..cdfec41a6
--- /dev/null
+++ b/client/ui/tray_status_enabled_windows.go
@@ -0,0 +1,13 @@
+//go:build windows
+
+package main
+
+// statusRowEnabled reports whether the informational status row at the
+// top of the tray menu should stay enabled. Always true on Windows:
+// the Win32 disabled-state mask desaturates both the row text and the
+// HBITMAP painted into the check-mark slot, so a disabled row would
+// render the coloured status dot in greyscale and defeat the indicator.
+// macOS/Linux disable the row (see tray_status_enabled_other.go) because
+// neither platform applies that desaturation and the visual cue that
+// the row is informational reads better.
+func statusRowEnabled() bool { return true }