Move account menu to a floating bottom-left card with light/dark/system theme control

This commit is contained in:
Kris Parish
2026-07-03 03:09:06 -05:00
parent 4f122a00df
commit 49bb4cf4f4
4 changed files with 338 additions and 83 deletions
+30 -2
View File
@@ -1,5 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import UserMenu from './UserMenu.vue';
const props = defineProps({
menu: {
@@ -30,8 +31,18 @@ const props = defineProps({
type: Array,
default: () => [],
},
serverData: {
type: Object,
required: true,
},
theme: {
type: String,
default: 'system',
},
});
const emit = defineEmits(['set-theme']);
// Custom logo from Settings > General. Dark mode prefers the dark variant
// and falls back to the standard logo; no logo set = "Aurora" wordmark.
const activeLogo = computed(() =>
@@ -93,6 +104,7 @@ const isAuroraActive = (item) => normalize(item.url) === current.value;
<template>
<nav class="sidebar" :class="{ 'sidebar--collapsed': collapsed }">
<div class="sidebar__scroll">
<div class="sidebar__brand">
<img v-if="activeLogo" :src="activeLogo" alt="" class="sidebar__brand-img" />
<template v-else>{{ collapsed ? 'A' : 'Aurora' }}</template>
@@ -192,6 +204,14 @@ const isAuroraActive = (item) => normalize(item.url) === current.value;
</li>
</ul>
</div>
</div>
<UserMenu
:server-data="serverData"
:collapsed="collapsed"
:theme="theme"
@set-theme="(t) => emit('set-theme', t)"
/>
</nav>
</template>
@@ -203,16 +223,24 @@ const isAuroraActive = (item) => normalize(item.url) === current.value;
width: var(--aurora-sidebar-w);
height: 100vh;
background: var(--aurora-app-bg);
overflow-y: auto;
transition: width 0.2s ease;
z-index: 99999;
padding: 12px 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
}
/* Scrollable nav content — separate from the pinned UserMenu footer below
it, so the account card stays visible regardless of scroll position. */
.sidebar__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px 0;
box-sizing: border-box;
}
.sidebar__brand {
font-size: 1.25rem;
font-weight: 700;
+1 -72
View File
@@ -7,15 +7,10 @@ const props = defineProps({
type: Object,
required: true,
},
isDark: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['toggle-dark', 'toggle-menu']);
const emit = defineEmits(['toggle-menu']);
const menuOpen = ref(false);
const searchOpen = ref(false);
const sitesOpen = ref(false);
@@ -120,40 +115,6 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
</a>
</div>
</div>
<button
type="button"
class="toolbar__icon-btn toolbar__theme-toggle"
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
@click="emit('toggle-dark')"
>
{{ isDark ? '☀' : '☾' }}
</button>
<div class="toolbar__user">
<button
type="button"
class="toolbar__user-btn"
@click="menuOpen = !menuOpen"
>
<img
v-if="serverData.avatar"
:src="serverData.avatar"
alt=""
class="toolbar__avatar"
/>
<span class="toolbar__user-name">{{ serverData.userName }}</span>
</button>
<div v-if="menuOpen" class="toolbar__dropdown">
<div class="toolbar__dropdown-header">
<div class="toolbar__dropdown-name">{{ serverData.userName }}</div>
<div class="toolbar__dropdown-email">{{ serverData.userEmail }}</div>
</div>
<a :href="serverData.logoutUrl" class="toolbar__dropdown-item">
Log out
</a>
</div>
</div>
</div>
<SearchModal
@@ -256,12 +217,6 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
color: var(--aurora-text);
}
.toolbar__theme-toggle {
font-size: 1.05rem;
line-height: 1;
}
.toolbar__user,
.toolbar__sites {
position: relative;
}
@@ -272,32 +227,6 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
margin-left: 8px;
}
.toolbar__user-btn {
display: flex;
align-items: center;
gap: 8px;
border: none;
background: none;
cursor: pointer;
padding: 4px 6px;
border-radius: 8px;
color: var(--aurora-text);
}
.toolbar__user-btn:hover {
background: var(--aurora-bg-subtle);
}
.toolbar__avatar {
width: 28px;
height: 28px;
border-radius: 50%;
}
.toolbar__user-name {
font-size: 0.875rem;
}
.toolbar__dropdown {
position: absolute;
top: calc(100% + 6px);
+266
View File
@@ -0,0 +1,266 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
const props = defineProps({
serverData: { type: Object, required: true },
collapsed: { type: Boolean, default: false },
theme: { type: String, default: 'system' }, // 'light' | 'dark' | 'system'
});
const emit = defineEmits(['set-theme']);
const open = ref(false);
const root = ref(null);
const profileUrl = computed(() => `${props.serverData.adminUrl}profile.php`);
const settingsUrl = computed(() => `${props.serverData.adminUrl}options-general.php`);
const onDocClick = (e) => {
if (root.value && !root.value.contains(e.target)) {
open.value = false;
}
};
onMounted(() => document.addEventListener('click', onDocClick));
onUnmounted(() => document.removeEventListener('click', onDocClick));
</script>
<template>
<div ref="root" class="user-menu">
<div v-if="open" class="user-menu__card">
<div class="user-menu__header">
<img v-if="serverData.avatar" :src="serverData.avatar" alt="" class="user-menu__avatar" />
<div class="user-menu__id">
<div class="user-menu__name">{{ serverData.userName }}</div>
<div class="user-menu__email">{{ serverData.userEmail }}</div>
</div>
</div>
<div class="user-menu__items">
<a :href="profileUrl" class="user-menu__item">
<span class="dashicons dashicons-admin-users" />
Edit Profile
</a>
<a :href="serverData.siteUrl" target="_blank" rel="noopener noreferrer" class="user-menu__item">
<span class="dashicons dashicons-admin-home" />
Visit site
</a>
<a :href="settingsUrl" class="user-menu__item">
<span class="dashicons dashicons-admin-generic" />
Site Settings
</a>
<a :href="serverData.logoutUrl" class="user-menu__item user-menu__item--danger">
<span class="dashicons dashicons-migrate" />
Sign Out
</a>
</div>
<div class="user-menu__theme">
<span class="user-menu__theme-label">Theme</span>
<div class="user-menu__segmented">
<button
type="button"
:class="{ 'is-active': theme === 'light' }"
title="Light"
@click="emit('set-theme', 'light')"
>
<span class="dashicons dashicons-sun" />
</button>
<button
type="button"
:class="{ 'is-active': theme === 'dark' }"
title="Dark"
@click="emit('set-theme', 'dark')"
>
<span class="dashicons dashicons-moon" />
</button>
<button
type="button"
:class="{ 'is-active': theme === 'system' }"
title="System"
@click="emit('set-theme', 'system')"
>
<span class="dashicons dashicons-admin-generic" />
</button>
</div>
</div>
</div>
<button
type="button"
class="user-menu__trigger"
:class="{ 'user-menu__trigger--collapsed': collapsed }"
@click="open = !open"
>
<img v-if="serverData.avatar" :src="serverData.avatar" alt="" class="user-menu__trigger-avatar" />
<span v-if="!collapsed" class="user-menu__trigger-name">{{ serverData.userName }}</span>
<span v-if="!collapsed" class="dashicons dashicons-arrow-up-alt2 user-menu__trigger-chevron" />
</button>
</div>
</template>
<style scoped>
.user-menu {
position: relative;
flex-shrink: 0;
padding: 8px;
border-top: 1px solid var(--aurora-border);
}
.user-menu__trigger {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
border: none;
background: none;
cursor: pointer;
padding: 6px 8px;
border-radius: 8px;
color: var(--aurora-text);
}
.user-menu__trigger:hover {
background: var(--aurora-bg-subtle);
}
.user-menu__trigger--collapsed {
justify-content: center;
padding: 6px 0;
}
.user-menu__trigger-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
}
.user-menu__trigger-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.8125rem;
text-align: left;
}
.user-menu__trigger-chevron {
font-size: 14px;
width: 14px;
height: 14px;
color: var(--aurora-text-muted);
}
.user-menu__card {
position: absolute;
bottom: calc(100% + 8px);
left: 8px;
width: 240px;
background: var(--aurora-bg);
border: 1px solid var(--aurora-border);
border-radius: 12px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
overflow: hidden;
z-index: 100000;
}
.user-menu__header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px;
border-bottom: 1px solid var(--aurora-border);
}
.user-menu__avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
}
.user-menu__id { min-width: 0; }
.user-menu__name {
font-size: 0.875rem;
font-weight: 600;
color: var(--aurora-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu__email {
font-size: 0.75rem;
color: var(--aurora-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu__items {
padding: 6px;
border-bottom: 1px solid var(--aurora-border);
}
.user-menu__item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
text-decoration: none;
color: var(--aurora-text);
font-size: 0.8125rem;
}
.user-menu__item:hover {
background: var(--aurora-bg-subtle);
}
.user-menu__item .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
color: var(--aurora-text-muted);
}
.user-menu__item--danger {
color: #ef4444;
}
.user-menu__item--danger .dashicons {
color: #ef4444;
}
.user-menu__theme {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
}
.user-menu__theme-label {
font-size: 0.8125rem;
color: var(--aurora-text-muted);
}
.user-menu__segmented {
display: flex;
gap: 2px;
background: var(--aurora-bg-subtle);
border-radius: 8px;
padding: 2px;
}
.user-menu__segmented button {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
background: none;
border-radius: 6px;
cursor: pointer;
color: var(--aurora-text-muted);
}
.user-menu__segmented button .dashicons {
font-size: 14px;
width: 14px;
height: 14px;
}
.user-menu__segmented button.is-active {
background: var(--aurora-bg);
color: var(--aurora-accent);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
</style>