diff --git a/.changeset/eighty-experts-camp.md b/.changeset/eighty-experts-camp.md new file mode 100644 index 000000000..d9a0f8738 --- /dev/null +++ b/.changeset/eighty-experts-camp.md @@ -0,0 +1,5 @@ +--- +"@rspress/core": minor +--- + +feat: add color-scheme on root to enable native color scheme features diff --git a/.changeset/great-boats-notice.md b/.changeset/great-boats-notice.md new file mode 100644 index 000000000..4b9bea5d4 --- /dev/null +++ b/.changeset/great-boats-notice.md @@ -0,0 +1,5 @@ +--- +"@rspress/core": patch +--- + +fix: avoid flash of theme appearance in dev mode diff --git a/packages/core/src/node/build.ts b/packages/core/src/node/build.ts index 8626d52c8..e423a60ae 100644 --- a/packages/core/src/node/build.ts +++ b/packages/core/src/node/build.ts @@ -6,7 +6,6 @@ import fs from '@rspress/shared/fs-extra'; import { PageData, UserConfig, - APPEARANCE_KEY, normalizeSlash, withBase, isDebugMode, @@ -27,19 +26,6 @@ import { PluginDriver } from './PluginDriver'; import type { Route } from '@/node/route/RouteService'; import { routeService } from '@/node/route/init'; -// In the first render, the theme will be set according to the user's system theme -const CHECK_DARK_LIGHT_SCRIPT = ` - -`; - interface BuildOptions { appDirectory: string; docDirectory: string; @@ -177,9 +163,6 @@ export async function renderPages( helmet?.link?.toString(), helmet?.style?.toString(), helmet?.script?.toString(), - config.themeConfig?.darkMode !== false - ? CHECK_DARK_LIGHT_SCRIPT - : '', ]) .join(''), ); diff --git a/packages/core/src/node/constants.ts b/packages/core/src/node/constants.ts index 11878bab8..650239153 100644 --- a/packages/core/src/node/constants.ts +++ b/packages/core/src/node/constants.ts @@ -1,6 +1,6 @@ import path from 'path'; import { fileURLToPath } from 'url'; -import { RSPRESS_TEMP_DIR } from '@rspress/shared'; +import { APPEARANCE_KEY, RSPRESS_TEMP_DIR } from '@rspress/shared'; export const isProduction = () => process.env.NODE_ENV === 'production'; @@ -8,6 +8,20 @@ export const isProduction = () => process.env.NODE_ENV === 'production'; export const importStatementRegex = /import\s+(.*?)\s+from\s+(['"])(.*?)(?:"|');?/gm; +// In the first render, the theme will be set according to the user's system theme +// - Should be injected into both development and production modes +// - Class hook (.dark) is set for internal use (Tailwind) +// - Style hook (colorScheme) is set for external use (CSS media queries or `light-dark()` function) +export const inlineThemeScript = `{ + const saved = localStorage.getItem('${APPEARANCE_KEY}') + const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const isDark = !saved || saved === 'auto' ? preferDark : saved === 'dark' + document.documentElement.classList.toggle('dark', isDark) + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light' +}` + .replace(/\n/g, ';') + .replace(/\s{2,}/g, ''); + // @ts-expect-error const dirname = path.dirname(fileURLToPath(new URL(import.meta.url))); diff --git a/packages/core/src/node/initRsbuild.ts b/packages/core/src/node/initRsbuild.ts index d009948a9..c0cd34823 100644 --- a/packages/core/src/node/initRsbuild.ts +++ b/packages/core/src/node/initRsbuild.ts @@ -14,6 +14,7 @@ import { PACKAGE_ROOT, OUTPUT_DIR, isProduction, + inlineThemeScript, PUBLIC_DIR, } from './constants'; import { rsbuildPluginDocVM } from './runtimeModule'; @@ -104,6 +105,13 @@ async function createInternalBuildConfig( title: config?.title, favicon: normalizeIcon(config?.icon), template: path.join(PACKAGE_ROOT, 'index.html'), + tags: [ + config.themeConfig?.darkMode !== false && { + tag: 'script', + children: inlineThemeScript, + append: false, + }, + ].filter(Boolean), }, output: { targets: isSSR ? ['node'] : ['web'],