Files
live_recorder/frontend/src/composables/useUiPreferences.ts
T

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
};
}