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..c413e33 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' @@ -28,6 +28,7 @@ beforeEach(() => { }) afterEach(() => { + vi.unstubAllGlobals() delete document.documentElement.dataset.theme window.localStorage.clear() window.history.replaceState({}, '', import.meta.env.BASE_URL) @@ -67,6 +68,20 @@ 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') + }) + it('generates navigation URLs beneath the configured base path', () => { renderApp()