Add theme presets, gradient backgrounds, and macOS-style window chrome
Replaces the old light/dark toggle with 10 fixed color themes (plus Black & Grey and White) that drive a coordinated gradient palette across the whole admin UI, a Google Fonts picker, and a native-page dark-mode stylesheet that now covers modals and plugin/theme card grids. Fixes several background/gradient seams where the sidebar, toolbar, and content frame were each painting their own independent copy of the page gradient instead of sharing one continuous plane. Also adds a macOS-style traffic-light title bar to every page (close, minimize, maximize) and a persistent bottom taskbar so a minimized page can be restored from anywhere, plus a Plugins management page. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
db07e21f9e
commit
4a547d2c50
+93
-71
@@ -1,7 +1,10 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Sidebar from './components/Sidebar.vue';
|
||||
import Toolbar from './components/Toolbar.vue';
|
||||
import { applyThemePalette, applyFontFamily } from '@/pages/settings/applyLiveTheme.js';
|
||||
import { initWindowChrome } from './windowChrome.js';
|
||||
import { getMinimizedWindow, clearMinimizedWindow } from './taskbar.js';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: {
|
||||
@@ -10,61 +13,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
// Three-state theme: 'light' | 'dark' | 'system'. Older stored values were
|
||||
// just 'on'/'off' — map those forward so existing installs keep working.
|
||||
const normalizeStoredTheme = (value) => {
|
||||
if (value === 'on') return 'dark';
|
||||
if (value === 'off') return 'light';
|
||||
if (value === 'light' || value === 'dark' || value === 'system') return value;
|
||||
return 'system';
|
||||
};
|
||||
|
||||
const theme = ref(normalizeStoredTheme(props.serverData?.settings?.dark_mode));
|
||||
|
||||
const systemPrefersDark = ref(
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
let mediaQuery = null;
|
||||
const onSystemChange = (e) => (systemPrefersDark.value = e.matches);
|
||||
|
||||
const isDark = computed(() =>
|
||||
theme.value === 'dark' || (theme.value === 'system' && systemPrefersDark.value)
|
||||
);
|
||||
|
||||
const applyTheme = () => {
|
||||
document.documentElement.classList.toggle('aurora-dark', isDark.value);
|
||||
};
|
||||
|
||||
const persistTheme = 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: theme.value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Non-critical: theme still applied for this session.
|
||||
}
|
||||
};
|
||||
|
||||
const setTheme = (next) => {
|
||||
theme.value = next;
|
||||
applyTheme();
|
||||
persistTheme();
|
||||
};
|
||||
|
||||
watch(isDark, applyTheme);
|
||||
|
||||
// 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');
|
||||
@@ -77,24 +25,52 @@ const toggleMenu = () => {
|
||||
localStorage.setItem('aurora-menu-collapsed', collapsed.value ? '1' : '0');
|
||||
};
|
||||
|
||||
// The block editor (Gutenberg) renders its own toolbar (undo/redo,
|
||||
// title, save/publish) directly under Aurora's fixed toolbar — stacking
|
||||
// both left a sliver of page gradient visible as a seam between them.
|
||||
// Skip Aurora's toolbar there and let the editor's own bar sit flush
|
||||
// against the top; shell-frame.css reads the aurora-no-toolbar class to
|
||||
// also close up the top inset it reserves for the (now-absent) toolbar.
|
||||
const isBlockEditor = document.body.classList.contains('block-editor-page');
|
||||
|
||||
// The one minimized window, if any (taskbar.js persists it in
|
||||
// sessionStorage across the page load minimizing triggers). Rendered as a
|
||||
// taskbar item at the bottom of every page since SiteShell mounts on all
|
||||
// of them — clicking it navigates back and clears the entry.
|
||||
const minimizedWindow = ref(null);
|
||||
const restoreWindow = () => {
|
||||
if (!minimizedWindow.value) return;
|
||||
const url = minimizedWindow.value.url;
|
||||
clearMinimizedWindow();
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
applyTheme();
|
||||
minimizedWindow.value = getMinimizedWindow();
|
||||
// Reserve bottom space for the taskbar (shell-frame.css) so page content
|
||||
// stops above it instead of running underneath.
|
||||
document.documentElement.classList.toggle('aurora-has-taskbar', !!minimizedWindow.value);
|
||||
const settings = props.serverData?.settings || {};
|
||||
// Every theme preset is its own self-contained dark-toned palette now —
|
||||
// no separate light/dark mode, so this class is always on. It's kept
|
||||
// (rather than renamed) because shell-frame.css and native-dark.css both
|
||||
// still gate their rules on it.
|
||||
document.documentElement.classList.add('aurora-dark');
|
||||
document.documentElement.classList.toggle('aurora-no-toolbar', isBlockEditor);
|
||||
applyThemePalette(settings.theme_preset || 'indigo');
|
||||
applyCollapsed();
|
||||
if (window.matchMedia) {
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', onSystemChange);
|
||||
applyFontFamily(settings.font_family);
|
||||
// Skipped on the block editor — #wpbody-content is bypassed by the
|
||||
// editor's own fixed skeleton there (see isBlockEditor above), so a
|
||||
// bar injected into it would just be invisible clutter.
|
||||
if (!isBlockEditor) {
|
||||
initWindowChrome(props.serverData?.adminUrl);
|
||||
}
|
||||
// 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');
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener('change', onSystemChange);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -105,23 +81,69 @@ onUnmounted(() => {
|
||||
:current-url="serverData.currentUrl"
|
||||
:logo="serverData.settings?.logo || ''"
|
||||
:dark-logo="serverData.settings?.dark_logo || ''"
|
||||
:is-dark="isDark"
|
||||
:collapsed="collapsed"
|
||||
:server-data="serverData"
|
||||
:theme="theme"
|
||||
@set-theme="setTheme"
|
||||
/>
|
||||
<Toolbar
|
||||
v-if="!isBlockEditor"
|
||||
:server-data="serverData"
|
||||
@toggle-menu="toggleMenu"
|
||||
/>
|
||||
<div v-if="minimizedWindow" class="aurora-taskbar">
|
||||
<button type="button" class="aurora-taskbar__item" @click="restoreWindow">
|
||||
<span class="aurora-taskbar__dot" />
|
||||
{{ minimizedWindow.title }}
|
||||
</button>
|
||||
</div>
|
||||
</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. */
|
||||
/* Sidebar, Toolbar and the taskbar are all position:fixed, so this
|
||||
wrapper has no layout role — it just groups them under one mounted
|
||||
root. */
|
||||
.site-shell {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Full-width docked bar (real taskbar, edge to edge) — page content
|
||||
reserves space above it via html.aurora-has-taskbar in shell-frame.css
|
||||
so nothing renders underneath. The item itself stays pill-shaped. */
|
||||
.aurora-taskbar {
|
||||
position: fixed;
|
||||
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: var(--aurora-taskbar-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
background: var(--aurora-frame-bg);
|
||||
border-top: 1px solid var(--aurora-frame-border);
|
||||
box-sizing: border-box;
|
||||
z-index: 200001;
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
.aurora-taskbar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 999px;
|
||||
padding: 8px 18px 8px 10px;
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.aurora-taskbar__item:hover {
|
||||
border-color: var(--aurora-accent);
|
||||
}
|
||||
.aurora-taskbar__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #febc2e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,10 +19,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isDark: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -35,19 +31,13 @@ const props = defineProps({
|
||||
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(() =>
|
||||
props.isDark ? props.darkLogo || props.logo : props.logo
|
||||
);
|
||||
// Custom logo from Settings > General. Every theme preset is a dark-toned
|
||||
// palette now (no separate light mode), so the dark-variant logo is always
|
||||
// preferred when set; falls back to the standard logo, or the "Aurora"
|
||||
// wordmark if neither is set.
|
||||
const activeLogo = computed(() => props.darkLogo || 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.
|
||||
@@ -209,8 +199,6 @@ const isAuroraActive = (item) => normalize(item.url) === current.value;
|
||||
<UserMenu
|
||||
:server-data="serverData"
|
||||
:collapsed="collapsed"
|
||||
:theme="theme"
|
||||
@set-theme="(t) => emit('set-theme', t)"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -218,11 +206,18 @@ const isAuroraActive = (item) => normalize(item.url) === current.value;
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
top: var(--aurora-sidebar-gap);
|
||||
left: var(--aurora-sidebar-gap);
|
||||
bottom: var(--aurora-sidebar-gap);
|
||||
width: var(--aurora-sidebar-w);
|
||||
height: 100vh;
|
||||
background: var(--aurora-app-bg);
|
||||
height: auto;
|
||||
/* Same surface treatment as the dashboard cards (AuroraCard.vue) —
|
||||
solid card background + rounded corners, floating over the page
|
||||
gradient with a gap on every side now that it's inset instead of
|
||||
flush against the viewport edges. */
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 20px;
|
||||
transition: width 0.2s ease;
|
||||
z-index: 99999;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -129,10 +129,11 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: var(--aurora-sidebar-w);
|
||||
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2);
|
||||
right: 0;
|
||||
height: var(--aurora-toolbar-h);
|
||||
background: var(--aurora-app-bg);
|
||||
/* No background — see Sidebar.vue; the fixed html/body gradient shows
|
||||
through so the whole shell reads as one continuous background. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -4,11 +4,8 @@ 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);
|
||||
|
||||
@@ -53,36 +50,6 @@ onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
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="user-menu__swatch user-menu__swatch--light" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ 'is-active': theme === 'dark' }"
|
||||
title="Dark"
|
||||
@click="emit('set-theme', 'dark')"
|
||||
>
|
||||
<span class="user-menu__swatch user-menu__swatch--dark" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ 'is-active': theme === 'system' }"
|
||||
title="Match system"
|
||||
@click="emit('set-theme', 'system')"
|
||||
>
|
||||
<span class="user-menu__swatch user-menu__swatch--system" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -224,59 +191,4 @@ onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
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: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 3px;
|
||||
}
|
||||
.user-menu__segmented button.is-active {
|
||||
background: var(--aurora-bg);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.user-menu__swatch {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(127, 127, 127, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.user-menu__swatch--light {
|
||||
background: #ffffff;
|
||||
}
|
||||
.user-menu__swatch--dark {
|
||||
background: #18181b;
|
||||
}
|
||||
.user-menu__swatch--system {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #ffffff 50%, #18181b 50%, #18181b 100%);
|
||||
}
|
||||
.user-menu__segmented button.is-active .user-menu__swatch {
|
||||
border-color: var(--aurora-accent);
|
||||
box-shadow: 0 0 0 1px var(--aurora-accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
:root {
|
||||
--aurora-sidebar-w: 264px;
|
||||
--aurora-toolbar-h: 56px;
|
||||
/* SiteShell.vue's taskbar, shown only while a window is minimized
|
||||
(html.aurora-has-taskbar toggles the bottom space reserved for it). */
|
||||
--aurora-taskbar-h: 56px;
|
||||
/* Space between the sidebar card and the browser's left/top/bottom
|
||||
edges (Sidebar.vue) — the toolbar and #wpcontent offsets below add
|
||||
the same amount so nothing overlaps the floating sidebar. */
|
||||
--aurora-sidebar-gap: 16px;
|
||||
/* Breathing room between page content and the right browser edge. */
|
||||
--aurora-gutter-r: 44px;
|
||||
|
||||
@@ -22,8 +29,15 @@
|
||||
|
||||
/* Light theme */
|
||||
--aurora-app-bg: #fafafa; /* zinc-50 */
|
||||
/* Flat fallback for the handful of consumers that need a solid color
|
||||
(--aurora-app-bg itself may be a gradient once a theme preset applies). */
|
||||
--aurora-app-bg-solid: #fafafa;
|
||||
--aurora-bg: #ffffff; /* frame */
|
||||
--aurora-card-bg: #fafafa; /* zinc-50 */
|
||||
/* Dashboard cards specifically (AuroraCard.vue) — a step further from
|
||||
--aurora-frame-bg than --aurora-card-bg so they read as distinct
|
||||
surfaces against the Dashboard's own frame, not a near-identical tone. */
|
||||
--aurora-card-bg-deep: #f0f0f2;
|
||||
--aurora-bg-subtle: #f4f4f5; /* zinc-100 */
|
||||
--aurora-text: #18181b; /* zinc-900 */
|
||||
--aurora-text-muted: #71717a; /* zinc-500 */
|
||||
@@ -34,6 +48,10 @@
|
||||
--aurora-accent: rgb(71 114 250);
|
||||
--aurora-accent-soft: rgb(71 114 250 / 0.1);
|
||||
--aurora-accent-text: #ffffff;
|
||||
|
||||
/* Overridden by the font-picker setting (see SiteShell.vue::applyFontFamily);
|
||||
this is the fallback when no Google Font has been chosen. */
|
||||
--aurora-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Minified menu: the toolbar's collapse button toggles this class; every
|
||||
@@ -46,8 +64,10 @@ html.aurora-collapsed {
|
||||
html.aurora-dark {
|
||||
/* Dark theme — the immersive near-black look of the original */
|
||||
--aurora-app-bg: #09090b; /* zinc-950 */
|
||||
--aurora-app-bg-solid: #09090b;
|
||||
--aurora-bg: #18181b; /* zinc-900 frame */
|
||||
--aurora-card-bg: #1f1f23; /* slightly elevated */
|
||||
--aurora-card-bg-deep: #131316;
|
||||
--aurora-bg-subtle: #27272a; /* zinc-800 */
|
||||
--aurora-text: #f4f4f5; /* zinc-100 */
|
||||
--aurora-text-muted: #a1a1aa; /* zinc-400 */
|
||||
@@ -64,6 +84,14 @@ html.aurora-dark {
|
||||
html.aurora-ready,
|
||||
html.aurora-ready body {
|
||||
background: var(--aurora-app-bg);
|
||||
/* Fixed so the gradient stays pinned to the viewport instead of
|
||||
scrolling with document height — the sidebar/toolbar are also
|
||||
position:fixed and paint no background of their own (see
|
||||
Sidebar.vue/Toolbar.vue), so this one gradient plane has to line up
|
||||
with them at every scroll position to read as a single unified
|
||||
background instead of a seam between "sidebar" and "page". */
|
||||
background-attachment: fixed;
|
||||
font-family: var(--aurora-font-family);
|
||||
}
|
||||
|
||||
/* !important here defeats WordPress's own responsive #wpcontent margin
|
||||
@@ -73,12 +101,31 @@ html.aurora-ready body {
|
||||
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;
|
||||
margin-left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2) !important;
|
||||
padding-left: 0 !important;
|
||||
padding-top: var(--aurora-toolbar-h);
|
||||
transition: margin-left 0.2s ease;
|
||||
}
|
||||
|
||||
/* The block editor (Gutenberg) doesn't live inside #wpcontent's normal
|
||||
box model — it renders its own skeleton as position:fixed, inset:0,
|
||||
completely independent of the margin-left above. Left unchecked it
|
||||
spans the full viewport and renders underneath the (higher z-index)
|
||||
sidebar and toolbar. Inset it the same amount #wpcontent gets. */
|
||||
html.aurora-ready .interface-interface-skeleton {
|
||||
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2) !important;
|
||||
top: var(--aurora-toolbar-h) !important;
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
|
||||
/* Aurora's own toolbar is skipped on the block editor (SiteShell.vue) —
|
||||
its own toolbar sits flush against the top instead, so no toolbar
|
||||
height needs to be reserved here; just the same top gap the sidebar
|
||||
gets, so the two line up. */
|
||||
html.aurora-ready.aurora-no-toolbar .interface-interface-skeleton {
|
||||
top: var(--aurora-sidebar-gap) !important;
|
||||
}
|
||||
|
||||
html.aurora-ready #wpbody-content {
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
@@ -89,12 +136,111 @@ html.aurora-ready #wpbody-content {
|
||||
padding: 32px;
|
||||
min-height: calc(100vh - var(--aurora-toolbar-h) - 40px);
|
||||
box-sizing: border-box;
|
||||
/* WP core sets #wpbody-content { float: left; width: 100% }, which (with
|
||||
box-sizing left alone) ignores the margin above and overflows past the
|
||||
right edge by margin-left + margin-right. Force it back to shrink-to-fit
|
||||
so the margin is actually respected. */
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* SiteShell.vue's taskbar docks along the bottom edge only while a window
|
||||
is minimized — push content up by its height so pages stop above it
|
||||
instead of rendering underneath. */
|
||||
html.aurora-has-taskbar #wpbody-content {
|
||||
margin-bottom: calc(24px + var(--aurora-taskbar-h));
|
||||
}
|
||||
html.aurora-has-taskbar .aurora-dashboard-frame {
|
||||
margin-bottom: calc(24px + var(--aurora-taskbar-h));
|
||||
}
|
||||
|
||||
/* macOS-style title bar (windowChrome.js). Most pages get it injected as
|
||||
the first child of #wpbody-content — negative margins cancel the 32px
|
||||
padding above so it spans edge-to-edge like a real window title bar. */
|
||||
.aurora-winbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
#wpbody-content > .aurora-winbar {
|
||||
margin: -32px -32px 20px -32px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--aurora-frame-border);
|
||||
border-top-left-radius: var(--aurora-radius-lg);
|
||||
border-top-right-radius: var(--aurora-radius-lg);
|
||||
}
|
||||
.aurora-winbar__dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.aurora-winbar__dot:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
.aurora-winbar__dot--close {
|
||||
background: #ff5f57;
|
||||
}
|
||||
.aurora-winbar__dot--minimize {
|
||||
background: #febc2e;
|
||||
}
|
||||
.aurora-winbar__dot--maximize {
|
||||
background: #28c840;
|
||||
}
|
||||
|
||||
/* The Dashboard bypasses #wpbody-content (DashboardTakeover.php renders
|
||||
it into a separate sibling — see windowChrome.js), so it never got the
|
||||
card frame every other page has. windowChrome.js wraps the bar + the
|
||||
dashboard's mount point in this div to give it the same treatment. */
|
||||
.aurora-dashboard-frame {
|
||||
background: var(--aurora-frame-bg);
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.aurora-dashboard-frame > .aurora-winbar {
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--aurora-frame-border);
|
||||
border-top-left-radius: var(--aurora-radius-lg);
|
||||
border-top-right-radius: var(--aurora-radius-lg);
|
||||
}
|
||||
|
||||
/* Yellow dot: on the Dashboard itself there's nowhere further "home" to
|
||||
send it, so minimize just collapses the frame down to the title bar in
|
||||
place — everywhere else, minimize navigates to the dashboard instead
|
||||
(see windowChrome.js) and SiteShell.vue's taskbar offers a way back. */
|
||||
html.aurora-minimized #aurora-admin-dashboard-root {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Green dot: hide Aurora's own chrome (sidebar/toolbar) and let the
|
||||
content reclaim the full browser window, like restoring a real OS
|
||||
window to fullscreen. */
|
||||
html.aurora-maximized .sidebar,
|
||||
html.aurora-maximized .toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
html.aurora-ready.aurora-maximized #wpcontent {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
html.aurora-ready.aurora-maximized .interface-interface-skeleton {
|
||||
left: 0 !important;
|
||||
}
|
||||
html.aurora-maximized .aurora-taskbar {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
/* The settings page renders its own two boxed panels, so drop the outer
|
||||
content frame there — leaving just those panels on the dark backdrop. */
|
||||
content frame there — leaving just those panels on the dark backdrop.
|
||||
No background here: #wpbody-content is inset from the sidebar/gutter,
|
||||
not the full viewport, so painting var(--aurora-app-bg) on it computes
|
||||
its own independent (and visibly mismatched) gradient instance instead
|
||||
of sharing the one fixed gradient plane painted on html/body. */
|
||||
html.aurora-ready body.toplevel_page_aurora-admin #wpbody-content {
|
||||
background: var(--aurora-app-bg);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Tracks the one "minimized" window across page loads. Aurora is a normal
|
||||
* multi-page WP admin (each nav is a full page load), so a real taskbar
|
||||
* needs to survive navigation — sessionStorage (tab-scoped, clears when the
|
||||
* tab closes) is enough for that. Only one item is tracked at a time: this
|
||||
* is "minimize sends you home with a way back", not a full window manager.
|
||||
*/
|
||||
const STORAGE_KEY = 'aurora-minimized-window';
|
||||
|
||||
export function getMinimizedWindow() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setMinimizedWindow(title, url) {
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ title, url }));
|
||||
} catch {
|
||||
/* sessionStorage unavailable (e.g. private mode) — minimize just won't persist */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearMinimizedWindow() {
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort readable label for the current page, used as the taskbar item's title. */
|
||||
export function currentPageTitle() {
|
||||
const raw = document.title || 'Window';
|
||||
return raw.split('‹')[0].trim() || 'Window';
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Adds a macOS-style traffic-light title bar to the top of the page content.
|
||||
* Most pages render inside #wpbody-content (native WP pages and all of
|
||||
* Aurora's own Vue pages, since each mounts its root div there). The
|
||||
* Dashboard is the one exception — DashboardTakeover.php renders it into a
|
||||
* separate #aurora-admin-dashboard-root sibling, above #wpbody (which it
|
||||
* hides), to fully replace the native dashboard, so it gets wrapped in its
|
||||
* own frame to match the card treatment #wpbody-content already has. The
|
||||
* block editor bypasses both (see SiteShell.vue isBlockEditor), so it's
|
||||
* skipped there.
|
||||
*
|
||||
* Red closes (navigates back to the Aurora dashboard). Yellow minimizes —
|
||||
* on any page other than the dashboard, that means "save where I am and
|
||||
* send me home", with SiteShell.vue's taskbar offering a way back; on the
|
||||
* dashboard itself (already home) it just collapses in place instead,
|
||||
* since there's nowhere further back to send it. Green hides Aurora's own
|
||||
* sidebar/toolbar so the content fills the browser window. All of this is
|
||||
* just classes on <html> plus one sessionStorage entry (taskbar.js) — no
|
||||
* server-side state.
|
||||
*/
|
||||
import { setMinimizedWindow, currentPageTitle } from './taskbar.js';
|
||||
|
||||
export function initWindowChrome(adminUrl) {
|
||||
if (document.querySelector('.aurora-winbar')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'aurora-winbar';
|
||||
|
||||
const makeDot = (variant, label, onClick) => {
|
||||
const dot = document.createElement('button');
|
||||
dot.type = 'button';
|
||||
dot.className = 'aurora-winbar__dot aurora-winbar__dot--' + variant;
|
||||
dot.setAttribute('aria-label', label);
|
||||
dot.title = label;
|
||||
dot.addEventListener('click', onClick);
|
||||
return dot;
|
||||
};
|
||||
|
||||
const root = document.documentElement;
|
||||
const dashboardRoot = document.getElementById('aurora-admin-dashboard-root');
|
||||
const isDashboard = !!dashboardRoot;
|
||||
|
||||
bar.appendChild(
|
||||
makeDot('close', 'Close', () => {
|
||||
window.location.href = adminUrl || '/wp-admin/';
|
||||
})
|
||||
);
|
||||
bar.appendChild(
|
||||
makeDot('minimize', 'Minimize', () => {
|
||||
if (isDashboard) {
|
||||
root.classList.toggle('aurora-minimized');
|
||||
root.classList.remove('aurora-maximized');
|
||||
} else {
|
||||
setMinimizedWindow(currentPageTitle(), window.location.href);
|
||||
window.location.href = adminUrl || '/wp-admin/';
|
||||
}
|
||||
})
|
||||
);
|
||||
bar.appendChild(
|
||||
makeDot('maximize', 'Maximize', () => {
|
||||
root.classList.toggle('aurora-maximized');
|
||||
root.classList.remove('aurora-minimized');
|
||||
})
|
||||
);
|
||||
|
||||
if (dashboardRoot) {
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'aurora-dashboard-frame';
|
||||
dashboardRoot.parentNode.insertBefore(frame, dashboardRoot);
|
||||
frame.appendChild(bar);
|
||||
frame.appendChild(dashboardRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
const wpbodyContent = document.getElementById('wpbody-content');
|
||||
if (wpbodyContent) {
|
||||
wpbodyContent.insertBefore(bar, wpbodyContent.firstChild);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user