From 4a547d2c5092557a0cdd6f4a6881b69ef1cb15f4 Mon Sep 17 00:00:00 2001 From: nixpanel Date: Fri, 3 Jul 2026 11:48:47 -0500 Subject: [PATCH] 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 --- .gitignore | 1 + admin/assets/native-dark.css | 105 ++- admin/src/App/AuroraAdmin.php | 2 + admin/src/Options/Settings.php | 7 +- admin/src/Pages/SubApps.php | 8 +- admin/src/Rest/PluginsData.php | 248 +++++++ admin/src/Shell/MenuSerializer.php | 4 + app/src/entries/plugins.js | 10 + app/src/pages/_shared/AppListShell.vue | 16 +- .../pages/dashboard/components/AuroraCard.vue | 6 +- app/src/pages/plugins/Plugins.vue | 690 ++++++++++++++++++ app/src/pages/settings/Settings.vue | 28 +- app/src/pages/settings/applyLiveTheme.js | 26 + .../settings/components/SettingsField.vue | 120 ++- app/src/pages/settings/googleFonts.js | 79 ++ app/src/pages/settings/settingsConfig.js | 18 +- app/src/pages/settings/themePresets.js | 103 +++ app/src/shell/SiteShell.vue | 164 +++-- app/src/shell/components/Sidebar.vue | 37 +- app/src/shell/components/Toolbar.vue | 5 +- app/src/shell/components/UserMenu.vue | 88 --- app/src/shell/shell-frame.css | 152 +++- app/src/shell/taskbar.js | 39 + app/src/shell/windowChrome.js | 81 ++ app/vite.config.js | 1 + aurora-admin.php | 1 + 26 files changed, 1816 insertions(+), 223 deletions(-) create mode 100644 admin/src/Rest/PluginsData.php create mode 100644 app/src/entries/plugins.js create mode 100644 app/src/pages/plugins/Plugins.vue create mode 100644 app/src/pages/settings/applyLiveTheme.js create mode 100644 app/src/pages/settings/googleFonts.js create mode 100644 app/src/pages/settings/themePresets.js create mode 100644 app/src/shell/taskbar.js create mode 100644 app/src/shell/windowChrome.js diff --git a/.gitignore b/.gitignore index 2975e2a..9b2b663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ app/node_modules/ app/dist/ +dist/ *.log .DS_Store Thumbs.db diff --git a/admin/assets/native-dark.css b/admin/assets/native-dark.css index 4606f52..791bf5e 100644 --- a/admin/assets/native-dark.css +++ b/admin/assets/native-dark.css @@ -163,9 +163,24 @@ html.aurora-dark.aurora-ready #wpbody-content .button-primary:hover { /* Nav tabs -------------------------------------------------------------- */ html.aurora-dark.aurora-ready #wpbody-content .nav-tab-wrapper, html.aurora-dark.aurora-ready #wpbody-content .wp-filter { + background: var(--aurora-card-bg); border-color: var(--aurora-border); } +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .filter-links a { + color: var(--aurora-accent); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .filter-links .current { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .search-form input[type='search'] { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + html.aurora-dark.aurora-ready #wpbody-content .nav-tab { background: var(--aurora-bg-subtle); border-color: var(--aurora-border); @@ -251,14 +266,14 @@ html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .wrap > p { html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card { background: var(--aurora-card-bg); border: 1px solid var(--aurora-card-border); - border-radius: 16px; - padding: 20px 24px; + border-radius: 20px; + padding: 20px; margin-bottom: 16px; } html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card h2 { - font-size: 1.05rem; + font-size: 1.0625rem; font-weight: 600; - margin: 4px 0 8px; + margin: 0 0 18px; color: var(--aurora-text); border: 0; padding-top: 0; @@ -280,3 +295,85 @@ html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-nativ html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card .button:hover { background: var(--aurora-accent-soft); } + +/* --------------------------------------------------------------------------- + * Modals & popups (media library, plugin/theme install details) — these are + * Backbone/thickbox views appended to , outside #wpbody-content, so + * every rule above misses them entirely. Scoped to html.aurora-dark only + * (no #wpbody-content ancestor requirement) since that's the only anchor + * available at body level. + * ------------------------------------------------------------------------ */ +html.aurora-dark.aurora-ready .media-modal, +html.aurora-dark.aurora-ready .media-frame, +html.aurora-dark.aurora-ready .media-frame-title, +html.aurora-dark.aurora-ready .media-frame-router, +html.aurora-dark.aurora-ready .media-frame-content, +html.aurora-dark.aurora-ready .media-frame-toolbar, +html.aurora-dark.aurora-ready .media-sidebar, +html.aurora-dark.aurora-ready #TB_window, +html.aurora-dark.aurora-ready #TB_ajaxContent, +html.aurora-dark.aurora-ready .plugin-details-modal, +html.aurora-dark.aurora-ready .plugin-install-php .plugin-card { + background: var(--aurora-card-bg); + color: var(--aurora-text); + border-color: var(--aurora-card-border); +} + +html.aurora-dark.aurora-ready .media-modal h1, +html.aurora-dark.aurora-ready .media-modal h2, +html.aurora-dark.aurora-ready .media-modal h3, +html.aurora-dark.aurora-ready .media-modal label, +html.aurora-dark.aurora-ready .media-frame-title h1, +html.aurora-dark.aurora-ready #TB_ajaxContent, +html.aurora-dark.aurora-ready #TB_ajaxContent p, +html.aurora-dark.aurora-ready #TB_ajaxContent h1, +html.aurora-dark.aurora-ready .plugin-card-top, +html.aurora-dark.aurora-ready .plugin-card .name, +html.aurora-dark.aurora-ready .plugin-card .desc { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .media-frame-menu, +html.aurora-dark.aurora-ready .media-menu, +html.aurora-dark.aurora-ready .attachments-browser .media-toolbar, +html.aurora-dark.aurora-ready .media-sidebar, +html.aurora-dark.aurora-ready .plugin-card-bottom { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); +} + +html.aurora-dark.aurora-ready .attachments-browser .attachments, +html.aurora-dark.aurora-ready .media-frame-content { + background: var(--aurora-app-bg-solid); +} + +html.aurora-dark.aurora-ready .media-modal input, +html.aurora-dark.aurora-ready .media-modal textarea, +html.aurora-dark.aurora-ready .media-modal select, +html.aurora-dark.aurora-ready #TB_ajaxContent input, +html.aurora-dark.aurora-ready #TB_ajaxContent textarea, +html.aurora-dark.aurora-ready #TB_ajaxContent select { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .media-modal-backdrop, +html.aurora-dark.aurora-ready #TB_overlay { + background: rgba(0, 0, 0, 0.7); +} + +/* Plugin/theme card grids on plugins.php / plugin-install.php / themes.php */ +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme, +html.aurora-dark.aurora-ready #wpbody-content .plugin-card { + background: var(--aurora-card-bg); + border-color: var(--aurora-card-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-name, +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-actions { + background: var(--aurora-card-bg); + color: var(--aurora-text); + border-color: var(--aurora-card-border); +} diff --git a/admin/src/App/AuroraAdmin.php b/admin/src/App/AuroraAdmin.php index 8c396f3..11b18ce 100644 --- a/admin/src/App/AuroraAdmin.php +++ b/admin/src/App/AuroraAdmin.php @@ -8,6 +8,7 @@ use AuroraAdmin\Pages\SettingsPage; use AuroraAdmin\Rest\DashboardData; use AuroraAdmin\Analytics\Analytics; use AuroraAdmin\Rest\AnalyticsData; +use AuroraAdmin\Rest\PluginsData; use AuroraAdmin\Posts\ScheduledDayFilter; use AuroraAdmin\WhiteLabel\WhiteLabel; use AuroraAdmin\Pages\SubApps; @@ -39,6 +40,7 @@ class AuroraAdmin new DashboardData(); new Analytics(); new AnalyticsData(); + new PluginsData(); new ScheduledDayFilter(); new WhiteLabel(); new SubApps(); diff --git a/admin/src/Options/Settings.php b/admin/src/Options/Settings.php index ff66d28..756609a 100644 --- a/admin/src/Options/Settings.php +++ b/admin/src/Options/Settings.php @@ -35,7 +35,12 @@ class Settings "schema" => [ "type" => "object", "properties" => [ - "dark_mode" => ["type" => "string"], + // Two producers write this key with different types: the + // toolbar's tri-state switcher sends 'light'/'dark'/'system' + // (string), the settings page's Dark Mode toggle sends a plain + // boolean. Leaving it untyped (falls through to + // additionalProperties) lets both save instead of one + // rejecting the other with a 400. "dashboard_card_order" => ["type" => "object"], ], "additionalProperties" => true, diff --git a/admin/src/Pages/SubApps.php b/admin/src/Pages/SubApps.php index 65de9bd..d5e6b7b 100644 --- a/admin/src/Pages/SubApps.php +++ b/admin/src/Pages/SubApps.php @@ -24,6 +24,7 @@ class SubApps "aurora-admin-roles" => ["title" => "Role Editor", "entry" => "role-editor"], "aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"], "aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"], + "aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"], ]; private static $hook_suffixes = []; @@ -38,11 +39,12 @@ class SubApps public static function register_menus() { foreach (self::PAGES as $slug => $page) { + $parent = $slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin"; $hook = add_submenu_page( - "aurora-admin", + $parent, __($page["title"], "aurora-admin"), __($page["title"], "aurora-admin"), - "manage_options", + $slug === "aurora-admin-plugins" ? "activate_plugins" : "manage_options", $slug, [self::class, "render"] ); @@ -53,7 +55,7 @@ class SubApps public static function hide_from_native_submenu() { foreach (array_keys(self::PAGES) as $slug) { - remove_submenu_page("aurora-admin", $slug); + remove_submenu_page($slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin", $slug); } } diff --git a/admin/src/Rest/PluginsData.php b/admin/src/Rest/PluginsData.php new file mode 100644 index 0000000..af8afc3 --- /dev/null +++ b/admin/src/Rest/PluginsData.php @@ -0,0 +1,248 @@ + "GET", + "callback" => [self::class, "list_plugins"], + "permission_callback" => function () { + return current_user_can("activate_plugins"); + }, + ]); + + register_rest_route("aurora-admin/v1", "/plugins/activate", [ + "methods" => "POST", + "callback" => [self::class, "activate_plugin"], + "permission_callback" => function () { + return current_user_can("activate_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/plugins/deactivate", [ + "methods" => "POST", + "callback" => [self::class, "deactivate_plugin"], + "permission_callback" => function () { + return current_user_can("activate_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/plugins/delete", [ + "methods" => "DELETE", + "callback" => [self::class, "delete_plugin"], + "permission_callback" => function () { + return current_user_can("delete_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + + register_rest_route("aurora-admin/v1", "/plugins/update", [ + "methods" => "POST", + "callback" => [self::class, "update_plugin"], + "permission_callback" => function () { + return current_user_can("update_plugins"); + }, + "args" => [ + "file" => ["type" => "string", "required" => true], + ], + ]); + } + + public static function list_plugins() + { + self::load_plugin_admin_functions(); + self::load_update_functions(); + + $updates = get_site_transient("update_plugins"); + if (!is_object($updates) && function_exists("wp_update_plugins")) { + wp_update_plugins(); + $updates = get_site_transient("update_plugins"); + } + + $update_response = is_object($updates) && isset($updates->response) && is_array($updates->response) + ? $updates->response + : []; + + $items = []; + foreach (get_plugins() as $file => $plugin) { + $update = isset($update_response[$file]) ? $update_response[$file] : null; + $items[] = [ + "file" => $file, + "slug" => self::plugin_slug($file), + "name" => isset($plugin["Name"]) ? $plugin["Name"] : self::plugin_slug($file), + "description" => isset($plugin["Description"]) ? wp_strip_all_tags($plugin["Description"]) : "", + "version" => isset($plugin["Version"]) ? $plugin["Version"] : "", + "author" => isset($plugin["Author"]) ? wp_strip_all_tags($plugin["Author"]) : "", + "pluginUri" => isset($plugin["PluginURI"]) ? esc_url_raw($plugin["PluginURI"]) : "", + "authorUri" => isset($plugin["AuthorURI"]) ? esc_url_raw($plugin["AuthorURI"]) : "", + "active" => is_plugin_active($file), + "networkActive" => is_multisite() && is_plugin_active_for_network($file), + "updateAvailable" => $update !== null, + "newVersion" => $update && isset($update->new_version) ? $update->new_version : "", + "canActivate" => current_user_can("activate_plugin", $file), + "canDelete" => current_user_can("delete_plugins") && !is_plugin_active($file), + "canUpdate" => current_user_can("update_plugins") && $update !== null, + ]; + } + + usort($items, function ($a, $b) { + if ($a["active"] !== $b["active"]) { + return $a["active"] ? -1 : 1; + } + return strcasecmp($a["name"], $b["name"]); + }); + + return new \WP_REST_Response([ + "items" => $items, + "stats" => self::stats($items), + "addUrl" => admin_url("plugin-install.php"), + ], 200); + } + + public static function activate_plugin($request) + { + self::load_plugin_admin_functions(); + $file = self::sanitize_file($request->get_param("file")); + $result = activate_plugin($file); + + if (is_wp_error($result)) { + return new \WP_Error("aurora_plugin_activate_failed", $result->get_error_message(), ["status" => 500]); + } + + self::clean_plugin_cache(); + return new \WP_REST_Response(["success" => true, "message" => __("Plugin activated.", "aurora-admin")], 200); + } + + public static function deactivate_plugin($request) + { + self::load_plugin_admin_functions(); + $file = self::sanitize_file($request->get_param("file")); + deactivate_plugins($file); + + self::clean_plugin_cache(); + return new \WP_REST_Response([ + "success" => true, + "message" => __("Plugin deactivated.", "aurora-admin"), + "redirect" => $file === "aurora-admin/aurora-admin.php" ? admin_url("plugins.php") : "", + ], 200); + } + + public static function delete_plugin($request) + { + self::load_plugin_admin_functions(); + if (!function_exists("delete_plugins")) { + require_once ABSPATH . "wp-admin/includes/plugin.php"; + } + + $file = self::sanitize_file($request->get_param("file")); + if (is_plugin_active($file)) { + return new \WP_Error("aurora_plugin_active_delete", __("Deactivate the plugin before deleting it.", "aurora-admin"), ["status" => 400]); + } + + $result = delete_plugins([$file]); + if (is_wp_error($result)) { + return new \WP_Error("aurora_plugin_delete_failed", $result->get_error_message(), ["status" => 500]); + } + + self::clean_plugin_cache(); + return new \WP_REST_Response(["success" => true, "message" => __("Plugin deleted.", "aurora-admin")], 200); + } + + public static function update_plugin($request) + { + self::load_plugin_admin_functions(); + self::load_update_functions(); + + require_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php"; + require_once ABSPATH . "wp-admin/includes/file.php"; + require_once ABSPATH . "wp-admin/includes/misc.php"; + + $file = self::sanitize_file($request->get_param("file")); + $skin = new \Automatic_Upgrader_Skin(); + $upgrader = new \Plugin_Upgrader($skin); + $result = $upgrader->upgrade($file); + + if (is_wp_error($result)) { + return new \WP_Error("aurora_plugin_update_failed", $result->get_error_message(), ["status" => 500]); + } + if (!$result) { + $error = $skin->get_errors(); + $message = is_wp_error($error) && $error->has_errors() + ? $error->get_error_message() + : __("Plugin update failed.", "aurora-admin"); + return new \WP_Error("aurora_plugin_update_failed", $message, ["status" => 500]); + } + + self::clean_plugin_cache(); + return new \WP_REST_Response(["success" => true, "message" => __("Plugin updated.", "aurora-admin")], 200); + } + + private static function load_plugin_admin_functions() + { + if (!function_exists("get_plugins") || !function_exists("is_plugin_active")) { + require_once ABSPATH . "wp-admin/includes/plugin.php"; + } + } + + private static function load_update_functions() + { + if (!function_exists("wp_update_plugins")) { + require_once ABSPATH . "wp-admin/includes/update.php"; + } + } + + private static function sanitize_file($file) + { + return plugin_basename(sanitize_text_field((string) $file)); + } + + private static function plugin_slug($file) + { + $parts = explode("/", $file); + return $parts[0]; + } + + private static function clean_plugin_cache() + { + if (function_exists("wp_clean_plugins_cache")) { + wp_clean_plugins_cache(true); + } + wp_cache_delete("plugins", "plugins"); + } + + private static function stats($items) + { + $stats = ["total" => count($items), "active" => 0, "inactive" => 0, "updates" => 0]; + foreach ($items as $item) { + if ($item["active"]) { + $stats["active"]++; + } else { + $stats["inactive"]++; + } + if ($item["updateAvailable"]) { + $stats["updates"]++; + } + } + return $stats; + } +} diff --git a/admin/src/Shell/MenuSerializer.php b/admin/src/Shell/MenuSerializer.php index 5d4afc9..58f91f9 100644 --- a/admin/src/Shell/MenuSerializer.php +++ b/admin/src/Shell/MenuSerializer.php @@ -132,6 +132,10 @@ class MenuSerializer return ""; } + if ($slug === "plugins.php") { + return admin_url("plugins.php?page=aurora-admin-plugins"); + } + // Already an absolute URL. if (preg_match('#^https?://#', $slug)) { return $slug; diff --git a/app/src/entries/plugins.js b/app/src/entries/plugins.js new file mode 100644 index 0000000..02d68d2 --- /dev/null +++ b/app/src/entries/plugins.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Plugins from '@/pages/plugins/Plugins.vue'; + +const mountEl = document.getElementById('aurora-admin-plugins-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Plugins, { serverData: data }).mount(mountEl); +} diff --git a/app/src/pages/_shared/AppListShell.vue b/app/src/pages/_shared/AppListShell.vue index 1200a26..097be39 100644 --- a/app/src/pages/_shared/AppListShell.vue +++ b/app/src/pages/_shared/AppListShell.vue @@ -76,15 +76,15 @@ const search = defineModel('search', { type: String, default: '' }); margin: -32px; padding: 22px; box-sizing: border-box; - background: var(--aurora-app-bg); + /* No background — html/body already paint the page gradient; this was + a redundant duplicate layer on top of it. */ } -/* Left — list box */ +/* Left — no background/border of its own; the page gradient shows + straight through so it reads as one surface instead of a boxed panel. */ .app-list-shell__nav { width: 340px; flex-shrink: 0; - background: var(--aurora-frame-bg); - border: 1px solid var(--aurora-frame-border); border-radius: 16px; padding: 24px 20px; box-sizing: border-box; @@ -127,7 +127,9 @@ const search = defineModel('search', { type: String, default: '' }); background: var(--aurora-bg-subtle); color: var(--aurora-text-muted); } .app-list-shell__filters :deep(button.is-active) { - background: var(--aurora-text); color: var(--aurora-app-bg); + /* --aurora-app-bg can be a gradient (invalid for `color`), so this one + inverted-contrast badge uses the flat fallback token instead. */ + background: var(--aurora-text); color: var(--aurora-app-bg-solid); } .app-list-shell__meta { @@ -145,11 +147,9 @@ const search = defineModel('search', { type: String, default: '' }); .app-list-shell__list { display: flex; flex-direction: column; gap: 2px; } -/* Right — content box */ +/* Right — no background/border of its own, same reasoning. */ .app-list-shell__content { flex: 1; min-width: 0; - background: var(--aurora-frame-bg); - border: 1px solid var(--aurora-frame-border); border-radius: 16px; padding: 28px 32px; min-height: 480px; diff --git a/app/src/pages/dashboard/components/AuroraCard.vue b/app/src/pages/dashboard/components/AuroraCard.vue index e6b16e3..7c470b0 100644 --- a/app/src/pages/dashboard/components/AuroraCard.vue +++ b/app/src/pages/dashboard/components/AuroraCard.vue @@ -30,7 +30,11 @@ defineProps({ display: flex; flex-direction: column; height: 100%; - background: var(--aurora-card-bg); + /* Deeper than the generic --aurora-card-bg — the Dashboard is now + wrapped in its own frame (windowChrome.js's .aurora-dashboard-frame, + using --aurora-frame-bg), and that's close in tone to --aurora-card-bg + for most presets, so cards would nearly disappear into it otherwise. */ + background: var(--aurora-card-bg-deep, var(--aurora-card-bg)); border: 1px solid var(--aurora-card-border); border-radius: 20px; padding: 20px; diff --git a/app/src/pages/plugins/Plugins.vue b/app/src/pages/plugins/Plugins.vue new file mode 100644 index 0000000..555278b --- /dev/null +++ b/app/src/pages/plugins/Plugins.vue @@ -0,0 +1,690 @@ + + + + + diff --git a/app/src/pages/settings/Settings.vue b/app/src/pages/settings/Settings.vue index b00464e..e529140 100644 --- a/app/src/pages/settings/Settings.vue +++ b/app/src/pages/settings/Settings.vue @@ -2,6 +2,7 @@ import { ref, reactive, computed, watch } from 'vue'; import { settingsCategories } from './settingsConfig.js'; import SettingsField from './components/SettingsField.vue'; +import { applyThemePalette, applyFontFamily } from './applyLiveTheme.js'; const props = defineProps({ serverData: { type: Object, required: true }, @@ -51,10 +52,17 @@ const optionsFor = (field) => { return field.options || []; }; -// Live dark-mode preview. +// Live preview — applied as soon as a picker changes, not just after +// Save + reload. watch( - () => settings.dark_mode, - (v) => document.documentElement.classList.toggle('aurora-dark', !!v) + () => settings.theme_preset, + (preset) => applyThemePalette(preset || 'indigo'), + { immediate: true } +); +watch( + () => settings.font_family, + (family) => applyFontFamily(family), + { immediate: true } ); // Save ------------------------------------------------------------------- @@ -189,16 +197,16 @@ const importSettings = (e) => { margin: -32px; /* cancel the frame padding */ padding: 22px; /* inset the two boxes; the frame's right gutter adds the rest */ box-sizing: border-box; - /* Darker backdrop so the two boxes read as distinct panels. */ - background: var(--aurora-app-bg); + /* No background here — html/body already paint the page gradient + (shell-frame.css); repainting the same var(--aurora-app-bg) here was + just a redundant duplicate layer on top of it. */ } -/* Left nav — its own box */ +/* Left nav — no background/border of its own; the page gradient shows + straight through so it reads as one surface instead of a boxed panel. */ .settings__nav { width: 340px; flex-shrink: 0; - background: var(--aurora-frame-bg); - border: 1px solid var(--aurora-frame-border); border-radius: 16px; padding: 24px 20px; box-sizing: border-box; @@ -252,11 +260,9 @@ const importSettings = (e) => { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* Right content — its own box */ +/* Right content — no background/border of its own, same reasoning. */ .settings__content { flex: 1; min-width: 0; - background: var(--aurora-frame-bg); - border: 1px solid var(--aurora-frame-border); border-radius: 16px; padding: 28px 32px; } diff --git a/app/src/pages/settings/applyLiveTheme.js b/app/src/pages/settings/applyLiveTheme.js new file mode 100644 index 0000000..a7aabb8 --- /dev/null +++ b/app/src/pages/settings/applyLiveTheme.js @@ -0,0 +1,26 @@ +/** + * Shared logic for applying the theme/font settings to the live document — + * used both for the settings page's live preview (Settings.vue) and the + * real per-page-load application (SiteShell.vue::onMounted). + */ +import { resolvePreset, buildPalette } from './themePresets.js'; + +export function applyThemePalette(themePresetId) { + const palette = buildPalette(resolvePreset(themePresetId)); + const root = document.documentElement.style; + Object.entries(palette).forEach(([prop, value]) => root.setProperty(prop, value)); +} + +export function applyFontFamily(family) { + if (!family) return; + const linkId = 'aurora-google-font'; + let link = document.getElementById(linkId); + if (!link) { + link = document.createElement('link'); + link.id = linkId; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400;500;600;700&display=swap`; + document.documentElement.style.setProperty('--aurora-font-family', `'${family}', sans-serif`); +} diff --git a/app/src/pages/settings/components/SettingsField.vue b/app/src/pages/settings/components/SettingsField.vue index 2dd720c..089be4d 100644 --- a/app/src/pages/settings/components/SettingsField.vue +++ b/app/src/pages/settings/components/SettingsField.vue @@ -1,6 +1,10 @@ @@ -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";