Dark mode by default
linter / quality (push) Successful in 4m25s
tests / ci (8.3) (push) Successful in 4m17s

This commit is contained in:
2026-09-19 18:04:29 +02:00
parent 7c223e0ba8
commit 21c02706de
5 changed files with 17 additions and 99 deletions
+2 -4
View File
@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next'; import { Moon } from 'lucide-vue-next';
import { useAppearance } from '@/composables/useAppearance'; import { useAppearance } from '@/composables/useAppearance';
const { appearance, updateAppearance } = useAppearance(); const { appearance, updateAppearance } = useAppearance();
const tabs = [ const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' },
{ value: 'dark', Icon: Moon, label: 'Dark' }, { value: 'dark', Icon: Moon, label: 'Dark' },
{ value: 'system', Icon: Monitor, label: 'System' },
] as const; ] as const;
</script> </script>
@@ -18,7 +16,7 @@ const tabs = [
<button <button
v-for="{ value, Icon, label } in tabs" v-for="{ value, Icon, label } in tabs"
:key="value" :key="value"
@click="updateAppearance(value)" @click="updateAppearance()"
:class="[ :class="[
'flex items-center rounded-md px-3.5 py-1.5 transition-colors', 'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
appearance === value appearance === value
+11 -73
View File
@@ -10,24 +10,12 @@ export type UseAppearanceReturn = {
updateAppearance: (value: Appearance) => void; updateAppearance: (value: Appearance) => void;
}; };
export function updateTheme(value: Appearance): void { export function updateTheme(): void {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
if (value === 'system') { document.documentElement.classList.add('dark');
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) => { const setCookie = (name: string, value: string, days = 365) => {
@@ -40,80 +28,30 @@ const setCookie = (name: string, value: string, days = 365) => {
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`; 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 prefersDark = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const handleSystemThemeChange = () => {
const currentAppearance = getStoredAppearance();
updateTheme(currentAppearance || 'system');
};
export function initializeTheme(): void { export function initializeTheme(): void {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
// Initialize theme from saved preference or default to system... updateTheme();
const savedAppearance = getStoredAppearance();
updateTheme(savedAppearance || 'system');
// Set up system theme change listener...
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
} }
const appearance = ref<Appearance>('system'); const appearance = ref<Appearance>('dark');
export function useAppearance(): UseAppearanceReturn { export function useAppearance(): UseAppearanceReturn {
onMounted(() => { onMounted(() => {
const savedAppearance = localStorage.getItem( appearance.value = 'dark';
'appearance',
) as Appearance | null;
if (savedAppearance) {
appearance.value = savedAppearance;
}
}); });
const resolvedAppearance = computed<ResolvedAppearance>(() => { const resolvedAppearance = computed<ResolvedAppearance>(() => {
if (appearance.value === 'system') { return 'dark';
return prefersDark() ? 'dark' : 'light';
}
return appearance.value;
}); });
function updateAppearance(value: Appearance) { function updateAppearance() {
appearance.value = value; appearance.value = 'dark';
localStorage.setItem('appearance', 'dark');
// Store in localStorage for client-side persistence... setCookie('appearance', 'dark');
localStorage.setItem('appearance', value); updateTheme();
// Store in cookie for SSR...
setCookie('appearance', value);
updateTheme(value);
} }
return { return {
+1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import AppLayout from '@/layouts/app/AppSidebarLayout.vue'; import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
import type { BreadcrumbItem } from '@/types'; import type { BreadcrumbItem } from '@/types';
import { useColorMode, useDark } from '@vueuse/core';
type Props = { type Props = {
breadcrumbs?: BreadcrumbItem[]; breadcrumbs?: BreadcrumbItem[];
+2 -2
View File
@@ -1,4 +1,4 @@
export type Appearance = 'light' | 'dark' | 'system'; export type Appearance = 'dark';
export type ResolvedAppearance = 'light' | 'dark'; export type ResolvedAppearance = 'dark';
export type AppShellVariant = 'header' | 'sidebar'; export type AppShellVariant = 'header' | 'sidebar';
+1 -20
View File
@@ -1,34 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
<script>
(function() {
const appearance = '{{ $appearance ?? "system" }}';
if (appearance === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark');
}
}
})();
</script>
{{-- Anonymous analytics --}} {{-- Anonymous analytics --}}
<script defer src="https://analytics.matthiasg.dev/recorder.js" data-website-id="e616767f-e0d3-4ce9-b551-33c7c3806fc0"></script> <script defer src="https://analytics.matthiasg.dev/recorder.js" data-website-id="e616767f-e0d3-4ce9-b551-33c7c3806fc0"></script>
{{-- Inline style to set the HTML background color based on our theme in app.css --}} {{-- Inline style to set the HTML background color based on our theme in app.css --}}
<style> <style>
html { html {
background-color: oklch(1 0 0);
}
html.dark {
background-color: oklch(0.145 0 0); background-color: oklch(0.145 0 0);
} }
</style> </style>