diff --git a/client/index.html b/client/index.html index 7ea758aacc..bb05828ff7 100644 --- a/client/index.html +++ b/client/index.html @@ -32,15 +32,35 @@ const loadingContainerStyle = document.createElement('style'); let backgroundColor; - if (theme === 'dark') { + if (theme === 'high-contrast-dark') { + backgroundColor = '#000000'; + } else if (theme === 'high-contrast-light') { + backgroundColor = '#ffffff'; + } else if (theme === 'dark') { backgroundColor = '#0d0d0d'; } else if (theme === 'light') { backgroundColor = '#ffffff'; - } else if (theme === 'system') { - const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches; - backgroundColor = prefersDarkScheme ? '#0d0d0d' : '#ffffff'; } else { - backgroundColor = '#ffffff'; + /* `system`, and anything unset or unrecognised, which ThemeProvider also + * resolves as `system`. The OS decides the scheme and the contrast, and + * a contrast request moves the dark canvas to pure black, so keying off + * the scheme alone would flash #0d0d0d behind the black palette. + * The three contrast queries mirror ThemeProvider's `CONTRAST_QUERIES`: + * a Windows Contrast Theme reports `forced-colors: active` with + * `prefers-contrast: custom`, never `more`. */ + const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + const prefersMoreContrast = [ + '(prefers-contrast: more)', + '(prefers-contrast: custom)', + '(forced-colors: active)', + ].some(function (query) { + return window.matchMedia(query).matches; + }); + backgroundColor = prefersDarkScheme + ? prefersMoreContrast + ? '#000000' + : '#0d0d0d' + : '#ffffff'; } loadingContainerStyle.innerHTML = ` diff --git a/client/src/__tests__/loadingCanvas.spec.ts b/client/src/__tests__/loadingCanvas.spec.ts new file mode 100644 index 0000000000..9d7369e6ad --- /dev/null +++ b/client/src/__tests__/loadingCanvas.spec.ts @@ -0,0 +1,76 @@ +import { join } from 'node:path'; +import { readFileSync } from 'node:fs'; + +/** The pre-React canvas is painted by an inline bootstrap in `index.html`, which + * no bundle imports, so the only way to hold it to the resolved palette is to + * run the script the document actually ships. */ +const bootstrap = (() => { + const html = readFileSync(join(__dirname, '..', '..', 'index.html'), 'utf8'); + const script = [...html.matchAll(/