feat(hooks): useThemePreference 扩展 compact 状态与同步

This commit is contained in:
2026-06-06 22:46:56 +08:00
parent eccc3f62d2
commit e0466f9b99
2 changed files with 103 additions and 1 deletions

View File

@@ -6,10 +6,13 @@ export type EffectiveTheme = "dark" | "light";
export type ThemePreference = "dark" | "light" | "system";
const PREFERENCE_CHANGE_EVENT = "theme-preference-change";
const COMPACT_CHANGE_EVENT = "theme-compact-change";
export const THEME_PREFERENCE_STORAGE_KEY = "theme.preference";
export const THEME_MEDIA_QUERY = "(prefers-color-scheme: dark)";
export const COMPACT_STORAGE_KEY = "theme.compact";
export function getSystemPrefersDark(matchMedia: Window["matchMedia"] = window.matchMedia): boolean {
try {
return matchMedia(THEME_MEDIA_QUERY).matches;
@@ -30,6 +33,14 @@ export function readThemePreference(storage: Storage = window.localStorage): The
}
}
export function readCompactPreference(storage: Storage = window.localStorage): boolean {
try {
return storage.getItem(COMPACT_STORAGE_KEY) === "true";
} catch {
return false;
}
}
export function resolveEffectiveTheme(preference: ThemePreference, systemPrefersDark: boolean): EffectiveTheme {
if (preference === "dark" || preference === "light") return preference;
return systemPrefersDark ? "dark" : "light";
@@ -37,6 +48,7 @@ export function resolveEffectiveTheme(preference: ThemePreference, systemPrefers
export function useThemePreference() {
const [preference, setPreferenceState] = useState<ThemePreference>(() => readThemePreference());
const [compact, setCompactState] = useState<boolean>(() => readCompactPreference());
const [systemPrefersDark, setSystemPrefersDark] = useState(() => getSystemPrefersDark());
const effectiveTheme = resolveEffectiveTheme(preference, systemPrefersDark);
@@ -52,6 +64,9 @@ export function useThemePreference() {
const apiTheme = parseThemePreference(data.theme);
setPreferenceState((prev) => (prev !== apiTheme ? apiTheme : prev));
writeThemePreference(apiTheme);
const apiCompact = typeof data.compact === "boolean" ? data.compact : false;
setCompactState((prev) => (prev !== apiCompact ? apiCompact : prev));
writeCompactPreference(apiCompact);
})
.catch(() => {
// API 不可用时维持 localStorage 缓存值
@@ -78,12 +93,26 @@ export function useThemePreference() {
return () => window.removeEventListener(PREFERENCE_CHANGE_EVENT, handleStorageEvent as EventListener);
}, []);
useEffect(() => {
const handleCompactEvent = (event: CustomEvent) => {
const next = typeof event.detail === "boolean" ? event.detail : false;
setCompactState((prev) => (prev !== next ? next : prev));
};
window.addEventListener(COMPACT_CHANGE_EVENT, handleCompactEvent as EventListener);
return () => window.removeEventListener(COMPACT_CHANGE_EVENT, handleCompactEvent as EventListener);
}, []);
const setPreference = useCallback((nextPreference: ThemePreference) => {
setPreferenceState(nextPreference);
writeThemePreference(nextPreference);
}, []);
return { effectiveTheme, preference, setPreference };
const setCompact = useCallback((nextCompact: boolean) => {
setCompactState(nextCompact);
writeCompactPreference(nextCompact);
}, []);
return { compact, effectiveTheme, preference, setCompact, setPreference };
}
export function writeThemePreference(preference: ThemePreference, storage: Storage = window.localStorage) {
@@ -98,3 +127,16 @@ export function writeThemePreference(preference: ThemePreference, storage: Stora
// jsdom 等环境可能不支持 CustomEvent
}
}
export function writeCompactPreference(value: boolean, storage: Storage = window.localStorage): void {
try {
storage.setItem(COMPACT_STORAGE_KEY, String(value));
} catch {
// 存储不可用时不阻断
}
try {
window.dispatchEvent(new CustomEvent(COMPACT_CHANGE_EVENT, { detail: value }));
} catch {
// jsdom 等环境兼容
}
}