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
@@ -1,5 +1,6 @@
|
||||
app/node_modules/
|
||||
app/dist/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -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 <body>, 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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Rest;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* REST endpoints for Aurora's custom Installed Plugins page.
|
||||
*/
|
||||
class PluginsData
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action("rest_api_init", [self::class, "register"]);
|
||||
}
|
||||
|
||||
public static function register()
|
||||
{
|
||||
register_rest_route("aurora-admin/v1", "/plugins", [
|
||||
"methods" => "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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const plugins = ref([]);
|
||||
const stats = ref({ total: 0, active: 0, inactive: 0, updates: 0 });
|
||||
const addUrl = ref('');
|
||||
const loading = ref(true);
|
||||
const busyFile = ref('');
|
||||
const error = ref('');
|
||||
const notice = ref('');
|
||||
const search = ref('');
|
||||
const status = ref('all');
|
||||
const compact = ref(false);
|
||||
|
||||
const apiBase = computed(() => `${props.serverData.restUrl}aurora-admin/v1`);
|
||||
|
||||
const request = async (path, options = {}) => {
|
||||
const response = await fetch(`${apiBase.value}${path}`, {
|
||||
credentials: 'same-origin',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': props.serverData.restNonce,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Request failed');
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const data = await request('/plugins');
|
||||
plugins.value = data.items || [];
|
||||
stats.value = data.stats || stats.value;
|
||||
addUrl.value = data.addUrl || '';
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Could not load plugins';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(load);
|
||||
|
||||
const filtered = computed(() => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
return plugins.value.filter((plugin) => {
|
||||
const matchesStatus =
|
||||
status.value === 'all' ||
|
||||
(status.value === 'active' && plugin.active) ||
|
||||
(status.value === 'inactive' && !plugin.active) ||
|
||||
(status.value === 'updates' && plugin.updateAvailable);
|
||||
if (!matchesStatus) return false;
|
||||
if (!query) return true;
|
||||
return [plugin.name, plugin.description, plugin.author, plugin.file]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
});
|
||||
});
|
||||
|
||||
const filters = computed(() => [
|
||||
{ key: 'all', label: 'All', count: stats.value.total },
|
||||
{ key: 'active', label: 'Active', count: stats.value.active },
|
||||
{ key: 'inactive', label: 'Inactive', count: stats.value.inactive },
|
||||
{ key: 'updates', label: 'Update Available', count: stats.value.updates },
|
||||
]);
|
||||
|
||||
const runAction = async (action, plugin) => {
|
||||
if (!plugin || busyFile.value) return;
|
||||
busyFile.value = plugin.file;
|
||||
error.value = '';
|
||||
notice.value = '';
|
||||
try {
|
||||
const data = await request(`/plugins/${action}`, {
|
||||
method: action === 'delete' ? 'DELETE' : 'POST',
|
||||
body: JSON.stringify({ file: plugin.file }),
|
||||
});
|
||||
notice.value = data.message || 'Done';
|
||||
if (data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Action failed';
|
||||
} finally {
|
||||
busyFile.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const openAdd = () => {
|
||||
if (addUrl.value) {
|
||||
window.location.href = addUrl.value;
|
||||
}
|
||||
};
|
||||
|
||||
const pluginInitial = (name) => (name || 'P').trim().charAt(0).toUpperCase();
|
||||
const statusLabel = (plugin) => {
|
||||
if (plugin.networkActive) return 'Network active';
|
||||
return plugin.active ? 'Active' : 'Inactive';
|
||||
};
|
||||
const versionLine = (plugin) => {
|
||||
if (plugin.updateAvailable && plugin.newVersion) {
|
||||
return `${plugin.version || 'Unknown'} -> ${plugin.newVersion}`;
|
||||
}
|
||||
return plugin.version || 'Unknown';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="plugins-page">
|
||||
<header class="plugins-page__header">
|
||||
<div>
|
||||
<p class="plugins-page__eyebrow">Aurora Admin</p>
|
||||
<h1>Plugins</h1>
|
||||
</div>
|
||||
<div class="plugins-page__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-button"
|
||||
:class="{ 'is-active': compact }"
|
||||
title="Toggle compact view"
|
||||
@click="compact = !compact"
|
||||
>
|
||||
<span class="dashicons dashicons-menu-alt3" />
|
||||
</button>
|
||||
<button type="button" class="icon-button" title="Refresh plugins" :disabled="loading" @click="load">
|
||||
<span class="dashicons dashicons-update" />
|
||||
</button>
|
||||
<button type="button" class="primary-button" @click="openAdd">
|
||||
<span class="dashicons dashicons-plus-alt2" />
|
||||
Add New
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="plugins-page__toolbar" aria-label="Plugin filters and search">
|
||||
<div class="plugins-page__filters">
|
||||
<button
|
||||
v-for="filter in filters"
|
||||
:key="filter.key"
|
||||
type="button"
|
||||
class="filter-pill"
|
||||
:class="{ 'is-active': status === filter.key }"
|
||||
@click="status = filter.key"
|
||||
>
|
||||
<span>{{ filter.label }}</span>
|
||||
<strong>{{ filter.count }}</strong>
|
||||
</button>
|
||||
</div>
|
||||
<label class="plugins-page__search">
|
||||
<span class="dashicons dashicons-search" />
|
||||
<input v-model="search" type="search" placeholder="Search plugins..." />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div v-if="notice" class="plugins-notice plugins-notice--success">{{ notice }}</div>
|
||||
<div v-if="error" class="plugins-notice plugins-notice--error">{{ error }}</div>
|
||||
|
||||
<section class="plugins-table" :class="{ 'is-compact': compact }" aria-label="Installed plugins">
|
||||
<div class="plugins-table__head">
|
||||
<div>Plugin Info</div>
|
||||
<div>Version <span class="dashicons dashicons-filter" /></div>
|
||||
<div>Status <span class="dashicons dashicons-filter" /></div>
|
||||
<div>Author <span class="dashicons dashicons-filter" /></div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="plugins-table__empty">Loading plugins...</div>
|
||||
<div v-else-if="!filtered.length" class="plugins-table__empty">No plugins match your filters.</div>
|
||||
|
||||
<article
|
||||
v-for="plugin in filtered"
|
||||
v-else
|
||||
:key="plugin.file"
|
||||
class="plugin-row"
|
||||
:class="{ 'is-active': plugin.active }"
|
||||
>
|
||||
<div class="plugin-row__info">
|
||||
<div class="plugin-row__mark">
|
||||
<span>{{ pluginInitial(plugin.name) }}</span>
|
||||
</div>
|
||||
<div class="plugin-row__copy">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<p>{{ plugin.description || 'No description provided.' }}</p>
|
||||
<div class="plugin-row__links">
|
||||
<a v-if="plugin.pluginUri" :href="plugin.pluginUri" target="_blank" rel="noopener noreferrer">
|
||||
Plugin site
|
||||
</a>
|
||||
<a v-if="plugin.authorUri" :href="plugin.authorUri" target="_blank" rel="noopener noreferrer">
|
||||
Author site
|
||||
</a>
|
||||
<span>{{ plugin.file }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__cell" data-label="Version">
|
||||
<strong>{{ versionLine(plugin) }}</strong>
|
||||
<span v-if="plugin.updateAvailable">Update ready</span>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__cell" data-label="Status">
|
||||
<span class="status-badge" :class="{ 'status-badge--active': plugin.active }">
|
||||
<i />
|
||||
{{ statusLabel(plugin) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__cell" data-label="Author">
|
||||
<strong>{{ plugin.author || 'Unknown' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__actions">
|
||||
<button
|
||||
v-if="!plugin.active"
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
title="Activate plugin"
|
||||
:disabled="busyFile === plugin.file || !plugin.canActivate"
|
||||
@click="runAction('activate', plugin)"
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="toggle-button is-on"
|
||||
title="Deactivate plugin"
|
||||
:disabled="busyFile === plugin.file"
|
||||
@click="runAction('deactivate', plugin)"
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
<button
|
||||
v-if="plugin.updateAvailable"
|
||||
type="button"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Update plugin"
|
||||
:disabled="busyFile === plugin.file || !plugin.canUpdate"
|
||||
@click="runAction('update', plugin)"
|
||||
>
|
||||
<span class="dashicons dashicons-update-alt" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-button icon-button--inline icon-button--danger"
|
||||
title="Delete plugin"
|
||||
:disabled="busyFile === plugin.file || !plugin.canDelete"
|
||||
@click="runAction('delete', plugin)"
|
||||
>
|
||||
<span class="dashicons dashicons-trash" />
|
||||
</button>
|
||||
<a
|
||||
v-if="plugin.pluginUri"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Open plugin site"
|
||||
:href="plugin.pluginUri"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="dashicons dashicons-ellipsis" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plugins-page {
|
||||
min-height: calc(100vh - 112px);
|
||||
padding: 0;
|
||||
color: var(--aurora-text);
|
||||
background: transparent;
|
||||
}
|
||||
.plugins-page__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.plugins-page__eyebrow {
|
||||
margin: 0 0 7px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.plugins-page h1 {
|
||||
margin: 0;
|
||||
color: var(--aurora-text);
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.plugins-page__actions,
|
||||
.plugins-page__toolbar,
|
||||
.plugins-page__filters,
|
||||
.plugin-row__actions,
|
||||
.plugin-row__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.plugins-page__actions {
|
||||
gap: 10px;
|
||||
}
|
||||
.plugins-page__toolbar {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.plugins-page__filters {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
padding: 0 12px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filter-pill strong {
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--aurora-frame-bg);
|
||||
color: inherit;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.filter-pill.is-active {
|
||||
border-color: var(--aurora-text-muted);
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.filter-pill.is-active strong {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.plugins-page__search {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.plugins-page__search input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
.plugins-page__search input::placeholder {
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.primary-button,
|
||||
.icon-button {
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
min-height: 42px;
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.primary-button {
|
||||
gap: 8px;
|
||||
border-color: var(--aurora-text-muted);
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
padding: 0 16px;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.icon-button {
|
||||
width: 42px;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.icon-button.is-active {
|
||||
border-color: var(--aurora-text-muted);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.icon-button--inline {
|
||||
width: 30px;
|
||||
min-height: 30px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.icon-button:hover:not(:disabled),
|
||||
.icon-button--inline:hover:not(:disabled) {
|
||||
color: var(--aurora-text);
|
||||
border-color: var(--aurora-text-muted);
|
||||
}
|
||||
.icon-button--danger:hover:not(:disabled) {
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.primary-button:disabled,
|
||||
.icon-button:disabled,
|
||||
.toggle-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.plugins-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.plugins-notice--success {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.plugins-notice--error {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.plugins-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-frame-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
.plugins-table__head,
|
||||
.plugin-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1fr) 140px 140px 170px 150px;
|
||||
align-items: center;
|
||||
}
|
||||
.plugins-table__head {
|
||||
min-height: 48px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.79rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.plugins-table__head > * {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.plugins-table__head > div {
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.plugin-row {
|
||||
min-height: 78px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
transition: background 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
.plugin-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.plugin-row:hover {
|
||||
background: var(--aurora-bg-subtle);
|
||||
}
|
||||
.plugin-row.is-active {
|
||||
background: var(--aurora-bg-subtle);
|
||||
}
|
||||
.plugin-row__info {
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
.plugin-row__mark {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 1rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
.plugin-row__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
.plugin-row__copy h2 {
|
||||
margin: 0 0 5px;
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.plugin-row__copy p {
|
||||
margin: 0;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.plugin-row__links {
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.plugin-row__links a {
|
||||
color: var(--aurora-text);
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
.plugin-row__links span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.plugin-row__cell {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.plugin-row__cell strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 850;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.plugin-row__cell span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.status-badge {
|
||||
width: fit-content;
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.status-badge i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.status-badge--active {
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
box-shadow: inset 0 0 0 1px var(--aurora-border);
|
||||
}
|
||||
.plugin-row__actions {
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
padding: 16px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row {
|
||||
min-height: 62px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row__info,
|
||||
.plugins-table.is-compact .plugin-row__cell,
|
||||
.plugins-table.is-compact .plugin-row__actions {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row__mark {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row__links {
|
||||
display: none;
|
||||
}
|
||||
.toggle-button {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-button span {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--aurora-text-muted);
|
||||
transition: transform 160ms ease, background 160ms ease;
|
||||
}
|
||||
.toggle-button.is-on {
|
||||
background: var(--aurora-text-muted);
|
||||
}
|
||||
.toggle-button.is-on span {
|
||||
transform: translateX(16px);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.plugins-table__empty {
|
||||
padding: 38px 18px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.plugins-page {
|
||||
padding: 24px 18px 34px;
|
||||
}
|
||||
.plugins-table__head {
|
||||
display: none;
|
||||
}
|
||||
.plugin-row {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.plugin-row__cell,
|
||||
.plugin-row__actions {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.plugin-row__cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.plugin-row__cell::before {
|
||||
content: attr(data-label);
|
||||
color: var(--aurora-text-muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.plugins-page__header,
|
||||
.plugins-page__toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.plugins-page__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.plugins-page__search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import Toggle from '@/components/Toggle.vue';
|
||||
import { themePresets, buildPalette } from '@/pages/settings/themePresets.js';
|
||||
import { googleFonts } from '@/pages/settings/googleFonts.js';
|
||||
|
||||
const swatchPalette = (preset) => buildPalette(preset);
|
||||
|
||||
const props = defineProps({
|
||||
field: { type: Object, required: true },
|
||||
@@ -66,6 +70,24 @@ const pickImage = () => {
|
||||
});
|
||||
frame.open();
|
||||
};
|
||||
|
||||
// Font picker (searchable Google Font dropdown) ----------------------------
|
||||
const fontQuery = ref('');
|
||||
const fontOpen = ref(false);
|
||||
const filteredFonts = computed(() => {
|
||||
const q = fontQuery.value.trim().toLowerCase();
|
||||
const list = q ? googleFonts.filter((f) => f.toLowerCase().includes(q)) : googleFonts;
|
||||
return list.slice(0, 40);
|
||||
});
|
||||
const pickFont = (name) => {
|
||||
emit('update:modelValue', name);
|
||||
fontQuery.value = name;
|
||||
fontOpen.value = false;
|
||||
};
|
||||
const openFontList = () => {
|
||||
fontQuery.value = props.modelValue || '';
|
||||
fontOpen.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -169,6 +191,59 @@ const pickImage = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme picker (10 fixed color presets, whole-UI preview) -->
|
||||
<div v-else-if="field.type === 'theme-picker'" class="field__theme">
|
||||
<button
|
||||
v-for="preset in themePresets"
|
||||
:key="preset.id"
|
||||
type="button"
|
||||
class="field__theme-card"
|
||||
:class="{ 'field__theme-card--active': (val || field.default) === preset.id }"
|
||||
@click="val = preset.id"
|
||||
>
|
||||
<span
|
||||
class="field__theme-swatch"
|
||||
:style="{
|
||||
background: swatchPalette(preset)['--aurora-app-bg'],
|
||||
borderColor: swatchPalette(preset)['--aurora-border'],
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="field__theme-swatch-accent"
|
||||
:style="{ background: swatchPalette(preset)['--aurora-accent'] }"
|
||||
/>
|
||||
</span>
|
||||
<span class="field__theme-name">{{ preset.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Font picker (searchable Google Font dropdown) -->
|
||||
<div v-else-if="field.type === 'font-picker'" class="field__font">
|
||||
<input
|
||||
type="text"
|
||||
class="field__input"
|
||||
placeholder="Search Google Fonts…"
|
||||
:value="fontOpen ? fontQuery : (val || 'Default')"
|
||||
@focus="openFontList"
|
||||
@input="fontQuery = $event.target.value"
|
||||
@blur="() => setTimeout(() => (fontOpen = false), 150)"
|
||||
/>
|
||||
<div v-if="fontOpen" class="field__font-list">
|
||||
<button
|
||||
v-for="name in filteredFonts"
|
||||
:key="name"
|
||||
type="button"
|
||||
class="field__font-item"
|
||||
:class="{ 'field__font-item--active': val === name }"
|
||||
:style="{ fontFamily: `'${name}', sans-serif` }"
|
||||
@mousedown.prevent="pickFont(name)"
|
||||
>
|
||||
{{ name }}
|
||||
</button>
|
||||
<p v-if="!filteredFonts.length" class="field__multi-empty">No fonts match.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repeater -->
|
||||
<div v-else-if="field.type === 'repeater'" class="field__repeater">
|
||||
<div v-for="(row, i) in rows" :key="i" class="field__repeater-row">
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user