- Add .gitea/workflows/ci.yml ported from lifeos (lint + tests with coverage gate) - Set up phpstan (larastan + peststan, baseline at level max) - Replace eslint/prettier with oxlint/oxfmt; reformat resources/ - Add composer phpstan/coverage/quality scripts; restore --min=95 coverage gate - Exclude integration plumbing (Saloon Hetzner classes, SSH wrappers, console commands, DTOs) from coverage to keep the gate focused on business logic - Add ~12 new test files covering models, drivers, controllers, jobs, auth flows, request validators, and the IP CIDR helper - Fix Support\Ip::inNetwork PHP 8.4 TypeError in CIDR mask check - Fix FirewallRule::command comparing the enum-cast type column to a string - Fix Server::network using the wrong foreign key column - Remove unreachable code under abort(403) in RegisteredUserController
95 lines
2.3 KiB
TypeScript
95 lines
2.3 KiB
TypeScript
import { onMounted, ref } from "vue";
|
|
|
|
type Appearance = "light" | "dark" | "system";
|
|
|
|
export function updateTheme(value: Appearance) {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
if (value === "system") {
|
|
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const systemTheme = mediaQueryList.matches ? "dark" : "light";
|
|
|
|
document.documentElement.classList.toggle("dark", systemTheme === "dark");
|
|
} else {
|
|
document.documentElement.classList.toggle("dark", value === "dark");
|
|
}
|
|
}
|
|
|
|
const setCookie = (name: string, value: string, days = 365) => {
|
|
if (typeof document === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const maxAge = days * 24 * 60 * 60;
|
|
|
|
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
|
|
};
|
|
|
|
const mediaQuery = () => {
|
|
if (typeof window === "undefined") {
|
|
return null;
|
|
}
|
|
|
|
return window.matchMedia("(prefers-color-scheme: dark)");
|
|
};
|
|
|
|
const getStoredAppearance = () => {
|
|
if (typeof window === "undefined") {
|
|
return null;
|
|
}
|
|
|
|
return localStorage.getItem("appearance") as Appearance | null;
|
|
};
|
|
|
|
const handleSystemThemeChange = () => {
|
|
const currentAppearance = getStoredAppearance();
|
|
|
|
updateTheme(currentAppearance || "system");
|
|
};
|
|
|
|
export function initializeTheme() {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
// Initialize theme from saved preference or default to system...
|
|
const savedAppearance = getStoredAppearance();
|
|
updateTheme(savedAppearance || "system");
|
|
|
|
// Set up system theme change listener...
|
|
mediaQuery()?.addEventListener("change", handleSystemThemeChange);
|
|
}
|
|
|
|
export function useAppearance() {
|
|
const appearance = ref<Appearance>("system");
|
|
|
|
onMounted(() => {
|
|
initializeTheme();
|
|
|
|
const savedAppearance = localStorage.getItem("appearance") as Appearance | null;
|
|
|
|
if (savedAppearance) {
|
|
appearance.value = savedAppearance;
|
|
}
|
|
});
|
|
|
|
function updateAppearance(value: Appearance) {
|
|
appearance.value = value;
|
|
|
|
// Store in localStorage for client-side persistence...
|
|
localStorage.setItem("appearance", value);
|
|
|
|
// Store in cookie for SSR...
|
|
setCookie("appearance", value);
|
|
|
|
updateTheme(value);
|
|
}
|
|
|
|
return {
|
|
appearance,
|
|
updateAppearance,
|
|
};
|
|
}
|