diff --git a/src/main/main.ts b/src/main/main.ts index 822d0a766..0f307a007 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -94,9 +94,15 @@ app.whenReady().then(async () => { nativeTheme.on('updated', () => { if (nativeTheme.shouldUseDarkColors) { - mb.window.webContents.send(namespacedEvent('update-theme'), 'DARK'); + mb.window.webContents.send( + namespacedEvent('update-theme'), + 'DARK_DEFAULT', + ); } else { - mb.window.webContents.send(namespacedEvent('update-theme'), 'LIGHT'); + mb.window.webContents.send( + namespacedEvent('update-theme'), + 'LIGHT_DEFAULT', + ); } }); diff --git a/src/renderer/App.css b/src/renderer/App.css index 3cfdea64d..112ddcdef 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -9,9 +9,15 @@ @import "@primer/primitives/dist/css/functional/typography/typography.css"; /* Themes and Colors */ -/* TODO Add support for setting color modes (dark_dimmed) - see #1748 */ @import "@primer/primitives/dist/css/functional/themes/light.css"; +@import "@primer/primitives/dist/css/functional/themes/light-tritanopia.css"; +@import "@primer/primitives/dist/css/functional/themes/light-high-contrast.css"; +@import "@primer/primitives/dist/css/functional/themes/light-colorblind.css"; @import "@primer/primitives/dist/css/functional/themes/dark.css"; +@import "@primer/primitives/dist/css/functional/themes/dark-colorblind.css"; +@import "@primer/primitives/dist/css/functional/themes/dark-dimmed.css"; +@import "@primer/primitives/dist/css/functional/themes/dark-high-contrast.css"; +@import "@primer/primitives/dist/css/functional/themes/dark-tritanopia.css"; html, body, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 34a75f1da..28da59496 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -21,6 +21,10 @@ import { NotificationsRoute } from './routes/Notifications'; import { SettingsRoute } from './routes/Settings'; import './App.css'; +import { + DEFAULT_DAY_COLOR_SCHEME, + DEFAULT_NIGHT_COLOR_SCHEME, +} from './utils/theme'; function RequireAuth({ children }) { const { isLoggedIn } = useContext(AppContext); @@ -35,8 +39,11 @@ function RequireAuth({ children }) { export const App = () => { return ( - // TODO Add support for setting color modes (dark_dimmed) - see #1748 - + diff --git a/src/renderer/components/settings/AppearanceSettings.test.tsx b/src/renderer/components/settings/AppearanceSettings.test.tsx index fd35625f2..5864e5918 100644 --- a/src/renderer/components/settings/AppearanceSettings.test.tsx +++ b/src/renderer/components/settings/AppearanceSettings.test.tsx @@ -17,7 +17,7 @@ describe('renderer/routes/components/settings/AppearanceSettings.tsx', () => { jest.clearAllMocks(); }); - it('should change the theme radio group', async () => { + it('should change the theme mode dropdown', async () => { await act(async () => { render( { ); }); - fireEvent.click(screen.getByLabelText('Light')); + const select = screen.getByTestId('settings-theme') as HTMLSelectElement; + fireEvent.change(select, { target: { value: 'LIGHT' } }); expect(updateSetting).toHaveBeenCalledTimes(1); expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT'); diff --git a/src/renderer/components/settings/AppearanceSettings.tsx b/src/renderer/components/settings/AppearanceSettings.tsx index 506604cc4..4567401e6 100644 --- a/src/renderer/components/settings/AppearanceSettings.tsx +++ b/src/renderer/components/settings/AppearanceSettings.tsx @@ -13,24 +13,34 @@ import { TagIcon, XCircleIcon, } from '@primer/octicons-react'; -import { Button, ButtonGroup, IconButton, useTheme } from '@primer/react'; +import { + Button, + ButtonGroup, + IconButton, + Select, + useTheme, +} from '@primer/react'; import { namespacedEvent } from '../../../shared/events'; import { AppContext } from '../../context/App'; import { Size, Theme } from '../../types'; import { hasMultipleAccounts } from '../../utils/auth/utils'; -import { getColorModeFromTheme, setTheme } from '../../utils/theme'; +import { + DEFAULT_DAY_COLOR_SCHEME, + DEFAULT_NIGHT_COLOR_SCHEME, + isDayScheme, + setScrollbarTheme, +} from '../../utils/theme'; import { zoomLevelToPercentage, zoomPercentageToLevel } from '../../utils/zoom'; import { Checkbox } from '../fields/Checkbox'; import { FieldLabel } from '../fields/FieldLabel'; -import { RadioGroup } from '../fields/RadioGroup'; import { Title } from '../primitives/Title'; let timeout: NodeJS.Timeout; const DELAY = 200; export const AppearanceSettings: FC = () => { - const { setColorMode } = useTheme(); + const { setColorMode, setDayScheme, setNightScheme } = useTheme(); const { auth, settings, updateSetting } = useContext(AppContext); const [zoomPercentage, setZoomPercentage] = useState( zoomLevelToPercentage(webFrame.getZoomLevel()), @@ -41,14 +51,15 @@ export const AppearanceSettings: FC = () => { namespacedEvent('update-theme'), (_, updatedTheme: Theme) => { if (settings.theme === Theme.SYSTEM) { - const mode = getColorModeFromTheme(updatedTheme); - - setTheme(updatedTheme); - setColorMode(mode); + const mode = isDayScheme(updatedTheme) ? 'day' : 'night'; + setColorMode('auto'); + setDayScheme(DEFAULT_DAY_COLOR_SCHEME); + setNightScheme(DEFAULT_NIGHT_COLOR_SCHEME); + setScrollbarTheme(mode); } }, ); - }, [settings.theme, setColorMode]); + }, [settings.theme, setColorMode, setDayScheme, setNightScheme]); window.addEventListener('resize', () => { // clear the timeout @@ -65,20 +76,48 @@ export const AppearanceSettings: FC = () => {
Appearance - updateSetting('theme', evt.target.value as Theme)} - /> +
+ + + +
- + >({}); export const AppProvider = ({ children }: { children: ReactNode }) => { - const { setColorMode } = useTheme(); + const { setColorMode, setDayScheme, setNightScheme } = useTheme(); const [auth, setAuth] = useState(defaultAuth); const [settings, setSettings] = useState(defaultSettings); const { @@ -144,11 +150,18 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { }, []); useEffect(() => { - const mode = getColorModeFromTheme(settings.theme); - setColorMode(mode); + const colorScheme = mapThemeModeToColorScheme(settings.theme); - setTheme(settings.theme); // TODO - Replace fully with Primer design tokens and components - }, [settings.theme, setColorMode]); + if (isDayScheme(settings.theme)) { + setDayScheme(colorScheme ?? DEFAULT_DAY_COLOR_SCHEME); + setColorMode('day'); + setScrollbarTheme('day'); + } else { + setNightScheme(colorScheme ?? DEFAULT_NIGHT_COLOR_SCHEME); + setColorMode('night'); + setScrollbarTheme('night'); + } + }, [settings.theme, setColorMode, setDayScheme, setNightScheme]); // biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for account changes useEffect(() => { diff --git a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap index 8a398bd14..69b4ec59b 100644 --- a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap @@ -126,7 +126,7 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] =
-
-
- - -
-
- - -
-
+ Light default + + + + + + + + + + + + + +
-
+ +
- Zoom + Zoom:
{ }, ], }, - settings: { theme: 'DARK' }, + settings: { theme: 'DARK_DEFAULT' }, }), ); const result = loadState(); @@ -35,7 +35,7 @@ describe('renderer/utils/storage.ts', () => { expect(result.auth.token).toBeUndefined(); expect(result.auth.enterpriseAccounts).toBeUndefined(); expect(result.auth.user).toBeUndefined(); - expect(result.settings.theme).toBe('DARK'); + expect(result.settings.theme).toBe('DARK_DEFAULT'); }); it('should load the state from localstorage - empty', () => { diff --git a/src/renderer/utils/theme.test.ts b/src/renderer/utils/theme.test.ts index a203ea98a..e353d13f7 100644 --- a/src/renderer/utils/theme.test.ts +++ b/src/renderer/utils/theme.test.ts @@ -1,5 +1,9 @@ import { Theme } from '../types'; -import { getColorModeFromTheme, getTheme, setTheme } from './theme'; +import { + getTheme, + mapThemeModeToColorScheme, + setScrollbarTheme, +} from './theme'; describe('renderer/utils/theme.ts', () => { const htmlElement = document.createElement('html'); @@ -9,12 +13,12 @@ describe('renderer/utils/theme.ts', () => { }); it('should change to light mode', () => { - setTheme(Theme.LIGHT); + setScrollbarTheme('day'); expect(getTheme()).toBe(Theme.LIGHT); }); it('should change to dark mode', () => { - setTheme(Theme.DARK); + setScrollbarTheme('night'); expect(getTheme()).toBe(Theme.DARK); }); @@ -25,7 +29,7 @@ describe('renderer/utils/theme.ts', () => { matches: false, })), }); - setTheme(); + setScrollbarTheme(); expect(getTheme()).toBe(Theme.LIGHT); }); @@ -36,13 +40,32 @@ describe('renderer/utils/theme.ts', () => { matches: true, })), }); - setTheme(); + setScrollbarTheme(); expect(getTheme()).toBe(Theme.DARK); }); - it('should get color mode from theme', () => { - expect(getColorModeFromTheme(Theme.LIGHT)).toBe('day'); - expect(getColorModeFromTheme(Theme.DARK)).toBe('night'); - expect(getColorModeFromTheme(Theme.SYSTEM)).toBe('auto'); + it('should map theme mode to github primer provider', () => { + expect(mapThemeModeToColorScheme(Theme.LIGHT)).toBe('light'); + expect(mapThemeModeToColorScheme(Theme.LIGHT_HIGH_CONTRAST)).toBe( + 'light_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.LIGHT_COLORBLIND)).toBe( + 'light_colorblind', + ); + expect(mapThemeModeToColorScheme(Theme.LIGHT_TRITANOPIA)).toBe( + 'light_tritanopia', + ); + expect(mapThemeModeToColorScheme(Theme.DARK)).toBe('dark'); + expect(mapThemeModeToColorScheme(Theme.DARK_HIGH_CONTRAST)).toBe( + 'dark_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.DARK_COLORBLIND)).toBe( + 'dark_colorblind', + ); + expect(mapThemeModeToColorScheme(Theme.DARK_TRITANOPIA)).toBe( + 'dark_tritanopia', + ); + expect(mapThemeModeToColorScheme(Theme.DARK_DIMMED)).toBe('dark_dimmed'); + expect(mapThemeModeToColorScheme(Theme.SYSTEM)).toBe(null); }); }); diff --git a/src/renderer/utils/theme.ts b/src/renderer/utils/theme.ts index 1e765a97c..eb8fccf65 100644 --- a/src/renderer/utils/theme.ts +++ b/src/renderer/utils/theme.ts @@ -1,9 +1,9 @@ import type { ColorModeWithAuto } from '@primer/react/lib/ThemeProvider'; import { Theme } from '../types'; -// TODO Add support for setting color modes (dark_dimmed) - see #1748 +export const DEFAULT_DAY_COLOR_SCHEME = 'light'; +export const DEFAULT_NIGHT_COLOR_SCHEME = 'dark'; -// TODO - Replace fully with Octicon primer theme provider /** * @deprecated */ @@ -29,16 +29,19 @@ export function setDarkMode() { document.querySelector('html').classList.add('dark'); } -export function setTheme(theme?: Theme) { - switch (theme) { - case Theme.LIGHT: +/** + * @deprecated + */ +export function setScrollbarTheme(mode?: ColorModeWithAuto) { + switch (mode) { + case 'day': + case 'light': setLightMode(); break; - - case Theme.DARK: + case 'night': + case 'dark': setDarkMode(); break; - default: if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) { setDarkMode(); @@ -48,13 +51,39 @@ export function setTheme(theme?: Theme) { } } -export function getColorModeFromTheme(theme: Theme): ColorModeWithAuto { - switch (theme) { +export function isDayScheme(themeMode: Theme) { + switch (themeMode) { + case Theme.LIGHT: + case Theme.LIGHT_HIGH_CONTRAST: + case Theme.LIGHT_COLORBLIND: + case Theme.LIGHT_TRITANOPIA: + return true; + default: + return false; + } +} + +export function mapThemeModeToColorScheme(themeMode: Theme): string | null { + switch (themeMode) { case Theme.LIGHT: - return 'day'; + return 'light'; + case Theme.LIGHT_HIGH_CONTRAST: + return 'light_high_contrast'; + case Theme.LIGHT_COLORBLIND: + return 'light_colorblind'; + case Theme.LIGHT_TRITANOPIA: + return 'light_tritanopia'; case Theme.DARK: - return 'night'; + return 'dark'; + case Theme.DARK_HIGH_CONTRAST: + return 'dark_high_contrast'; + case Theme.DARK_COLORBLIND: + return 'dark_colorblind'; + case Theme.DARK_TRITANOPIA: + return 'dark_tritanopia'; + case Theme.DARK_DIMMED: + return 'dark_dimmed'; default: - return 'auto'; + return null; } }