diff --git a/.dumi/hooks/useLocalStorage.ts b/.dumi/hooks/useLocalStorage.ts new file mode 100644 index 0000000000..4ebc0c331e --- /dev/null +++ b/.dumi/hooks/useLocalStorage.ts @@ -0,0 +1,101 @@ +import React, { useCallback, useEffect, useEffectEvent } from 'react'; + +const ANT_SYNC_STORAGE_EVENT_KEY = 'ANT_SYNC_STORAGE_EVENT_KEY'; + +const isFunction = (val: any): val is (...args: any[]) => any => { + return typeof val === 'function'; +}; + +interface Options { + defaultValue?: T; + serializer?: (value: T) => string; + deserializer?: (value: string) => T; + onError?: (error: unknown) => void; +} + +const useLocalStorage = (key: string, options: Options = {}) => { + const storage = typeof window !== 'undefined' ? localStorage : null; + + const { + defaultValue, + serializer = JSON.stringify, + deserializer = JSON.parse, + onError = console.error, + } = options; + + const getStoredValue = () => { + try { + const rawData = storage?.getItem(key); + if (rawData) { + return deserializer(rawData); + } + } catch (e) { + onError(e); + } + return defaultValue; + }; + + const [state, setState] = React.useState(getStoredValue); + + useEffect(() => { + setState(getStoredValue()); + }, [key]); + + const updateState: React.Dispatch> = (value) => { + const currentState = isFunction(value) ? value(state) : value; + setState(currentState); + try { + let newValue: string | null; + const oldValue = storage?.getItem(key); + if (typeof currentState === 'undefined') { + newValue = null; + storage?.removeItem(key); + } else { + newValue = serializer(currentState); + storage?.setItem(key, newValue); + } + dispatchEvent( + new CustomEvent(ANT_SYNC_STORAGE_EVENT_KEY, { + detail: { key, newValue, oldValue, storageArea: storage }, + }), + ); + } catch (e) { + onError(e); + } + }; + + const shouldSync = (ev: StorageEvent) => { + return ev && ev.key === key && ev.storageArea === storage; + }; + + const onNativeStorage = useCallback( + (event: StorageEvent) => { + if (shouldSync(event)) { + setState(getStoredValue()); + } + }, + [key], + ); + + const onCustomStorage = useCallback( + (event: Event) => { + if (shouldSync(event as StorageEvent)) { + setState(getStoredValue()); + } + }, + [key], + ); + + useEffect(() => { + window?.addEventListener('storage', onNativeStorage); + window?.addEventListener(ANT_SYNC_STORAGE_EVENT_KEY, onCustomStorage); + return () => { + window?.removeEventListener('storage', onNativeStorage); + window?.removeEventListener(ANT_SYNC_STORAGE_EVENT_KEY, onCustomStorage); + }; + }, [key, onNativeStorage, onCustomStorage]); + + return [state, useEffectEvent(updateState)] as const; +}; + +export default useLocalStorage; diff --git a/.dumi/pages/index/components/PreviewBanner/index.tsx b/.dumi/pages/index/components/PreviewBanner/index.tsx index 7535dc7e3c..ecd4f75ea4 100644 --- a/.dumi/pages/index/components/PreviewBanner/index.tsx +++ b/.dumi/pages/index/components/PreviewBanner/index.tsx @@ -31,15 +31,17 @@ const locales = { const useStyle = createStyles(({ cssVar, css, cx }, siteConfig: SiteContextProps) => { const textShadow = `0 0 4px ${cssVar.colorBgContainer}`; - const isDark = siteConfig.theme.includes('dark'); const mask = cx(css` position: absolute; inset: 0; backdrop-filter: blur(2px); opacity: 1; - background-color: ${isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'}; + background-color: rgba(255, 255, 255, 0.2); transition: all 1s ease; pointer-events: none; + [data-prefers-color='dark'] & { + background-color: rgba(0, 0, 0, 0.2); + } `); const block = cx(css` diff --git a/.dumi/pages/index/components/util.ts b/.dumi/pages/index/components/util.ts index e9bdf3a1c3..e4d769c3a0 100644 --- a/.dumi/pages/index/components/util.ts +++ b/.dumi/pages/index/components/util.ts @@ -60,7 +60,15 @@ export type Extras = { export type Icons = Icon[]; +export type HeadingBanner = { + [key in 'cn' | 'en']: { + title?: string; + href?: string; + }; +}; + export type SiteData = { + headingBanner: HeadingBanner; articles: Articles; authors: Authors; recommendations: Recommendations; diff --git a/.dumi/pages/theme-editor/index.tsx b/.dumi/pages/theme-editor/index.tsx index 77bf319c3d..85d6f0e98c 100644 --- a/.dumi/pages/theme-editor/index.tsx +++ b/.dumi/pages/theme-editor/index.tsx @@ -1,10 +1,11 @@ -import React, { Suspense, useEffect } from 'react'; +import React, { Suspense } from 'react'; import { App, Button, ConfigProvider, Skeleton } from 'antd'; import { enUS, zhCN } from 'antd-token-previewer'; import type { ThemeConfig } from 'antd/es/config-provider/context'; import { Helmet } from 'dumi'; import useLocale from '../../hooks/useLocale'; +import useLocalStorage from '../../hooks/useLocalStorage'; const ThemeEditor = React.lazy(() => import('antd-token-previewer/lib/ThemeEditor')); @@ -33,24 +34,18 @@ const locales = { }, }; -const ANT_DESIGN_V5_THEME_EDITOR_THEME = 'ant-design-v5-theme-editor-theme'; +const ANT_THEME_EDITOR_THEME = 'ant-theme-editor-theme'; const CustomTheme: React.FC = () => { const { message } = App.useApp(); const [locale, lang] = useLocale(locales); - const [theme, setTheme] = React.useState({}); - - useEffect(() => { - const storedConfig = localStorage.getItem(ANT_DESIGN_V5_THEME_EDITOR_THEME); - if (storedConfig) { - const themeConfig = JSON.parse(storedConfig); - setTheme(themeConfig); - } - }, []); + const [themeConfig, setThemeConfig] = useLocalStorage(ANT_THEME_EDITOR_THEME, { + defaultValue: {}, + }); const handleSave = () => { - localStorage.setItem(ANT_DESIGN_V5_THEME_EDITOR_THEME, JSON.stringify(theme)); + setThemeConfig(themeConfig); message.success(locale.saveSuccessfully); }; @@ -65,11 +60,9 @@ const CustomTheme: React.FC = () => { { - setTheme(newTheme.config); - }} + onThemeChange={(newTheme) => setThemeConfig(newTheme.config)} locale={lang === 'cn' ? zhCN : enUS} actions={