chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"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;
|
||||
}
|
||||
Reference in New Issue
Block a user