mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 08:35:33 +08:00
fix(web): apply theme class before first paint so Dark Reader detects dark mode (#7571)
Dark Reader checks html.dark / color-scheme only once during page load, but the theme class was added by ThemeProvider after React mounted. Resolve the saved theme in an inline head script and declare color-scheme: dark on .dark.
This commit is contained in:
1 parent
f92c576506
commit
b0a98ac49c
3 files changed
+115
No files matched your search
@@ -14,6 +14,23 @@
|
||||
/>
|
||||
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<!-- Resolve the theme before first paint so the page never flashes light
|
||||
and one-shot detectors (e.g. Dark Reader) see the real theme.
|
||||
Keep in sync with ThemeProvider and THEME_STORAGE_KEYS.mode. -->
|
||||
<script>
|
||||
;(function () {
|
||||
var theme = 'system'
|
||||
try {
|
||||
theme = localStorage.getItem('newapi:theme:v1:mode') || 'system'
|
||||
} catch (e) {}
|
||||
if (theme !== 'dark' && theme !== 'light') {
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
document.documentElement.classList.add(theme)
|
||||
})()
|
||||
</script>
|
||||
<!--umami-->
|
||||
<!--Google Analytics-->
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright (C) 2023-2026 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { THEME_STORAGE_KEYS } from '@/lib/theme-storage'
|
||||
|
||||
// The inline script in index.html must apply the theme class before React
|
||||
// mounts; Dark Reader checks `html.dark` only once while the page loads.
|
||||
const html = readFileSync(
|
||||
resolve(import.meta.dirname, '../../../index.html'),
|
||||
'utf8'
|
||||
)
|
||||
const bootScript = html.match(/<script>([\s\S]*?)<\/script>/)?.[1] ?? ''
|
||||
|
||||
function runBootScript() {
|
||||
new Function(bootScript)()
|
||||
}
|
||||
|
||||
function mockSystemDarkMode(dark: boolean) {
|
||||
vi.spyOn(window, 'matchMedia').mockImplementation(
|
||||
(query: string) =>
|
||||
({
|
||||
matches: dark && query === '(prefers-color-scheme: dark)',
|
||||
media: query,
|
||||
}) as MediaQueryList
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
localStorage.clear()
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
})
|
||||
|
||||
describe('index.html theme boot script', () => {
|
||||
it('applies the saved dark preference to html before React mounts', () => {
|
||||
mockSystemDarkMode(false)
|
||||
localStorage.setItem(THEME_STORAGE_KEYS.mode, 'dark')
|
||||
|
||||
runBootScript()
|
||||
|
||||
expect(document.documentElement).toHaveClass('dark')
|
||||
expect(document.documentElement).not.toHaveClass('light')
|
||||
})
|
||||
|
||||
it('applies the saved light preference even when the system is dark', () => {
|
||||
mockSystemDarkMode(true)
|
||||
localStorage.setItem(THEME_STORAGE_KEYS.mode, 'light')
|
||||
|
||||
runBootScript()
|
||||
|
||||
expect(document.documentElement).toHaveClass('light')
|
||||
expect(document.documentElement).not.toHaveClass('dark')
|
||||
})
|
||||
|
||||
it('follows the system dark mode when no preference is saved', () => {
|
||||
mockSystemDarkMode(true)
|
||||
|
||||
runBootScript()
|
||||
|
||||
expect(document.documentElement).toHaveClass('dark')
|
||||
})
|
||||
|
||||
it('follows the system light mode when the saved value is invalid', () => {
|
||||
mockSystemDarkMode(false)
|
||||
localStorage.setItem(THEME_STORAGE_KEYS.mode, 'sepia')
|
||||
|
||||
runBootScript()
|
||||
|
||||
expect(document.documentElement).toHaveClass('light')
|
||||
})
|
||||
})
|
||||
@@ -179,6 +179,9 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Native controls follow the theme; Dark Reader also reads this to detect
|
||||
the built-in dark theme. */
|
||||
color-scheme: dark;
|
||||
/* OpenAI-like dark mode with a crisp charcoal canvas. */
|
||||
--background: oklch(0.235 0 0);
|
||||
--foreground: oklch(0.965 0 0);
|
||||
|
||||
Reference in new issue
Block a user