135 lines
3.7 KiB
TypeScript
135 lines
3.7 KiB
TypeScript
import { computed, reactive } from "vue";
|
|
|
|
type ThemeMode = "light" | "dark" | "system";
|
|
type DensityMode = "comfortable" | "compact";
|
|
|
|
const STORAGE_KEYS = {
|
|
themeMode: "live-recorder-ui-theme",
|
|
density: "live-recorder-ui-density",
|
|
sidebarCollapsed: "live-recorder-ui-sidebar-collapsed"
|
|
} as const;
|
|
|
|
const state = reactive({
|
|
themeMode: "system" as ThemeMode,
|
|
density: "comfortable" as DensityMode,
|
|
sidebarCollapsed: false,
|
|
systemTheme: "light" as "light" | "dark",
|
|
initialized: false
|
|
});
|
|
|
|
let mediaQueryList: MediaQueryList | null = null;
|
|
|
|
function readStoredThemeMode(): ThemeMode {
|
|
const stored = window.localStorage.getItem(STORAGE_KEYS.themeMode);
|
|
return stored === "light" || stored === "dark" || stored === "system" ? stored : "system";
|
|
}
|
|
|
|
function readStoredDensity(): DensityMode {
|
|
return window.localStorage.getItem(STORAGE_KEYS.density) === "compact" ? "compact" : "comfortable";
|
|
}
|
|
|
|
function readStoredSidebarCollapsed() {
|
|
return window.localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === "true";
|
|
}
|
|
|
|
function resolveTheme(): "light" | "dark" {
|
|
return state.themeMode === "system" ? state.systemTheme : state.themeMode;
|
|
}
|
|
|
|
function applyDocumentPreferences() {
|
|
if (typeof document === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const resolvedTheme = resolveTheme();
|
|
document.documentElement.dataset.theme = resolvedTheme;
|
|
document.documentElement.dataset.themeMode = state.themeMode;
|
|
document.documentElement.dataset.density = state.density;
|
|
}
|
|
|
|
function syncSystemTheme() {
|
|
state.systemTheme = mediaQueryList?.matches ? "dark" : "light";
|
|
applyDocumentPreferences();
|
|
}
|
|
|
|
function ensureInitialized() {
|
|
if (state.initialized || typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
state.themeMode = readStoredThemeMode();
|
|
state.density = readStoredDensity();
|
|
state.sidebarCollapsed = readStoredSidebarCollapsed();
|
|
mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
|
|
|
if (typeof mediaQueryList.addEventListener === "function") {
|
|
mediaQueryList.addEventListener("change", syncSystemTheme);
|
|
} else {
|
|
mediaQueryList.addListener(syncSystemTheme);
|
|
}
|
|
|
|
syncSystemTheme();
|
|
state.initialized = true;
|
|
}
|
|
|
|
function persistPreferences() {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
window.localStorage.setItem(STORAGE_KEYS.themeMode, state.themeMode);
|
|
window.localStorage.setItem(STORAGE_KEYS.density, state.density);
|
|
window.localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, String(state.sidebarCollapsed));
|
|
}
|
|
|
|
export function useUiPreferences() {
|
|
ensureInitialized();
|
|
|
|
const resolvedTheme = computed(() => resolveTheme());
|
|
|
|
function setThemeMode(value: ThemeMode) {
|
|
state.themeMode = value;
|
|
persistPreferences();
|
|
applyDocumentPreferences();
|
|
}
|
|
|
|
function cycleThemeMode() {
|
|
const nextValue =
|
|
state.themeMode === "light" ? "dark" : state.themeMode === "dark" ? "system" : "light";
|
|
setThemeMode(nextValue);
|
|
}
|
|
|
|
function setDensity(value: DensityMode) {
|
|
state.density = value;
|
|
persistPreferences();
|
|
applyDocumentPreferences();
|
|
}
|
|
|
|
function setSidebarCollapsed(value: boolean) {
|
|
state.sidebarCollapsed = value;
|
|
persistPreferences();
|
|
}
|
|
|
|
function toggleSidebarCollapsed() {
|
|
setSidebarCollapsed(!state.sidebarCollapsed);
|
|
}
|
|
|
|
return {
|
|
themeMode: computed({
|
|
get: () => state.themeMode,
|
|
set: (value: ThemeMode) => setThemeMode(value)
|
|
}),
|
|
resolvedTheme,
|
|
density: computed({
|
|
get: () => state.density,
|
|
set: (value: DensityMode) => setDensity(value)
|
|
}),
|
|
sidebarCollapsed: computed({
|
|
get: () => state.sidebarCollapsed,
|
|
set: (value: boolean) => setSidebarCollapsed(value)
|
|
}),
|
|
cycleThemeMode,
|
|
toggleSidebarCollapsed
|
|
};
|
|
}
|