From 1274dece676ce18be600751213c3a80c0793f59c Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 1 Oct 2026 09:55:02 +0300 Subject: [PATCH] theme --- src/composables/useThemeMode.ts | 20 ++++++++++++++++++++ src/layouts/DefaultLayout.vue | 26 +++++++++++++------------- src/lib/theme.ts | 21 +++++++++++++++++++++ src/main.ts | 3 ++- 4 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 src/composables/useThemeMode.ts create mode 100644 src/lib/theme.ts diff --git a/src/composables/useThemeMode.ts b/src/composables/useThemeMode.ts new file mode 100644 index 0000000..77c2540 --- /dev/null +++ b/src/composables/useThemeMode.ts @@ -0,0 +1,20 @@ +import { watch } from 'vue' +import { useTheme } from 'vuetify' +import { useStorage } from '@vueuse/core' +import { THEME_MODES, THEME_STORAGE_KEY, type ThemeMode } from '@/lib/theme.ts' + +/** Persisted light/dark/system theme mode, cycled in that order on each call to `cycleThemeMode`. */ +export function useThemeMode() { + const theme = useTheme() + const mode = useStorage(THEME_STORAGE_KEY, 'system') + + watch(mode, (value) => theme.change(value), { immediate: true }) + + function cycleThemeMode(e?: MouseEvent) { + if (e) theme.setTransitionOrigin(e.target as HTMLElement) + const nextIndex = (THEME_MODES.indexOf(mode.value) + 1) % THEME_MODES.length + mode.value = THEME_MODES[nextIndex] + } + + return { mode, cycleThemeMode } +} diff --git a/src/layouts/DefaultLayout.vue b/src/layouts/DefaultLayout.vue index 66407a6..60672a3 100644 --- a/src/layouts/DefaultLayout.vue +++ b/src/layouts/DefaultLayout.vue @@ -2,17 +2,17 @@ import { useRouter } from 'vue-router' import { authClient } from '@/lib/auth-client.ts' import { computed, ref } from 'vue' -import { useTheme } from 'vuetify' import ProjectMenu from '@/components/ProjectMenu.vue' import AppBreadcrumbs from '@/components/AppBreadcrumbs.vue' import BackButton from '@/components/BackButton.vue' +import { useThemeMode } from '@/composables/useThemeMode.ts' +import { THEME_ICONS } from '@/lib/theme.ts' const router = useRouter() const session = authClient.useSession() -const theme = useTheme() +const { mode: themeMode, cycleThemeMode } = useThemeMode() -const title = 'APP' -const version = '0.0.1' +const title = 'Ulitka' const isAuth = computed(() => !!session.value.data) @@ -56,12 +56,6 @@ async function goToSignOut() { }) } -function toggleTheme(e: MouseEvent) { - const target = e.target as HTMLElement - theme.setTransitionOrigin(target) - theme.toggle() -} - const goToSettings = () => { router.push({ name: 'settings' }) } @@ -90,8 +84,9 @@ const goToSettings = () => { - - {{ title }} + + +
{{ title }}
@@ -113,7 +108,12 @@ const goToSettings = () => { - +
diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..8d3f448 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,21 @@ +export type ThemeMode = 'light' | 'dark' | 'system' + +export const THEME_MODES: ThemeMode[] = ['light', 'dark', 'system'] +export const THEME_STORAGE_KEY = 'theme-mode' + +export const THEME_ICONS: Record = { + light: 'mdi-white-balance-sunny', + dark: 'mdi-weather-night', + system: 'mdi-theme-light-dark', +} + +export function isThemeMode(value: unknown): value is ThemeMode { + return THEME_MODES.includes(value as ThemeMode) +} + +/** Read the persisted theme mode synchronously, before Vuetify is created, to avoid a flash of the wrong theme. */ +export function readStoredThemeMode(): ThemeMode { + if (typeof localStorage === 'undefined') return 'system' + const stored = localStorage.getItem(THEME_STORAGE_KEY) + return isThemeMode(stored) ? stored : 'system' +} diff --git a/src/main.ts b/src/main.ts index 26ac1bc..ba9bc2d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,7 @@ import * as components from 'vuetify/components' import { VAvatarGroup } from 'vuetify/labs/VAvatarGroup' import * as directives from 'vuetify/directives' import { registerLayouts } from '@/layouts/register.ts' +import { readStoredThemeMode } from '@/lib/theme.ts' const app = createApp(App) @@ -24,7 +25,7 @@ const vuetify = createVuetify({ components: { ...components, VAvatarGroup }, directives, theme: { - defaultTheme: 'dark', // 'system' | 'light' | 'dark' + defaultTheme: readStoredThemeMode(), themes: { dark: { colors: {