agent-ecosystem/landing/composables/useBrowserTheme.ts

176 lines
4.8 KiB
TypeScript

import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from "vue";
import type { Ref } from "vue";
import { useThemeStore } from "~/stores/theme";
type ThemeName = "light" | "dark";
type VuetifyThemeInstance = {
global: {
name: Ref<string>;
current: Ref<unknown>;
};
change?: (name: string) => void;
};
function isThemeName(value: string | null | undefined): value is ThemeName {
return value === "dark" || value === "light";
}
export const useBrowserTheme = () => {
const themeStore = useThemeStore();
const { $vuetifyTheme } = useNuxtApp();
const vuetifyTheme = $vuetifyTheme as VuetifyThemeInstance | null;
const documentTheme = ref<ThemeName | null>(null);
let mediaQueryHandler: ((event: MediaQueryListEvent) => void) | null = null;
let mediaQuery: MediaQueryList | null = null;
let themeClassObserver: MutationObserver | null = null;
const getDocumentTheme = (): ThemeName | null => {
if (!import.meta.client) return null;
const appClass = document.querySelector(".v-application")?.classList;
if (appClass?.contains("v-theme--dark")) return "dark";
if (appClass?.contains("v-theme--light")) return "light";
return null;
};
const refreshDocumentTheme = () => {
documentTheme.value = getDocumentTheme();
return documentTheme.value;
};
const applyDocumentTheme = (name: ThemeName) => {
if (!import.meta.client) return;
document.querySelectorAll(".v-application").forEach((app) => {
app.classList.toggle("v-theme--dark", name === "dark");
app.classList.toggle("v-theme--light", name === "light");
});
documentTheme.value = name;
};
const getAppliedTheme = (): ThemeName => {
const domTheme = getDocumentTheme() ?? documentTheme.value;
if (domTheme) return domTheme;
const vuetifyName = vuetifyTheme?.global.name.value;
if (isThemeName(vuetifyName)) return vuetifyName;
return themeStore.current;
};
const syncStoreFromAppliedTheme = () => {
const appliedTheme = getAppliedTheme();
if (themeStore.current !== appliedTheme) {
themeStore.setTheme(appliedTheme, false);
}
return appliedTheme;
};
const applyVuetifyTheme = (name: ThemeName) => {
if (!vuetifyTheme) return;
if (vuetifyTheme.change) {
vuetifyTheme.change(name);
return;
}
vuetifyTheme.global.name.value = name;
};
const applyTheme = (name: ThemeName, fromUser = true) => {
applyVuetifyTheme(name);
applyDocumentTheme(name);
themeStore.setTheme(name, fromUser);
return name;
};
const observeDocumentTheme = () => {
if (!import.meta.client || themeClassObserver) return;
const app = document.querySelector(".v-application");
if (!app) return;
refreshDocumentTheme();
themeClassObserver = new MutationObserver(() => {
refreshDocumentTheme();
});
themeClassObserver.observe(app, { attributes: true, attributeFilter: ["class"] });
};
const initTheme = () => {
if (!import.meta.client) return;
const initialTheme = themeStore.getInitialTheme();
applyTheme(initialTheme, false);
if (mediaQuery && mediaQueryHandler) {
mediaQuery.removeEventListener("change", mediaQueryHandler);
mediaQuery = null;
mediaQueryHandler = null;
}
if (!themeStore.userSelected) {
mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQueryHandler = (event: MediaQueryListEvent) => {
if (!themeStore.userSelected) {
const newTheme = event.matches ? "dark" : "light";
applyTheme(newTheme, false);
}
};
mediaQuery.addEventListener("change", mediaQueryHandler);
}
};
const toggleTheme = () => {
const appliedTheme = syncStoreFromAppliedTheme();
return applyTheme(appliedTheme === "dark" ? "light" : "dark");
};
if (getCurrentInstance()) {
onMounted(() => {
refreshDocumentTheme();
observeDocumentTheme();
});
onUnmounted(() => {
if (mediaQuery && mediaQueryHandler) {
mediaQuery.removeEventListener("change", mediaQueryHandler);
}
themeClassObserver?.disconnect();
});
}
watch(
() => themeStore.current,
(value) => {
applyVuetifyTheme(value);
}
);
if (vuetifyTheme) {
watch(
() => vuetifyTheme.global.name.value,
(value) => {
if (isThemeName(value) && themeStore.current !== value) {
themeStore.setTheme(value, false);
}
}
);
}
const currentTheme = computed(() => {
if (documentTheme.value) return documentTheme.value;
const vuetifyName = vuetifyTheme?.global.name.value;
return isThemeName(vuetifyName) ? vuetifyName : themeStore.current;
});
const isDark = computed(() => currentTheme.value === "dark");
return {
currentTheme,
isDark,
initTheme,
toggleTheme
};
};