Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

174 lines
4.9 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
getStoredTheme,
getSystemTheme,
setTheme as applyThemePreference,
subscribeToThemeChanges,
type Theme,
} from "@/lib/theme";
import {
ACTIVE_SESSION_EVENT,
ACTIVE_SESSION_STORAGE_KEY,
LANGUAGE_EVENT,
LANGUAGE_STORAGE_KEY,
SIDEBAR_COLLAPSED_EVENT,
SIDEBAR_COLLAPSED_STORAGE_KEY,
normalizeLanguage,
readStoredActiveSessionId,
readStoredLanguage,
readStoredSidebarCollapsed,
writeStoredActiveSessionId,
writeStoredLanguage,
writeStoredSidebarCollapsed,
type AppLanguage,
} from "@/context/app-shell-storage";
interface AppShellContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
language: AppLanguage;
setLanguage: (language: AppLanguage) => void;
activeSessionId: string | null;
setActiveSessionId: (sessionId: string | null) => void;
sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void;
}
const AppShellContext = createContext<AppShellContextValue | null>(null);
export function AppShellProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
return getStoredTheme() ?? getSystemTheme();
});
// Always start with "en" to match SSR; hydrate from localStorage after mount
const [language, setLanguageState] = useState<AppLanguage>("en");
const [activeSessionId, setActiveSessionIdState] = useState<string | null>(
() => readStoredActiveSessionId(),
);
// Always start expanded to match SSR; hydrate from localStorage after mount
const [sidebarCollapsed, setSidebarCollapsedState] = useState<boolean>(false);
useEffect(() => {
// Hydrate client-only preferences after SSR-safe first render.
// eslint-disable-next-line react-hooks/set-state-in-effect
setLanguageState(readStoredLanguage());
setSidebarCollapsedState(readStoredSidebarCollapsed());
}, []);
useEffect(() => {
return subscribeToThemeChanges((nextTheme) => {
setThemeState(nextTheme);
});
}, []);
useEffect(() => {
if (typeof window === "undefined") return;
const onStorage = (event: StorageEvent) => {
if (event.key === LANGUAGE_STORAGE_KEY) {
setLanguageState(normalizeLanguage(event.newValue));
}
if (event.key === ACTIVE_SESSION_STORAGE_KEY) {
setActiveSessionIdState(event.newValue);
}
if (event.key === SIDEBAR_COLLAPSED_STORAGE_KEY) {
setSidebarCollapsedState(event.newValue === "1");
}
};
const onLanguage = (event: Event) => {
const detail = (event as CustomEvent<{ language?: AppLanguage }>).detail;
setLanguageState(normalizeLanguage(detail?.language));
};
const onActiveSession = (event: Event) => {
const detail = (event as CustomEvent<{ sessionId?: string | null }>)
.detail;
setActiveSessionIdState(detail?.sessionId ?? null);
};
const onSidebarCollapsed = (event: Event) => {
const detail = (event as CustomEvent<{ collapsed?: boolean }>).detail;
setSidebarCollapsedState(Boolean(detail?.collapsed));
};
window.addEventListener("storage", onStorage);
window.addEventListener(LANGUAGE_EVENT, onLanguage);
window.addEventListener(ACTIVE_SESSION_EVENT, onActiveSession);
window.addEventListener(SIDEBAR_COLLAPSED_EVENT, onSidebarCollapsed);
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener(LANGUAGE_EVENT, onLanguage);
window.removeEventListener(ACTIVE_SESSION_EVENT, onActiveSession);
window.removeEventListener(SIDEBAR_COLLAPSED_EVENT, onSidebarCollapsed);
};
}, []);
const setTheme = useCallback((nextTheme: Theme) => {
applyThemePreference(nextTheme);
setThemeState(nextTheme);
}, []);
const setLanguage = useCallback((nextLanguage: AppLanguage) => {
writeStoredLanguage(nextLanguage);
setLanguageState(nextLanguage);
}, []);
const setActiveSessionId = useCallback((sessionId: string | null) => {
writeStoredActiveSessionId(sessionId);
setActiveSessionIdState(sessionId);
}, []);
const setSidebarCollapsed = useCallback((collapsed: boolean) => {
writeStoredSidebarCollapsed(collapsed);
setSidebarCollapsedState(collapsed);
}, []);
const value = useMemo<AppShellContextValue>(
() => ({
theme,
setTheme,
language,
setLanguage,
activeSessionId,
setActiveSessionId,
sidebarCollapsed,
setSidebarCollapsed,
}),
[
activeSessionId,
language,
setActiveSessionId,
setLanguage,
setSidebarCollapsed,
setTheme,
sidebarCollapsed,
theme,
],
);
return (
<AppShellContext.Provider value={value}>
{children}
</AppShellContext.Provider>
);
}
export function useAppShell() {
const context = useContext(AppShellContext);
if (!context) {
throw new Error("useAppShell must be used inside AppShellProvider");
}
return context;
}