Improved theme picker
This commit is contained in:
@@ -1,14 +1,7 @@
|
||||
<template>
|
||||
<section class="view-root" data-view="profile">
|
||||
<div class="theme-toggle">
|
||||
<button class="theme-btn" @click="themeMenuOpen = !themeMenuOpen" :title="themeTitle">
|
||||
{{ themeEmoji }}
|
||||
</button>
|
||||
<div v-if="themeMenuOpen" class="theme-menu" @click="themeMenuOpen = false">
|
||||
<button class="theme-option top" :class="{ active: selectedTheme === '' }" @click.stop="setTheme('')" title="Auto">🌓</button>
|
||||
<button class="theme-option left" :class="{ active: selectedTheme === 'light' }" @click.stop="setTheme('light')" title="Light">☀️</button>
|
||||
<button class="theme-option right" :class="{ active: selectedTheme === 'dark' }" @click.stop="setTheme('dark')" title="Dark">🌙</button>
|
||||
</div>
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
<header class="view-header">
|
||||
<h1>User Profile</h1>
|
||||
@@ -127,6 +120,7 @@
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||
import CredentialList from '@/components/CredentialList.vue'
|
||||
import ThemeSelector from '@/components/ThemeSelector.vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import NameEditForm from '@/components/NameEditForm.vue'
|
||||
@@ -139,7 +133,6 @@ import passkey from '@/utils/passkey'
|
||||
import { goBack } from '@/utils/helpers'
|
||||
import { apiJson } from 'paskia'
|
||||
import { navigateButtonRow, focusPreferred, focusAtIndex, getDirection } from '@/utils/keynav'
|
||||
import { updateThemeFromSession } from '@/utils/theme'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const updateInterval = ref(null)
|
||||
@@ -159,22 +152,6 @@ const breadcrumbs = ref(null)
|
||||
const userBasicInfo = ref(null)
|
||||
const userInfoSection = ref(null)
|
||||
|
||||
// Theme preference
|
||||
const selectedTheme = ref('')
|
||||
const themeMenuOpen = ref(false)
|
||||
const themeEmoji = computed(() => ({ '': '🌓', light: '☀️', dark: '🌙' })[selectedTheme.value] || '🌓')
|
||||
const themeTitle = computed(() => ({ '': 'Auto (system)', light: 'Light mode', dark: 'Dark mode' })[selectedTheme.value] || 'Theme')
|
||||
watch(() => authStore.userInfo?.ctx?.user?.theme, (t) => { selectedTheme.value = t || '' }, { immediate: true })
|
||||
function setTheme(theme) {
|
||||
selectedTheme.value = theme
|
||||
themeMenuOpen.value = false
|
||||
// Apply immediately for instant feedback
|
||||
updateThemeFromSession({ user: { theme } }, true)
|
||||
// Save to server in background
|
||||
apiJson('/auth/api/user/theme', { method: 'PATCH', body: { theme } })
|
||||
.catch(e => authStore.showMessage(e.message, 'error'))
|
||||
}
|
||||
|
||||
// Check if any modal/dialog is open (blocks arrow key navigation)
|
||||
const hasActiveModal = computed(() => showNameDialog.value || showRegLink.value)
|
||||
|
||||
@@ -381,14 +358,4 @@ const saveName = async () => {
|
||||
.remote-auth-label { display: block; margin: 0; font-size: 0.875rem; color: var(--color-text-muted); font-weight: 500; }
|
||||
.remote-auth-description { font-size: 0.75rem; color: var(--color-text-muted); }
|
||||
.theme-toggle { position: absolute; top: var(--layout-padding); right: var(--layout-padding); }
|
||||
.theme-btn { background: none; border: none; padding: 0.25rem; font-size: 1.25rem; cursor: pointer; opacity: 0.5; transition: opacity 0.15s; }
|
||||
.theme-btn:hover { opacity: 0.8; }
|
||||
.theme-menu { position: absolute; top: 100%; right: 0; width: 5rem; height: 4rem; margin-top: 0.25rem; }
|
||||
.theme-option { position: absolute; background: none; border: none; font-size: 1.25rem; cursor: pointer; opacity: 0.5; padding: 0.25rem; border-radius: var(--radius-sm); transition: opacity 0.15s, transform 0.15s; }
|
||||
.theme-option:hover { opacity: 1; transform: scale(1.2); }
|
||||
.theme-option.active { opacity: 1; }
|
||||
.theme-option.top { top: 0; left: 50%; transform: translateX(-50%); }
|
||||
.theme-option.top:hover { transform: translateX(-50%) scale(1.2); }
|
||||
.theme-option.left { bottom: 0; left: 0; }
|
||||
.theme-option.right { bottom: 0; right: 0; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="theme-selector" @click.stop>
|
||||
<button v-for="t in themes" :key="t.value" class="theme-icon" :class="{ hidden: isHidden(t.value) }"
|
||||
:style="{ top: getPos(t.value).y + 'px', left: getPos(t.value).x + 'px' }" :title="t.title"
|
||||
@click="handleClick(t.value)">{{ t.icon }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { apiJson } from 'paskia'
|
||||
import { updateThemeFromSession, getCachedTheme } from '@/utils/theme'
|
||||
|
||||
const open = ref(false), closing = ref(false), closingValue = ref(null), selected = ref(getCachedTheme())
|
||||
const themes = [{ value: '', icon: '🌓', title: 'Auto' }, { value: 'light', icon: '☀️', title: 'Light' }, { value: 'dark', icon: '🌙', title: 'Dark' }]
|
||||
const center = { x: 16, y: 16 }
|
||||
const expanded = { '': { x: 16, y: 0 }, light: { x: 0, y: 28 }, dark: { x: 32, y: 28 } }
|
||||
|
||||
const getPos = v => closing.value ? (v === closingValue.value ? center : expanded[v]) : open.value ? expanded[v] : (v === selected.value ? center : expanded[v])
|
||||
const isHidden = v => closing.value ? v !== closingValue.value : !open.value && v !== selected.value
|
||||
|
||||
function close(v) {
|
||||
closingValue.value = v
|
||||
closing.value = true
|
||||
setTimeout(() => { open.value = closing.value = false; closingValue.value = null }, 200)
|
||||
}
|
||||
|
||||
function handleClick(v) {
|
||||
if (!open.value) { open.value = true; return }
|
||||
close(v)
|
||||
setTimeout(() => {
|
||||
selected.value = v
|
||||
updateThemeFromSession({ user: { theme: v } }, true)
|
||||
apiJson('/auth/api/user/theme', { method: 'PATCH', body: { theme: v } }).catch(() => {})
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function onOutside(e) { if (open.value && !closing.value && !e.target.closest('.theme-selector')) close(selected.value) }
|
||||
onMounted(() => document.addEventListener('click', onOutside))
|
||||
onUnmounted(() => document.removeEventListener('click', onOutside))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.theme-selector { position: relative; width: 2rem; height: 2rem; }
|
||||
.theme-icon { position: absolute; transform: translate(-50%, -50%); background: none; border: none; font-size: 1.25rem; cursor: pointer; padding: 0.25rem; transition: top 0.2s, left 0.2s, opacity 0.15s; }
|
||||
.theme-icon:hover, .theme-icon:focus-visible { transform: translate(-50%, -50%) scale(1.15); }
|
||||
.theme-icon.hidden { opacity: 0; pointer-events: none; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user