414 lines
10 KiB
Vue
414 lines
10 KiB
Vue
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
|
||
const props = defineProps({
|
||
menu: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
currentUrl: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
logo: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
darkLogo: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
isDark: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
collapsed: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
auroraNav: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
});
|
||
|
||
// 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(() =>
|
||
props.isDark ? props.darkLogo || props.logo : props.logo
|
||
);
|
||
|
||
// Normalize any admin URL down to the part after /wp-admin/ so we can
|
||
// compare the current page against menu targets regardless of host/scheme.
|
||
const normalize = (url) => {
|
||
if (!url) return '';
|
||
const idx = url.indexOf('/wp-admin/');
|
||
const path = idx === -1 ? url : url.slice(idx + '/wp-admin/'.length);
|
||
// The bare admin root ("/wp-admin/" with nothing after it) IS the
|
||
// dashboard, same as an explicit index.php — normalize them to one form
|
||
// so the Dashboard section correctly reads as active/expanded there.
|
||
return path === '' || path.startsWith('?') ? 'index.php' + path : path;
|
||
};
|
||
|
||
const current = computed(() => normalize(props.currentUrl));
|
||
|
||
const isItemActive = (item) => {
|
||
if (!item.url) return false;
|
||
return normalize(item.url) === current.value;
|
||
};
|
||
|
||
const isSectionActive = (item) => {
|
||
if (isItemActive(item)) return true;
|
||
return (item.submenu || []).some((sub) => normalize(sub.url) === current.value);
|
||
};
|
||
|
||
// Which sections are manually expanded. Active section starts expanded.
|
||
const expanded = ref(new Set());
|
||
|
||
const isExpanded = (item) => {
|
||
return expanded.value.has(item.id) || isSectionActive(item);
|
||
};
|
||
|
||
const toggle = (item) => {
|
||
const next = new Set(expanded.value);
|
||
if (next.has(item.id)) {
|
||
next.delete(item.id);
|
||
} else {
|
||
next.add(item.id);
|
||
}
|
||
expanded.value = next;
|
||
};
|
||
|
||
// Aurora's own branded nav section (Settings, Menu Creator, Admin Notices,
|
||
// Role Editor, Activity Log, Database Explorer) — separate from the
|
||
// WP-native menu list above it. Expanded by default; per-browser preference
|
||
// kept in localStorage, same pattern as the main collapsed-menu state.
|
||
const auroraExpanded = ref(localStorage.getItem('aurora-nav-expanded') !== '0');
|
||
const toggleAuroraNav = () => {
|
||
auroraExpanded.value = !auroraExpanded.value;
|
||
localStorage.setItem('aurora-nav-expanded', auroraExpanded.value ? '1' : '0');
|
||
};
|
||
const isAuroraActive = (item) => normalize(item.url) === current.value;
|
||
</script>
|
||
|
||
<template>
|
||
<nav class="sidebar" :class="{ 'sidebar--collapsed': collapsed }">
|
||
<div class="sidebar__brand">
|
||
<img v-if="activeLogo" :src="activeLogo" alt="" class="sidebar__brand-img" />
|
||
<template v-else>{{ collapsed ? 'A' : 'Aurora' }}</template>
|
||
</div>
|
||
|
||
<ul class="sidebar__list">
|
||
<template v-for="(item, index) in menu" :key="item.id || index">
|
||
<li v-if="item.separator" class="sidebar__separator" />
|
||
|
||
<li v-else class="sidebar__item">
|
||
<div
|
||
class="sidebar__row"
|
||
:class="{ 'sidebar__row--active': isSectionActive(item) }"
|
||
>
|
||
<a :href="item.url" class="sidebar__link" :title="collapsed ? item.title : null">
|
||
<span class="sidebar__icon">
|
||
<span
|
||
v-if="item.icon && item.icon.type === 'dashicon'"
|
||
class="dashicons"
|
||
:class="item.icon.value"
|
||
/>
|
||
<img
|
||
v-else-if="item.icon && item.icon.type === 'image'"
|
||
:src="item.icon.value"
|
||
alt=""
|
||
class="sidebar__icon-img"
|
||
/>
|
||
<span v-else class="sidebar__icon-dot" />
|
||
</span>
|
||
<span class="sidebar__title">{{ item.title }}</span>
|
||
<span v-if="item.count" class="sidebar__count">{{ item.count }}</span>
|
||
</a>
|
||
<button
|
||
v-if="item.submenu && item.submenu.length"
|
||
type="button"
|
||
class="sidebar__chevron"
|
||
:class="{ 'sidebar__chevron--open': isExpanded(item) }"
|
||
:aria-label="`Toggle ${item.title} submenu`"
|
||
@click="toggle(item)"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
|
||
<ul
|
||
v-if="item.submenu && item.submenu.length && isExpanded(item)"
|
||
class="sidebar__sublist"
|
||
>
|
||
<li
|
||
v-for="(sub, subIndex) in item.submenu"
|
||
:key="sub.id || subIndex"
|
||
class="sidebar__subitem"
|
||
>
|
||
<a
|
||
:href="sub.url"
|
||
class="sidebar__sublink"
|
||
:class="{ 'sidebar__sublink--active': normalize(sub.url) === current }"
|
||
>
|
||
{{ sub.title }}
|
||
<span v-if="sub.count" class="sidebar__count">{{ sub.count }}</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
|
||
<div v-if="auroraNav.length" class="sidebar__separator" />
|
||
|
||
<div v-if="auroraNav.length" class="sidebar__aurora">
|
||
<div class="sidebar__row sidebar__aurora-brand" @click="toggleAuroraNav">
|
||
<a href="#" class="sidebar__link" @click.prevent :title="collapsed ? 'Aurora' : null">
|
||
<span class="sidebar__icon">
|
||
<span class="dashicons dashicons-superhero-alt" />
|
||
</span>
|
||
<span class="sidebar__title">Aurora</span>
|
||
</a>
|
||
<button
|
||
type="button"
|
||
class="sidebar__chevron"
|
||
:class="{ 'sidebar__chevron--open': auroraExpanded }"
|
||
aria-label="Toggle Aurora menu"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
|
||
<ul v-if="auroraExpanded" class="sidebar__sublist">
|
||
<li v-for="item in auroraNav" :key="item.id" class="sidebar__subitem">
|
||
<a
|
||
:href="item.url"
|
||
class="sidebar__sublink"
|
||
:class="{ 'sidebar__sublink--active': isAuroraActive(item) }"
|
||
>
|
||
{{ item.title }}
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</nav>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
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;
|
||
}
|
||
|
||
.sidebar__brand {
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
color: var(--aurora-text);
|
||
padding: 8px 20px 16px;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.sidebar__brand-img {
|
||
display: block;
|
||
max-height: 30px;
|
||
max-width: 190px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.sidebar__list,
|
||
.sidebar__sublist {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.sidebar__separator {
|
||
height: 1px;
|
||
background: var(--aurora-border);
|
||
margin: 8px 16px;
|
||
}
|
||
|
||
.sidebar__row {
|
||
display: flex;
|
||
align-items: center;
|
||
border-radius: 8px;
|
||
margin: 1px 8px;
|
||
}
|
||
|
||
.sidebar__row--active {
|
||
background: var(--aurora-accent-soft);
|
||
}
|
||
|
||
.sidebar__link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex: 1;
|
||
padding: 8px 10px;
|
||
text-decoration: none;
|
||
color: var(--aurora-text);
|
||
font-size: 0.875rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.sidebar__row--active .sidebar__link {
|
||
color: var(--aurora-accent);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sidebar__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
flex-shrink: 0;
|
||
border-radius: 8px;
|
||
background: var(--aurora-bg-subtle);
|
||
color: var(--aurora-text-muted);
|
||
transition: background 0.12s ease, color 0.12s ease;
|
||
}
|
||
|
||
.sidebar__row--active .sidebar__icon {
|
||
background: var(--aurora-accent-soft);
|
||
color: var(--aurora-accent);
|
||
}
|
||
|
||
.sidebar__icon .dashicons {
|
||
font-size: 16px;
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.sidebar__icon-img {
|
||
width: 16px;
|
||
height: 16px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.sidebar__icon-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--aurora-text-muted);
|
||
}
|
||
|
||
.sidebar__title {
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.sidebar__count {
|
||
background: var(--aurora-accent);
|
||
color: var(--aurora-accent-text);
|
||
font-size: 0.6875rem;
|
||
font-weight: 600;
|
||
border-radius: 999px;
|
||
padding: 1px 7px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar__chevron {
|
||
background: none;
|
||
border: none;
|
||
color: var(--aurora-text-muted);
|
||
font-size: 1.1rem;
|
||
cursor: pointer;
|
||
padding: 4px 12px;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
|
||
.sidebar__chevron--open {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.sidebar__sublist {
|
||
padding: 2px 0 6px;
|
||
}
|
||
|
||
.sidebar__sublink {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
padding: 6px 10px 6px 42px;
|
||
text-decoration: none;
|
||
color: var(--aurora-text-muted);
|
||
font-size: 0.8125rem;
|
||
border-radius: 6px;
|
||
margin: 1px 8px;
|
||
}
|
||
|
||
.sidebar__sublink:hover {
|
||
background: var(--aurora-bg-subtle);
|
||
color: var(--aurora-text);
|
||
}
|
||
|
||
.sidebar__sublink--active {
|
||
color: var(--aurora-accent);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* --- Aurora branded nav (right under the native menu's own divider) ---- */
|
||
.sidebar__aurora {
|
||
padding-bottom: 4px;
|
||
}
|
||
.sidebar__aurora-brand {
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
}
|
||
.sidebar__aurora-brand .sidebar__title {
|
||
color: var(--aurora-text-muted);
|
||
font-size: 0.8125rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.sidebar__aurora-brand .sidebar__icon {
|
||
background: var(--aurora-accent-soft);
|
||
color: var(--aurora-accent);
|
||
}
|
||
|
||
/* --- Collapsed (icon-only) rail --------------------------------------- */
|
||
.sidebar--collapsed {
|
||
overflow-x: hidden;
|
||
}
|
||
.sidebar--collapsed .sidebar__brand {
|
||
padding: 8px 0 16px;
|
||
text-align: center;
|
||
font-size: 1.1rem;
|
||
}
|
||
.sidebar--collapsed .sidebar__brand-img {
|
||
margin: 0 auto;
|
||
max-width: 40px;
|
||
}
|
||
.sidebar--collapsed .sidebar__title,
|
||
.sidebar--collapsed .sidebar__count,
|
||
.sidebar--collapsed .sidebar__chevron,
|
||
.sidebar--collapsed .sidebar__sublist {
|
||
display: none;
|
||
}
|
||
.sidebar--collapsed .sidebar__row {
|
||
margin: 1px 10px;
|
||
}
|
||
.sidebar--collapsed .sidebar__link {
|
||
justify-content: center;
|
||
padding: 10px 0;
|
||
gap: 0;
|
||
}
|
||
</style>
|