diff --git a/assets/images/tray-idle-black.png b/assets/images/tray-idle-black.png new file mode 100644 index 000000000..7ccebca06 Binary files /dev/null and b/assets/images/tray-idle-black.png differ diff --git a/assets/images/tray-idle-black@2x.png b/assets/images/tray-idle-black@2x.png new file mode 100644 index 000000000..4dd542bfa Binary files /dev/null and b/assets/images/tray-idle-black@2x.png differ diff --git a/src/main/handlers/tray.test.ts b/src/main/handlers/tray.test.ts index 318b74e7c..a6891a3a3 100644 --- a/src/main/handlers/tray.test.ts +++ b/src/main/handlers/tray.test.ts @@ -1,3 +1,6 @@ +import { EventEmitter } from 'node:events'; + +import { app, nativeTheme } from 'electron'; import type { Menubar } from 'electron-menubar'; import { EVENTS } from '../../shared/events'; @@ -8,15 +11,25 @@ import { registerTrayHandlers } from './tray'; const onMock = vi.fn(); vi.mock('electron', () => ({ + app: new EventEmitter(), + nativeTheme: Object.assign(new EventEmitter(), { + shouldUseDarkColorsForSystemIntegratedUI: false, + }), ipcMain: { on: (...args: unknown[]) => onMock(...args), } satisfies Pick, })); +vi.mock('../../shared/platform', () => ({ isMacOS: () => false, isWindows: () => true })); + describe('main/handlers/tray.ts', () => { let menubar: Menubar; beforeEach(() => { + vi.clearAllMocks(); + nativeTheme.removeAllListeners(); + app.removeAllListeners(); + Object.assign(nativeTheme, { shouldUseDarkColorsForSystemIntegratedUI: false }); menubar = { tray: { isDestroyed: vi.fn().mockReturnValue(false), @@ -36,7 +49,7 @@ describe('main/handlers/tray.ts', () => { const registeredEvents = onMock.mock.calls.map((call: unknown[]) => call[0]); - expect(registeredEvents).toContain(EVENTS.USE_ALTERNATE_IDLE_ICON); + expect(registeredEvents).toContain(EVENTS.SET_TRAY_ICON_APPEARANCE); expect(registeredEvents).toContain(EVENTS.USE_UNREAD_ACTIVE_ICON); expect(registeredEvents).toContain(EVENTS.UPDATE_ICON_COLOR); expect(registeredEvents).toContain(EVENTS.UPDATE_ICON_TITLE); @@ -63,7 +76,7 @@ describe('main/handlers/tray.ts', () => { )?.[1]; updateColorHandler?.({}, { notificationsCount: 0, isOnline: true }); - expect(menubar.tray.setImage).toHaveBeenCalledWith(TrayIcons.idle); + expect(menubar.tray.setImage).toHaveBeenCalledWith(TrayIcons.dark); }); it('sets active icon when notifications count is positive', () => { @@ -109,4 +122,49 @@ describe('main/handlers/tray.ts', () => { expect(menubar.tray.setTitle).toHaveBeenCalledWith('5'); }); + it('updates an idle icon when the system theme changes and honors a manual override', () => { + registerTrayHandlers(menubar); + const appearance = onMock.mock.calls.find( + (call) => call[0] === EVENTS.SET_TRAY_ICON_APPEARANCE, + )?.[1]; + Object.assign(nativeTheme, { shouldUseDarkColorsForSystemIntegratedUI: true }); + nativeTheme.emit('updated'); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(TrayIcons.light); + appearance({}, 'dark'); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(TrayIcons.dark); + nativeTheme.emit('updated'); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(TrayIcons.dark); + appearance({}, 'auto'); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(TrayIcons.light); + }); + + it.each([ + [{ notificationsCount: 3, isOnline: true }, TrayIcons.active], + [{ notificationsCount: -1, isOnline: true }, TrayIcons.error], + [{ notificationsCount: 0, isOnline: false }, TrayIcons.offline], + ])('preserves notification state across theme and preference changes: %j', (state, icon) => { + registerTrayHandlers(menubar); + onMock.mock.calls.find((call) => call[0] === EVENTS.UPDATE_ICON_COLOR)?.[1]({}, state); + nativeTheme.emit('updated'); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(icon); + onMock.mock.calls.find((call) => call[0] === EVENTS.SET_TRAY_ICON_APPEARANCE)?.[1]({}, 'light'); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(icon); + }); + + it('applies the idle appearance immediately when unread highlighting is disabled', () => { + registerTrayHandlers(menubar); + onMock.mock.calls.find((call) => call[0] === EVENTS.UPDATE_ICON_COLOR)?.[1]( + {}, + { notificationsCount: 3, isOnline: true }, + ); + onMock.mock.calls.find((call) => call[0] === EVENTS.USE_UNREAD_ACTIVE_ICON)?.[1]({}, false); + expect(menubar.tray.setImage).toHaveBeenLastCalledWith(TrayIcons.dark); + }); + + it('removes the native theme listener on quit', () => { + registerTrayHandlers(menubar); + expect(nativeTheme.listenerCount('updated')).toBe(1); + app.emit('will-quit'); + expect(nativeTheme.listenerCount('updated')).toBe(0); + }); }); diff --git a/src/main/handlers/tray.ts b/src/main/handlers/tray.ts index 352707333..67b552a1b 100644 --- a/src/main/handlers/tray.ts +++ b/src/main/handlers/tray.ts @@ -1,87 +1,56 @@ +import { app, nativeTheme } from 'electron'; import type { Menubar } from 'electron-menubar'; -import { EVENTS, type ITrayColorUpdate } from '../../shared/events'; +import { + EVENTS, + isTrayIconAppearance, + type ITrayColorUpdate, + type TrayIconAppearance, +} from '../../shared/events'; import { onMainEvent } from '../events'; -import { TrayIcons } from '../icons'; +import { getIdleTrayIcon, TrayIcons } from '../icons'; -let shouldUseAlternateIdleIcon = false; -let shouldUseUnreadActiveIcon = true; - -function setIdleIcon(mb: Menubar): void { - if (shouldUseAlternateIdleIcon) { - mb.tray.setImage(TrayIcons.idleAlternate); - } else { - mb.tray.setImage(TrayIcons.idle); - } -} - -function setActiveIcon(mb: Menubar): void { - if (shouldUseUnreadActiveIcon) { - mb.tray.setImage(TrayIcons.active); - } else { - setIdleIcon(mb); - } -} - -function setErrorIcon(mb: Menubar): void { - mb.tray.setImage(TrayIcons.error); -} - -function setOfflineIcon(mb: Menubar): void { - mb.tray.setImage(TrayIcons.offline); -} - -/** - * Register IPC handlers for tray icon visual state. - * - * @param mb - The menubar instance whose tray is controlled. - */ export function registerTrayHandlers(mb: Menubar): void { - /** - * Toggle the alternate idle tray icon variant. - */ - onMainEvent(EVENTS.USE_ALTERNATE_IDLE_ICON, (_, useAlternateIdleIcon: boolean) => { - shouldUseAlternateIdleIcon = useAlternateIdleIcon; - }); + let appearance: TrayIconAppearance = 'auto'; + let highlightUnread = true; + let status: ITrayColorUpdate = { notificationsCount: 0, isOnline: true }; - /** - * Toggle whether unread notifications show an active (coloured) tray icon. - */ - onMainEvent(EVENTS.USE_UNREAD_ACTIVE_ICON, (_, useUnreadActiveIcon: boolean) => { - shouldUseUnreadActiveIcon = useUnreadActiveIcon; - }); - - /** - * Update the tray icon based on the current notification count. - */ - onMainEvent(EVENTS.UPDATE_ICON_COLOR, (_, { notificationsCount, isOnline }: ITrayColorUpdate) => { - if (!mb.tray.isDestroyed()) { - if (!isOnline) { - setOfflineIcon(mb); - return; - } - - if (notificationsCount < 0) { - setErrorIcon(mb); - return; - } - - if (notificationsCount > 0) { - setActiveIcon(mb); - return; - } - - setIdleIcon(mb); + const refresh = () => { + if (mb.tray.isDestroyed()) { + return; + } + const { notificationsCount, isOnline } = status; + const icon = !isOnline + ? TrayIcons.offline + : notificationsCount < 0 + ? TrayIcons.error + : notificationsCount > 0 && highlightUnread + ? TrayIcons.active + : getIdleTrayIcon(appearance); + mb.tray.setImage(icon); + }; + + onMainEvent(EVENTS.SET_TRAY_ICON_APPEARANCE, (_, value) => { + if (isTrayIconAppearance(value)) { + appearance = value; + refresh(); } }); - - /** - * Update the tray icon title (notification count label on macOS). - */ - onMainEvent(EVENTS.UPDATE_ICON_TITLE, (_, title: string) => { + onMainEvent(EVENTS.USE_UNREAD_ACTIVE_ICON, (_, value) => { + highlightUnread = value; + refresh(); + }); + onMainEvent(EVENTS.UPDATE_ICON_COLOR, (_, value) => { + status = value; + refresh(); + }); + onMainEvent(EVENTS.UPDATE_ICON_TITLE, (_, title) => { if (!mb.tray.isDestroyed()) { mb.tray.setTitle(title); } }); + + nativeTheme.on('updated', refresh); + app.once('will-quit', () => nativeTheme.removeListener('updated', refresh)); } diff --git a/src/main/icons.test.ts b/src/main/icons.test.ts index edb0f53be..9b79bce14 100644 --- a/src/main/icons.test.ts +++ b/src/main/icons.test.ts @@ -1,15 +1,50 @@ -import { TrayIcons } from './icons'; +import { nativeTheme } from 'electron'; -describe('main/icons.ts', () => { - it('should return icon images', () => { - expect(TrayIcons.active).toContain('assets/images/tray-active.png'); +import { isMacOS, isWindows } from '../shared/platform'; - expect(TrayIcons.idle).toContain('assets/images/tray-idleTemplate.png'); +import { getIdleTrayIcon, TrayIcons } from './icons'; - expect(TrayIcons.idleAlternate).toContain('assets/images/tray-idle-white.png'); +vi.mock('electron', () => ({ + nativeTheme: { shouldUseDarkColorsForSystemIntegratedUI: false, shouldUseDarkColors: false }, +})); +vi.mock('../shared/platform', () => ({ isMacOS: vi.fn(), isWindows: vi.fn() })); - expect(TrayIcons.error).toContain('assets/images/tray-error.png'); +describe('tray icon appearance', () => { + beforeEach(() => { + vi.mocked(isMacOS).mockReturnValue(false); + vi.mocked(isWindows).mockReturnValue(false); + }); - expect(TrayIcons.offline).toContain('assets/images/tray-offline.png'); + it('uses a template only for automatic macOS appearance', () => { + vi.mocked(isMacOS).mockReturnValue(true); + expect(getIdleTrayIcon('auto')).toBe(TrayIcons.idle); + expect(getIdleTrayIcon('light')).toBe(TrayIcons.light); + expect(getIdleTrayIcon('dark')).toBe(TrayIcons.dark); + expect(TrayIcons.dark).not.toContain('Template'); }); + + it.each([true, false])( + 'follows Windows taskbar theme, regardless of app dark mode %s', + (appDark) => { + vi.mocked(isWindows).mockReturnValue(true); + Object.assign(nativeTheme, { + shouldUseDarkColors: appDark, + shouldUseDarkColorsForSystemIntegratedUI: true, + }); + expect(getIdleTrayIcon('auto')).toBe(TrayIcons.light); + Object.assign(nativeTheme, { shouldUseDarkColorsForSystemIntegratedUI: false }); + expect(getIdleTrayIcon('auto')).toBe(TrayIcons.dark); + expect(getIdleTrayIcon('light')).toBe(TrayIcons.light); + }, + ); + + it.each(['GNOME', 'KDE', ''])( + 'uses the Linux fallback and respects overrides on %s', + (desktop) => { + vi.stubEnv('XDG_CURRENT_DESKTOP', desktop); + expect(getIdleTrayIcon('auto')).toBe(TrayIcons.light); + expect(getIdleTrayIcon('dark')).toBe(TrayIcons.dark); + vi.unstubAllEnvs(); + }, + ); }); diff --git a/src/main/icons.ts b/src/main/icons.ts index 22f17df98..548ff0412 100644 --- a/src/main/icons.ts +++ b/src/main/icons.ts @@ -1,13 +1,33 @@ import path from 'node:path'; +import { nativeTheme } from 'electron'; + +import type { TrayIconAppearance } from '../shared/events'; +import { isMacOS, isWindows } from '../shared/platform'; + export const TrayIcons = { active: getIconPath('tray-active.png'), idle: getIconPath('tray-idleTemplate.png'), - idleAlternate: getIconPath('tray-idle-white.png'), + light: getIconPath('tray-idle-white.png'), + dark: getIconPath('tray-idle-black.png'), error: getIconPath('tray-error.png'), offline: getIconPath('tray-offline.png'), }; +export function getIdleTrayIcon(appearance: TrayIconAppearance): string { + if (appearance !== 'auto') { + return TrayIcons[appearance]; + } + if (isMacOS()) { + return TrayIcons.idle; + } + if (isWindows()) { + return nativeTheme.shouldUseDarkColorsForSystemIntegratedUI ? TrayIcons.light : TrayIcons.dark; + } + // Linux does not expose the panel's colour scheme through Electron. + return TrayIcons.light; +} + function getIconPath(iconName: string) { return path.resolve(__dirname, 'assets', 'images', iconName); } diff --git a/src/main/index.ts b/src/main/index.ts index 3d403932c..f790e6797 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,7 +10,7 @@ import { registerTrayHandlers, registerUpdaterHandlers, } from './handlers'; -import { TrayIcons } from './icons'; +import { getIdleTrayIcon } from './icons'; import { configureWindowEvents, handleProtocolURL, @@ -33,7 +33,7 @@ if (!app.isPackaged) { } const mb = menubar({ - icon: TrayIcons.idle, + icon: getIdleTrayIcon('auto'), index: Paths.indexHtml, browserWindow: WindowConfig, preloadWindow: true, diff --git a/src/preload/index.ts b/src/preload/index.ts index 0160095e8..a97af8728 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,6 @@ import { contextBridge, webFrame } from 'electron'; -import type { IKeyboardShortcut, NativeThemeSource } from '../shared/events'; +import type { IKeyboardShortcut, NativeThemeSource, TrayIconAppearance } from '../shared/events'; import { EVENTS } from '../shared/events'; import { isLinux, isMacOS, isWindows } from '../shared/platform'; @@ -126,11 +126,11 @@ export const api = { updateTitle: (title = '') => sendMainEvent(EVENTS.UPDATE_ICON_TITLE, title), /** - * Switch the tray icon to an alternate idle icon variant. + * Set the idle tray icon appearance independently of the app theme. * - * @param value - `true` to use the alternate idle icon, `false` for the default. */ - useAlternateIdleIcon: (value: boolean) => sendMainEvent(EVENTS.USE_ALTERNATE_IDLE_ICON, value), + setAppearance: (value: TrayIconAppearance) => + sendMainEvent(EVENTS.SET_TRAY_ICON_APPEARANCE, value), /** * Switch the tray icon to an "active" variant when there are unread notifications. diff --git a/src/renderer/__helpers__/visual.setup.ts b/src/renderer/__helpers__/visual.setup.ts index 3e05ceb49..accb6511b 100644 --- a/src/renderer/__helpers__/visual.setup.ts +++ b/src/renderer/__helpers__/visual.setup.ts @@ -100,7 +100,7 @@ function createGitifyBridgeApi(): Window['gitify'] { tray: { updateColor: vi.fn(), updateTitle: vi.fn(), - useAlternateIdleIcon: vi.fn(), + setAppearance: vi.fn(), useUnreadActiveIcon: vi.fn(), }, notificationSoundPath: vi.fn(), diff --git a/src/renderer/__helpers__/vitest.setup.ts b/src/renderer/__helpers__/vitest.setup.ts index b172ce9d5..9793acf64 100644 --- a/src/renderer/__helpers__/vitest.setup.ts +++ b/src/renderer/__helpers__/vitest.setup.ts @@ -98,7 +98,7 @@ function createGitifyBridgeApi(): Window['gitify'] { tray: { updateColor: vi.fn(), updateTitle: vi.fn(), - useAlternateIdleIcon: vi.fn(), + setAppearance: vi.fn(), useUnreadActiveIcon: vi.fn(), }, notificationSoundPath: vi.fn(), diff --git a/src/renderer/__mocks__/state-mocks.ts b/src/renderer/__mocks__/state-mocks.ts index 2bf7c5162..5506b49e3 100644 --- a/src/renderer/__mocks__/state-mocks.ts +++ b/src/renderer/__mocks__/state-mocks.ts @@ -51,7 +51,7 @@ const mockNotificationSettings: NotificationSettingsState = { const mockTraySettings: TraySettingsState = { showNotificationsCountInTray: true, useUnreadActiveIcon: true, - useAlternateIdleIcon: false, + trayIconAppearance: 'auto', }; const mockSystemSettings: SystemSettingsState = { diff --git a/src/renderer/components/GlobalEffects.tsx b/src/renderer/components/GlobalEffects.tsx index ce3685af4..cf028a115 100644 --- a/src/renderer/components/GlobalEffects.tsx +++ b/src/renderer/components/GlobalEffects.tsx @@ -31,7 +31,7 @@ export const GlobalEffects: FC = () => { // Subscribe to tray-related settings for useEffect dependencies const showNotificationsCountInTray = useSettingsStore((s) => s.showNotificationsCountInTray); const useUnreadActiveIcon = useSettingsStore((s) => s.useUnreadActiveIcon); - const useAlternateIdleIcon = useSettingsStore((s) => s.useAlternateIdleIcon); + const trayIconAppearance = useSettingsStore((s) => s.trayIconAppearance); const isOnline = useOnlineStatus(); @@ -54,7 +54,7 @@ export const GlobalEffects: FC = () => { }, [ showNotificationsCountInTray, useUnreadActiveIcon, - useAlternateIdleIcon, + trayIconAppearance, status, notificationCount, isOnline, diff --git a/src/renderer/components/settings/SystemSettings.test.tsx b/src/renderer/components/settings/SystemSettings.test.tsx index 2a7dbb5e8..e9f483c63 100644 --- a/src/renderer/components/settings/SystemSettings.test.tsx +++ b/src/renderer/components/settings/SystemSettings.test.tsx @@ -60,6 +60,9 @@ describe('renderer/components/settings/SystemSettings.tsx', () => { }); expect(screen.queryByTestId('checkbox-useX11Backend')).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'GNOME extension installation' }), + ).not.toBeInTheDocument(); }); it('is shown and toggles on Linux', async () => { @@ -73,6 +76,18 @@ describe('renderer/components/settings/SystemSettings.tsx', () => { expect(toggleSettingSpy).toHaveBeenCalledWith('useX11Backend'); }); + + it('opens standalone GNOME installation instructions on Linux', async () => { + isLinuxMock().mockReturnValue(true); + renderWithProviders(); + + await userEvent.click(screen.getByRole('button', { name: 'GNOME extension installation' })); + + expect(window.gitify.openExternalLink).toHaveBeenCalledWith( + 'https://github.com/gitify-app/gnome#install', + expect.any(Boolean), + ); + }); }); it('should reset global shortcut to default when customized', async () => { diff --git a/src/renderer/components/settings/SystemSettings.tsx b/src/renderer/components/settings/SystemSettings.tsx index 01977263d..1b2c5813f 100644 --- a/src/renderer/components/settings/SystemSettings.tsx +++ b/src/renderer/components/settings/SystemSettings.tsx @@ -12,8 +12,9 @@ import { Checkbox } from '../fields/Checkbox'; import { RadioGroup } from '../fields/RadioGroup'; import { Title } from '../primitives/Title'; -import { type KeyboardAcceleratorShortcut, OpenPreference } from '../../types'; +import { type KeyboardAcceleratorShortcut, OpenPreference, toLink } from '../../types'; +import { openExternalLink } from '../../utils/system/comms'; import { formatAcceleratorForDisplay, keyboardEventToAccelerator, @@ -367,15 +368,30 @@ export const SystemSettings: FC = () => { onChange={() => toggleSetting('useX11Backend')} tooltip={ - Run under X11/XWayland so the window opens next to the tray icon. On Wayland the - compositor decides where windows appear, so {APPLICATION.NAME} opens in the middle of - the screen. Enabling this also disables Vulkan, which crashes under X11 on some - drivers, and may soften text on displays using fractional scaling. Takes effect after - restarting {APPLICATION.NAME}. + Run under X11/XWayland so the window opens next to the tray icon. On Wayland, window + placement depends on your compositor and its extensions. Enabling this also disables + Vulkan, which crashes under X11 on some drivers, and may soften text on displays using + fractional scaling. Takes effect after restarting {APPLICATION.NAME}. } visible={window.gitify.platform.isLinux()} /> + {window.gitify.platform.isLinux() && ( + + + On GNOME Wayland, install the Gitify extension to open the window next to its tray + icon. Install and manage it through GNOME's extension tools. + + + + )} ); diff --git a/src/renderer/components/settings/TraySettings.test.tsx b/src/renderer/components/settings/TraySettings.test.tsx index a1ee66cd5..53fa38055 100644 --- a/src/renderer/components/settings/TraySettings.test.tsx +++ b/src/renderer/components/settings/TraySettings.test.tsx @@ -21,11 +21,17 @@ describe('renderer/components/settings/TraySettings.tsx', () => { it.each([ ['checkbox-showNotificationsCountInTray', 'showNotificationsCountInTray'], ['checkbox-useUnreadActiveIcon', 'useUnreadActiveIcon'], - ['checkbox-useAlternateIdleIcon', 'useAlternateIdleIcon'], ] as const)('should toggle %s checkbox', async (testId, setting) => { await userEvent.click(screen.getByTestId(testId)); expect(toggleSettingSpy).toHaveBeenCalledTimes(1); expect(toggleSettingSpy).toHaveBeenCalledWith(setting); }); + it.each(['auto', 'light', 'dark'] as const)( + 'selects the %s icon appearance', + async (appearance) => { + await userEvent.click(screen.getByTestId(`radio-trayIconAppearance-${appearance}`)); + expect(useSettingsStore.getState().trayIconAppearance).toBe(appearance); + }, + ); }); diff --git a/src/renderer/components/settings/TraySettings.tsx b/src/renderer/components/settings/TraySettings.tsx index 66e4f67e3..f74065f63 100644 --- a/src/renderer/components/settings/TraySettings.tsx +++ b/src/renderer/components/settings/TraySettings.tsx @@ -4,20 +4,24 @@ import { DevicesIcon } from '@primer/octicons-react'; import { Stack, Text } from '@primer/react'; import { APPLICATION } from '../../../shared/constants'; +import { isTrayIconAppearance } from '../../../shared/events'; import { useSettingsStore } from '../../stores'; import { Checkbox } from '../fields/Checkbox'; +import { RadioGroup } from '../fields/RadioGroup'; import { Title } from '../primitives/Title'; export const TraySettings: FC = () => { + const updateSetting = useSettingsStore((s) => s.updateSetting); + // Setting store actions const toggleSetting = useSettingsStore((s) => s.toggleSetting); // Setting store values const showNotificationsCountInTray = useSettingsStore((s) => s.showNotificationsCountInTray); const useUnreadActiveIcon = useSettingsStore((s) => s.useUnreadActiveIcon); - const useAlternateIdleIcon = useSettingsStore((s) => s.useAlternateIdleIcon); + const trayIconAppearance = useSettingsStore((s) => s.trayIconAppearance); return (
@@ -50,20 +54,29 @@ export const TraySettings: FC = () => { } /> - toggleSetting('useAlternateIdleIcon')} + { + const value = event.target.value; + if (isTrayIconAppearance(value)) { + updateSetting('trayIconAppearance', value); + } + }} tooltip={ + Choose a light icon for a dark panel, or a dark icon for a light panel. - Use a white {APPLICATION.NAME} logo (instead of the default black logo) when all - notifications are read. - - - This is particularly useful for devices which have a dark-themed menubar or taskbar. + Automatic follows the system tray appearance on macOS and Windows. On Linux, it uses + a light icon because panel appearance cannot be detected reliably. + This setting is independent of Gitify’s app theme. } /> diff --git a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap index 91e99a220..d31704f15 100644 --- a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap @@ -1561,24 +1561,94 @@ exports[`renderer/routes/Settings.tsx > should render itself & its children 1`] data-padding="none" data-wrap="nowrap" > - +
+ + +
+
+ + +
+
+ + +