diff --git a/package.json b/package.json index ebfdb86bb..e53249428 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@discordapp/twemoji": "16.0.1", "@electron/notarize": "3.0.1", "@primer/octicons-react": "19.15.3", - "@primer/primitives": "10.7.0", + "@primer/primitives": "11.0.0", "@primer/react": "36.27.0", "@tailwindcss/postcss": "4.1.11", "@testing-library/jest-dom": "6.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97d15d1df..6bb99a573 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ importers: specifier: 19.15.3 version: 19.15.3(react@19.1.0) '@primer/primitives': - specifier: 10.7.0 - version: 10.7.0 + specifier: 11.0.0 + version: 11.0.0 '@primer/react': specifier: 36.27.0 version: 36.27.0(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(styled-components@6.1.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) @@ -771,8 +771,8 @@ packages: peerDependencies: react: '>=16.3' - '@primer/primitives@10.7.0': - resolution: {integrity: sha512-732Dq7c4znkewwRkXldV0NLLppfGrDJHPXsXO2QLHwJxKLwi2icKkzcOR77vb5g0ThObZJrRqlB/4DYajIiyyQ==} + '@primer/primitives@11.0.0': + resolution: {integrity: sha512-GREFF8jTAyfs8lMhLQLAtuqmd9LJqodfI1ejmUSL3IEWtoATRSQAy8eqksjV/LcJnJoKNft42hyAeAbawskpaw==} '@primer/primitives@7.17.1': resolution: {integrity: sha512-SiPzEb+up1nDpV2NGwNiY8m6sGnF3OUqRb0has5s6T40vq6Li/g3cYVgl+oolEa4DUoNygEPs09jwJt24f/3zg==} @@ -5461,7 +5461,7 @@ snapshots: dependencies: react: 19.1.0 - '@primer/primitives@10.7.0': {} + '@primer/primitives@11.0.0': {} '@primer/primitives@7.17.1': {} diff --git a/src/renderer/App.css b/src/renderer/App.css index 55a42450f..dff880d28 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -3,8 +3,14 @@ /** GitHub Primer Design System */ /* Size & Typography */ +@import "@primer/primitives/dist/css/primitives.css"; + +/* Base */ +@import "@primer/primitives/dist/css/base/motion/motion.css"; @import "@primer/primitives/dist/css/base/size/size.css"; @import "@primer/primitives/dist/css/base/typography/typography.css"; + +/* Functional */ @import "@primer/primitives/dist/css/functional/size/border.css"; @import "@primer/primitives/dist/css/functional/size/breakpoints.css"; @import "@primer/primitives/dist/css/functional/size/size.css"; @@ -12,15 +18,20 @@ @import "@primer/primitives/dist/css/functional/typography/typography.css"; /* Themes and Colors */ -@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-high-contrast.css"; @import "@primer/primitives/dist/css/functional/themes/dark-colorblind.css"; +@import "@primer/primitives/dist/css/functional/themes/dark-dimmed-high-contrast.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-high-contrast.css"; @import "@primer/primitives/dist/css/functional/themes/dark-tritanopia.css"; +@import "@primer/primitives/dist/css/functional/themes/dark.css"; +@import "@primer/primitives/dist/css/functional/themes/light-colorblind-high-contrast.css"; +@import "@primer/primitives/dist/css/functional/themes/light-colorblind.css"; +@import "@primer/primitives/dist/css/functional/themes/light-high-contrast.css"; +@import "@primer/primitives/dist/css/functional/themes/light-tritanopia-high-contrast.css"; +@import "@primer/primitives/dist/css/functional/themes/light-tritanopia.css"; +@import "@primer/primitives/dist/css/functional/themes/light.css"; /** Tailwind CSS Configuration */ @config '../../tailwind.config.ts'; diff --git a/src/renderer/__mocks__/state-mocks.ts b/src/renderer/__mocks__/state-mocks.ts index 63ea4921e..295c420a8 100644 --- a/src/renderer/__mocks__/state-mocks.ts +++ b/src/renderer/__mocks__/state-mocks.ts @@ -78,6 +78,7 @@ export const mockToken = 'token-123-456' as Token; const mockAppearanceSettings: AppearanceSettingsState = { theme: Theme.SYSTEM, + increaseContrast: false, zoomPercentage: 100, showAccountHeader: false, wrapNotificationTitle: false, diff --git a/src/renderer/components/settings/AppearanceSettings.test.tsx b/src/renderer/components/settings/AppearanceSettings.test.tsx index d3d5fda83..946dd2427 100644 --- a/src/renderer/components/settings/AppearanceSettings.test.tsx +++ b/src/renderer/components/settings/AppearanceSettings.test.tsx @@ -46,6 +46,31 @@ describe('renderer/components/settings/AppearanceSettings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('theme', 'LIGHT'); }); + it('should toggle increase contrast checkbox', async () => { + await act(async () => { + render( + + + + + , + ); + }); + + await userEvent.click(screen.getByTestId('checkbox-increaseContrast')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('increaseContrast', true); + }); + it('should update the zoom value when using CMD + and CMD -', async () => { webFrame.getZoomLevel = jest.fn().mockReturnValue(-1); diff --git a/src/renderer/components/settings/AppearanceSettings.tsx b/src/renderer/components/settings/AppearanceSettings.tsx index 12ebcc1dc..6dbf75c70 100644 --- a/src/renderer/components/settings/AppearanceSettings.tsx +++ b/src/renderer/components/settings/AppearanceSettings.tsx @@ -70,11 +70,8 @@ export const AppearanceSettings: FC = () => { Light default - - Light high contrast - - Light Protanopia & Deuteranopia + Light colorblind Light Tritanopia @@ -82,22 +79,27 @@ export const AppearanceSettings: FC = () => { Dark default - - Dark high contrast - - Dark Protanopia & Deuteranopia + Dark colorblind Dark Tritanopia - - Dark dimmed - + Soft dark + + updateSetting('increaseContrast', evt.target.checked) + } + tooltip={Enable high contrast.} + /> + { useEffect(() => { const colorMode = mapThemeModeToColorMode(settings.theme); - const colorScheme = mapThemeModeToColorScheme(settings.theme); + const colorScheme = mapThemeModeToColorScheme( + settings.theme, + settings.increaseContrast, + ); setColorMode(colorMode); setDayScheme(colorScheme ?? DEFAULT_DAY_COLOR_SCHEME); setNightScheme(colorScheme ?? DEFAULT_NIGHT_COLOR_SCHEME); - }, [settings.theme, setColorMode, setDayScheme, setNightScheme]); + }, [ + settings.theme, + settings.increaseContrast, + setColorMode, + setDayScheme, + setNightScheme, + ]); // biome-ignore lint/correctness/useExhaustiveDependencies: We only want fetchNotifications to be called for particular state changes useEffect(() => { diff --git a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap index 01898c65f..543df50cc 100644 --- a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap @@ -189,15 +189,10 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] = > Light default - - @@ -249,6 +239,51 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] = +
+ + +
+ +
+
{ it('should map theme mode to github primer color mode', () => { expect(mapThemeModeToColorMode(Theme.LIGHT)).toBe('day'); - expect(mapThemeModeToColorMode(Theme.LIGHT_HIGH_CONTRAST)).toBe('day'); expect(mapThemeModeToColorMode(Theme.LIGHT_COLORBLIND)).toBe('day'); expect(mapThemeModeToColorMode(Theme.LIGHT_TRITANOPIA)).toBe('day'); expect(mapThemeModeToColorMode(Theme.DARK)).toBe('night'); - expect(mapThemeModeToColorMode(Theme.DARK_HIGH_CONTRAST)).toBe('night'); expect(mapThemeModeToColorMode(Theme.DARK_COLORBLIND)).toBe('night'); expect(mapThemeModeToColorMode(Theme.DARK_TRITANOPIA)).toBe('night'); expect(mapThemeModeToColorMode(Theme.DARK_DIMMED)).toBe('night'); @@ -16,27 +14,44 @@ describe('renderer/utils/theme.ts', () => { }); it('should map theme mode to github primer color scheme', () => { - expect(mapThemeModeToColorScheme(Theme.LIGHT)).toBe('light'); - expect(mapThemeModeToColorScheme(Theme.LIGHT_HIGH_CONTRAST)).toBe( + expect(mapThemeModeToColorScheme(Theme.LIGHT, false)).toBe('light'); + expect(mapThemeModeToColorScheme(Theme.LIGHT, true)).toBe( 'light_high_contrast', ); - expect(mapThemeModeToColorScheme(Theme.LIGHT_COLORBLIND)).toBe( + expect(mapThemeModeToColorScheme(Theme.LIGHT_COLORBLIND, false)).toBe( 'light_colorblind', ); - expect(mapThemeModeToColorScheme(Theme.LIGHT_TRITANOPIA)).toBe( + expect(mapThemeModeToColorScheme(Theme.LIGHT_COLORBLIND, true)).toBe( + 'light_colorblind_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.LIGHT_TRITANOPIA, false)).toBe( 'light_tritanopia', ); - expect(mapThemeModeToColorScheme(Theme.DARK)).toBe('dark'); - expect(mapThemeModeToColorScheme(Theme.DARK_HIGH_CONTRAST)).toBe( + expect(mapThemeModeToColorScheme(Theme.LIGHT_TRITANOPIA, true)).toBe( + 'light_tritanopia_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.DARK, false)).toBe('dark'); + expect(mapThemeModeToColorScheme(Theme.DARK, true)).toBe( 'dark_high_contrast', ); - expect(mapThemeModeToColorScheme(Theme.DARK_COLORBLIND)).toBe( + expect(mapThemeModeToColorScheme(Theme.DARK_COLORBLIND, false)).toBe( 'dark_colorblind', ); - expect(mapThemeModeToColorScheme(Theme.DARK_TRITANOPIA)).toBe( + expect(mapThemeModeToColorScheme(Theme.DARK_COLORBLIND, true)).toBe( + 'dark_colorblind_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.DARK_TRITANOPIA, false)).toBe( 'dark_tritanopia', ); - expect(mapThemeModeToColorScheme(Theme.DARK_DIMMED)).toBe('dark_dimmed'); - expect(mapThemeModeToColorScheme(Theme.SYSTEM)).toBe(null); + expect(mapThemeModeToColorScheme(Theme.DARK_TRITANOPIA, true)).toBe( + 'dark_tritanopia_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.DARK_DIMMED, false)).toBe( + 'dark_dimmed', + ); + expect(mapThemeModeToColorScheme(Theme.DARK_DIMMED, true)).toBe( + 'dark_dimmed_high_contrast', + ); + expect(mapThemeModeToColorScheme(Theme.SYSTEM, false)).toBe(null); }); }); diff --git a/src/renderer/utils/theme.ts b/src/renderer/utils/theme.ts index eca0e631a..47de55e06 100644 --- a/src/renderer/utils/theme.ts +++ b/src/renderer/utils/theme.ts @@ -8,12 +8,10 @@ export const DEFAULT_NIGHT_COLOR_SCHEME = 'dark'; export function mapThemeModeToColorMode(themeMode: Theme): ColorModeWithAuto { switch (themeMode) { case Theme.LIGHT: - case Theme.LIGHT_HIGH_CONTRAST: case Theme.LIGHT_COLORBLIND: case Theme.LIGHT_TRITANOPIA: return 'day'; case Theme.DARK: - case Theme.DARK_HIGH_CONTRAST: case Theme.DARK_COLORBLIND: case Theme.DARK_TRITANOPIA: case Theme.DARK_DIMMED: @@ -23,27 +21,37 @@ export function mapThemeModeToColorMode(themeMode: Theme): ColorModeWithAuto { } } -export function mapThemeModeToColorScheme(themeMode: Theme): string | null { +export function mapThemeModeToColorScheme( + themeMode: Theme, + increaseContrast: boolean, +): string | null { + let base: string | null; + switch (themeMode) { case Theme.LIGHT: - return 'light'; - case Theme.LIGHT_HIGH_CONTRAST: - return 'light_high_contrast'; + base = 'light'; + break; case Theme.LIGHT_COLORBLIND: - return 'light_colorblind'; + base = 'light_colorblind'; + break; case Theme.LIGHT_TRITANOPIA: - return 'light_tritanopia'; + base = 'light_tritanopia'; + break; case Theme.DARK: - return 'dark'; - case Theme.DARK_HIGH_CONTRAST: - return 'dark_high_contrast'; + base = 'dark'; + break; case Theme.DARK_COLORBLIND: - return 'dark_colorblind'; + base = 'dark_colorblind'; + break; case Theme.DARK_TRITANOPIA: - return 'dark_tritanopia'; + base = 'dark_tritanopia'; + break; case Theme.DARK_DIMMED: - return 'dark_dimmed'; + base = 'dark_dimmed'; + break; default: return null; } + + return increaseContrast ? `${base}_high_contrast` : base; }