From 59cecc8504271ea25acde5c17a10067c0d1bb482 Mon Sep 17 00:00:00 2001 From: Ronja Date: Sat, 7 Nov 2026 16:38:38 +0100 Subject: [PATCH 1/2] feat(theme): follow the system colour scheme on the first visit Closes #168 --- src/App.tsx | 9 ++++++--- src/tests/App.test.tsx | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b9bd1be..cf2187d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,9 +19,12 @@ import './styles/variables.css' import './styles/global.css' export function App() { - const [isDarkMode, setIsDarkMode] = useState( - () => window.localStorage.getItem('theme') === 'dark', - ) + // A stored choice wins; on the first visit the system setting decides. + const [isDarkMode, setIsDarkMode] = useState(() => { + const stored = window.localStorage.getItem('theme') + if (stored) return stored === 'dark' + return !!window.matchMedia?.('(prefers-color-scheme: dark)').matches + }) useEffect(() => { document.documentElement.dataset.theme = isDarkMode ? 'dark' : 'light' diff --git a/src/tests/App.test.tsx b/src/tests/App.test.tsx index cd9fbd2..4a085ee 100644 --- a/src/tests/App.test.tsx +++ b/src/tests/App.test.tsx @@ -2,7 +2,7 @@ import { ApolloProvider } from '@apollo/client/react' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createTestClient, ok, type Handler } from '@/tests/apollo' import App from '@/App' @@ -67,6 +67,21 @@ describe('App', () => { ).toBe('true') }) + it('starts from the system colour scheme when no theme is stored, and a stored theme wins', () => { + const systemIsDark = (dark: boolean) => + vi.stubGlobal('matchMedia', () => ({ matches: dark })) + + systemIsDark(true) + renderApp() + expect(document.documentElement.getAttribute('data-theme')).toBe('dark') + cleanup() + + window.localStorage.setItem('theme', 'light') + renderApp() + expect(document.documentElement.getAttribute('data-theme')).toBe('light') + vi.unstubAllGlobals() + }) + it('generates navigation URLs beneath the configured base path', () => { renderApp() From 68e2421e77f589b8670ec6bee1cc0d405bc9fbcb Mon Sep 17 00:00:00 2001 From: Ronja Date: Tue, 10 Nov 2026 20:16:24 +0100 Subject: [PATCH 2/2] test(theme): restore the stubbed matchMedia after every test --- src/tests/App.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/App.test.tsx b/src/tests/App.test.tsx index 4a085ee..c413e33 100644 --- a/src/tests/App.test.tsx +++ b/src/tests/App.test.tsx @@ -28,6 +28,7 @@ beforeEach(() => { }) afterEach(() => { + vi.unstubAllGlobals() delete document.documentElement.dataset.theme window.localStorage.clear() window.history.replaceState({}, '', import.meta.env.BASE_URL) @@ -79,7 +80,6 @@ describe('App', () => { window.localStorage.setItem('theme', 'light') renderApp() expect(document.documentElement.getAttribute('data-theme')).toBe('light') - vi.unstubAllGlobals() }) it('generates navigation URLs beneath the configured base path', () => {