diff --git a/src/context/ThemeModeContext.ts b/src/context/ThemeModeContext.ts index 10278fb0..c4b2065d 100644 --- a/src/context/ThemeModeContext.ts +++ b/src/context/ThemeModeContext.ts @@ -1,9 +1,13 @@ import { createContext } from 'react'; -import { ThemeContextState } from '@/types'; +import { ThemeAppearance, ThemeContextState } from '@/types'; + +const matchThemeMode = (mode: ThemeAppearance) => + matchMedia && matchMedia(`(prefers-color-scheme: ${mode})`); export const ThemeModeContext = createContext({ appearance: 'light', isDarkMode: false, themeMode: 'light', + browserPrefers: matchThemeMode('dark')?.matches ? 'dark' : 'light', }); diff --git a/src/factories/createThemeProvider/ThemeSwitcher.tsx b/src/factories/createThemeProvider/ThemeSwitcher.tsx index f3cfb51a..8f50e6fd 100644 --- a/src/factories/createThemeProvider/ThemeSwitcher.tsx +++ b/src/factories/createThemeProvider/ThemeSwitcher.tsx @@ -2,7 +2,7 @@ import { useMergeValue } from '@/utils/useMergeValue'; import { FC, memo, ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { ThemeModeContext } from '@/context'; -import { ThemeAppearance, ThemeMode, UseTheme } from '@/types'; +import { BrowserPrefers, ThemeAppearance, ThemeMode, UseTheme } from '@/types'; let darkThemeMatch: MediaQueryList; @@ -12,7 +12,8 @@ const matchThemeMode = (mode: ThemeAppearance) => const ThemeObserver: FC<{ themeMode: ThemeMode; setAppearance: (value: ThemeAppearance) => void; -}> = ({ themeMode, setAppearance }) => { + setBrowserPrefers: (value: BrowserPrefers) => void; +}> = ({ themeMode, setAppearance, setBrowserPrefers }) => { const matchBrowserTheme = () => { if (matchThemeMode('dark').matches) { setAppearance('dark'); @@ -21,6 +22,14 @@ const ThemeObserver: FC<{ } }; + const updateBrowserTheme = () => { + if (matchThemeMode('dark').matches) { + setBrowserPrefers('dark'); + } else { + setBrowserPrefers('light'); + } + }; + // 自动监听系统主题变更 useLayoutEffect(() => { // 如果不是自动,就明确设定亮暗色 @@ -41,6 +50,18 @@ const ThemeObserver: FC<{ }; }, [themeMode]); + useEffect(() => { + if (!darkThemeMatch) { + darkThemeMatch = matchThemeMode('dark'); + } + + darkThemeMatch.addEventListener('change', updateBrowserTheme); + + return () => { + darkThemeMatch.removeEventListener('change', updateBrowserTheme); + }; + }, []); + return null; }; @@ -85,6 +106,10 @@ const ThemeSwitcher: FC = memo( onChange: onAppearanceChange, }); + const [browserPrefers, setBrowserPrefers] = useState( + matchThemeMode('dark')?.matches ? 'dark' : 'light', + ); + const [startObserver, setStartObserver] = useState(false); // Wait until after client-side hydration to show @@ -98,9 +123,16 @@ const ThemeSwitcher: FC = memo( themeMode, appearance, isDarkMode: appearance === 'dark', + browserPrefers, }} > - {startObserver && } + {startObserver && ( + + )} {children} ); diff --git a/src/factories/createThemeProvider/type.ts b/src/factories/createThemeProvider/type.ts index fc661b1f..abae1493 100644 --- a/src/factories/createThemeProvider/type.ts +++ b/src/factories/createThemeProvider/type.ts @@ -61,8 +61,27 @@ export interface ThemeProviderProps> { themeMode?: ThemeMode; } +/** + * 静态实例 + */ export interface StaticInstance { + /** + * 消息实例 + */ message: MessageInstance; + /** + * 通知实例 + */ notification: NotificationInstance; + /** + * 弹窗实例,不包含 warn 方法 + * @typedef {object} Omit + * @property {Function} info - info 弹窗 + * @property {Function} success - success 弹窗 + * @property {Function} error - error 弹窗 + * @property {Function} warning - warning 弹窗 + * @property {Function} confirm - confirm 弹窗 + * @property {Function} destroyAll - 关闭所有弹窗 + */ modal: Omit; } diff --git a/src/types/appearance.ts b/src/types/appearance.ts index 8c4228c6..7345460d 100644 --- a/src/types/appearance.ts +++ b/src/types/appearance.ts @@ -1,3 +1,5 @@ +export type BrowserPrefers = 'dark' | 'light'; + export type ThemeAppearance = 'dark' | 'light' | string; export type ThemeMode = 'auto' | 'dark' | 'light'; diff --git a/src/types/theme.ts b/src/types/theme.ts index 2cf9fa8d..5f674adf 100644 --- a/src/types/theme.ts +++ b/src/types/theme.ts @@ -2,15 +2,34 @@ import { ThemeConfig } from 'antd'; import { MappingAlgorithm } from 'antd/es/config-provider/context'; import { AliasToken } from 'antd/es/theme/interface'; -import { ThemeAppearance, ThemeMode } from './appearance'; +import { BrowserPrefers, ThemeAppearance, ThemeMode } from './appearance'; +/** + * @title 主题上下文状态 + */ export interface ThemeContextState { + /** + * @title 外观 + */ appearance: ThemeAppearance; + /** + * @title 主题模式 + * @enum ["light", "dark"] + * @enumNames ["亮色模式", "暗色模式"] + * @default "light" + */ themeMode: ThemeMode; + /** + * @title 是否为暗色模式 + */ isDarkMode: boolean; + /** + * @title 浏览器偏好的外观 + */ + browserPrefers: BrowserPrefers; } -export type AppearanceState = Omit; +export type AppearanceState = Omit; export type AntdToken = AliasToken; @@ -21,9 +40,15 @@ export interface AntdStylish { buttonDefaultHover: string; } +/** + * @title 获取 Antd 主题的函数 + * @param appearance - 主题外观 + * @returns Antd 主题配置对象或 undefined + */ export interface GetAntdTheme { (appearance: ThemeAppearance): ThemeConfig | undefined; } + export type { MappingAlgorithm }; // eslint-disable-next-line @typescript-eslint/no-empty-interface