From 4af62864cd778c96a06f1f8cf2964c4894accc68 Mon Sep 17 00:00:00 2001 From: mtyszczak <mateusz.tyszczak@gmail.com> Date: Mon, 17 Mar 2025 17:58:23 +0100 Subject: [PATCH] Add theme toggle --- src/App.vue | 9 -- .../wallets/metamask/MetamaskConnect.vue | 2 +- src/components/sidebar/AppSidebar.vue | 4 +- .../ui/theme-switch/ThemeSwitch.vue | 151 ++++++++++++++++++ src/components/ui/theme-switch/index.ts | 1 + 5 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 src/components/ui/theme-switch/ThemeSwitch.vue create mode 100644 src/components/ui/theme-switch/index.ts diff --git a/src/App.vue b/src/App.vue index bdaa168..ba83458 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,15 +16,6 @@ const settingsStore = useSettingsStore(); const walletStore = useWalletStore(); const userStore = useUserStore(); onMounted(async() => { - if (window.matchMedia) { - const media = window.matchMedia('(prefers-color-scheme: dark)'); - if (media.matches) - document.documentElement.classList.add('dark'); - media.addEventListener('change', event => { - document.documentElement.classList[event.matches ? 'add' : 'remove']('dark'); - }); - } - settingsStore.loadSettings(); hasUser.value = settingsStore.settings.account !== undefined; if (hasUser.value) { diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue index 2f5c780..ac77878 100644 --- a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue +++ b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue @@ -260,7 +260,7 @@ const updateAccountName = (value: string | any) => { </div> </div> <div v-else-if="showCreateAccountModal"> - <p class="mb-4">Step 6: Fill in this form in order to create account create operation with requested metadata. Copy the signing link and send it to someone who already has an account:</p> + <p class="mb-4">Step 6: Fill in this form in order to prepare the operation to create an account with requested metadata. Then please copy the signing link, and send it to someone who already has an account to execute this operation in blockchain:</p> <div class="grid mb-2 w-full max-w-sm items-center gap-1.5"> <Label for="metamask_createAuth_account">New account name</Label> <Input v-model="createAccountNameOperation!" @update:model-value="validateAccountName()" id="metamask_createAuth_account" /> diff --git a/src/components/sidebar/AppSidebar.vue b/src/components/sidebar/AppSidebar.vue index 4e69153..762ec45 100644 --- a/src/components/sidebar/AppSidebar.vue +++ b/src/components/sidebar/AppSidebar.vue @@ -7,6 +7,7 @@ import { useSettingsStore, getWalletIcon } from '@/stores/settings.store'; import { computed } from 'vue'; import { Button } from '@/components/ui/button'; import { useUserStore } from "@/stores/user.store"; +import ThemeSwitch from "../ui/theme-switch"; const settingsStore = useSettingsStore(); const hasUser = computed(() => settingsStore.settings.account !== undefined); @@ -57,7 +58,8 @@ const items = [ <AvatarImage v-if="userStore.profileImage" :src="userStore.profileImage" /> <AvatarFallback>{{ settingsStore.settings.account?.slice(0, 2) }}</AvatarFallback> </Avatar> - <span class="font-bold">@{{ settingsStore.settings.account }}</span> + <span class="font-bold max-w-[140px] truncate">@{{ settingsStore.settings.account }}</span> + <ThemeSwitch class="ml-auto w-5 h-5 mr-1" /> </div> </SidebarHeader> <SidebarContent> diff --git a/src/components/ui/theme-switch/ThemeSwitch.vue b/src/components/ui/theme-switch/ThemeSwitch.vue new file mode 100644 index 0000000..fda4d60 --- /dev/null +++ b/src/components/ui/theme-switch/ThemeSwitch.vue @@ -0,0 +1,151 @@ +<script setup lang="ts"> +// https://web.dev/patterns/theming/theme-switch + +import { onMounted, ref } from 'vue'; + +type Preference = 'light' | 'dark'; + +const storageKey = 'theme-preference'; + +const themePreference = ref<Preference | undefined>(); + +const switchPreference = () => { + setPreference(themePreference.value = themePreference.value === 'light' ? 'dark' : 'light'); +}; + +const reflectPreference = () => { + if (themePreference.value === 'dark') + document.documentElement.classList.add('dark'); + else + document.documentElement.classList.remove('dark'); +}; + +const setPreference = (value: Preference) => { + localStorage.setItem(storageKey, value); + + reflectPreference(); +}; + +onMounted(() => { + const media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : undefined; + + const getColorPreference = (): Preference => { + if (localStorage.getItem(storageKey)) + return localStorage.getItem(storageKey) as Preference; + else + return media?.matches ? 'dark' : 'light'; + }; + + themePreference.value = getColorPreference(); + + reflectPreference(); + + media?.addEventListener('change', ({ matches: isDark }) => { + setPreference(isDark ? 'dark' : 'light') + }); +}); + +const props = defineProps<{ + class?: string; +}>(); +</script> + +<template> + <button :class="['theme-toggle', props.class]" id="theme-toggle" title="Toggles light & dark" @click="switchPreference" aria-live="polite"> + <svg :class="['sun-and-moon', props.class]" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24"> + <mask class="moon" id="moon-mask"> + <rect x="0" y="0" width="100%" height="100%" fill="white" /> + <circle cx="24" cy="10" r="6" fill="black" /> + </mask> + <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" /> + <g class="sun-beams" stroke="currentColor"> + <line x1="12" y1="1" x2="12" y2="3" /> + <line x1="12" y1="21" x2="12" y2="23" /> + <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /> + <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" /> + <line x1="1" y1="12" x2="3" y2="12" /> + <line x1="21" y1="12" x2="23" y2="12" /> + <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /> + <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" /> + </g> + </svg> + </button> +</template> + +<style scoped> +.sun-and-moon > :is(.moon, .sun, .sun-beams) { + transform-origin: center; +} + +.sun-and-moon > :is(.moon, .sun) { + fill: hsl(var(--foreground)); +} + +.theme-toggle:is(:hover, :focus-visible) > .sun-and-moon > :is(.moon, .sun) { + fill: hsla(var(--foreground) / 80%); +} + +.sun-and-moon > .sun-beams { + stroke: hsl(var(--foreground)); + stroke-width: 2px; +} + +.theme-toggle:is(:hover, :focus-visible) .sun-and-moon > .sun-beams { + stroke: hsla(var(--foreground) / 80%); +} + +.dark .sun-and-moon > .sun { + transform: scale(1.75); +} + +.dark .sun-and-moon > .sun-beams { + opacity: 0; +} + +.dark .sun-and-moon > .moon > circle { + transform: translateX(-7px); +} + +@supports (cx: 1) { + .dark .sun-and-moon > .moon > circle { + cx: 17; + transform: translateX(0); + } +} + +@media (prefers-reduced-motion: no-preference) { + .sun-and-moon > .sun { + transition: transform .5s cubic-bezier(.5,1.25,.75,1.25); + } + + .sun-and-moon > .sun-beams { + transition: transform .5s cubic-bezier(.5,1.5,.75,1.25), opacity .5s cubic-bezier(.25,0,.3,1); + } + + .sun-and-moon .moon > circle { + transition: transform .25s cubic-bezier(0,0,0,1); + } + + @supports (cx: 1) { + .sun-and-moon .moon > circle { + transition: cx .25s cubic-bezier(0,0,0,1); + } + } + + .dark .sun-and-moon > .sun { + transition-timing-function: cubic-bezier(.25,0,.3,1); + transition-duration: .25s; + transform: scale(1.75); + } + + .dark .sun-and-moon > .sun-beams { + transition-duration: .15s; + transform: rotateZ(-25deg); + } + + .dark .sun-and-moon > .moon > circle { + transition-duration: .5s; + transition-delay: .25s; + } +} +</style> diff --git a/src/components/ui/theme-switch/index.ts b/src/components/ui/theme-switch/index.ts new file mode 100644 index 0000000..e639c75 --- /dev/null +++ b/src/components/ui/theme-switch/index.ts @@ -0,0 +1 @@ +export { default } from "./ThemeSwitch.vue"; \ No newline at end of file -- GitLab