Files
alhp-web/frontend/src/composables/useTheme.ts
vikingowl 5fac66a38c Add mobile accessibility and UI/UX improvements
- Add mobile card view for packages (replaces table on small screens)
- Implement design tokens system for consistent styling
- Add dark/light theme toggle with system preference support
- Create reusable StatusBadge and EmptyState components
- Add accessible form labels to package filters
- Add compact mobile stats display in navigation
- Add safe area insets for notched devices
- Add reduced motion support for accessibility
- Improve touch targets for WCAG compliance
- Add unit tests for composables
- Update Vuetify configuration and styling
2025-11-26 16:46:02 +01:00

123 lines
3.2 KiB
TypeScript

import { ref, watch, onMounted } from 'vue'
import { useTheme as useVuetifyTheme } from 'vuetify'
type ThemeMode = 'light' | 'dark' | 'system'
const STORAGE_KEY = 'alhp-theme-preference'
// Global reactive state (shared across components)
const themeMode = ref<ThemeMode>('system')
const isDark = ref(true)
/**
* Theme management composable
* Handles dark/light mode with OS preference detection and persistence
*/
export function useTheme() {
const vuetifyTheme = useVuetifyTheme()
/**
* Get system preference for dark mode
*/
const getSystemPreference = (): boolean => {
if (typeof window === 'undefined') return true
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
/**
* Apply theme to document and Vuetify
*/
const applyTheme = (dark: boolean, animate = false) => {
isDark.value = dark
// Add transition class for smooth theme switching
if (animate) {
document.documentElement.classList.add('theme-transition')
setTimeout(() => {
document.documentElement.classList.remove('theme-transition')
}, 300)
}
// Apply to Vuetify
vuetifyTheme.global.name.value = dark ? 'darkTheme' : 'lightTheme'
// Apply data attribute for CSS variables
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light')
// Update meta theme-color for mobile browsers
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', dark ? '#0f1419' : '#ffffff')
}
}
/**
* Set theme mode and persist preference
*/
const setThemeMode = (mode: ThemeMode, animate = true) => {
themeMode.value = mode
localStorage.setItem(STORAGE_KEY, mode)
if (mode === 'system') {
applyTheme(getSystemPreference(), animate)
} else {
applyTheme(mode === 'dark', animate)
}
}
/**
* Toggle between dark and light modes
*/
const toggleTheme = () => {
if (themeMode.value === 'system') {
// If currently following system, switch to opposite of current
setThemeMode(isDark.value ? 'light' : 'dark')
} else {
setThemeMode(themeMode.value === 'dark' ? 'light' : 'dark')
}
}
/**
* Initialize theme from storage or system preference
*/
const initTheme = () => {
const stored = localStorage.getItem(STORAGE_KEY) as ThemeMode | null
if (stored && ['light', 'dark', 'system'].includes(stored)) {
themeMode.value = stored
} else {
themeMode.value = 'system'
}
// Don't animate on initial load
if (themeMode.value === 'system') {
applyTheme(getSystemPreference(), false)
} else {
applyTheme(themeMode.value === 'dark', false)
}
}
// Watch for system preference changes
onMounted(() => {
initTheme()
// Listen for system preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => {
if (themeMode.value === 'system') {
applyTheme(e.matches, true)
}
}
mediaQuery.addEventListener('change', handleChange)
})
return {
themeMode,
isDark,
toggleTheme,
setThemeMode,
initTheme,
}
}