Initial commit: Aurora Admin plugin

This commit is contained in:
Kris Parish
2026-07-03 02:53:29 -05:00
commit 03cabe2fe4
73 changed files with 9591 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
<script setup>
import { ref, onMounted } from 'vue';
import Sidebar from './components/Sidebar.vue';
import Toolbar from './components/Toolbar.vue';
const props = defineProps({
serverData: {
type: Object,
required: true,
},
});
const isDark = ref(props.serverData?.settings?.dark_mode === 'on');
const applyTheme = () => {
document.documentElement.classList.toggle('aurora-dark', isDark.value);
};
const persistDark = async () => {
try {
await fetch(`${props.serverData.restUrl}wp/v2/settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': props.serverData.restNonce,
},
// Partial payload: the option's sanitize callback merges this into
// the stored settings, so we never write back the (secret-stripped)
// settings copy this page received.
body: JSON.stringify({
aurora_admin_settings: {
dark_mode: isDark.value ? 'on' : 'off',
},
}),
});
} catch {
// Non-critical: theme still applied for this session.
}
};
const toggleDark = () => {
isDark.value = !isDark.value;
applyTheme();
persistDark();
};
// Minified (icon-only) menu — a per-browser preference kept in
// localStorage; the html class drives all layout via --aurora-sidebar-w.
const collapsed = ref(localStorage.getItem('aurora-menu-collapsed') === '1');
const applyCollapsed = () => {
document.documentElement.classList.toggle('aurora-collapsed', collapsed.value);
};
const toggleMenu = () => {
collapsed.value = !collapsed.value;
applyCollapsed();
localStorage.setItem('aurora-menu-collapsed', collapsed.value ? '1' : '0');
};
onMounted(() => {
applyTheme();
applyCollapsed();
// Signal the shell is live: reveals the framed content and commits to
// keeping native chrome hidden (the PHP failsafe stops watching once
// aurora-ready is present).
document.documentElement.classList.add('aurora-ready');
});
</script>
<template>
<div class="site-shell">
<Sidebar
:menu="serverData.menu"
:aurora-nav="serverData.auroraNav"
:current-url="serverData.currentUrl"
:logo="serverData.settings?.logo || ''"
:dark-logo="serverData.settings?.dark_logo || ''"
:is-dark="isDark"
:collapsed="collapsed"
/>
<Toolbar
:server-data="serverData"
:is-dark="isDark"
@toggle-dark="toggleDark"
@toggle-menu="toggleMenu"
/>
</div>
</template>
<style scoped>
/* Sidebar and Toolbar are both position:fixed, so this wrapper has no
layout role — it just groups them under one mounted root. */
.site-shell {
display: contents;
}
</style>
+219
View File
@@ -0,0 +1,219 @@
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
const props = defineProps({
serverData: { type: Object, required: true },
open: { type: Boolean, default: false },
});
const emit = defineEmits(['close']);
const input = ref(null);
const query = ref('');
const results = ref([]);
const loading = ref(false);
const highlighted = ref(0);
let debounceTimer;
let requestSeq = 0;
const search = async () => {
const q = query.value.trim();
if (q.length < 2) {
results.value = [];
return;
}
const seq = ++requestSeq;
loading.value = true;
try {
const url = new URL(`${props.serverData.restUrl}wp/v2/search`);
url.searchParams.set('search', q);
url.searchParams.set('per_page', '10');
// Settings > General > "Search post types" limits which post types the
// global search covers; empty means all searchable types.
const types = props.serverData.settings?.search_post_types;
if (Array.isArray(types) && types.length) {
url.searchParams.set('subtype', types.join(','));
}
const res = await fetch(url, {
headers: { 'X-WP-Nonce': props.serverData.restNonce },
});
if (!res.ok) throw new Error();
const items = await res.json();
if (seq === requestSeq) {
results.value = items;
highlighted.value = 0;
}
} catch {
if (seq === requestSeq) results.value = [];
} finally {
if (seq === requestSeq) loading.value = false;
}
};
watch(query, () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(search, 250);
});
watch(
() => props.open,
(open) => {
if (open) {
query.value = '';
results.value = [];
nextTick(() => input.value?.focus());
}
}
);
const openResult = (item) => {
if (!item) return;
window.location.href = `${props.serverData.adminUrl}post.php?post=${item.id}&action=edit`;
};
const onKeydown = (e) => {
if (!props.open) return;
if (e.key === 'Escape') {
emit('close');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
highlighted.value = Math.min(highlighted.value + 1, results.value.length - 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
highlighted.value = Math.max(highlighted.value - 1, 0);
} else if (e.key === 'Enter') {
openResult(results.value[highlighted.value]);
}
};
onMounted(() => document.addEventListener('keydown', onKeydown));
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
</script>
<template>
<Teleport to="body">
<div v-if="open" class="smodal__overlay" @click.self="emit('close')">
<div class="smodal">
<div class="smodal__inputwrap">
<span class="dashicons dashicons-search smodal__icon" />
<input
ref="input"
v-model="query"
type="text"
class="smodal__input"
placeholder="Search posts, pages…"
/>
<span class="smodal__esc">Esc</span>
</div>
<div v-if="loading" class="smodal__state">Searching…</div>
<div v-else-if="query.trim().length >= 2 && !results.length" class="smodal__state">
No results for “{{ query.trim() }}”
</div>
<ul v-if="results.length" class="smodal__results">
<li v-for="(item, i) in results" :key="item.id">
<button
type="button"
class="smodal__result"
:class="{ 'smodal__result--active': i === highlighted }"
@mouseenter="highlighted = i"
@click="openResult(item)"
>
<span class="smodal__result-title" v-html="item.title" />
<span class="smodal__result-type">{{ item.subtype }}</span>
</button>
</li>
</ul>
</div>
</div>
</Teleport>
</template>
<style scoped>
.smodal__overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 100000;
display: flex;
justify-content: center;
padding-top: 12vh;
}
.smodal {
width: min(620px, calc(100vw - 40px));
align-self: flex-start;
background: var(--aurora-frame-bg, #1c1c20);
border: 1px solid var(--aurora-frame-border, #34343b);
border-radius: 14px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.smodal__inputwrap {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--aurora-border, #27272a);
}
.smodal__icon { color: var(--aurora-text-muted); }
.smodal__input {
flex: 1;
border: none;
background: none;
outline: none;
color: var(--aurora-text);
font-size: 0.95rem;
}
.smodal__esc {
font-size: 0.6875rem;
color: var(--aurora-text-muted);
border: 1px solid var(--aurora-border, #27272a);
border-radius: 5px;
padding: 2px 6px;
}
.smodal__state {
padding: 18px 16px;
font-size: 0.875rem;
color: var(--aurora-text-muted);
}
.smodal__results {
list-style: none;
margin: 0;
padding: 6px;
max-height: 50vh;
overflow-y: auto;
}
.smodal__result {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: none;
background: none;
cursor: pointer;
text-align: left;
padding: 10px 12px;
border-radius: 9px;
}
.smodal__result--active { background: var(--aurora-accent-soft); }
.smodal__result-title {
color: var(--aurora-text);
font-size: 0.875rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.smodal__result-type {
flex-shrink: 0;
font-size: 0.6875rem;
color: var(--aurora-text-muted);
border: 1px solid var(--aurora-border, #27272a);
border-radius: 999px;
padding: 2px 8px;
text-transform: capitalize;
}
</style>
+414
View File
@@ -0,0 +1,414 @@
<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__list sidebar__aurora-list">
<li v-for="item in auroraNav" :key="item.id" class="sidebar__item">
<div class="sidebar__row" :class="{ 'sidebar__row--active': isAuroraActive(item) }">
<a :href="item.url" class="sidebar__link" :title="collapsed ? item.title : null">
<span class="sidebar__icon">
<span class="dashicons" :class="item.icon?.value" />
</span>
<span class="sidebar__title">{{ item.title }}</span>
</a>
</div>
</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>
+341
View File
@@ -0,0 +1,341 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import SearchModal from './SearchModal.vue';
const props = defineProps({
serverData: {
type: Object,
required: true,
},
isDark: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['toggle-dark', 'toggle-menu']);
const menuOpen = ref(false);
const searchOpen = ref(false);
const sitesOpen = ref(false);
// Settings > General > "Disable global search" hides the search entirely.
const searchDisabled = computed(
() => !!props.serverData.settings?.disable_global_search
);
// Remote sites (secrets already stripped server-side). The switcher only
// shows for users holding the configured capability (canSwitchSites).
const remoteSites = computed(() => {
const sites = props.serverData.settings?.remote_sites;
return Array.isArray(sites) ? sites.filter((s) => s && s.url) : [];
});
const showSwitcher = computed(
() => !!props.serverData.canSwitchSites && remoteSites.value.length > 0
);
const siteAdminUrl = (site) => {
let url = (site.url || '').trim();
if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
return url.replace(/\/+$/, '') + '/wp-admin/';
};
const siteHost = (site) => {
try {
return new URL(siteAdminUrl(site)).host;
} catch {
return site.url;
}
};
const onKeydown = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
if (searchDisabled.value) return;
e.preventDefault();
searchOpen.value = true;
}
};
onMounted(() => document.addEventListener('keydown', onKeydown));
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
</script>
<template>
<header class="toolbar">
<div class="toolbar__left">
<button
type="button"
class="toolbar__icon-btn"
title="Toggle menu"
@click="emit('toggle-menu')"
>
<span class="dashicons dashicons-menu" />
</button>
<a
:href="serverData.siteUrl || '/'"
class="toolbar__home"
title="View website"
target="_blank"
rel="noopener noreferrer"
>
<span class="dashicons dashicons-admin-home" />
</a>
<a :href="serverData.adminUrl" class="toolbar__home" title="Dashboard">
<span class="dashicons dashicons-dashboard" />
</a>
<button
v-if="!searchDisabled"
type="button"
class="toolbar__search"
@click="searchOpen = true"
>
<span class="dashicons dashicons-search toolbar__search-icon" />
<span class="toolbar__search-text">Search…</span>
<span class="toolbar__search-kbd">Ctrl K</span>
</button>
</div>
<div class="toolbar__right">
<div v-if="showSwitcher" class="toolbar__sites">
<button
type="button"
class="toolbar__icon-btn"
title="Switch site"
@click="sitesOpen = !sitesOpen"
>
<span class="dashicons dashicons-admin-multisite" />
</button>
<div v-if="sitesOpen" class="toolbar__dropdown">
<div class="toolbar__dropdown-header">
<div class="toolbar__dropdown-name">Remote sites</div>
</div>
<a
v-for="(site, i) in remoteSites"
:key="i"
:href="siteAdminUrl(site)"
target="_blank"
rel="noopener noreferrer"
class="toolbar__dropdown-item"
>
{{ siteHost(site) }}
<span v-if="site.username" class="toolbar__site-user">{{ site.username }}</span>
</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
:server-data="serverData"
:open="searchOpen"
@close="searchOpen = false"
/>
</header>
</template>
<style scoped>
.toolbar {
position: fixed;
top: 0;
left: var(--aurora-sidebar-w);
right: 0;
height: var(--aurora-toolbar-h);
background: var(--aurora-app-bg);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 99998;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.toolbar__left,
.toolbar__right {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar__home {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 8px;
color: var(--aurora-text-muted);
text-decoration: none;
}
.toolbar__home:hover {
background: var(--aurora-bg-subtle);
color: var(--aurora-text);
}
.toolbar__search {
display: flex;
align-items: center;
gap: 8px;
height: 34px;
padding: 0 10px;
min-width: 240px;
border: 1px solid var(--aurora-border);
border-radius: 10px;
background: var(--aurora-bg-subtle);
color: var(--aurora-text-muted);
font-size: 0.8125rem;
cursor: text;
}
.toolbar__search-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
.toolbar__search-text {
flex: 1;
text-align: left;
}
.toolbar__search-kbd {
font-size: 0.6875rem;
border: 1px solid var(--aurora-border);
border-radius: 5px;
padding: 1px 6px;
background: var(--aurora-bg);
}
.toolbar__icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
color: var(--aurora-text-muted);
}
.toolbar__icon-btn:hover {
background: var(--aurora-bg-subtle);
color: var(--aurora-text);
}
.toolbar__theme-toggle {
font-size: 1.05rem;
line-height: 1;
}
.toolbar__user,
.toolbar__sites {
position: relative;
}
.toolbar__site-user {
color: var(--aurora-text-muted);
font-size: 0.75rem;
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);
right: 0;
min-width: 200px;
background: var(--aurora-bg);
border: 1px solid var(--aurora-border);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
z-index: 100000;
}
.toolbar__dropdown-header {
padding: 12px 14px;
border-bottom: 1px solid var(--aurora-border);
}
.toolbar__dropdown-name {
font-size: 0.875rem;
font-weight: 600;
color: var(--aurora-text);
}
.toolbar__dropdown-email {
font-size: 0.75rem;
color: var(--aurora-text-muted);
}
.toolbar__dropdown-item {
display: block;
padding: 10px 14px;
text-decoration: none;
color: var(--aurora-text);
font-size: 0.875rem;
}
.toolbar__dropdown-item:hover {
background: var(--aurora-bg-subtle);
}
</style>
+109
View File
@@ -0,0 +1,109 @@
/*
* The ONLY global stylesheet in the plugin. Every rule here either defines
* an --aurora-* theme variable or targets a specific WordPress structural ID
* to frame the native content. No element/tag resets, no utility classes —
* so it cannot leak onto or restyle WordPress's own page content the way the
* old plugin's Tailwind preflight did.
*
* Theme colors live here as plain CSS variables we fully control. Dark mode
* is a single class on <html> (.aurora-dark) that swaps the variable values.
*/
/* Palette carried over from the original plugin's design tokens: a zinc
neutral scale with a blue/indigo accent, large rounded surfaces. */
:root {
--aurora-sidebar-w: 264px;
--aurora-toolbar-h: 56px;
/* Breathing room between page content and the right browser edge. */
--aurora-gutter-r: 44px;
--aurora-radius: 14px;
--aurora-radius-lg: 24px;
/* Light theme */
--aurora-app-bg: #fafafa; /* zinc-50 */
--aurora-bg: #ffffff; /* frame */
--aurora-card-bg: #fafafa; /* zinc-50 */
--aurora-bg-subtle: #f4f4f5; /* zinc-100 */
--aurora-text: #18181b; /* zinc-900 */
--aurora-text-muted: #71717a; /* zinc-500 */
--aurora-border: #e4e4e7; /* zinc-200 */
--aurora-card-border: #ececef;
--aurora-frame-bg: #ffffff; /* native-page content panel */
--aurora-frame-border: #e0e0e3;
--aurora-accent: rgb(71 114 250);
--aurora-accent-soft: rgb(71 114 250 / 0.1);
--aurora-accent-text: #ffffff;
}
/* Minified menu: the toolbar's collapse button toggles this class; every
layout offset derives from --aurora-sidebar-w, so this one override
reflows the toolbar and content automatically. */
html.aurora-collapsed {
--aurora-sidebar-w: 68px;
}
html.aurora-dark {
/* Dark theme — the immersive near-black look of the original */
--aurora-app-bg: #09090b; /* zinc-950 */
--aurora-bg: #18181b; /* zinc-900 frame */
--aurora-card-bg: #1f1f23; /* slightly elevated */
--aurora-bg-subtle: #27272a; /* zinc-800 */
--aurora-text: #f4f4f5; /* zinc-100 */
--aurora-text-muted: #a1a1aa; /* zinc-400 */
--aurora-border: #27272a; /* zinc-800 */
--aurora-card-border: #2e2e33;
--aurora-frame-bg: #1c1c20; /* slightly elevated panel */
--aurora-frame-border: #34343b;
--aurora-accent: rgb(125 158 255);
--aurora-accent-soft: rgb(125 158 255 / 0.14);
}
/* --- Framing: only applies once the Vue shell has signalled ready --- */
html.aurora-ready,
html.aurora-ready body {
background: var(--aurora-app-bg);
}
/* !important here defeats WordPress's own responsive #wpcontent margin
rules (.auto-fold / folded menu states live in media queries at equal
specificity). This is positioning the frame, not restyling content.
The toolbar clearance lives here (on #wpcontent, which holds everything)
rather than on #wpbody, so content rendered above #wpbody — like the
takeover dashboard — also clears the fixed toolbar. */
html.aurora-ready #wpcontent {
margin-left: var(--aurora-sidebar-w) !important;
padding-left: 0 !important;
padding-top: var(--aurora-toolbar-h);
transition: margin-left 0.2s ease;
}
html.aurora-ready #wpbody-content {
background: var(--aurora-frame-bg);
color: var(--aurora-text);
border: 1px solid var(--aurora-frame-border);
border-radius: var(--aurora-radius-lg);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
margin: 16px var(--aurora-gutter-r) 24px 24px;
padding: 32px;
min-height: calc(100vh - var(--aurora-toolbar-h) - 40px);
box-sizing: border-box;
}
/* The settings page renders its own two boxed panels, so drop the outer
content frame there — leaving just those panels on the dark backdrop. */
html.aurora-ready body.toplevel_page_aurora-admin #wpbody-content {
background: var(--aurora-app-bg);
border: none;
border-radius: 0;
box-shadow: none;
}
/*
* Native WordPress pages are fully dark-themed by a dedicated stylesheet
* (admin/assets/native-dark.css), enqueued only on native pages and gated
* on dark mode. It restyles WP's postboxes, forms, tables, buttons, etc.
* Keeping that large surface in one isolated, native-only file means it
* can never leak onto Aurora's own pages or affect light mode.
*/