@@ -309,6 +384,49 @@ const pickImage = () => {
.field__sites-user { color: var(--aurora-text-muted); }
.field__sites-help { margin: 10px 0 0; font-size: 0.75rem; color: var(--aurora-text-muted); }
+/* Theme picker */
+.field__theme { display: flex; flex-wrap: wrap; gap: 10px; width: 100%; }
+.field__theme-card {
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
+ width: 76px; padding: 10px 6px;
+ border: 1px solid var(--aurora-border);
+ border-radius: 10px;
+ background: var(--aurora-bg-subtle);
+ cursor: pointer;
+}
+.field__theme-card--active { border-color: var(--aurora-accent); box-shadow: 0 0 0 1px var(--aurora-accent); }
+.field__theme-swatch {
+ width: 36px; height: 36px; border-radius: 10px;
+ border: 1px solid var(--aurora-card-border);
+ display: flex; align-items: flex-end; justify-content: flex-end;
+ padding: 4px;
+ box-sizing: border-box;
+}
+.field__theme-swatch-accent {
+ width: 10px; height: 10px; border-radius: 50%;
+}
+.field__theme-name { font-size: 0.6875rem; color: var(--aurora-text-muted); }
+
+/* Font picker */
+.field__font { position: relative; width: 100%; max-width: 320px; }
+.field__font-list {
+ position: absolute; top: calc(100% + 4px); left: 0; right: 0;
+ max-height: 260px; overflow-y: auto; z-index: 20;
+ background: var(--aurora-card-bg);
+ border: 1px solid var(--aurora-border);
+ border-radius: 10px;
+ padding: 6px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+}
+.field__font-item {
+ display: block; width: 100%; text-align: left;
+ border: none; background: none; cursor: pointer;
+ padding: 8px 10px; border-radius: 7px;
+ font-size: 0.875rem; color: var(--aurora-text);
+}
+.field__font-item:hover { background: var(--aurora-bg-subtle); }
+.field__font-item--active { color: var(--aurora-accent); }
+
@media (max-width: 782px) {
.field { grid-template-columns: 1fr; gap: 12px; }
}
diff --git a/app/src/pages/settings/googleFonts.js b/app/src/pages/settings/googleFonts.js
new file mode 100644
index 0000000..d1a66d0
--- /dev/null
+++ b/app/src/pages/settings/googleFonts.js
@@ -0,0 +1,79 @@
+/**
+ * Curated list of popular Google Font family names for the font-picker
+ * field. Not the full ~1700-family catalog — a representative set across
+ * sans, serif, display, and monospace so the picker has something useful
+ * to search without shipping/maintaining a giant metadata blob. Each
+ * family is requested from Google's CSS2 API on demand (see
+ * SiteShell.vue::applyFontFamily), never bundled as font files.
+ */
+export const googleFonts = [
+ 'Inter',
+ 'Roboto',
+ 'Open Sans',
+ 'Lato',
+ 'Montserrat',
+ 'Poppins',
+ 'Nunito',
+ 'Nunito Sans',
+ 'Source Sans 3',
+ 'Work Sans',
+ 'Rubik',
+ 'Manrope',
+ 'DM Sans',
+ 'Karla',
+ 'Mulish',
+ 'Figtree',
+ 'Plus Jakarta Sans',
+ 'Outfit',
+ 'Space Grotesk',
+ 'Sora',
+ 'Albert Sans',
+ 'Urbanist',
+ 'Public Sans',
+ 'IBM Plex Sans',
+ 'Barlow',
+ 'Cabin',
+ 'Hanken Grotesk',
+ 'Lexend',
+ 'Jost',
+ 'Red Hat Display',
+ 'Playfair Display',
+ 'Merriweather',
+ 'Lora',
+ 'PT Serif',
+ 'Crimson Text',
+ 'Source Serif 4',
+ 'Libre Baskerville',
+ 'Cormorant Garamond',
+ 'EB Garamond',
+ 'Bitter',
+ 'Domine',
+ 'Fraunces',
+ 'Newsreader',
+ 'Spectral',
+ 'Frank Ruhl Libre',
+ 'Josefin Sans',
+ 'Quicksand',
+ 'Comfortaa',
+ 'Raleway',
+ 'Oswald',
+ 'Bebas Neue',
+ 'Archivo',
+ 'Archivo Black',
+ 'Anton',
+ 'Righteous',
+ 'Fjalla One',
+ 'Abril Fatface',
+ 'Cormorant',
+ 'Zilla Slab',
+ 'IBM Plex Mono',
+ 'JetBrains Mono',
+ 'Space Mono',
+ 'Roboto Mono',
+ 'Fira Code',
+ 'Source Code Pro',
+ 'Courier Prime',
+ 'Instrument Sans',
+ 'Instrument Serif',
+ 'Bricolage Grotesque',
+];
diff --git a/app/src/pages/settings/settingsConfig.js b/app/src/pages/settings/settingsConfig.js
index 314c39a..3e2acb7 100644
--- a/app/src/pages/settings/settingsConfig.js
+++ b/app/src/pages/settings/settingsConfig.js
@@ -94,17 +94,17 @@ export const settingsCategories = [
icon: 'dashicons-admin-appearance',
fields: [
{
- key: 'dark_mode',
- type: 'toggle',
- label: 'Dark mode',
- description: 'Use the dark interface theme across the admin.',
+ key: 'theme_preset',
+ type: 'theme-picker',
+ label: 'Theme',
+ description: 'Pick a color for the whole admin interface.',
+ default: 'indigo',
},
{
- key: 'accent_color',
- type: 'color',
- label: 'Accent color',
- description: 'Primary accent color used throughout the interface.',
- default: '#4772fa',
+ key: 'font_family',
+ type: 'font-picker',
+ label: 'Font family',
+ description: 'Google Font used across the Aurora interface.',
},
],
},
diff --git a/app/src/pages/settings/themePresets.js b/app/src/pages/settings/themePresets.js
new file mode 100644
index 0000000..a67829e
--- /dev/null
+++ b/app/src/pages/settings/themePresets.js
@@ -0,0 +1,103 @@
+/**
+ * Fixed set of theme presets for the theme-picker field. No custom color
+ * picker — exactly these 10 predetermined options. Each preset is a hue;
+ * the main background gradient is mostly two vivid, saturated stops of
+ * that hue (like FleekDash's "Fresh"/"Sunrise" palette bars), only
+ * dipping to a near-black anchor in the last stretch — not a 50/50
+ * dark-to-vivid split, which reads washed out rather than "poppy".
+ * Cards/borders/text derive as solid, more muted shades of the primary
+ * hue so the gradient stays a deliberate hero background rather than
+ * making every surface busy.
+ */
+export const themePresets = [
+ { id: 'indigo', label: 'Indigo', hue: 235 },
+ { id: 'sky', label: 'Sky', hue: 205 },
+ { id: 'teal', label: 'Teal', hue: 175 },
+ { id: 'forest', label: 'Forest', hue: 140 },
+ { id: 'lime', label: 'Lime', hue: 95 },
+ { id: 'amber', label: 'Amber', hue: 45 },
+ { id: 'orange', label: 'Orange', hue: 22 },
+ { id: 'rose', label: 'Rose', hue: 345 },
+ { id: 'berry', label: 'Berry', hue: 320 },
+ { id: 'violet', label: 'Violet', hue: 265 },
+ // Two flat neutrals, no gradient — for anyone who finds the color
+ // themes above too much. Same values the shell originally shipped with
+ // before theme presets existed (zinc dark / zinc light).
+ {
+ id: 'mono',
+ label: 'Black & Grey',
+ palette: {
+ '--aurora-app-bg': '#09090b',
+ '--aurora-app-bg-solid': '#09090b',
+ '--aurora-bg': '#18181b',
+ '--aurora-card-bg': '#1f1f23',
+ '--aurora-card-bg-deep': '#131316',
+ '--aurora-bg-subtle': '#27272a',
+ '--aurora-text': '#f4f4f5',
+ '--aurora-text-muted': '#a1a1aa',
+ '--aurora-border': '#27272a',
+ '--aurora-card-border': '#2e2e33',
+ '--aurora-frame-bg': '#1c1c20',
+ '--aurora-frame-border': '#34343b',
+ '--aurora-accent': 'rgb(125 158 255)',
+ '--aurora-accent-soft': 'rgb(125 158 255 / 0.14)',
+ },
+ },
+ {
+ id: 'white',
+ label: 'White',
+ palette: {
+ '--aurora-app-bg': '#fafafa',
+ '--aurora-app-bg-solid': '#fafafa',
+ '--aurora-bg': '#ffffff',
+ '--aurora-card-bg': '#fafafa',
+ '--aurora-card-bg-deep': '#f0f0f2',
+ '--aurora-bg-subtle': '#f4f4f5',
+ '--aurora-text': '#18181b',
+ '--aurora-text-muted': '#71717a',
+ '--aurora-border': '#e4e4e7',
+ '--aurora-card-border': '#ececef',
+ '--aurora-frame-bg': '#ffffff',
+ '--aurora-frame-border': '#e0e0e3',
+ '--aurora-accent': 'rgb(71 114 250)',
+ '--aurora-accent-soft': 'rgb(71 114 250 / 0.1)',
+ },
+ },
+];
+
+const hsl = (h, s, l, a) =>
+ a === undefined ? `hsl(${h} ${s}% ${l}%)` : `hsl(${h} ${s}% ${l}% / ${a})`;
+
+/** The full set of --aurora-* color tokens for a preset. */
+export function buildPalette(preset) {
+ if (preset.palette) return preset.palette;
+ const { hue } = preset;
+ // Two vivid, bright stops of the same hue family carry most of the bar
+ // (0% → 55%); the near-black anchor only takes over in the final
+ // stretch (100%). That's the "mostly color, dark tail" look the
+ // FleekDash palette bars have — full saturation start to middle keeps
+ // it feeling poppy instead of muddy.
+ const hue2 = (hue + 35) % 360;
+ const solidAppBg = hsl(hue, 45, 16);
+ return {
+ '--aurora-app-bg': `linear-gradient(135deg, ${hsl(hue, 78, 48)} 0%, ${hsl(hue2, 78, 45)} 55%, ${hsl(hue, 32, 9)} 100%)`,
+ '--aurora-app-bg-solid': solidAppBg,
+ '--aurora-bg': hsl(hue, 40, 20),
+ '--aurora-card-bg': hsl(hue, 36, 24),
+ '--aurora-card-bg-deep': hsl(hue, 38, 14),
+ '--aurora-bg-subtle': hsl(hue, 32, 28),
+ '--aurora-text': hsl(hue, 20, 97),
+ '--aurora-text-muted': hsl(hue, 14, 74),
+ '--aurora-border': hsl(hue, 28, 30),
+ '--aurora-card-border': hsl(hue, 26, 33),
+ '--aurora-frame-bg': hsl(hue, 34, 22),
+ '--aurora-frame-border': hsl(hue, 26, 33),
+ '--aurora-accent': hsl(hue, 85, 62),
+ '--aurora-accent-soft': hsl(hue, 85, 62, 0.16),
+ };
+}
+
+/** Resolve a theme_preset id to the preset object (falls back to the first preset). */
+export function resolvePreset(themePresetId) {
+ return themePresets.find((p) => p.id === themePresetId) || themePresets[0];
+}
diff --git a/app/src/shell/SiteShell.vue b/app/src/shell/SiteShell.vue
index e3ce43d..d677118 100644
--- a/app/src/shell/SiteShell.vue
+++ b/app/src/shell/SiteShell.vue
@@ -1,7 +1,10 @@
@@ -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"
/>
+
+
+
diff --git a/app/src/shell/components/Sidebar.vue b/app/src/shell/components/Sidebar.vue
index 0225408..705eab2 100644
--- a/app/src/shell/components/Sidebar.vue
+++ b/app/src/shell/components/Sidebar.vue
@@ -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;
emit('set-theme', t)"
/>
@@ -218,11 +206,18 @@ const isAuroraActive = (item) => normalize(item.url) === current.value;
diff --git a/app/src/shell/shell-frame.css b/app/src/shell/shell-frame.css
index 0cef835..5085df9 100644
--- a/app/src/shell/shell-frame.css
+++ b/app/src/shell/shell-frame.css
@@ -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;
diff --git a/app/src/shell/taskbar.js b/app/src/shell/taskbar.js
new file mode 100644
index 0000000..53a5d3f
--- /dev/null
+++ b/app/src/shell/taskbar.js
@@ -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';
+}
diff --git a/app/src/shell/windowChrome.js b/app/src/shell/windowChrome.js
new file mode 100644
index 0000000..aa8ad4d
--- /dev/null
+++ b/app/src/shell/windowChrome.js
@@ -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 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);
+ }
+}
diff --git a/app/vite.config.js b/app/vite.config.js
index 1c0e737..2aa7774 100644
--- a/app/vite.config.js
+++ b/app/vite.config.js
@@ -28,6 +28,7 @@ export default defineConfig({
'role-editor': fileURLToPath(new URL('./src/entries/role-editor.js', import.meta.url)),
'activity-log': fileURLToPath(new URL('./src/entries/activity-log.js', import.meta.url)),
'database-explorer': fileURLToPath(new URL('./src/entries/database-explorer.js', import.meta.url)),
+ plugins: fileURLToPath(new URL('./src/entries/plugins.js', import.meta.url)),
},
output: {
entryFileNames: '[name].[hash].js',
diff --git a/aurora-admin.php b/aurora-admin.php
index 60bc9de..0024539 100644
--- a/aurora-admin.php
+++ b/aurora-admin.php
@@ -28,6 +28,7 @@ require AURORA_ADMIN_PATH . "admin/src/Pages/SettingsPage.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/DashboardData.php";
require AURORA_ADMIN_PATH . "admin/src/Analytics/Analytics.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/AnalyticsData.php";
+require AURORA_ADMIN_PATH . "admin/src/Rest/PluginsData.php";
require AURORA_ADMIN_PATH . "admin/src/Posts/ScheduledDayFilter.php";
require AURORA_ADMIN_PATH . "admin/src/WhiteLabel/WhiteLabel.php";
require AURORA_ADMIN_PATH . "admin/src/Pages/SubApps.php";