- 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
123 lines
3.2 KiB
TypeScript
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,
|
|
}
|
|
}
|