Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf5e2652f | ||
|
|
3815dfb671 | ||
|
|
7d2bc032a5 | ||
|
|
0a85fad7cf | ||
|
|
8b66155ba5 | ||
|
|
d39155e4d5 | ||
|
|
88e7a724d5 | ||
|
|
24be114f9a | ||
|
|
faf9f35992 | ||
|
|
f5c48bced9 | ||
|
|
f0bc04a110 | ||
|
|
2fec4cf34b | ||
|
|
2da3883d20 | ||
|
|
b3ff365483 | ||
|
|
53ca6f62b9 | ||
|
|
dc2a4435c5 | ||
|
|
da856dc853 | ||
|
|
1c8c351654 | ||
|
|
ce8fcc8fdd |
+51
-1291
File diff suppressed because it is too large
Load Diff
-2084
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,11 @@ html.aurora-dark.aurora-ready #wpbody-content .postbox,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .card,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .welcome-panel,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table,
|
||||
/* .widefat: WP's general "wide flat table" utility class, used beyond just
|
||||
WP_List_Table screens — e.g. update-core.php's plugin/theme update
|
||||
tables (class="widefat updates-table", no .wp-list-table and no
|
||||
.striped either), which were completely uncovered without this. */
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .stuffbox {
|
||||
background: var(--aurora-card-bg);
|
||||
border-color: var(--aurora-card-border);
|
||||
@@ -79,17 +84,53 @@ html.aurora-dark.aurora-ready #wpbody-content .postbox .handle-order-lower {
|
||||
|
||||
/* Tables ---------------------------------------------------------------- */
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table th,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table td {
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table td,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat th,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat td {
|
||||
border-color: var(--aurora-border);
|
||||
/* Core sets an explicit white background directly on <td> (not just the
|
||||
row), which otherwise wins over .wp-list-table/.widefat's own dark
|
||||
background above — transparent lets the row's actual background
|
||||
(plain dark, or the striped-odd shade below) show through instead of a
|
||||
white cell sitting on an otherwise-dark row. Seen first on Export/
|
||||
Erase Personal Data's request-queue table, but this is a generic
|
||||
WP_List_Table/.widefat gap, not specific to any one screen. */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table thead th,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table tfoot th,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table thead th a,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table tfoot th a {
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table tfoot th a,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat thead th,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat tfoot th,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat thead th a,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .widefat tfoot th a {
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
|
||||
/* core's list-tables.css sets `.plugins tr { background: #fff; }` for the
|
||||
native Plugins screen's list table — .plugins is a plain class name, not
|
||||
scoped to that screen, so it also matches update-core.php's plugin-update
|
||||
table (its <tbody> reuses the same class). Aurora's own Plugins screen is
|
||||
a full custom rebuild that never hits this native markup, which is why
|
||||
this collision hadn't surfaced before update-core.php. */
|
||||
html.aurora-dark.aurora-ready #wpbody-content .plugins tr {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Theme File Editor / Plugin File Editor (theme-editor.php, plugin-editor.php)
|
||||
share the same #templateside file-tree component. Core sets an explicit
|
||||
light background directly on `#templateside > ul` (not just inherited from
|
||||
the page), which nothing else here targets — combined with this file's own
|
||||
light text/link colors (meant for a dark surface), that produced light text
|
||||
on a light background, i.e. effectively unreadable rather than just
|
||||
unstyled. Both screens are deliberately left as framed native content (not
|
||||
rebuilt), same as Site Health/Updates, but still need to be legible. */
|
||||
html.aurora-dark.aurora-ready #wpbody-content #templateside > ul {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
html.aurora-dark.aurora-ready #wpbody-content .wp-list-table.striped > tbody > :nth-child(odd),
|
||||
html.aurora-dark.aurora-ready #wpbody-content .striped > tbody > :nth-child(odd) {
|
||||
background: var(--aurora-bg-subtle);
|
||||
@@ -492,3 +533,31 @@ html.aurora-dark.aurora-ready #wpbody-content .font-library__font-card {
|
||||
border-color: var(--aurora-card-border);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
|
||||
/* Site Health (site-health.php) is left running WordPress core's own test
|
||||
* logic rather than reimplemented — its Status tab async checks hit core's
|
||||
* own /wp-site-health/v1/tests/* REST routes, and third-party plugins can
|
||||
* add their own checks via the site_status_tests filter, so reproducing it
|
||||
* risks both drifting from core and silently dropping other plugins'
|
||||
* checks. Only the framing/theming is Aurora's, same "frame native content"
|
||||
* treatment as the block editor above.
|
||||
*
|
||||
* Three of its own elements paint a hardcoded white/off-white background
|
||||
* that the generic #wpbody-content light-text cascade doesn't reach,
|
||||
* leaving light text unreadable against it — the same "boot-layout" pattern
|
||||
* fixed above, just a different screen. */
|
||||
html.aurora-dark.aurora-ready #wpbody-content .health-check-header,
|
||||
html.aurora-dark.aurora-ready #wpbody-content .health-check-accordion-panel {
|
||||
background: var(--aurora-card-bg);
|
||||
}
|
||||
html.aurora-dark.aurora-ready #wpbody-content .health-check-accordion-trigger {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
/* The Security/Performance category pill inside each trigger row keeps its
|
||||
* own dark text color (readable against the trigger's original near-white
|
||||
* background) — needs to flip to light along with the trigger's background
|
||||
* above, or it becomes unreadable in the other direction. */
|
||||
html.aurora-dark.aurora-ready #wpbody-content .health-check-accordion-trigger .badge {
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ defined("ABSPATH") || exit();
|
||||
class ActivityLog
|
||||
{
|
||||
const TABLE = "aurora_admin_activity_log";
|
||||
const DB_VERSION = "1";
|
||||
const DB_VERSION = "2";
|
||||
const DB_OPTION = "aurora_admin_activity_log_db_version";
|
||||
|
||||
public function __construct()
|
||||
@@ -19,9 +19,12 @@ class ActivityLog
|
||||
|
||||
add_action("save_post", [self::class, "on_save_post"], 10, 3);
|
||||
add_action("before_delete_post", [self::class, "on_delete_post"]);
|
||||
add_action("add_attachment", [self::class, "on_add_attachment"]);
|
||||
add_action("delete_attachment", [self::class, "on_delete_attachment"]);
|
||||
add_action("user_register", [self::class, "on_user_register"]);
|
||||
add_action("profile_update", [self::class, "on_profile_update"]);
|
||||
add_action("wp_login", [self::class, "on_login"], 10, 2);
|
||||
add_action("wp_logout", [self::class, "on_logout"]);
|
||||
add_action("activated_plugin", [self::class, "on_plugin_activated"]);
|
||||
add_action("deactivated_plugin", [self::class, "on_plugin_deactivated"]);
|
||||
add_action("switch_theme", [self::class, "on_theme_switch"]);
|
||||
@@ -52,6 +55,7 @@ class ActivityLog
|
||||
object_label VARCHAR(191) NOT NULL DEFAULT '',
|
||||
user_id BIGINT UNSIGNED NULL,
|
||||
user_name VARCHAR(191) NOT NULL DEFAULT '',
|
||||
ip_address VARCHAR(45) NULL,
|
||||
details LONGTEXT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
@@ -66,10 +70,23 @@ class ActivityLog
|
||||
update_option(self::DB_OPTION, self::DB_VERSION);
|
||||
}
|
||||
|
||||
private static function record($action_type, $object_type, $object_id, $object_label, $details = [])
|
||||
/**
|
||||
* @param array|null $actor Overrides the "current user" attribution with
|
||||
* an explicit ["id" => int, "name" => string] — needed for wp_logout,
|
||||
* where wp_get_current_user() has already been reset to the logged-out
|
||||
* (id 0) user by the time this hook fires, which would otherwise record
|
||||
* every logout as done by nobody.
|
||||
*/
|
||||
private static function record($action_type, $object_type, $object_id, $object_label, $details = [], $actor = null)
|
||||
{
|
||||
global $wpdb;
|
||||
$user = wp_get_current_user();
|
||||
if ($actor === null) {
|
||||
$current = wp_get_current_user();
|
||||
$actor = [
|
||||
"id" => $current && $current->exists() ? $current->ID : null,
|
||||
"name" => $current && $current->exists() ? $current->display_name : "",
|
||||
];
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- $wpdb->insert() is WordPress's own escaping-safe API for writing to a custom table (not raw SQL); there's no core post/postmeta abstraction for Aurora's own activity-log table, so any access to it is necessarily "direct" by this sniff's definition.
|
||||
$wpdb->insert(self::table_name(), [
|
||||
@@ -77,69 +94,133 @@ class ActivityLog
|
||||
"object_type" => $object_type,
|
||||
"object_id" => $object_id,
|
||||
"object_label" => substr((string) $object_label, 0, 191),
|
||||
"user_id" => $user && $user->exists() ? $user->ID : null,
|
||||
"user_name" => $user && $user->exists() ? $user->display_name : "",
|
||||
"user_id" => $actor["id"],
|
||||
"user_name" => $actor["name"],
|
||||
"ip_address" => self::client_ip(),
|
||||
"details" => $details ? wp_json_encode($details) : null,
|
||||
"created_at" => current_time("mysql"),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* REMOTE_ADDR only — the actual TCP peer, not spoofable by a request
|
||||
* header. Behind a reverse proxy/load balancer this will read as the
|
||||
* proxy's own address rather than the original visitor; deliberately not
|
||||
* trusting X-Forwarded-For or similar instead, since blindly trusting a
|
||||
* client-supplied header for anything (even just display, as here) is the
|
||||
* standard IP-spoofing footgun, and this plugin has no reliable way to
|
||||
* know whether a given host's proxy is trusted.
|
||||
*/
|
||||
private static function client_ip()
|
||||
{
|
||||
$ip = isset($_SERVER["REMOTE_ADDR"]) ? sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"])) : "";
|
||||
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : null;
|
||||
}
|
||||
|
||||
public static function on_save_post($post_id, $post, $update)
|
||||
{
|
||||
// save_post fires on autosaves and revisions too — neither represents a
|
||||
// real user-visible change, so both are skipped here.
|
||||
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
|
||||
// save_post fires on autosaves, revisions, and attachments too — none
|
||||
// represent a real user-visible content change (attachments get their
|
||||
// own add_attachment/delete_attachment hooks below), so all three are
|
||||
// skipped here.
|
||||
if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id) || $post->post_type === "attachment") {
|
||||
return;
|
||||
}
|
||||
if (!in_array($post->post_status, ["publish", "draft", "pending", "future", "private"], true)) {
|
||||
return;
|
||||
}
|
||||
self::record($update ? "post_updated" : "post_created", "post", $post_id, $post->post_title ?: "(no title)");
|
||||
// object_type is the post's real type (post/page/a custom post type),
|
||||
// not a hardcoded "post" — the Activity Log's Type column needs this to
|
||||
// read correctly for anything that isn't the built-in Posts type.
|
||||
self::record($update ? "updated" : "created", $post->post_type, $post_id, $post->post_title ?: "(no title)");
|
||||
}
|
||||
|
||||
public static function on_delete_post($post_id)
|
||||
{
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_type === "revision") {
|
||||
if (!$post || $post->post_type === "revision" || $post->post_type === "attachment") {
|
||||
return;
|
||||
}
|
||||
self::record("post_deleted", "post", $post_id, $post->post_title ?: "(no title)");
|
||||
self::record("deleted", $post->post_type, $post_id, $post->post_title ?: "(no title)");
|
||||
}
|
||||
|
||||
public static function on_add_attachment($post_id)
|
||||
{
|
||||
$post = get_post($post_id);
|
||||
self::record("created", "attachment", $post_id, $post ? $post->post_title : "(no title)");
|
||||
}
|
||||
|
||||
public static function on_delete_attachment($post_id)
|
||||
{
|
||||
$post = get_post($post_id);
|
||||
self::record("deleted", "attachment", $post_id, $post ? $post->post_title : "(no title)");
|
||||
}
|
||||
|
||||
public static function on_user_register($user_id)
|
||||
{
|
||||
$user = get_userdata($user_id);
|
||||
self::record("user_registered", "user", $user_id, $user ? $user->user_login : "");
|
||||
// Self-registration means the visitor isn't authenticated yet at this
|
||||
// point, so wp_get_current_user() would show "not logged in" rather
|
||||
// than the account that was just created. Attributing explicitly to the
|
||||
// new account instead — including when an admin creates the account —
|
||||
// is a deliberate choice for this specific event: unlike profile edits
|
||||
// (where the acting admin, shown via the default wp_get_current_user()
|
||||
// attribution, is the more useful "who did this"), a registration event
|
||||
// is overwhelmingly self-service, so "who this account belongs to" is
|
||||
// the more useful read here, and treating both origins the same way
|
||||
// keeps this one event type's Author column consistent regardless of
|
||||
// which flow created the account.
|
||||
self::record("registered", "user", $user_id, $user ? $user->user_login : "", [], [
|
||||
"id" => $user_id,
|
||||
"name" => $user ? $user->display_name : "",
|
||||
]);
|
||||
}
|
||||
|
||||
public static function on_profile_update($user_id)
|
||||
{
|
||||
$user = get_userdata($user_id);
|
||||
self::record("user_updated", "user", $user_id, $user ? $user->user_login : "");
|
||||
self::record("profile_updated", "user", $user_id, $user ? $user->user_login : "");
|
||||
}
|
||||
|
||||
public static function on_login($user_login, $user)
|
||||
{
|
||||
self::record("login", "user", $user->ID, $user_login);
|
||||
// Not wp_get_current_user(): wp_login fires from inside wp_signon(),
|
||||
// before wp-login.php's own form handler calls wp_set_current_user() —
|
||||
// the same ordering issue on_logout() has, just in the other direction.
|
||||
// $user is passed in for exactly this reason; use it directly instead.
|
||||
self::record("login", "user", $user->ID, $user_login, [], [
|
||||
"id" => $user->ID,
|
||||
"name" => $user->display_name,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function on_logout($user_id)
|
||||
{
|
||||
$user = $user_id ? get_userdata($user_id) : false;
|
||||
$login = $user ? $user->user_login : "";
|
||||
self::record("logout", "user", $user_id ?: null, $login, [], [
|
||||
"id" => $user_id ?: null,
|
||||
"name" => $user ? $user->display_name : "",
|
||||
]);
|
||||
}
|
||||
|
||||
public static function on_plugin_activated($plugin)
|
||||
{
|
||||
self::record("plugin_activated", "plugin", null, $plugin);
|
||||
self::record("activated", "plugin", null, $plugin);
|
||||
}
|
||||
|
||||
public static function on_plugin_deactivated($plugin)
|
||||
{
|
||||
self::record("plugin_deactivated", "plugin", null, $plugin);
|
||||
self::record("deactivated", "plugin", null, $plugin);
|
||||
}
|
||||
|
||||
public static function on_theme_switch($new_name)
|
||||
{
|
||||
self::record("theme_switched", "theme", null, $new_name);
|
||||
self::record("switched", "theme", null, $new_name);
|
||||
}
|
||||
|
||||
public static function on_settings_updated()
|
||||
{
|
||||
self::record("settings_updated", "settings", null, "Aurora Admin settings");
|
||||
self::record("updated", "settings", null, "Aurora Admin settings");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ class ActivityLogData
|
||||
return current_user_can("manage_options");
|
||||
},
|
||||
"args" => [
|
||||
// "admin" = site/content activity (everything except user-account
|
||||
// events), "user" = account activity (login/logout/registration/
|
||||
// profile updates) — the Activity Log page's two tabs. Omit for
|
||||
// unfiltered (both).
|
||||
"scope" => ["type" => "string", "required" => false, "enum" => ["admin", "user"]],
|
||||
"action_type" => ["type" => "string", "required" => false],
|
||||
"object_type" => ["type" => "string", "required" => false],
|
||||
"search" => ["type" => "string", "required" => false],
|
||||
@@ -40,6 +45,13 @@ class ActivityLogData
|
||||
$where = ["1=1"];
|
||||
$params = [];
|
||||
|
||||
$scope = $request->get_param("scope");
|
||||
if ($scope === "user") {
|
||||
$where[] = "object_type = 'user'";
|
||||
} elseif ($scope === "admin") {
|
||||
$where[] = "object_type != 'user'";
|
||||
}
|
||||
|
||||
if ($request->get_param("action_type")) {
|
||||
$where[] = "action_type = %s";
|
||||
$params[] = sanitize_text_field($request->get_param("action_type"));
|
||||
@@ -73,15 +85,50 @@ class ActivityLogData
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- reading Aurora's own custom activity-log table; always passed through $wpdb->prepare() with $params plus the trailing $per_page/$offset bound via %d/%d.
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, array_merge($params, [$per_page, $offset])));
|
||||
|
||||
$items = array_map(function ($row) {
|
||||
// Live user info (avatar/role/email) rather than freezing it into each
|
||||
// log row at record time — reflects the account's CURRENT role/email
|
||||
// (correct if either changes later) and avoids duplicating PII into
|
||||
// every single event row when user_id is already a foreign key back to
|
||||
// it. Batch-fetched once for every distinct user_id on this page,
|
||||
// rather than one get_userdata() call per row.
|
||||
$user_ids = array_values(array_unique(array_filter(array_map(function ($row) {
|
||||
return $row->user_id ? (int) $row->user_id : 0;
|
||||
}, $rows))));
|
||||
$users_by_id = [];
|
||||
if ($user_ids) {
|
||||
// $roles[0] is a role SLUG (e.g. "shop_manager") — its translated
|
||||
// display name has to come from wp_roles()'s own registered name for
|
||||
// that slug, not a guess like ucfirst($slug); that only happens to
|
||||
// work for the single-word built-in roles.
|
||||
$all_roles = wp_roles()->roles;
|
||||
foreach (get_users(["include" => $user_ids, "fields" => "all_with_meta"]) as $u) {
|
||||
$slug = $u->roles ? $u->roles[0] : "";
|
||||
$role_name = $slug && isset($all_roles[$slug]) ? translate_user_role($all_roles[$slug]["name"]) : "";
|
||||
$users_by_id[$u->ID] = [
|
||||
"avatar" => get_avatar_url($u->ID, ["size" => 64]),
|
||||
"role" => $role_name,
|
||||
"email" => $u->user_email,
|
||||
"editUrl" => esc_url_raw(admin_url("user-edit.php?user_id=" . $u->ID)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$items = array_map(function ($row) use ($users_by_id) {
|
||||
$uid = $row->user_id ? (int) $row->user_id : null;
|
||||
$u = $uid && isset($users_by_id[$uid]) ? $users_by_id[$uid] : null;
|
||||
return [
|
||||
"id" => (int) $row->id,
|
||||
"actionType" => $row->action_type,
|
||||
"objectType" => $row->object_type,
|
||||
"objectId" => $row->object_id ? (int) $row->object_id : null,
|
||||
"objectLabel" => $row->object_label,
|
||||
"userId" => $row->user_id ? (int) $row->user_id : null,
|
||||
"userId" => $uid,
|
||||
"userName" => $row->user_name,
|
||||
"userAvatar" => $u ? $u["avatar"] : null,
|
||||
"userRole" => $u ? $u["role"] : "",
|
||||
"userEmail" => $u ? $u["email"] : "",
|
||||
"userEditUrl" => $u ? $u["editUrl"] : "",
|
||||
"ipAddress" => $row->ip_address,
|
||||
"details" => $row->details ? json_decode($row->details, true) : null,
|
||||
"createdAt" => $row->created_at,
|
||||
];
|
||||
|
||||
@@ -13,6 +13,7 @@ use AuroraAdmin\Options\Settings;
|
||||
use AuroraAdmin\Shell\Shell;
|
||||
use AuroraAdmin\Pages\DashboardTakeover;
|
||||
use AuroraAdmin\Pages\SettingsPage;
|
||||
use AuroraAdmin\Pages\SetupWizard;
|
||||
use AuroraAdmin\Rest\DashboardData;
|
||||
use AuroraAdmin\Analytics\Analytics;
|
||||
use AuroraAdmin\Rest\AnalyticsData;
|
||||
@@ -30,6 +31,9 @@ use AuroraAdmin\Pages\NativeSettingsPages;
|
||||
use AuroraAdmin\Rest\PermalinksData;
|
||||
use AuroraAdmin\Rest\DiscussionData;
|
||||
use AuroraAdmin\Pages\AppearancePages;
|
||||
use AuroraAdmin\Pages\ToolsPage;
|
||||
use AuroraAdmin\Pages\ExportPage;
|
||||
use AuroraAdmin\Pages\ImportPage;
|
||||
use AuroraAdmin\Rest\NavMenusData;
|
||||
use AuroraAdmin\Rest\ThemesData;
|
||||
use AuroraAdmin\Rest\WidgetsData;
|
||||
@@ -42,9 +46,13 @@ use AuroraAdmin\Roles\RolesData;
|
||||
use AuroraAdmin\ActivityLog\ActivityLog;
|
||||
use AuroraAdmin\ActivityLog\ActivityLogData;
|
||||
use AuroraAdmin\Login\LoginStyle;
|
||||
use AuroraAdmin\Login\LoginRedirect;
|
||||
use AuroraAdmin\Editor\DisableGutenberg;
|
||||
use AuroraAdmin\Media\SvgUploads;
|
||||
use AuroraAdmin\Media\MediaFolders;
|
||||
use AuroraAdmin\ImageFormats\ImageFormats;
|
||||
use AuroraAdmin\Security\Security;
|
||||
use AuroraAdmin\Code\CodeInjection;
|
||||
use AuroraAdmin\Rest\MediaFolderData;
|
||||
use AuroraAdmin\Rest\BugReportData;
|
||||
use AuroraAdmin\Rest\CompanionPluginData;
|
||||
@@ -63,6 +71,7 @@ class AuroraAdmin
|
||||
new Shell();
|
||||
new DashboardTakeover();
|
||||
new SettingsPage();
|
||||
new SetupWizard();
|
||||
new DashboardData();
|
||||
new Analytics();
|
||||
new AnalyticsData();
|
||||
@@ -80,6 +89,9 @@ class AuroraAdmin
|
||||
new PermalinksData();
|
||||
new DiscussionData();
|
||||
new AppearancePages();
|
||||
new ToolsPage();
|
||||
new ExportPage();
|
||||
new ImportPage();
|
||||
new NavMenusData();
|
||||
new ThemesData();
|
||||
new WidgetsData();
|
||||
@@ -92,9 +104,13 @@ class AuroraAdmin
|
||||
new ActivityLog();
|
||||
new ActivityLogData();
|
||||
new LoginStyle();
|
||||
new LoginRedirect();
|
||||
new DisableGutenberg();
|
||||
new SvgUploads();
|
||||
new MediaFolders();
|
||||
new ImageFormats();
|
||||
new Security();
|
||||
new CodeInjection();
|
||||
new MediaFolderData();
|
||||
new BugReportData();
|
||||
new CompanionPluginData();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Code;
|
||||
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Custom code injection (Settings > Code).
|
||||
*
|
||||
* Four admin-authored fields, only settable by manage_options users on the
|
||||
* settings page:
|
||||
* - Admin CSS → wrapped in a <style> in the admin <head>
|
||||
* - Admin JS → wrapped in a <script> in the admin footer
|
||||
* - Header scripts → printed raw in the front-end <head>
|
||||
* - Footer scripts → printed raw before the front-end </body>
|
||||
*
|
||||
* Header/footer scripts are printed verbatim (they're meant to hold full
|
||||
* <script>/<meta> tags — analytics, verification, etc.), the same trusted-
|
||||
* admin model as any "insert headers and footers" tool. Admin CSS/JS is the
|
||||
* user's own code too, but is wrapped in the right tag for them and guarded
|
||||
* against accidentally breaking out of that tag.
|
||||
*/
|
||||
class CodeInjection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action("admin_head", [self::class, "admin_css"], 999);
|
||||
add_action("admin_print_footer_scripts", [self::class, "admin_js"], 999);
|
||||
add_action("wp_head", [self::class, "header_scripts"], 999);
|
||||
add_action("wp_footer", [self::class, "footer_scripts"], 999);
|
||||
}
|
||||
|
||||
public static function admin_css()
|
||||
{
|
||||
$css = trim((string) Settings::get("custom_admin_css", ""));
|
||||
if ($css === "") {
|
||||
return;
|
||||
}
|
||||
// Keep the CSS from breaking out of its own <style> element.
|
||||
$css = str_ireplace("</style>", "", $css);
|
||||
echo "\n<style id=\"aurora-custom-admin-css\">\n" . $css . "\n</style>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored stylesheet, output raw inside a <style> tag by design; </style> stripped above to prevent breakout.
|
||||
}
|
||||
|
||||
public static function admin_js()
|
||||
{
|
||||
$js = trim((string) Settings::get("custom_admin_js", ""));
|
||||
if ($js === "") {
|
||||
return;
|
||||
}
|
||||
$js = str_ireplace("</script>", "<\\/script>", $js);
|
||||
echo "\n<script id=\"aurora-custom-admin-js\">\n" . $js . "\n</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored script, output raw inside a <script> tag by design.
|
||||
}
|
||||
|
||||
public static function header_scripts()
|
||||
{
|
||||
$code = trim((string) Settings::get("header_scripts", ""));
|
||||
if ($code === "") {
|
||||
return;
|
||||
}
|
||||
echo "\n" . $code . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored header markup (scripts/meta), printed verbatim by design.
|
||||
}
|
||||
|
||||
public static function footer_scripts()
|
||||
{
|
||||
$code = trim((string) Settings::get("footer_scripts", ""));
|
||||
if ($code === "") {
|
||||
return;
|
||||
}
|
||||
echo "\n" . $code . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored footer markup (scripts), printed verbatim by design.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Editor;
|
||||
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Settings > Screens > Editor: "Disable Gutenberg" — opt-in, off by default.
|
||||
* Falls back to WordPress core's own classic (meta-box/TinyMCE) post editor
|
||||
* via the same core-supported filter the official Classic Editor plugin
|
||||
* uses, rather than framing or rebuilding the block editor itself — the
|
||||
* block editor stays one of the two deliberate "still native" screens in
|
||||
* the admin takeover (alongside the Theme/Plugin File Editor), so this is
|
||||
* an escape hatch around it, not a takeover of it.
|
||||
*/
|
||||
class DisableGutenberg
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_filter("use_block_editor_for_post_type", [self::class, "maybe_disable"], 100);
|
||||
}
|
||||
|
||||
public static function maybe_disable($use_block_editor)
|
||||
{
|
||||
if ((bool) Settings::get("disable_gutenberg", false)) {
|
||||
return false;
|
||||
}
|
||||
return $use_block_editor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Login;
|
||||
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Settings > Login > "Login Redirect": optionally sends logged-out wp-admin
|
||||
* visitors to a custom URL instead of the login screen, and/or sends
|
||||
* specific roles to a custom URL after logging in instead of the dashboard.
|
||||
* Both are opt-in and empty by default (normal WordPress behavior).
|
||||
*
|
||||
* Deliberately does NOT touch wp-login.php itself — it is never hidden,
|
||||
* blocked, or moved, so this can only change where someone lands
|
||||
* before/after that point, never whether they can reach the login form.
|
||||
* Changing/hiding the login URL itself is a separate, higher-risk feature
|
||||
* not implemented here.
|
||||
*/
|
||||
class LoginRedirect
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_filter("login_redirect", [self::class, "after_login"], 10, 3);
|
||||
add_filter("allowed_redirect_hosts", [self::class, "allow_configured_hosts"]);
|
||||
// wp_loaded, not admin_init: WordPress's own auth_redirect() (called
|
||||
// from wp-admin/admin.php, before admin_init fires) already redirects
|
||||
// an unauthenticated visitor to wp-login.php and exits — admin_init
|
||||
// would never run for the case this needs to catch. wp_loaded fires
|
||||
// from inside wp-settings.php, which wp-admin/admin.php requires and
|
||||
// waits on BEFORE calling auth_redirect(), so it reliably runs first.
|
||||
add_action("wp_loaded", [self::class, "guard_admin"]);
|
||||
}
|
||||
|
||||
private static function enabled()
|
||||
{
|
||||
return (bool) Settings::get("login_redirect_enabled", false);
|
||||
}
|
||||
|
||||
/** Destination after a successful login, for the configured roles. */
|
||||
public static function after_login($redirect_to, $requested_redirect_to, $user)
|
||||
{
|
||||
if (!self::enabled() || !($user instanceof \WP_User) || !$user->exists()) {
|
||||
return $redirect_to;
|
||||
}
|
||||
|
||||
$roles = (array) Settings::get("redirect_roles", []);
|
||||
if (!$roles || !array_intersect((array) $user->roles, $roles)) {
|
||||
return $redirect_to;
|
||||
}
|
||||
|
||||
$target = trim((string) Settings::get("redirect_after_login_url", ""));
|
||||
return $target !== "" ? esc_url_raw($target) : $redirect_to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends logged-out visitors to a custom URL instead of the default
|
||||
* wp-login.php screen when they try to open wp-admin. Scoped tightly to
|
||||
* real browser page loads under wp-admin — admin-ajax.php and
|
||||
* admin-post.php (both used by logged-out requests via the *_nopriv_*
|
||||
* hooks other plugins rely on for public form handling), async uploads,
|
||||
* REST, and multisite's network admin are all left untouched, so nothing
|
||||
* outside the login screen itself is affected.
|
||||
*/
|
||||
public static function guard_admin()
|
||||
{
|
||||
if (!self::enabled() || is_user_logged_in()) {
|
||||
return;
|
||||
}
|
||||
if (!is_admin() || is_network_admin() || wp_doing_ajax()) {
|
||||
return;
|
||||
}
|
||||
if (defined("REST_REQUEST") && REST_REQUEST) {
|
||||
return;
|
||||
}
|
||||
$excluded_scripts = ["admin-post.php", "admin-ajax.php", "async-upload.php"];
|
||||
if (in_array($GLOBALS["pagenow"] ?? "", $excluded_scripts, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = trim((string) Settings::get("redirect_unauthenticated_url", ""));
|
||||
// Empty, or would send a logged-out visitor straight back into
|
||||
// wp-admin — the latter is almost certainly a misconfiguration and
|
||||
// would otherwise create a redirect loop.
|
||||
if ($target === "" || stripos($target, "wp-admin") !== false) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect(esc_url_raw($target));
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets wp_safe_redirect() honor an external target the admin configured
|
||||
* for either redirect above, instead of silently falling back to the site
|
||||
* home URL — both settings come from manage_options-gated Settings, never
|
||||
* from request input, so there's no open-redirect exposure in trusting
|
||||
* their host.
|
||||
*/
|
||||
public static function allow_configured_hosts($hosts)
|
||||
{
|
||||
foreach (["redirect_unauthenticated_url", "redirect_after_login_url"] as $key) {
|
||||
$host = wp_parse_url(trim((string) Settings::get($key, "")), PHP_URL_HOST);
|
||||
if ($host) {
|
||||
$hosts[] = $host;
|
||||
}
|
||||
}
|
||||
return $hosts;
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,25 @@ class LoginStyle
|
||||
}
|
||||
|
||||
$logo = trim((string) Settings::get("login_logo", ""));
|
||||
$bg_image = trim((string) Settings::get("login_bg_image", ""));
|
||||
// Colors fall back to the original fixed dark theme when unset; invalid
|
||||
// values are dropped by sanitize_hex_color (returns null) then fall back.
|
||||
$bg_color = sanitize_hex_color((string) Settings::get("login_bg_color", "")) ?: "";
|
||||
$form_bg = sanitize_hex_color((string) Settings::get("login_form_bg", "")) ?: "#1c2333";
|
||||
$accent = sanitize_hex_color((string) Settings::get("login_button_color", "")) ?: "#4f7cff";
|
||||
$custom_css = str_ireplace("</style>", "", (string) Settings::get("login_custom_css", ""));
|
||||
|
||||
if ($bg_image) {
|
||||
$background = "url(" . esc_url($bg_image) . ") center / cover no-repeat";
|
||||
} elseif ($bg_color) {
|
||||
$background = $bg_color;
|
||||
} else {
|
||||
$background = "linear-gradient(135deg, #1b2333 0%, #0f1420 100%)";
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
body.login {
|
||||
background: linear-gradient(135deg, #1b2333 0%, #0f1420 100%);
|
||||
background: <?php echo $background; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- composed from a sanitize_hex_color() color or esc_url()'d image above. ?>;
|
||||
}
|
||||
body.login #login h1 a {
|
||||
<?php if ($logo): ?>
|
||||
@@ -64,7 +79,7 @@ class LoginStyle
|
||||
body.login form#loginform,
|
||||
body.login form#registerform,
|
||||
body.login form#lostpasswordform {
|
||||
background: #1c2333;
|
||||
background: <?php echo esc_attr($form_bg); ?>;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||
@@ -84,8 +99,8 @@ class LoginStyle
|
||||
body.login input[type="text"]:focus,
|
||||
body.login input[type="password"]:focus,
|
||||
body.login input[type="email"]:focus {
|
||||
border-color: #4f7cff;
|
||||
box-shadow: 0 0 0 1px #4f7cff;
|
||||
border-color: <?php echo esc_attr($accent); ?>;
|
||||
box-shadow: 0 0 0 1px <?php echo esc_attr($accent); ?>;
|
||||
}
|
||||
body.login .forgetmenot label {
|
||||
color: #9fb0d0;
|
||||
@@ -99,23 +114,23 @@ class LoginStyle
|
||||
color: #fff;
|
||||
}
|
||||
body.login .wp-core-ui .button-primary {
|
||||
background: #4f7cff;
|
||||
border-color: #4f7cff;
|
||||
background: <?php echo esc_attr($accent); ?>;
|
||||
border-color: <?php echo esc_attr($accent); ?>;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
body.login .wp-core-ui .button-primary:hover,
|
||||
body.login .wp-core-ui .button-primary:focus {
|
||||
background: #3b63e0;
|
||||
border-color: #3b63e0;
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
body.login #login_error,
|
||||
body.login .message,
|
||||
body.login .success {
|
||||
background: #232b40;
|
||||
border-left-color: #4f7cff;
|
||||
border-left-color: <?php echo esc_attr($accent); ?>;
|
||||
color: #e2e8f4;
|
||||
}
|
||||
<?php echo $custom_css; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored login CSS, output raw by design; </style> stripped above. ?>
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -14,14 +14,38 @@ class MenuCreator
|
||||
const DB_VERSION = "1";
|
||||
const DB_OPTION = "aurora_admin_menus_db_version";
|
||||
|
||||
/** Cached merged item list for the current user (per request). */
|
||||
private static $items_cache = null;
|
||||
|
||||
/** Raw top-level menu snapshot, captured before apply() hides anything. */
|
||||
private static $native_snapshot = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
add_action("init", [self::class, "maybe_install"]);
|
||||
// Late priority so it runs after all other plugins have registered
|
||||
// their own menu items — there's nothing left to reorder/hide otherwise.
|
||||
// Snapshot the full native menu just before apply() runs, so the editor
|
||||
// can list (and re-enable) items this config hides. Both run after every
|
||||
// other plugin has registered its menu items.
|
||||
add_action("admin_menu", [self::class, "capture_native"], PHP_INT_MAX - 1);
|
||||
add_action("admin_menu", [self::class, "apply"], PHP_INT_MAX);
|
||||
}
|
||||
|
||||
/** Captures the raw top-level menu for the editor (see native_snapshot()). */
|
||||
public static function capture_native()
|
||||
{
|
||||
self::$native_snapshot = \AuroraAdmin\Shell\MenuSerializer::top_level_items();
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw top-level menu items for the current admin page load. The Menu
|
||||
* Creator page feeds this into its serverData (SubApps.php) because the
|
||||
* REST route can't build the admin menu.
|
||||
*/
|
||||
public static function native_snapshot()
|
||||
{
|
||||
return is_array(self::$native_snapshot) ? self::$native_snapshot : [];
|
||||
}
|
||||
|
||||
public static function table_name()
|
||||
{
|
||||
global $wpdb;
|
||||
@@ -57,64 +81,112 @@ class MenuCreator
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies every published config whose target roles match the current
|
||||
* user: hides items marked hidden and relabels items with a custom label.
|
||||
* Merged item list from every published config whose target roles match
|
||||
* the current user. Cached for the request (read by both apply() and
|
||||
* ordered_slugs() on every admin page load).
|
||||
*/
|
||||
public static function apply()
|
||||
private static function current_user_items()
|
||||
{
|
||||
global $menu, $wpdb;
|
||||
if (self::$items_cache !== null) {
|
||||
return self::$items_cache;
|
||||
}
|
||||
self::$items_cache = [];
|
||||
|
||||
global $wpdb;
|
||||
if (!is_admin() || !function_exists("wp_get_current_user")) {
|
||||
return;
|
||||
return self::$items_cache;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
if (!$user || !$user->exists()) {
|
||||
return;
|
||||
return self::$items_cache;
|
||||
}
|
||||
|
||||
$table = self::table_name();
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name, not user input; no user-supplied values in this query. Runs on every admin page load to build the sidebar menu, so a per-request cache wouldn't help; a persistent cache would need its own invalidation on every config save, more complexity than this read is worth.
|
||||
$configs = $wpdb->get_results("SELECT roles, items FROM {$table} WHERE status = 'published'");
|
||||
if (!$configs) {
|
||||
return self::$items_cache;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($configs as $config) {
|
||||
$roles = json_decode($config->roles ?: "[]", true);
|
||||
if (!is_array($roles) || !$roles || !array_intersect((array) $user->roles, $roles)) {
|
||||
continue;
|
||||
}
|
||||
$decoded = json_decode($config->items ?: "[]", true);
|
||||
if (is_array($decoded)) {
|
||||
$items = array_merge($items, $decoded);
|
||||
}
|
||||
}
|
||||
|
||||
self::$items_cache = $items;
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides items marked hidden and relabels items with a custom label, on the
|
||||
* native top-level $menu (which Aurora's MenuSerializer then reads).
|
||||
*/
|
||||
public static function apply()
|
||||
{
|
||||
global $menu;
|
||||
if (!is_array($menu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($configs as $config) {
|
||||
$roles = json_decode($config->roles ?: "[]", true);
|
||||
if (!is_array($roles) || !$roles) {
|
||||
continue;
|
||||
}
|
||||
if (!array_intersect((array) $user->roles, $roles)) {
|
||||
foreach (self::current_user_items() as $item) {
|
||||
$native_id = isset($item["native_id"]) ? $item["native_id"] : "";
|
||||
if ($native_id === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = json_decode($config->items ?: "[]", true);
|
||||
if (!is_array($items)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$native_id = isset($item["native_id"]) ? $item["native_id"] : "";
|
||||
if ($native_id === "") {
|
||||
foreach ($menu as $key => $entry) {
|
||||
if (!isset($entry[2]) || $entry[2] !== $native_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($menu as $key => $entry) {
|
||||
if (!isset($entry[2]) || $entry[2] !== $native_id) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($item["hidden"])) {
|
||||
unset($menu[$key]);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!empty($item["hidden"])) {
|
||||
unset($menu[$key]);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!empty($item["label"])) {
|
||||
$menu[$key][0] = sanitize_text_field($item["label"]);
|
||||
}
|
||||
if (!empty($item["label"])) {
|
||||
$menu[$key][0] = sanitize_text_field($item["label"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desired top-level slug order for the current user, from the saved config
|
||||
* (items sorted by their 'order' field). Empty when no config applies.
|
||||
*
|
||||
* Consumed by MenuSerializer so Aurora's own sidebar reflects the order —
|
||||
* WordPress's native `menu_order` filter only reorders the core admin menu,
|
||||
* which Aurora hides, so it can't be used here.
|
||||
*/
|
||||
public static function ordered_slugs()
|
||||
{
|
||||
$items = self::current_user_items();
|
||||
if (!$items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
usort($items, function ($a, $b) {
|
||||
$ao = isset($a["order"]) ? (int) $a["order"] : 0;
|
||||
$bo = isset($b["order"]) ? (int) $b["order"] : 0;
|
||||
return $ao <=> $bo;
|
||||
});
|
||||
|
||||
$slugs = [];
|
||||
foreach ($items as $item) {
|
||||
$slug = isset($item["native_id"]) ? $item["native_id"] : "";
|
||||
if ($slug !== "" && !in_array($slug, $slugs, true)) {
|
||||
$slugs[] = $slug;
|
||||
}
|
||||
}
|
||||
return $slugs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,17 +82,15 @@ class MenuCreatorData
|
||||
{
|
||||
$menu = \AuroraAdmin\Shell\MenuSerializer::serialize();
|
||||
$flat = [];
|
||||
// Top-level items only. These are what the Menu Creator can reorder,
|
||||
// hide, and rename today; submenu editing isn't applied yet, so it isn't
|
||||
// offered (showing controls that do nothing is worse than not showing
|
||||
// them).
|
||||
foreach ($menu as $item) {
|
||||
if (!empty($item["separator"]) || empty($item["id"])) {
|
||||
continue;
|
||||
}
|
||||
$flat[] = ["native_id" => $item["id"], "title" => $item["title"], "icon" => $item["icon"]];
|
||||
foreach ($item["submenu"] ?? [] as $sub) {
|
||||
if (empty($sub["id"])) {
|
||||
continue;
|
||||
}
|
||||
$flat[] = ["native_id" => $sub["id"], "title" => $item["title"] . " › " . $sub["title"], "icon" => null];
|
||||
}
|
||||
}
|
||||
return new \WP_REST_Response($flat, 200);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Registers the Export screen (export.php) for the "full admin takeover"
|
||||
* initiative — a real Aurora-built form, not a reframed native page.
|
||||
*
|
||||
* Safe to fully rebuild (unlike Tools/Site Health): export.php's actual file
|
||||
* generation (the `?download=true` request) calls export_wp() and die()s
|
||||
* before ever reaching in_admin_header/admin_footer — the two hooks
|
||||
* ScreenHijack's buffering relies on — so that request is never touched by
|
||||
* the hijack at all and runs completely native, unmodified. Only the HTML
|
||||
* form (the plain GET view with no `download` param) is replaced; Aurora's
|
||||
* form submits via a real browser navigation to the same
|
||||
* export.php?download=true&... URL, so the download itself is byte-for-byte
|
||||
* what core would have produced.
|
||||
*/
|
||||
class ExportPage
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
ScreenHijack::register(
|
||||
"load-export.php",
|
||||
"export", // Matches core's own capability for this screen.
|
||||
"full_admin_takeover",
|
||||
"export",
|
||||
"aurora-admin-export-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
return [
|
||||
"exportUrl" => esc_url_raw(admin_url("export.php")),
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Registers the Import screen (import.php) for the "full admin takeover"
|
||||
* initiative — a real Aurora-built listing, not a reframed native page.
|
||||
*
|
||||
* Only the LISTING page is rebuilt. Running an already-active importer
|
||||
* (native "Run Importer" link) still goes to admin.php?import={id} — a
|
||||
* screen registered and entirely rendered by whatever third-party importer
|
||||
* plugin owns that id (register_importer()'s own callback). That page is
|
||||
* arbitrary, unpredictable third-party UI outside this hijack's scope
|
||||
* (a different load-{hook}), and stays native, same as e.g. the block
|
||||
* editor stays native beyond its frame.
|
||||
*
|
||||
* Install/activate reuses existing, already-proven mechanisms instead of
|
||||
* new logic: WordPress core's own admin-ajax "install-plugin" action (same
|
||||
* one native's "Install Now" button triggers) for installing, and Aurora's
|
||||
* own aurora-admin/v1/plugins/activate route (see PluginsData.php) for
|
||||
* activation.
|
||||
*/
|
||||
class ImportPage
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
ScreenHijack::register(
|
||||
"load-import.php",
|
||||
"import", // Matches core's own capability for this screen.
|
||||
"full_admin_takeover",
|
||||
"import",
|
||||
"aurora-admin-import-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
return self::build_data();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static function build_data()
|
||||
{
|
||||
if (!function_exists("get_importers")) {
|
||||
require_once ABSPATH . "wp-admin/includes/import.php";
|
||||
}
|
||||
|
||||
// Real, already-active importers (registered via register_importer() by
|
||||
// an active importer plugin) — global $wp_importers. get_importers()
|
||||
// returns that global as-is, which is null (not an empty array) until
|
||||
// something actually calls register_importer() — true whenever no
|
||||
// importer plugin is active, e.g. a fresh install. Native import.php
|
||||
// never hits this directly: it only ever reads $importers via isset(),
|
||||
// and PHP auto-vivifies null into an array the moment popular_importers
|
||||
// gets merged in via `$importers[$id] = [...]`, so by the time IT
|
||||
// foreach()es, an assignment has always already happened. This code
|
||||
// foreach()es the raw result directly, so it needs its own guard.
|
||||
$active = get_importers();
|
||||
if (!is_array($active)) {
|
||||
$active = [];
|
||||
}
|
||||
|
||||
$can_install = current_user_can("install_plugins");
|
||||
$popular = $can_install ? wp_get_popular_importers() : [];
|
||||
|
||||
$merged = [];
|
||||
|
||||
foreach ($active as $id => $data) {
|
||||
$merged[] = [
|
||||
"id" => $id,
|
||||
"name" => $data[0],
|
||||
"description" => $data[1],
|
||||
"status" => "active",
|
||||
"runUrl" => esc_url_raw(self_admin_url("admin.php?import=" . $id)),
|
||||
];
|
||||
}
|
||||
|
||||
// Same de-dup + dummy-registration logic as native import.php: a
|
||||
// popular importer already covered by an active one is skipped.
|
||||
foreach ($popular as $pop_key => $pop) {
|
||||
$importer_id = isset($pop["importer-id"]) ? $pop["importer-id"] : $pop_key;
|
||||
if (isset($active[$pop_key]) || isset($active[$importer_id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin_slug = isset($pop["plugin-slug"]) ? $pop["plugin-slug"] : "";
|
||||
$entry = [
|
||||
"id" => $importer_id,
|
||||
"name" => $pop["name"],
|
||||
"description" => $pop["description"],
|
||||
"slug" => $plugin_slug,
|
||||
// Deterministic regardless of current status — provided for every
|
||||
// entry so the front end never has to construct an admin URL
|
||||
// itself (this respects any custom/network admin path).
|
||||
"runUrl" => esc_url_raw(self_admin_url("admin.php?import=" . $importer_id)),
|
||||
];
|
||||
|
||||
if ($plugin_slug !== "" && file_exists(WP_PLUGIN_DIR . "/" . $plugin_slug)) {
|
||||
if (!function_exists("get_plugins")) {
|
||||
require_once ABSPATH . "wp-admin/includes/plugin.php";
|
||||
}
|
||||
$plugins = get_plugins("/" . $plugin_slug);
|
||||
if ($plugins) {
|
||||
$keys = array_keys($plugins);
|
||||
$entry["status"] = "installed_inactive";
|
||||
$entry["pluginFile"] = $plugin_slug . "/" . $keys[0];
|
||||
} else {
|
||||
$entry["status"] = "not_installed";
|
||||
}
|
||||
} else {
|
||||
$entry["status"] = "not_installed";
|
||||
}
|
||||
|
||||
$merged[] = $entry;
|
||||
}
|
||||
|
||||
usort($merged, function ($a, $b) {
|
||||
return strcasecmp($a["name"], $b["name"]);
|
||||
});
|
||||
|
||||
// Any third-party output hooked to import_filters (a genuine, if rarely
|
||||
// used, extension point) — same capture-and-relay approach as Tools'
|
||||
// tool_box. Empty on a site with nothing hooked to it.
|
||||
ob_start();
|
||||
do_action("import_filters");
|
||||
$native_filters_html = trim((string) ob_get_clean());
|
||||
|
||||
return [
|
||||
"importers" => $merged,
|
||||
"canInstallPlugins" => $can_install,
|
||||
"installNonce" => wp_create_nonce("updates"),
|
||||
"ajaxUrl" => esc_url_raw(admin_url("admin-ajax.php")),
|
||||
"pluginSearchUrl" => esc_url_raw(network_admin_url("plugin-install.php?tab=search&type=tag&s=importer")),
|
||||
"nativeFiltersHtml" => $native_filters_html,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
use AuroraAdmin\Utility\Assets;
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* First-run onboarding wizard.
|
||||
*
|
||||
* A full-screen, self-contained page (no Aurora sidebar/toolbar shell, no
|
||||
* native WP chrome) walking a new user through appearance, branding, and
|
||||
* which replacement screens to enable. Registered as a hidden submenu of the
|
||||
* "aurora-admin" menu — MenuSerializer skips that slug, so it never shows in
|
||||
* the Aurora sidebar, and the native menu it does technically live under is
|
||||
* hidden by the shell on every other page anyway.
|
||||
*
|
||||
* First run: on activation an option flags a one-time redirect here; the
|
||||
* wizard clears it and sets a "complete" option when finished or skipped, so
|
||||
* it never auto-opens again (but stays reachable at its URL).
|
||||
*/
|
||||
class SetupWizard
|
||||
{
|
||||
const PAGE = "aurora-admin-setup";
|
||||
const OPT_COMPLETE = "aurora_admin_setup_complete";
|
||||
const OPT_REDIRECT = "aurora_admin_setup_redirect";
|
||||
|
||||
private static $hook_suffix = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
register_activation_hook(AURORA_ADMIN_PATH . "aurora-admin.php", [self::class, "on_activate"]);
|
||||
add_action("admin_menu", [self::class, "register_menu"]);
|
||||
add_action("admin_enqueue_scripts", [self::class, "maybe_enqueue"]);
|
||||
add_action("admin_init", [self::class, "maybe_redirect"]);
|
||||
add_action("rest_api_init", [self::class, "register_rest"]);
|
||||
// Turn the immersive shell off on this page so the wizard owns the whole
|
||||
// viewport; native chrome is hidden separately (printed below).
|
||||
add_filter("aurora_admin_shell_enabled", [self::class, "disable_shell_here"]);
|
||||
add_action("admin_head", [self::class, "hide_native_chrome"]);
|
||||
}
|
||||
|
||||
/** Flag a one-time redirect to the wizard, unless setup was already done. */
|
||||
public static function on_activate()
|
||||
{
|
||||
if (!get_option(self::OPT_COMPLETE)) {
|
||||
update_option(self::OPT_REDIRECT, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static function is_wizard_page()
|
||||
{
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page match; no state change.
|
||||
return isset($_GET["page"]) && sanitize_text_field(wp_unslash($_GET["page"])) === self::PAGE;
|
||||
}
|
||||
|
||||
public static function disable_shell_here($enabled)
|
||||
{
|
||||
return self::is_wizard_page() ? false : $enabled;
|
||||
}
|
||||
|
||||
public static function register_menu()
|
||||
{
|
||||
self::$hook_suffix = add_submenu_page(
|
||||
"aurora-admin",
|
||||
__("Setup", "aurora-admin"),
|
||||
__("Setup", "aurora-admin"),
|
||||
"manage_options",
|
||||
self::PAGE,
|
||||
[self::class, "render"]
|
||||
);
|
||||
}
|
||||
|
||||
public static function maybe_redirect()
|
||||
{
|
||||
if (!get_option(self::OPT_REDIRECT)) {
|
||||
return;
|
||||
}
|
||||
if (wp_doing_ajax() || !current_user_can("manage_options") || self::is_wizard_page()) {
|
||||
return;
|
||||
}
|
||||
delete_option(self::OPT_REDIRECT);
|
||||
wp_safe_redirect(admin_url("admin.php?page=" . self::PAGE));
|
||||
exit();
|
||||
}
|
||||
|
||||
public static function maybe_enqueue($hook_suffix)
|
||||
{
|
||||
if ($hook_suffix !== self::$hook_suffix) {
|
||||
return;
|
||||
}
|
||||
// Image fields (logo, dark logo, favicon) use the real media library.
|
||||
wp_enqueue_media();
|
||||
Assets::enqueue("aurora-admin-setup", "entries/setup-wizard.js");
|
||||
}
|
||||
|
||||
/** Hide native WP chrome on the wizard page (the shell, which normally does
|
||||
* this, is turned off here). The Vue app also renders as a full-screen
|
||||
* overlay, but this keeps the admin bar/menu from flashing first. */
|
||||
public static function hide_native_chrome()
|
||||
{
|
||||
if (!self::is_wizard_page()) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<style id="aurora-setup-hide-chrome">
|
||||
#adminmenumain, #wpadminbar, #wpfooter { display: none !important; }
|
||||
html.wp-toolbar { padding-top: 0 !important; }
|
||||
#wpcontent { margin-left: 0 !important; }
|
||||
#wpbody-content { padding: 0 !important; float: none; width: auto; }
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
public static function register_rest()
|
||||
{
|
||||
register_rest_route("aurora-admin/v1", "/setup/complete", [
|
||||
"methods" => "POST",
|
||||
"callback" => [self::class, "rest_complete"],
|
||||
"permission_callback" => function () {
|
||||
return current_user_can("manage_options");
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public static function rest_complete()
|
||||
{
|
||||
update_option(self::OPT_COMPLETE, 1);
|
||||
delete_option(self::OPT_REDIRECT);
|
||||
return ["success" => true];
|
||||
}
|
||||
|
||||
public static function render()
|
||||
{
|
||||
$data = [
|
||||
"restUrl" => esc_url_raw(get_rest_url()),
|
||||
"restNonce" => wp_create_nonce("wp_rest"),
|
||||
"userName" => wp_get_current_user()->display_name,
|
||||
"settings" => Settings::get(),
|
||||
"dashboardUrl" => esc_url_raw(admin_url("index.php")),
|
||||
"settingsUrl" => esc_url_raw(admin_url("admin.php?page=aurora-admin")),
|
||||
];
|
||||
?>
|
||||
<div
|
||||
id="aurora-admin-setup-root"
|
||||
data-aurora-admin="<?php echo esc_attr(wp_json_encode($data)); ?>"
|
||||
></div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class SubApps
|
||||
public static function pages()
|
||||
{
|
||||
return [
|
||||
"aurora-admin-modules" => ["title" => __("Modules", "aurora-admin"), "entry" => "modules"],
|
||||
"aurora-admin-menu-creator" => ["title" => __("Menu Creator", "aurora-admin"), "entry" => "menu-creator"],
|
||||
"aurora-admin-notices" => ["title" => __("Admin Notices", "aurora-admin"), "entry" => "admin-notices"],
|
||||
"aurora-admin-roles" => ["title" => __("Role Editor", "aurora-admin"), "entry" => "role-editor"],
|
||||
@@ -164,6 +165,13 @@ class SubApps
|
||||
$data["theme"] = wp_get_theme()->get("Name");
|
||||
}
|
||||
|
||||
// The Menu Creator editor needs the raw top-level menu, which can't be
|
||||
// read over REST (the admin $menu isn't built there). Pass the snapshot
|
||||
// captured during this admin page load instead.
|
||||
if ($page === "aurora-admin-menu-creator") {
|
||||
$data["nativeItems"] = \AuroraAdmin\MenuCreator\MenuCreator::native_snapshot();
|
||||
}
|
||||
|
||||
$root_id = "aurora-admin-" . $pages[$page]["entry"] . "-root";
|
||||
?>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Registers the "Available Tools" screen (tools.php) for the "full admin
|
||||
* takeover" initiative — a real hijacked replacement, not just a reframed
|
||||
* native page.
|
||||
*
|
||||
* tools.php's whole purpose is being an extension point: other plugins add
|
||||
* their own cards to it via the `tool_box` action (WooCommerce, migration
|
||||
* tools, etc. have historically used it). A pure from-scratch rebuild would
|
||||
* silently drop that content the moment Aurora takes over the screen, so
|
||||
* this still captures and re-renders whatever `tool_box` produces — Aurora
|
||||
* owns the page shell (header, card styling, layout) but not the content of
|
||||
* hooks it doesn't control.
|
||||
*
|
||||
* Import, Export, Site Health, Export/Erase Personal Data, and the Theme/
|
||||
* Plugin File Editors are separate screens (their own load-{file} hooks) and
|
||||
* are not covered by this — Available Tools only.
|
||||
*
|
||||
* Native tools.php's "Categories and Tags Converter" card is NOT produced by
|
||||
* the tool_box hook — it's hardcoded directly in core's own template, gated
|
||||
* on the `import` capability plus category/tag manage_terms. Replicated here
|
||||
* with the identical gate so visibility exactly matches what core itself
|
||||
* would have shown.
|
||||
*/
|
||||
class ToolsPage
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
ScreenHijack::register(
|
||||
"load-tools.php",
|
||||
"edit_posts", // Matches core's own capability for this screen.
|
||||
"full_admin_takeover",
|
||||
"tools",
|
||||
"aurora-admin-tools-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
// Same gate core's own tools.php applies before showing the
|
||||
// Categories and Tags Converter card.
|
||||
$show_converter = false;
|
||||
if (current_user_can("import")) {
|
||||
$cats = get_taxonomy("category");
|
||||
$tags = get_taxonomy("post_tag");
|
||||
$show_converter =
|
||||
($cats && current_user_can($cats->cap->manage_terms)) ||
|
||||
($tags && current_user_can($tags->cap->manage_terms));
|
||||
}
|
||||
|
||||
// The native call to do_action('tool_box') already happened during
|
||||
// this same request (inside the buffer ScreenHijack discards) — this
|
||||
// is a second, independent invocation solely to capture its output
|
||||
// for Aurora's own page, so any OTHER plugin's tool_box card still
|
||||
// shows up here. Standard "buffer a hook to capture its markup"
|
||||
// pattern; safe for the well-behaved, output-only callbacks this
|
||||
// hook is meant for.
|
||||
ob_start();
|
||||
do_action("tool_box");
|
||||
$native_tools_html = trim((string) ob_get_clean());
|
||||
|
||||
return [
|
||||
"showCategoriesConverter" => $show_converter,
|
||||
"importUrl" => esc_url_raw(admin_url("import.php")),
|
||||
"nativeToolsHtml" => $native_tools_html,
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Security;
|
||||
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Security + performance hardening toggles (Settings > Security & performance).
|
||||
*
|
||||
* Each feature is gated on its own setting and wired here by adding/removing
|
||||
* core hooks — no output is buffered or rewritten, so anything left off
|
||||
* behaves exactly like stock WordPress. Settings are read once at construct
|
||||
* time (plugins_loaded), the same pattern the rest of the plugin uses.
|
||||
*/
|
||||
class Security
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (Settings::get("disable_xmlrpc")) {
|
||||
self::disable_xmlrpc();
|
||||
}
|
||||
if (Settings::get("disable_comments")) {
|
||||
self::disable_comments();
|
||||
}
|
||||
if (Settings::get("remove_head_junk")) {
|
||||
self::clean_head();
|
||||
}
|
||||
if (Settings::get("disable_emojis")) {
|
||||
self::disable_emojis();
|
||||
}
|
||||
if (Settings::get("disable_oembed")) {
|
||||
self::disable_oembed();
|
||||
}
|
||||
|
||||
$heartbeat = Settings::get("heartbeat_mode", "default");
|
||||
if ($heartbeat === "disabled") {
|
||||
add_action("init", function () {
|
||||
wp_deregister_script("heartbeat");
|
||||
}, 1);
|
||||
} elseif ($heartbeat === "slow") {
|
||||
add_filter("heartbeat_settings", function ($settings) {
|
||||
$settings["interval"] = 60;
|
||||
return $settings;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static function disable_xmlrpc()
|
||||
{
|
||||
add_filter("xmlrpc_enabled", "__return_false");
|
||||
// Drop the X-Pingback header that advertises the endpoint.
|
||||
add_filter("wp_headers", function ($headers) {
|
||||
unset($headers["X-Pingback"]);
|
||||
return $headers;
|
||||
});
|
||||
add_filter("pings_open", "__return_false", 20);
|
||||
}
|
||||
|
||||
private static function disable_comments()
|
||||
{
|
||||
// Front end: closed everywhere, and existing comments never render.
|
||||
add_filter("comments_open", "__return_false", 20);
|
||||
add_filter("pings_open", "__return_false", 20);
|
||||
add_filter("comments_array", "__return_empty_array", 20);
|
||||
|
||||
// Strip comment support from every post type.
|
||||
add_action("init", function () {
|
||||
foreach (get_post_types() as $post_type) {
|
||||
if (post_type_supports($post_type, "comments")) {
|
||||
remove_post_type_support($post_type, "comments");
|
||||
remove_post_type_support($post_type, "trackbacks");
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Admin: remove the Comments menu, the dashboard widget, and the toolbar
|
||||
// bubble so there's no dangling entry point.
|
||||
add_action("admin_menu", function () {
|
||||
remove_menu_page("edit-comments.php");
|
||||
});
|
||||
add_action("wp_dashboard_setup", function () {
|
||||
remove_meta_box("dashboard_recent_comments", "dashboard", "normal");
|
||||
});
|
||||
add_action("admin_bar_menu", function ($bar) {
|
||||
$bar->remove_node("comments");
|
||||
}, 999);
|
||||
}
|
||||
|
||||
private static function clean_head()
|
||||
{
|
||||
remove_action("wp_head", "rsd_link");
|
||||
remove_action("wp_head", "wlwmanifest_link");
|
||||
remove_action("wp_head", "wp_generator");
|
||||
remove_action("wp_head", "wp_shortlink_wp_head");
|
||||
add_filter("the_generator", "__return_empty_string");
|
||||
}
|
||||
|
||||
private static function disable_emojis()
|
||||
{
|
||||
remove_action("wp_head", "print_emoji_detection_script", 7);
|
||||
remove_action("admin_print_scripts", "print_emoji_detection_script");
|
||||
remove_action("wp_print_styles", "print_emoji_styles");
|
||||
remove_action("admin_print_styles", "print_emoji_styles");
|
||||
remove_filter("the_content_feed", "wp_staticize_emoji");
|
||||
remove_filter("comment_text_rss", "wp_staticize_emoji");
|
||||
remove_filter("wp_mail", "wp_staticize_emoji_for_email");
|
||||
add_filter("tiny_mce_plugins", function ($plugins) {
|
||||
return is_array($plugins) ? array_diff($plugins, ["wpemoji"]) : [];
|
||||
});
|
||||
// Drop the s.w.org DNS-prefetch the emoji script adds.
|
||||
add_filter("wp_resource_hints", function ($hints, $relation_type) {
|
||||
if ($relation_type === "dns-prefetch") {
|
||||
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- "emoji_svg_url" is a WordPress core filter (wp-includes/formatting.php), not a hook this plugin owns; it is applied here only to read core's own emoji SVG base URL so the matching s.w.org dns-prefetch hint can be identified and removed.
|
||||
$emoji_svg = apply_filters("emoji_svg_url", "https://s.w.org/images/core/emoji/");
|
||||
$hints = array_filter($hints, function ($h) use ($emoji_svg) {
|
||||
return strpos((string) $h, $emoji_svg) === false && strpos((string) $h, "s.w.org") === false;
|
||||
});
|
||||
}
|
||||
return $hints;
|
||||
}, 10, 2);
|
||||
}
|
||||
|
||||
private static function disable_oembed()
|
||||
{
|
||||
// Core registers these on wp_head at a non-default priority (the
|
||||
// discovery links sit at priority 4, not 10), so match whatever priority
|
||||
// they're actually hooked at rather than assuming the default.
|
||||
self::remove_wp_head_action("wp_oembed_add_discovery_links");
|
||||
self::remove_wp_head_action("wp_oembed_add_host_js");
|
||||
add_filter("embed_oembed_discover", "__return_false");
|
||||
remove_action("rest_api_init", "wp_oembed_register_route");
|
||||
// Stop the front-end wp-embed.min.js from loading.
|
||||
add_action("wp_footer", function () {
|
||||
wp_dequeue_script("wp-embed");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a wp_head callback at whatever priority (or priorities) it's
|
||||
* registered at — core hooks the oEmbed discovery links at a non-default
|
||||
* priority, and it can end up registered at more than one, so loop until
|
||||
* none remain (bounded, so a stubborn callback can't spin forever).
|
||||
*/
|
||||
private static function remove_wp_head_action($function)
|
||||
{
|
||||
$guard = 0;
|
||||
while (($priority = has_action("wp_head", $function)) !== false && $guard++ < 20) {
|
||||
remove_action("wp_head", $function, $priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,62 @@ class MenuSerializer
|
||||
$items[] = $entry;
|
||||
}
|
||||
|
||||
// Apply the Menu Creator's saved order (if any) for the current user.
|
||||
// Configured items come first in their saved order; anything not in the
|
||||
// config — separators, and newly installed plugins' menu items — keeps
|
||||
// its original position after them, so new menu items never disappear.
|
||||
$order = \AuroraAdmin\MenuCreator\MenuCreator::ordered_slugs();
|
||||
if ($order) {
|
||||
$rank = array_flip($order);
|
||||
$tail = count($order);
|
||||
$decorated = [];
|
||||
foreach ($items as $i => $entry) {
|
||||
$slug = isset($entry["id"]) ? $entry["id"] : "";
|
||||
$r = $slug !== "" && isset($rank[$slug]) ? $rank[$slug] : $tail + $i;
|
||||
$decorated[] = ["r" => $r, "i" => $i, "entry" => $entry];
|
||||
}
|
||||
usort($decorated, function ($a, $b) {
|
||||
return $a["r"] <=> $b["r"] ?: $a["i"] <=> $b["i"];
|
||||
});
|
||||
$items = array_map(function ($d) {
|
||||
return $d["entry"];
|
||||
}, $decorated);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw top-level menu items ({native_id,title,icon}) for the Menu Creator
|
||||
* editor. Unlike serialize() this must be captured BEFORE MenuCreator hides
|
||||
* anything (so hidden items still appear and can be toggled back on) — and
|
||||
* it can't run over REST, where the admin $menu isn't built. See
|
||||
* MenuCreator::capture_native().
|
||||
*/
|
||||
public static function top_level_items()
|
||||
{
|
||||
global $menu;
|
||||
if (!is_array($menu)) {
|
||||
return [];
|
||||
}
|
||||
$items = [];
|
||||
foreach ($menu as $item) {
|
||||
$classes = isset($item[4]) ? $item[4] : "";
|
||||
if (strpos($classes, "wp-menu-separator") !== false) {
|
||||
continue;
|
||||
}
|
||||
$cap = isset($item[1]) ? $item[1] : "";
|
||||
$slug = isset($item[2]) ? $item[2] : "";
|
||||
if ($slug === "" || $slug === "aurora-admin" || empty($cap) || !current_user_can($cap)) {
|
||||
continue;
|
||||
}
|
||||
$title = self::clean_title(isset($item[0]) ? $item[0] : "");
|
||||
$items[] = [
|
||||
"native_id" => $slug,
|
||||
"title" => $title["title"],
|
||||
"icon" => self::resolve_icon(isset($item[6]) ? $item[6] : ""),
|
||||
];
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
@@ -250,6 +250,7 @@ class Shell
|
||||
{
|
||||
$items = [
|
||||
["id" => "aurora-admin", "title" => __("Settings", "aurora-admin"), "icon" => "dashicons-admin-generic"],
|
||||
["id" => "aurora-admin-modules", "title" => __("Modules", "aurora-admin"), "icon" => "dashicons-screenoptions"],
|
||||
["id" => "aurora-admin-menu-creator", "title" => __("Menu Creator", "aurora-admin"), "icon" => "dashicons-menu-alt"],
|
||||
["id" => "aurora-admin-notices", "title" => __("Admin Notices", "aurora-admin"), "icon" => "dashicons-megaphone"],
|
||||
["id" => "aurora-admin-roles", "title" => __("Role Editor", "aurora-admin"), "icon" => "dashicons-groups"],
|
||||
|
||||
Generated
+2
-342
@@ -1,22 +1,13 @@
|
||||
{
|
||||
"name": "aurora-admin-app",
|
||||
"version": "2.16.0",
|
||||
"version": "3.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aurora-admin-app",
|
||||
"version": "2.16.0",
|
||||
"version": "3.1.0",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"vue": "^3.5.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -70,198 +61,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
|
||||
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-php": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
|
||||
"integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/php": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-sql": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
|
||||
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-xml": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/xml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.42.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.5.tgz",
|
||||
"integrity": "sha512-7uT/vUgH6dfXWn3WqOe23KneILMvGy5wQjNMEcRXLKzziJ9NOktpW6tGoyQpwVkBgE5Gj6hKkCcsddbnkaWrOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
@@ -710,112 +509,6 @@
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.4.tgz",
|
||||
"integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.4.tgz",
|
||||
"integrity": "sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/php": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz",
|
||||
"integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/xml": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
|
||||
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||
@@ -1326,27 +1019,6 @@
|
||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -1574,12 +1246,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -1692,12 +1358,6 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,6 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"vue": "^3.5.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import '@/shell/shell-frame.css';
|
||||
import Export from '@/pages/export/Export.vue';
|
||||
|
||||
const mountEl = document.getElementById('aurora-admin-export-root');
|
||||
|
||||
if (mountEl) {
|
||||
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
|
||||
createApp(Export, { serverData: data }).mount(mountEl);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import '@/shell/shell-frame.css';
|
||||
import Import from '@/pages/import/Import.vue';
|
||||
|
||||
const mountEl = document.getElementById('aurora-admin-import-root');
|
||||
|
||||
if (mountEl) {
|
||||
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
|
||||
createApp(Import, { serverData: data }).mount(mountEl);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import '@/shell/shell-frame.css';
|
||||
import Modules from '@/pages/modules/Modules.vue';
|
||||
|
||||
const mountEl = document.getElementById('aurora-admin-modules-root');
|
||||
|
||||
if (mountEl) {
|
||||
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
|
||||
createApp(Modules, { serverData: data }).mount(mountEl);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import '@/shell/shell-frame.css';
|
||||
import SetupWizard from '@/pages/setup-wizard/SetupWizard.vue';
|
||||
|
||||
const mountEl = document.getElementById('aurora-admin-setup-root');
|
||||
|
||||
if (mountEl) {
|
||||
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
|
||||
createApp(SetupWizard, { serverData: data }).mount(mountEl);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import '@/shell/shell-frame.css';
|
||||
import Tools from '@/pages/tools/Tools.vue';
|
||||
|
||||
const mountEl = document.getElementById('aurora-admin-tools-root');
|
||||
|
||||
if (mountEl) {
|
||||
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
|
||||
createApp(Tools, { serverData: data }).mount(mountEl);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ defineEmits(['click']);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
text-align: left;
|
||||
}
|
||||
.app-list-item:hover {
|
||||
@@ -49,7 +49,7 @@ defineEmits(['click']);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
display: flex;
|
||||
|
||||
@@ -80,7 +80,10 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
current scroll position, looking like nothing happened until you
|
||||
scrolled all the way back up. */
|
||||
height: calc(100vh - var(--aurora-toolbar-h) - 60px);
|
||||
margin: -32px;
|
||||
/* -20px cancels the scroller's 20px padding (shell-frame.css's two-layer
|
||||
scroll rule) so the shell still bleeds to the rounded frame's inner
|
||||
edge. Was -32px back when #wpbody-content padded the content directly. */
|
||||
margin: -20px;
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
/* No background — html/body already paint the page gradient; this was
|
||||
@@ -94,7 +97,7 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
padding: 24px 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -106,7 +109,7 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
.app-list-shell__actions { display: flex; gap: 6px; align-items: center; }
|
||||
.app-list-shell__actions :deep(button) {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
height: 30px; padding: 0 10px; border: none; border-radius: 7px;
|
||||
height: 30px; padding: 0 10px; border: none; border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||||
font-size: 0.75rem; font-weight: 500; cursor: pointer; gap: 6px;
|
||||
}
|
||||
@@ -119,7 +122,7 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px; padding: 8px 12px; margin-bottom: 14px;
|
||||
border-radius: var(--aurora-radius-md); padding: 8px 12px; margin-bottom: 14px;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.app-list-shell__search input {
|
||||
@@ -161,7 +164,7 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
flex: 1; min-width: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
padding: 28px 32px;
|
||||
min-height: 480px;
|
||||
box-sizing: border-box;
|
||||
@@ -174,7 +177,7 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
text-align: center; gap: 6px;
|
||||
}
|
||||
.app-list-shell__empty-icon {
|
||||
width: 56px; height: 56px; border-radius: 14px;
|
||||
width: 56px; height: 56px; border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px; margin-bottom: 10px;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import AppListShell from '../_shared/AppListShell.vue';
|
||||
import AppListItem from '../_shared/AppListItem.vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
@@ -15,28 +13,67 @@ const api = (path) =>
|
||||
return r.json();
|
||||
});
|
||||
|
||||
const ACTION_TYPES = [
|
||||
// Two tabs: "Admin" = site/content activity, "User" = account activity
|
||||
// (login/logout/registration/profile updates) — object_type 'user' is the
|
||||
// dividing line the REST route's `scope` param filters on.
|
||||
const TABS = [
|
||||
{ key: 'admin', label: 'Admin' },
|
||||
{ key: 'user', label: 'User' },
|
||||
];
|
||||
|
||||
const ADMIN_ACTIONS = [
|
||||
['', 'All Actions'],
|
||||
['post_created', 'Post created'],
|
||||
['post_updated', 'Post updated'],
|
||||
['post_deleted', 'Post deleted'],
|
||||
['user_registered', 'User registered'],
|
||||
['user_updated', 'User updated'],
|
||||
['login', 'Login'],
|
||||
['plugin_activated', 'Plugin activated'],
|
||||
['plugin_deactivated', 'Plugin deactivated'],
|
||||
['theme_switched', 'Theme switched'],
|
||||
['settings_updated', 'Settings updated'],
|
||||
['created', 'Created'],
|
||||
['updated', 'Updated'],
|
||||
['deleted', 'Deleted'],
|
||||
['activated', 'Activated'],
|
||||
['deactivated', 'Deactivated'],
|
||||
['switched', 'Switched'],
|
||||
];
|
||||
const USER_ACTIONS = [
|
||||
['', 'All Actions'],
|
||||
['registered', 'Registered'],
|
||||
['profile_updated', 'Profile updated'],
|
||||
['login', 'Logged in'],
|
||||
['logout', 'Logged out'],
|
||||
];
|
||||
const OBJECT_TYPES = [
|
||||
['', 'All Types'],
|
||||
['post', 'Post'],
|
||||
['user', 'User'],
|
||||
['page', 'Page'],
|
||||
['attachment', 'Attachment'],
|
||||
['plugin', 'Plugin'],
|
||||
['theme', 'Theme'],
|
||||
['settings', 'Settings'],
|
||||
];
|
||||
|
||||
const TYPE_LABELS = {
|
||||
post: 'Post',
|
||||
page: 'Page',
|
||||
attachment: 'Attachment',
|
||||
user: 'User',
|
||||
plugin: 'Plugin',
|
||||
theme: 'Theme',
|
||||
settings: 'Settings',
|
||||
};
|
||||
const ACTION_LABELS = {
|
||||
created: 'Created',
|
||||
updated: 'Updated',
|
||||
deleted: 'Deleted',
|
||||
registered: 'Registered',
|
||||
profile_updated: 'Profile updated',
|
||||
login: 'Logged in',
|
||||
logout: 'Logged out',
|
||||
activated: 'Activated',
|
||||
deactivated: 'Deactivated',
|
||||
switched: 'Switched',
|
||||
};
|
||||
const typeLabel = (t) => TYPE_LABELS[t] || (t ? t.charAt(0).toUpperCase() + t.slice(1) : '—');
|
||||
const actionLabel = (a) => ACTION_LABELS[a] || a;
|
||||
|
||||
const scope = ref('admin');
|
||||
const actionOptions = computed(() => (scope.value === 'user' ? USER_ACTIONS : ADMIN_ACTIONS));
|
||||
|
||||
const items = ref([]);
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
@@ -45,14 +82,13 @@ const totalPages = ref(1);
|
||||
const actionFilter = ref('');
|
||||
const objectFilter = ref('');
|
||||
const search = ref('');
|
||||
const selectedId = ref(null);
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
const params = new URLSearchParams({ scope: scope.value });
|
||||
if (actionFilter.value) params.set('action_type', actionFilter.value);
|
||||
if (objectFilter.value) params.set('object_type', objectFilter.value);
|
||||
if (scope.value === 'admin' && objectFilter.value) params.set('object_type', objectFilter.value);
|
||||
if (search.value.trim()) params.set('search', search.value.trim());
|
||||
params.set('page', page.value);
|
||||
const data = await api(`/activity-log?${params.toString()}`);
|
||||
@@ -71,83 +107,218 @@ watch([actionFilter, objectFilter, search], () => {
|
||||
});
|
||||
watch(page, load);
|
||||
|
||||
const selected = computed(() => items.value.find((i) => i.id === selectedId.value) || null);
|
||||
const setScope = (key) => {
|
||||
if (scope.value === key) return;
|
||||
scope.value = key;
|
||||
actionFilter.value = '';
|
||||
objectFilter.value = '';
|
||||
page.value = 1;
|
||||
load();
|
||||
};
|
||||
|
||||
const formatDate = (s) => (s ? new Date(s.replace(' ', 'T')).toLocaleString() : '');
|
||||
|
||||
const rowRange = computed(() => {
|
||||
if (!total.value) return '0';
|
||||
const start = (page.value - 1) * 20 + 1;
|
||||
const end = Math.min(page.value * 20, total.value);
|
||||
return `${start}–${end}`;
|
||||
});
|
||||
const goPage = (p) => {
|
||||
page.value = Math.min(Math.max(1, p), totalPages.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppListShell
|
||||
v-model:search="search"
|
||||
title="Activity Log"
|
||||
search-placeholder="Search activities…"
|
||||
:count-label="`${total} ITEMS`"
|
||||
:page="page"
|
||||
:has-prev="page > 1"
|
||||
:has-next="page < totalPages"
|
||||
detail-icon="dashicons-clock"
|
||||
detail-title="Activity Details"
|
||||
detail-text="Select an activity from the list to view detailed information."
|
||||
:has-selection="!!selected"
|
||||
@prev-page="page--"
|
||||
@next-page="page++"
|
||||
>
|
||||
<template #header-actions>
|
||||
<button type="button" @click="load">
|
||||
<main class="activity-page">
|
||||
<header class="activity-page__header">
|
||||
<div>
|
||||
<p class="activity-page__eyebrow">Aurora Admin</p>
|
||||
<h1>Activity Log</h1>
|
||||
</div>
|
||||
<button type="button" class="icon-button" title="Refresh" :disabled="loading" @click="load">
|
||||
<span class="dashicons dashicons-update" />
|
||||
</button>
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<template #filters>
|
||||
<select v-model="actionFilter" class="activity-log__select">
|
||||
<option v-for="[v, l] in ACTION_TYPES" :key="v" :value="v">{{ l }}</option>
|
||||
</select>
|
||||
<select v-model="objectFilter" class="activity-log__select">
|
||||
<option v-for="[v, l] in OBJECT_TYPES" :key="v" :value="v">{{ l }}</option>
|
||||
</select>
|
||||
</template>
|
||||
<div class="activity-page__tabs">
|
||||
<button
|
||||
v-for="tab in TABS"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
class="filter-pill"
|
||||
:class="{ 'is-active': scope === tab.key }"
|
||||
@click="setScope(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template #list>
|
||||
<p v-if="!loading && !items.length" style="font-size: 0.8125rem; color: var(--aurora-text-muted); padding: 20px 4px; text-align: center;">
|
||||
No activities found
|
||||
</p>
|
||||
<AppListItem
|
||||
v-for="i in items"
|
||||
:key="i.id"
|
||||
icon="dashicons-clock"
|
||||
:title="i.objectLabel || i.actionType"
|
||||
:subtitle="`${i.userName || 'System'} · ${formatDate(i.createdAt)}`"
|
||||
:active="i.id === selectedId"
|
||||
@click="selectedId = i.id"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #detail>
|
||||
<div v-if="selected" class="activity-detail">
|
||||
<h2>{{ selected.objectLabel || selected.actionType }}</h2>
|
||||
<dl>
|
||||
<dt>Action</dt><dd>{{ selected.actionType }}</dd>
|
||||
<dt>Object type</dt><dd>{{ selected.objectType }}</dd>
|
||||
<dt>User</dt><dd>{{ selected.userName || 'System' }}</dd>
|
||||
<dt>Date</dt><dd>{{ formatDate(selected.createdAt) }}</dd>
|
||||
</dl>
|
||||
<pre v-if="selected.details">{{ JSON.stringify(selected.details, null, 2) }}</pre>
|
||||
<section class="activity-page__toolbar" aria-label="Activity filters and search">
|
||||
<div class="activity-page__filters">
|
||||
<select v-model="actionFilter" class="activity-page__select">
|
||||
<option v-for="[v, l] in actionOptions" :key="v" :value="v">{{ l }}</option>
|
||||
</select>
|
||||
<select v-if="scope === 'admin'" v-model="objectFilter" class="activity-page__select">
|
||||
<option v-for="[v, l] in OBJECT_TYPES" :key="v" :value="v">{{ l }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
</AppListShell>
|
||||
<label class="activity-page__search">
|
||||
<span class="dashicons dashicons-search" />
|
||||
<input v-model="search" type="search" placeholder="Search activity…" />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="activity-table" aria-label="Activity Log">
|
||||
<div class="activity-table__head">
|
||||
<div>Date</div>
|
||||
<div>Author</div>
|
||||
<div>IP Address</div>
|
||||
<div>Type</div>
|
||||
<div>Action</div>
|
||||
<div>Description</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="activity-table__empty">Loading activity...</div>
|
||||
<div v-else-if="!items.length" class="activity-table__empty">No activity found.</div>
|
||||
|
||||
<article v-for="i in items" v-else :key="i.id" class="activity-row">
|
||||
<div class="activity-row__cell" data-label="Date">{{ formatDate(i.createdAt) }}</div>
|
||||
<div class="activity-row__cell activity-row__author" data-label="Author">
|
||||
<template v-if="i.userId">
|
||||
<img class="activity-row__avatar" :src="i.userAvatar" :alt="i.userName" width="32" height="32" />
|
||||
<div class="activity-row__author-info">
|
||||
<a :href="i.userEditUrl">{{ i.userName }}</a>
|
||||
<span v-if="i.userRole" class="activity-row__role">{{ i.userRole }}</span>
|
||||
<span v-if="i.userEmail" class="activity-row__email">{{ i.userEmail }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>System</span>
|
||||
</div>
|
||||
<div class="activity-row__cell" data-label="IP Address">{{ i.ipAddress || '—' }}</div>
|
||||
<div class="activity-row__cell" data-label="Type">
|
||||
<span class="type-badge">{{ typeLabel(i.objectType) }}</span>
|
||||
</div>
|
||||
<div class="activity-row__cell" data-label="Action">{{ actionLabel(i.actionType) }}</div>
|
||||
<div class="activity-row__cell activity-row__desc" data-label="Description">{{ i.objectLabel || '—' }}</div>
|
||||
</article>
|
||||
|
||||
<footer v-if="!loading && items.length" class="activity-table__footer">
|
||||
<div>{{ rowRange }} of {{ total }}</div>
|
||||
<div class="activity-table__pager">
|
||||
<button type="button" :disabled="page <= 1" @click="goPage(1)">
|
||||
<span class="dashicons dashicons-controls-skipback" />
|
||||
</button>
|
||||
<button type="button" :disabled="page <= 1" @click="goPage(page - 1)">
|
||||
<span class="dashicons dashicons-arrow-left-alt2" />
|
||||
</button>
|
||||
<span>Page {{ page }} of {{ totalPages }}</span>
|
||||
<button type="button" :disabled="page >= totalPages" @click="goPage(page + 1)">
|
||||
<span class="dashicons dashicons-arrow-right-alt2" />
|
||||
</button>
|
||||
<button type="button" :disabled="page >= totalPages" @click="goPage(totalPages)">
|
||||
<span class="dashicons dashicons-controls-skipforward" />
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.activity-log__select {
|
||||
padding: 5px 10px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.75rem;
|
||||
.activity-page { min-height: calc(100vh - 112px); padding: 0; color: var(--aurora-text); background: transparent; }
|
||||
.activity-page__header {
|
||||
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 18px;
|
||||
}
|
||||
.activity-detail h2 { margin: 0 0 16px; font-size: 1.1rem; color: var(--aurora-text); }
|
||||
.activity-detail dl { display: grid; grid-template-columns: 120px 1fr; gap: 8px 12px; font-size: 0.8125rem; margin: 0; }
|
||||
.activity-detail dt { color: var(--aurora-text-muted); }
|
||||
.activity-detail dd { margin: 0; color: var(--aurora-text); }
|
||||
.activity-detail pre {
|
||||
margin-top: 18px; padding: 12px; background: var(--aurora-bg-subtle);
|
||||
border-radius: 8px; font-size: 0.75rem; overflow-x: auto; color: var(--aurora-text);
|
||||
.activity-page__eyebrow {
|
||||
margin: 0 0 7px; color: var(--aurora-text-muted); font-size: 0.72rem;
|
||||
font-weight: 800; letter-spacing: 0; text-transform: uppercase;
|
||||
}
|
||||
.activity-page h1 { margin: 0; color: var(--aurora-text); font-size: 1.45rem; line-height: 1.15; }
|
||||
|
||||
.icon-button {
|
||||
width: 42px; min-height: 42px; border: 1px solid var(--aurora-border); border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text); background: var(--aurora-bg-subtle); display: inline-flex;
|
||||
align-items: center; justify-content: center; cursor: pointer;
|
||||
}
|
||||
.icon-button:hover:not(:disabled) { border-color: var(--aurora-text-muted); }
|
||||
.icon-button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.activity-page__tabs { display: flex; gap: 8px; margin-bottom: 14px; }
|
||||
.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 16px; font-size: 0.78rem; font-weight: 800; cursor: pointer;
|
||||
}
|
||||
.filter-pill.is-active { border-color: var(--aurora-text-muted); background: var(--aurora-frame-bg); color: var(--aurora-text); }
|
||||
|
||||
.activity-page__toolbar { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin-bottom: 12px; }
|
||||
.activity-page__filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.activity-page__select {
|
||||
padding: 7px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.78rem;
|
||||
}
|
||||
.activity-page__search {
|
||||
width: min(280px, 100%); min-height: 38px; border: 1px solid var(--aurora-border); border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle); display: flex; align-items: center; gap: 8px; padding: 0 12px; color: var(--aurora-text-muted);
|
||||
}
|
||||
.activity-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;
|
||||
}
|
||||
.activity-page__search input::placeholder { color: var(--aurora-text-muted); }
|
||||
|
||||
.activity-table { overflow: hidden; border: 1px solid var(--aurora-frame-border); border-radius: var(--aurora-radius-sm); background: var(--aurora-frame-bg); }
|
||||
.activity-table__head, .activity-row {
|
||||
display: grid;
|
||||
grid-template-columns: 190px minmax(160px, 0.8fr) 130px 110px 140px minmax(180px, 1.6fr);
|
||||
align-items: center;
|
||||
}
|
||||
.activity-table__head {
|
||||
min-height: 44px; border-bottom: 1px solid var(--aurora-border); color: var(--aurora-text);
|
||||
font-size: 0.79rem; font-weight: 850;
|
||||
}
|
||||
.activity-table__head > div { min-height: 44px; padding: 0 16px; display: flex; align-items: center; }
|
||||
.activity-row { min-height: 76px; border-bottom: 1px solid var(--aurora-border); }
|
||||
.activity-row:hover { background: var(--aurora-bg-subtle); }
|
||||
.activity-row__cell {
|
||||
min-width: 0; padding: 12px 16px; color: var(--aurora-text-muted); font-size: 0.8rem;
|
||||
line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.activity-row__desc { color: var(--aurora-text); white-space: normal; }
|
||||
|
||||
.activity-row__author { display: flex; align-items: center; gap: 10px; white-space: normal; }
|
||||
.activity-row__avatar { border-radius: 50%; flex-shrink: 0; display: block; }
|
||||
.activity-row__author-info { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
||||
.activity-row__author-info a {
|
||||
color: var(--aurora-accent); font-weight: 700; font-size: 0.8125rem; text-decoration: none;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.activity-row__author-info a:hover { text-decoration: underline; }
|
||||
.activity-row__role, .activity-row__email {
|
||||
font-size: 0.71rem; color: var(--aurora-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.type-badge {
|
||||
display: inline-block; border-radius: 999px; background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||||
padding: 2px 10px; font-size: 0.68rem; font-weight: 850; box-shadow: inset 0 0 0 1px var(--aurora-border);
|
||||
}
|
||||
.activity-table__empty { padding: 38px 18px; color: var(--aurora-text-muted); font-size: 0.85rem; text-align: center; }
|
||||
.activity-table__footer {
|
||||
min-height: 50px; border-top: 1px solid var(--aurora-border); display: flex; align-items: center;
|
||||
justify-content: flex-end; gap: 26px; padding: 0 16px; color: var(--aurora-text-muted); font-size: 0.8rem;
|
||||
}
|
||||
.activity-table__pager { display: flex; align-items: center; gap: 8px; }
|
||||
.activity-table__pager button {
|
||||
width: 28px; height: 28px; border: 0; border-radius: var(--aurora-radius-sm);
|
||||
background: transparent; color: var(--aurora-text-muted); cursor: pointer;
|
||||
}
|
||||
.activity-table__pager button:not(:disabled):hover { background: var(--aurora-bg-subtle); color: var(--aurora-text); }
|
||||
.activity-table__pager button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.activity-table__head { display: none; }
|
||||
.activity-row { grid-template-columns: 1fr; padding: 10px 0; }
|
||||
.activity-row__cell { white-space: normal; }
|
||||
.activity-row__cell::before { content: attr(data-label) ': '; font-weight: 700; color: var(--aurora-text); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -190,11 +190,11 @@ const remove = async () => {
|
||||
.notice-editor__row { display: flex; gap: 10px; }
|
||||
.notice-editor__title {
|
||||
flex: 1; font-size: 1.05rem; font-weight: 700; padding: 8px 10px;
|
||||
border: 1px solid var(--aurora-border); border-radius: 8px;
|
||||
border: 1px solid var(--aurora-border); border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
||||
}
|
||||
.notice-editor select, .notice-editor input[type='text'], .notice-editor textarea {
|
||||
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
padding: 8px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.8125rem;
|
||||
font-family: inherit; width: 100%; box-sizing: border-box;
|
||||
}
|
||||
@@ -208,7 +208,7 @@ const remove = async () => {
|
||||
.notice-editor__toggles label { display: flex; align-items: center; gap: 6px; }
|
||||
.notice-editor__actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.notice-editor__actions button {
|
||||
border: none; border-radius: 8px; padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
||||
border: none; border-radius: var(--aurora-radius-sm); padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
||||
}
|
||||
.notice-editor__actions .is-primary { background: var(--aurora-accent); color: #fff; }
|
||||
|
||||
@@ -98,7 +98,7 @@ const send = async () => {
|
||||
.bug-report-page h1 { margin: 0; font-size: 1.6rem; font-weight: 700; color: var(--aurora-text); }
|
||||
.bug-report-page__sub { margin: 8px 0 0; font-size: 0.875rem; color: var(--aurora-text-muted); }
|
||||
|
||||
.bug-report-notice { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.bug-report-notice { padding: 10px 14px; border-radius: var(--aurora-radius-sm); margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.bug-report-notice--success { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.bug-report-notice--error { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
|
||||
@@ -106,14 +106,14 @@ const send = async () => {
|
||||
.bug-report-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.bug-report-field span { font-size: 0.8125rem; font-weight: 600; color: var(--aurora-text); }
|
||||
.bug-report-field textarea {
|
||||
padding: 10px 12px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
padding: 10px 12px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.875rem;
|
||||
font-family: inherit; resize: vertical;
|
||||
}
|
||||
|
||||
.bug-report-diagnostics {
|
||||
background: var(--aurora-bg-subtle); border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px; padding: 14px 16px;
|
||||
border-radius: var(--aurora-radius-md); padding: 14px 16px;
|
||||
}
|
||||
.bug-report-diagnostics h2 {
|
||||
margin: 0 0 10px; font-size: 0.75rem; text-transform: uppercase;
|
||||
|
||||
@@ -398,7 +398,7 @@ const rowRange = computed(() => {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -423,7 +423,7 @@ const rowRange = computed(() => {
|
||||
width: 42px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
@@ -451,7 +451,7 @@ const rowRange = computed(() => {
|
||||
.comments-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
@@ -460,7 +460,7 @@ const rowRange = computed(() => {
|
||||
.comments-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.comments-table__head,
|
||||
@@ -636,7 +636,7 @@ const rowRange = computed(() => {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -176,7 +176,7 @@ onMounted(() => reload(range.value[0], range.value[1]));
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
color: var(--aurora-text);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 8px 12px;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
@@ -186,7 +186,7 @@ onMounted(() => reload(range.value[0], range.value[1]));
|
||||
position: absolute; right: 0; top: calc(100% + 6px);
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 6px;
|
||||
z-index: 10;
|
||||
min-width: 160px;
|
||||
@@ -195,14 +195,14 @@ onMounted(() => reload(range.value[0], range.value[1]));
|
||||
.dash__presets button {
|
||||
display: block; width: 100%; text-align: left;
|
||||
background: none; border: none; color: var(--aurora-text);
|
||||
padding: 8px 10px; border-radius: 6px; cursor: pointer; font-size: 0.8125rem;
|
||||
padding: 8px 10px; border-radius: var(--aurora-radius-sm); cursor: pointer; font-size: 0.8125rem;
|
||||
}
|
||||
.dash__presets button:hover { background: var(--aurora-bg-subtle); }
|
||||
.dash__tabs {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 3px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -214,7 +214,7 @@ onMounted(() => reload(range.value[0], range.value[1]));
|
||||
}
|
||||
.dash__tab {
|
||||
border: none; background: none; cursor: pointer;
|
||||
padding: 6px 16px; border-radius: 8px;
|
||||
padding: 6px 16px; border-radius: var(--aurora-radius-sm);
|
||||
font-size: 0.8125rem; font-weight: 500;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
.ma__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.ma__box {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ const onDayClick = (cell) => {
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ const onDayClick = (cell) => {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
@@ -156,7 +156,7 @@ const onDayClick = (cell) => {
|
||||
background: var(--aurora-card-bg, #1f1f23);
|
||||
color: var(--aurora-text, #f4f4f5);
|
||||
border: 1px solid var(--aurora-border, #2e2e33);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 12px 18px;
|
||||
font-size: 0.875rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
|
||||
@@ -58,7 +58,7 @@ defineProps({ data: { type: Object, default: () => ({}) } });
|
||||
.sh__versions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
|
||||
.sh__ver {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.sh__ver-label { font-size: 0.6875rem; color: var(--aurora-text-muted); }
|
||||
|
||||
@@ -31,12 +31,12 @@ defineProps({
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
/* Deeper than the generic --aurora-card-bg — the Dashboard is now
|
||||
wrapped in its own frame (windowChrome.js's .aurora-dashboard-frame,
|
||||
wrapped in its own frame (dashboardFrame.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;
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ defineProps({
|
||||
<style scoped>
|
||||
.pbar {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.pbar__top {
|
||||
|
||||
@@ -24,7 +24,7 @@ defineProps({
|
||||
}
|
||||
.statrow--boxed {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 16px 8px;
|
||||
}
|
||||
.statrow__value {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const contentType = ref('all');
|
||||
|
||||
const filters = reactive({
|
||||
category: '',
|
||||
post_author: '',
|
||||
post_start_date: '',
|
||||
post_end_date: '',
|
||||
post_status: '',
|
||||
page_author: '',
|
||||
page_start_date: '',
|
||||
page_end_date: '',
|
||||
page_status: '',
|
||||
attachment_start_date: '',
|
||||
attachment_end_date: '',
|
||||
});
|
||||
|
||||
// Standard WP post statuses (matches core's own get_post_stati({internal: false})).
|
||||
const STATUSES = [
|
||||
{ value: '', label: 'All' },
|
||||
{ value: 'publish', label: 'Published' },
|
||||
{ value: 'future', label: 'Scheduled' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
{ value: 'pending', label: 'Pending Review' },
|
||||
{ value: 'private', label: 'Private' },
|
||||
{ value: 'trash', label: 'Trash' },
|
||||
];
|
||||
|
||||
const categories = ref([]);
|
||||
const authors = ref([]);
|
||||
const loadingOptions = ref(true);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const headers = { 'X-WP-Nonce': props.serverData.restNonce };
|
||||
const [catsRes, usersRes] = await Promise.all([
|
||||
fetch(`${props.serverData.restUrl}wp/v2/categories?per_page=100`, { headers }),
|
||||
fetch(`${props.serverData.restUrl}wp/v2/users?per_page=100`, { headers }),
|
||||
]);
|
||||
categories.value = catsRes.ok ? await catsRes.json() : [];
|
||||
authors.value = usersRes.ok ? await usersRes.json() : [];
|
||||
} finally {
|
||||
loadingOptions.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Building and navigating to a real export.php?download=true&... URL (not a
|
||||
// fetch) so the browser's normal file-download flow handles the response —
|
||||
// this is the exact same request native WordPress's own form would submit,
|
||||
// so the file it returns is unmodified.
|
||||
//
|
||||
// Every relevant param is always sent, even when blank. Native's own HTML
|
||||
// <select> elements always submit a name=value pair regardless of which
|
||||
// option is picked, and export.php's own PHP reads $_GET['cat'] etc.
|
||||
// directly with no isset() guard — omitting a param when "All" is selected
|
||||
// (as this used to) triggers PHP "Undefined array key" warnings that, with
|
||||
// display_errors on (any site with debugging enabled), get echoed straight
|
||||
// into the downloaded file before the XML, corrupting it.
|
||||
const download = () => {
|
||||
const params = new URLSearchParams({ download: 'true', content: contentType.value });
|
||||
|
||||
if (contentType.value === 'posts') {
|
||||
params.set('cat', filters.category);
|
||||
params.set('post_author', filters.post_author);
|
||||
params.set('post_start_date', filters.post_start_date);
|
||||
params.set('post_end_date', filters.post_end_date);
|
||||
params.set('post_status', filters.post_status);
|
||||
} else if (contentType.value === 'pages') {
|
||||
params.set('page_author', filters.page_author);
|
||||
params.set('page_start_date', filters.page_start_date);
|
||||
params.set('page_end_date', filters.page_end_date);
|
||||
params.set('page_status', filters.page_status);
|
||||
} else if (contentType.value === 'attachment') {
|
||||
params.set('attachment_start_date', filters.attachment_start_date);
|
||||
params.set('attachment_end_date', filters.attachment_end_date);
|
||||
}
|
||||
|
||||
window.location.href = `${props.serverData.exportUrl}?${params.toString()}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="export-page">
|
||||
<header class="export-page__header">
|
||||
<p class="export-page__eyebrow">Aurora Admin</p>
|
||||
<h1>Export</h1>
|
||||
<p class="export-page__lead">
|
||||
Create an XML file (WordPress eXtended RSS, or WXR) containing your posts, pages, comments,
|
||||
custom fields, categories, and tags. Import it into another WordPress site to bring this
|
||||
content over.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="export-page__card">
|
||||
<h2>Choose what to export</h2>
|
||||
|
||||
<div class="export-page__options">
|
||||
<label class="export-page__radio">
|
||||
<input type="radio" value="all" v-model="contentType" />
|
||||
<div>
|
||||
<strong>All content</strong>
|
||||
<p>All of your posts, pages, comments, custom fields, terms, navigation menus, and custom posts.</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="export-page__radio">
|
||||
<input type="radio" value="posts" v-model="contentType" />
|
||||
<strong>Posts</strong>
|
||||
</label>
|
||||
<div v-if="contentType === 'posts'" class="export-page__filters">
|
||||
<label>
|
||||
Category
|
||||
<select v-model="filters.category" :disabled="loadingOptions">
|
||||
<option value="">All</option>
|
||||
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Author
|
||||
<select v-model="filters.post_author" :disabled="loadingOptions">
|
||||
<option value="">All</option>
|
||||
<option v-for="a in authors" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Start date <input type="date" v-model="filters.post_start_date" /></label>
|
||||
<label>End date <input type="date" v-model="filters.post_end_date" /></label>
|
||||
<label>
|
||||
Status
|
||||
<select v-model="filters.post_status">
|
||||
<option v-for="s in STATUSES" :key="s.value" :value="s.value">{{ s.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="export-page__radio">
|
||||
<input type="radio" value="pages" v-model="contentType" />
|
||||
<strong>Pages</strong>
|
||||
</label>
|
||||
<div v-if="contentType === 'pages'" class="export-page__filters">
|
||||
<label>
|
||||
Author
|
||||
<select v-model="filters.page_author" :disabled="loadingOptions">
|
||||
<option value="">All</option>
|
||||
<option v-for="a in authors" :key="a.id" :value="a.id">{{ a.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Start date <input type="date" v-model="filters.page_start_date" /></label>
|
||||
<label>End date <input type="date" v-model="filters.page_end_date" /></label>
|
||||
<label>
|
||||
Status
|
||||
<select v-model="filters.page_status">
|
||||
<option v-for="s in STATUSES" :key="s.value" :value="s.value">{{ s.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="export-page__radio">
|
||||
<input type="radio" value="attachment" v-model="contentType" />
|
||||
<strong>Media</strong>
|
||||
</label>
|
||||
<div v-if="contentType === 'attachment'" class="export-page__filters">
|
||||
<label>Start date <input type="date" v-model="filters.attachment_start_date" /></label>
|
||||
<label>End date <input type="date" v-model="filters.attachment_end_date" /></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="export-page__submit" @click="download">Download Export File</button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.export-page { min-height: calc(100vh - 112px); color: var(--aurora-text); background: transparent; }
|
||||
.export-page__eyebrow {
|
||||
margin: 0 0 7px; color: var(--aurora-text-muted); font-size: 0.72rem;
|
||||
font-weight: 800; text-transform: uppercase;
|
||||
}
|
||||
.export-page__header h1 { margin: 0; font-size: 1.45rem; line-height: 1.15; }
|
||||
.export-page__lead {
|
||||
margin: 12px 0 0; max-width: 640px; color: var(--aurora-text-muted);
|
||||
font-size: 0.875rem; line-height: 1.6;
|
||||
}
|
||||
|
||||
.export-page__card {
|
||||
margin-top: 24px; background: var(--aurora-card-bg); border: 1px solid var(--aurora-card-border);
|
||||
border-radius: var(--aurora-radius-lg); padding: 22px 24px; max-width: 560px;
|
||||
}
|
||||
.export-page__card h2 { margin: 0 0 16px; font-size: 1rem; font-weight: 700; }
|
||||
|
||||
.export-page__options { display: flex; flex-direction: column; gap: 4px; }
|
||||
.export-page__radio {
|
||||
display: flex; align-items: flex-start; gap: 10px; padding: 8px 4px;
|
||||
cursor: pointer; font-size: 0.875rem;
|
||||
}
|
||||
.export-page__radio input { margin-top: 3px; accent-color: var(--aurora-accent); }
|
||||
.export-page__radio p { margin: 4px 0 0; font-size: 0.8125rem; color: var(--aurora-text-muted); font-weight: 400; }
|
||||
|
||||
.export-page__filters {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
|
||||
margin: 0 0 8px 30px; padding: 12px; background: var(--aurora-bg-subtle);
|
||||
border-radius: var(--aurora-radius-md);
|
||||
}
|
||||
.export-page__filters label {
|
||||
display: flex; flex-direction: column; gap: 4px; font-size: 0.75rem;
|
||||
color: var(--aurora-text-muted); font-weight: 600;
|
||||
}
|
||||
.export-page__filters select, .export-page__filters input[type='date'] {
|
||||
padding: 6px 8px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-card-bg); color: var(--aurora-text); font-size: 0.8125rem; font-weight: 400;
|
||||
}
|
||||
|
||||
.export-page__submit {
|
||||
margin-top: 20px; border: none; border-radius: var(--aurora-radius-sm); padding: 10px 20px;
|
||||
font-size: 0.875rem; font-weight: 600; cursor: pointer; background: var(--aurora-accent); color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const api = (path, options = {}) =>
|
||||
fetch(`${props.serverData.restUrl}aurora-admin/v1${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': props.serverData.restNonce,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
}).then(async (r) => {
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(data.message || 'Request failed');
|
||||
return data;
|
||||
});
|
||||
|
||||
// Local, mutable copy so a row's status/action can update in place once
|
||||
// installed/activated, without needing a full page reload.
|
||||
const importers = reactive(props.serverData.importers.map((i) => ({ ...i, busy: false, error: '' })));
|
||||
|
||||
const goToImporter = (importer) => {
|
||||
window.location.href = importer.runUrl;
|
||||
};
|
||||
|
||||
const activate = async (importer) => {
|
||||
importer.busy = true;
|
||||
importer.error = '';
|
||||
try {
|
||||
await api('/plugins/activate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file: importer.pluginFile }),
|
||||
});
|
||||
goToImporter(importer);
|
||||
} catch (e) {
|
||||
importer.error = e.message || 'Could not activate the importer.';
|
||||
} finally {
|
||||
importer.busy = false;
|
||||
}
|
||||
};
|
||||
|
||||
const installAndActivate = async (importer) => {
|
||||
importer.busy = true;
|
||||
importer.error = '';
|
||||
try {
|
||||
// Reuses WordPress core's own admin-ajax "install-plugin" action — the
|
||||
// exact same request native's "Install Now" button triggers — rather
|
||||
// than reimplementing plugin installation.
|
||||
const body = new URLSearchParams({
|
||||
action: 'install-plugin',
|
||||
slug: importer.slug,
|
||||
_ajax_nonce: props.serverData.installNonce,
|
||||
});
|
||||
const res = await fetch(props.serverData.ajaxUrl, { method: 'POST', body });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data.success) {
|
||||
throw new Error(data.data?.errorMessage || 'Installation failed.');
|
||||
}
|
||||
|
||||
// Find the freshly-installed plugin's file path (rather than assuming a
|
||||
// naming convention) via Aurora's own plugins list, then activate it
|
||||
// through the same route the Plugins screen uses.
|
||||
const list = await api('/plugins');
|
||||
const installed = (Array.isArray(list) ? list : []).find((p) => p.slug === importer.slug);
|
||||
if (!installed) {
|
||||
throw new Error('Installed, but the plugin could not be found afterward.');
|
||||
}
|
||||
await api('/plugins/activate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file: installed.file }),
|
||||
});
|
||||
goToImporter(importer);
|
||||
} catch (e) {
|
||||
importer.error = e.message || 'Could not install the importer.';
|
||||
} finally {
|
||||
importer.busy = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="import-page">
|
||||
<header class="import-page__header">
|
||||
<p class="import-page__eyebrow">Aurora Admin</p>
|
||||
<h1>Import</h1>
|
||||
<p class="import-page__lead">
|
||||
If you have posts or comments in another system, WordPress can import those into this
|
||||
site. Choose a system to import from below.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="import-page__list">
|
||||
<article v-for="imp in importers" :key="imp.id" class="import-card">
|
||||
<div class="import-card__info">
|
||||
<h3>{{ imp.name }}</h3>
|
||||
<p>{{ imp.description }}</p>
|
||||
<p v-if="imp.error" class="import-card__error">{{ imp.error }}</p>
|
||||
</div>
|
||||
<div class="import-card__action">
|
||||
<a v-if="imp.status === 'active'" class="import-card__btn" @click.prevent="goToImporter(imp)" href="#">
|
||||
Run Importer
|
||||
</a>
|
||||
<button
|
||||
v-else-if="imp.status === 'installed_inactive'"
|
||||
type="button"
|
||||
class="import-card__btn"
|
||||
:disabled="imp.busy"
|
||||
@click="activate(imp)"
|
||||
>
|
||||
{{ imp.busy ? 'Activating…' : 'Run Importer' }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="import-card__btn"
|
||||
:disabled="imp.busy"
|
||||
@click="installAndActivate(imp)"
|
||||
>
|
||||
{{ imp.busy ? 'Installing…' : 'Install Now' }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p v-if="!importers.length" class="import-page__empty">No importers are available.</p>
|
||||
</div>
|
||||
|
||||
<p v-if="serverData.canInstallPlugins" class="import-page__more">
|
||||
If the importer you need is not listed,
|
||||
<a :href="serverData.pluginSearchUrl">search the plugin directory</a>
|
||||
to see if an importer is available.
|
||||
</p>
|
||||
|
||||
<!-- Rendered from WordPress's own `import_filters` hook — whatever other
|
||||
active plugins add there. Not Aurora-authored content, only
|
||||
Aurora-styled, so nothing another plugin adds here disappears. -->
|
||||
<div v-if="serverData.nativeFiltersHtml" class="import-page__native" v-html="serverData.nativeFiltersHtml" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.import-page { min-height: calc(100vh - 112px); color: var(--aurora-text); background: transparent; }
|
||||
.import-page__eyebrow {
|
||||
margin: 0 0 7px; color: var(--aurora-text-muted); font-size: 0.72rem;
|
||||
font-weight: 800; text-transform: uppercase;
|
||||
}
|
||||
.import-page__header h1 { margin: 0; font-size: 1.45rem; line-height: 1.15; }
|
||||
.import-page__lead {
|
||||
margin: 12px 0 0; max-width: 640px; color: var(--aurora-text-muted);
|
||||
font-size: 0.875rem; line-height: 1.6;
|
||||
}
|
||||
|
||||
.import-page__list { margin-top: 24px; display: flex; flex-direction: column; gap: 10px; max-width: 720px; }
|
||||
.import-card {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 20px;
|
||||
background: var(--aurora-card-bg); border: 1px solid var(--aurora-card-border);
|
||||
border-radius: var(--aurora-radius-lg); padding: 16px 20px;
|
||||
}
|
||||
.import-card__info h3 { margin: 0 0 4px; font-size: 0.9375rem; font-weight: 700; }
|
||||
.import-card__info p { margin: 0; font-size: 0.8125rem; color: var(--aurora-text-muted); line-height: 1.5; }
|
||||
.import-card__error { color: #e5484d !important; margin-top: 6px !important; }
|
||||
.import-card__action { flex-shrink: 0; }
|
||||
.import-card__btn {
|
||||
display: inline-block; border: none; border-radius: var(--aurora-radius-sm); padding: 8px 16px;
|
||||
font-size: 0.8125rem; font-weight: 600; cursor: pointer; background: var(--aurora-accent);
|
||||
color: #fff; text-decoration: none; white-space: nowrap;
|
||||
}
|
||||
.import-card__btn:disabled { opacity: 0.6; cursor: default; }
|
||||
|
||||
.import-page__empty { color: var(--aurora-text-muted); font-size: 0.875rem; }
|
||||
.import-page__more { margin-top: 18px; font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.import-page__more a { color: var(--aurora-accent); }
|
||||
|
||||
.import-page__native { margin-top: 18px; }
|
||||
</style>
|
||||
@@ -122,7 +122,7 @@ const clearDone = () => {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 10px; padding: 48px 24px; text-align: center; cursor: pointer;
|
||||
border: 2px dashed var(--aurora-border);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
color: var(--aurora-text-muted);
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ const clearDone = () => {
|
||||
|
||||
.media-upload-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 14px; border-radius: 8px;
|
||||
padding: 10px 14px; border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
margin-bottom: 6px; font-size: 0.875rem;
|
||||
|
||||
@@ -479,7 +479,7 @@ const typeIcon = (mimeType) => {
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-accent);
|
||||
color: var(--aurora-accent-text);
|
||||
font-size: 0.84rem;
|
||||
@@ -529,7 +529,7 @@ const typeIcon = (mimeType) => {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -554,7 +554,7 @@ const typeIcon = (mimeType) => {
|
||||
width: 42px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
@@ -581,7 +581,7 @@ const typeIcon = (mimeType) => {
|
||||
.media-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
@@ -590,7 +590,7 @@ const typeIcon = (mimeType) => {
|
||||
.media-grid__empty {
|
||||
padding: 60px 18px;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.85rem;
|
||||
@@ -603,7 +603,7 @@ const typeIcon = (mimeType) => {
|
||||
}
|
||||
.media-card {
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-frame-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -688,7 +688,7 @@ const typeIcon = (mimeType) => {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
cursor: pointer;
|
||||
@@ -733,7 +733,7 @@ const typeIcon = (mimeType) => {
|
||||
width: 220px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.media-page__main {
|
||||
@@ -753,7 +753,7 @@ const typeIcon = (mimeType) => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: none;
|
||||
padding: 6px 6px;
|
||||
color: var(--aurora-text);
|
||||
@@ -815,7 +815,7 @@ const typeIcon = (mimeType) => {
|
||||
min-width: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
padding: 4px;
|
||||
@@ -826,7 +826,7 @@ const typeIcon = (mimeType) => {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
text-align: left;
|
||||
|
||||
@@ -129,7 +129,7 @@ const runFromMenu = (fn) => {
|
||||
.mf-tree { list-style: none; margin: 0; padding-left: 14px; }
|
||||
.mf-tree:first-of-type { padding-left: 0; }
|
||||
.mf-tree__row {
|
||||
display: flex; align-items: center; gap: 2px; border-radius: 6px;
|
||||
display: flex; align-items: center; gap: 2px; border-radius: var(--aurora-radius-sm);
|
||||
}
|
||||
.mf-tree__row:hover { background: var(--aurora-bg-subtle); }
|
||||
.mf-tree__row--active { background: var(--aurora-accent-soft); }
|
||||
@@ -163,7 +163,7 @@ const runFromMenu = (fn) => {
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
@@ -175,7 +175,7 @@ const runFromMenu = (fn) => {
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.8rem;
|
||||
text-align: left;
|
||||
|
||||
@@ -37,10 +37,12 @@ const load = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
load();
|
||||
api('/menus/native-items').then((items) => (nativeItems.value = items));
|
||||
roleOptions.value = props.serverData.roles || [];
|
||||
// The native top-level menu is provided in serverData (captured server-side
|
||||
// at page load); the REST route can't build the admin menu.
|
||||
nativeItems.value = props.serverData.nativeItems || [];
|
||||
});
|
||||
|
||||
const filteredMenus = computed(() => {
|
||||
@@ -55,48 +57,84 @@ const filteredMenus = computed(() => {
|
||||
|
||||
const selected = computed(() => menus.value.find((m) => m.id === selectedId.value) || null);
|
||||
|
||||
const draft = reactive({ name: '', status: 'draft', roles: [], items: [] });
|
||||
const draft = reactive({ name: '', status: 'draft', roles: [] });
|
||||
|
||||
// The full ordered list of top-level items being edited. Row order IS the
|
||||
// menu order (saved as each item's `order` index). Each row carries its
|
||||
// native title/icon plus the per-item overrides (custom label, hidden).
|
||||
const orderedItems = ref([]);
|
||||
|
||||
// Merge a config's saved items with the live native top-level list: saved
|
||||
// items first, in their saved order, then any native items the config
|
||||
// doesn't mention yet (e.g. a newly installed plugin) appended at the end,
|
||||
// so new menu items are never silently dropped.
|
||||
const buildOrdered = (savedItems = []) => {
|
||||
const byId = Object.fromEntries(nativeItems.value.map((n) => [n.native_id, n]));
|
||||
const saved = [...savedItems].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const s of saved) {
|
||||
const n = byId[s.native_id];
|
||||
if (!n) continue; // saved item no longer exists in the admin menu
|
||||
result.push({ native_id: n.native_id, title: n.title, icon: n.icon, label: s.label || '', hidden: !!s.hidden });
|
||||
seen.add(s.native_id);
|
||||
}
|
||||
for (const n of nativeItems.value) {
|
||||
if (seen.has(n.native_id)) continue;
|
||||
result.push({ native_id: n.native_id, title: n.title, icon: n.icon, label: '', hidden: false });
|
||||
}
|
||||
orderedItems.value = result;
|
||||
};
|
||||
|
||||
const selectMenu = (menu) => {
|
||||
selectedId.value = menu.id;
|
||||
Object.assign(draft, {
|
||||
name: menu.name,
|
||||
status: menu.status,
|
||||
roles: [...menu.roles],
|
||||
items: menu.items.map((i) => ({ ...i })),
|
||||
});
|
||||
Object.assign(draft, { name: menu.name, status: menu.status, roles: [...menu.roles] });
|
||||
buildOrdered(menu.items);
|
||||
};
|
||||
|
||||
const isCreating = ref(false);
|
||||
const newMenu = () => {
|
||||
selectedId.value = null;
|
||||
isCreating.value = true;
|
||||
Object.assign(draft, { name: 'New menu', status: 'draft', roles: [], items: [] });
|
||||
Object.assign(draft, { name: 'New menu', status: 'draft', roles: [] });
|
||||
buildOrdered([]);
|
||||
};
|
||||
|
||||
const itemState = (nativeId) => draft.items.find((i) => i.native_id === nativeId);
|
||||
const toggleHidden = (nativeId) => {
|
||||
let item = itemState(nativeId);
|
||||
if (!item) {
|
||||
item = { native_id: nativeId, label: '', hidden: false, order: 0 };
|
||||
draft.items.push(item);
|
||||
}
|
||||
item.hidden = !item.hidden;
|
||||
// Reorder controls: drag-and-drop plus keyboard-friendly up/down buttons.
|
||||
const dragIndex = ref(-1);
|
||||
const onDragStart = (i) => { dragIndex.value = i; };
|
||||
const onDrop = (i) => {
|
||||
const from = dragIndex.value;
|
||||
dragIndex.value = -1;
|
||||
if (from < 0 || from === i) return;
|
||||
const arr = orderedItems.value;
|
||||
const [moved] = arr.splice(from, 1);
|
||||
arr.splice(i, 0, moved);
|
||||
};
|
||||
const setLabel = (nativeId, label) => {
|
||||
let item = itemState(nativeId);
|
||||
if (!item) {
|
||||
item = { native_id: nativeId, label: '', hidden: false, order: 0 };
|
||||
draft.items.push(item);
|
||||
}
|
||||
item.label = label;
|
||||
const move = (i, dir) => {
|
||||
const j = i + dir;
|
||||
const arr = orderedItems.value;
|
||||
if (j < 0 || j >= arr.length) return;
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
};
|
||||
|
||||
const saving = ref(false);
|
||||
const save = async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = { name: draft.name, status: draft.status, roles: draft.roles, items: draft.items };
|
||||
const payload = {
|
||||
name: draft.name,
|
||||
status: draft.status,
|
||||
roles: draft.roles,
|
||||
// Row order becomes each item's `order`; only send items that actually
|
||||
// override something (renamed or hidden) or that need an explicit order.
|
||||
items: orderedItems.value.map((it, index) => ({
|
||||
native_id: it.native_id,
|
||||
label: it.label || '',
|
||||
hidden: !!it.hidden,
|
||||
order: index,
|
||||
})),
|
||||
};
|
||||
if (isCreating.value) {
|
||||
const created = await api('/menus', { method: 'POST', body: JSON.stringify(payload) });
|
||||
await load();
|
||||
@@ -188,23 +226,29 @@ const toggleRole = (slug) => {
|
||||
</div>
|
||||
|
||||
<div class="menu-editor__section">
|
||||
<h3>Menu items</h3>
|
||||
<h3>Menu items — drag to reorder</h3>
|
||||
<div class="menu-editor__items">
|
||||
<div v-for="ni in nativeItems" :key="ni.native_id" class="menu-editor__item">
|
||||
<span class="dashicons" :class="ni.icon?.value || 'dashicons-admin-generic'" />
|
||||
<input
|
||||
type="text"
|
||||
:value="itemState(ni.native_id)?.label || ni.title"
|
||||
@input="setLabel(ni.native_id, $event.target.value)"
|
||||
/>
|
||||
<div
|
||||
v-for="(it, i) in orderedItems"
|
||||
:key="it.native_id"
|
||||
class="menu-editor__item"
|
||||
:class="{ 'is-hidden': it.hidden }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(i)"
|
||||
@dragover.prevent
|
||||
@drop="onDrop(i)"
|
||||
>
|
||||
<span class="menu-editor__drag dashicons dashicons-move" title="Drag to reorder" />
|
||||
<span class="dashicons" :class="it.icon?.value || 'dashicons-admin-generic'" />
|
||||
<input type="text" v-model="it.label" :placeholder="it.title" />
|
||||
<label class="menu-editor__hide">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!!itemState(ni.native_id)?.hidden"
|
||||
@change="toggleHidden(ni.native_id)"
|
||||
/>
|
||||
<input type="checkbox" v-model="it.hidden" />
|
||||
Hide
|
||||
</label>
|
||||
<div class="menu-editor__move">
|
||||
<button type="button" :disabled="i === 0" title="Move up" @click="move(i, -1)">▲</button>
|
||||
<button type="button" :disabled="i === orderedItems.length - 1" title="Move down" @click="move(i, 1)">▼</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,11 +269,11 @@ const toggleRole = (slug) => {
|
||||
.menu-editor__row { display: flex; gap: 10px; }
|
||||
.menu-editor__name {
|
||||
flex: 1; font-size: 1.1rem; font-weight: 700; padding: 8px 10px;
|
||||
border: 1px solid var(--aurora-border); border-radius: 8px;
|
||||
border: 1px solid var(--aurora-border); border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
||||
}
|
||||
.menu-editor__status {
|
||||
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
padding: 8px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
||||
}
|
||||
.menu-editor__section h3 {
|
||||
@@ -243,14 +287,24 @@ const toggleRole = (slug) => {
|
||||
display: flex; align-items: center; gap: 10px; padding: 6px 4px;
|
||||
border-bottom: 1px solid var(--aurora-frame-border);
|
||||
}
|
||||
.menu-editor__item.is-hidden { opacity: 0.5; }
|
||||
.menu-editor__drag { cursor: grab; color: var(--aurora-text-muted); flex-shrink: 0; }
|
||||
.menu-editor__drag:active { cursor: grabbing; }
|
||||
.menu-editor__item input[type='text'] {
|
||||
flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--aurora-border);
|
||||
flex: 1; padding: 6px 8px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.8125rem;
|
||||
}
|
||||
.menu-editor__hide { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: var(--aurora-text-muted); }
|
||||
.menu-editor__hide { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: var(--aurora-text-muted); flex-shrink: 0; }
|
||||
.menu-editor__move { display: flex; flex-direction: column; gap: 2px; flex-shrink: 0; }
|
||||
.menu-editor__move button {
|
||||
border: none; background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||||
border-radius: 4px; width: 20px; height: 15px; line-height: 1; font-size: 9px; cursor: pointer; padding: 0;
|
||||
}
|
||||
.menu-editor__move button:disabled { opacity: 0.35; cursor: default; }
|
||||
.menu-editor__move button:not(:disabled):hover { color: var(--aurora-accent); }
|
||||
.menu-editor__actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.menu-editor__actions button {
|
||||
border: none; border-radius: 8px; padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
||||
border: none; border-radius: var(--aurora-radius-sm); padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
||||
}
|
||||
.menu-editor__actions .is-primary { background: var(--aurora-accent); color: #fff; }
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import { companionPlugins } from './companionPlugins.js';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const api = (path, options = {}) =>
|
||||
fetch(`${props.serverData.restUrl}aurora-admin/v1${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': props.serverData.restNonce,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
}).then(async (r) => {
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(data.message || 'Request failed');
|
||||
return data;
|
||||
});
|
||||
|
||||
// Per-plugin UI state, keyed by slug: { status: {installed,active}|null, busy, error }
|
||||
const state = reactive(
|
||||
Object.fromEntries(companionPlugins.map((p) => [p.slug, { status: null, busy: false, error: '' }]))
|
||||
);
|
||||
|
||||
const loadStatus = async (plugin) => {
|
||||
try {
|
||||
state[plugin.slug].status = await api(plugin.statusEndpoint);
|
||||
} catch {
|
||||
state[plugin.slug].status = { installed: false, active: false };
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
companionPlugins.forEach(loadStatus);
|
||||
});
|
||||
|
||||
const install = async (plugin) => {
|
||||
const s = state[plugin.slug];
|
||||
if (s.busy) return;
|
||||
s.busy = true;
|
||||
s.error = '';
|
||||
try {
|
||||
const result = await api(plugin.installEndpoint, { method: 'POST' });
|
||||
if (!result.success) {
|
||||
s.error = result.message || 'Installation failed.';
|
||||
}
|
||||
await loadStatus(plugin);
|
||||
} catch (e) {
|
||||
s.error = e.message || 'Installation failed.';
|
||||
} finally {
|
||||
s.busy = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="modules">
|
||||
<header class="modules__head">
|
||||
<p class="modules__eyebrow">Aurora Admin</p>
|
||||
<h1>Modules</h1>
|
||||
<p class="modules__lead">
|
||||
Install additional modules to extend Aurora Admin. Each is a separate, standalone plugin —
|
||||
install and activate it here with one click.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="modules__grid">
|
||||
<article v-for="p in companionPlugins" :key="p.slug" class="module-card">
|
||||
<div class="module-card__top">
|
||||
<span class="module-card__icon dashicons" :class="p.icon" />
|
||||
<span
|
||||
v-if="state[p.slug].status"
|
||||
class="module-card__badge"
|
||||
:class="{ 'is-active': state[p.slug].status.active }"
|
||||
>
|
||||
{{ state[p.slug].status.active ? 'Active' : state[p.slug].status.installed ? 'Installed' : 'Not installed' }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="module-card__title">{{ p.label }}</h3>
|
||||
<p class="module-card__desc">{{ p.description }}</p>
|
||||
|
||||
<div class="module-card__action">
|
||||
<div v-if="!state[p.slug].status" class="module-card__checking">Checking…</div>
|
||||
<a v-else-if="state[p.slug].status.active" class="module-card__manage" :href="p.manageUrl">
|
||||
Open {{ p.label }} →
|
||||
</a>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="module-card__install"
|
||||
:disabled="state[p.slug].busy"
|
||||
@click="install(p)"
|
||||
>
|
||||
{{ state[p.slug].busy ? 'Installing…' : state[p.slug].status.installed ? 'Activate' : 'Install & Activate' }}
|
||||
</button>
|
||||
<p v-if="state[p.slug].error" class="module-card__error">{{ state[p.slug].error }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modules { min-height: calc(100vh - 112px); color: var(--aurora-text); background: transparent; }
|
||||
.modules__eyebrow {
|
||||
margin: 0 0 7px; color: var(--aurora-text-muted); font-size: 0.72rem;
|
||||
font-weight: 800; text-transform: uppercase;
|
||||
}
|
||||
.modules__head h1 { margin: 0; font-size: 1.45rem; line-height: 1.15; }
|
||||
.modules__lead { margin: 8px 0 0; color: var(--aurora-text-muted); font-size: 0.9rem; max-width: 640px; line-height: 1.6; }
|
||||
.modules__grid {
|
||||
margin-top: 24px; display: grid; gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
.module-card {
|
||||
background: var(--aurora-card-bg); border: 1px solid var(--aurora-card-border);
|
||||
border-radius: var(--aurora-radius-lg); padding: 20px 22px;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
.module-card__top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.module-card__icon { font-size: 26px; width: 26px; height: 26px; color: var(--aurora-accent); }
|
||||
.module-card__badge {
|
||||
font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
|
||||
color: var(--aurora-text-muted); background: var(--aurora-bg-subtle);
|
||||
border-radius: 999px; padding: 3px 10px;
|
||||
}
|
||||
.module-card__badge.is-active { color: var(--aurora-accent); }
|
||||
.module-card__title { margin: 4px 0 0; font-size: 1rem; font-weight: 700; }
|
||||
.module-card__desc { margin: 0; font-size: 0.8125rem; color: var(--aurora-text-muted); line-height: 1.55; flex: 1; }
|
||||
.module-card__action { margin-top: 6px; }
|
||||
.module-card__checking { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.module-card__manage { font-size: 0.8125rem; font-weight: 600; color: var(--aurora-accent); text-decoration: none; }
|
||||
.module-card__manage:hover { text-decoration: underline; }
|
||||
.module-card__install {
|
||||
border: none; border-radius: var(--aurora-radius-sm); padding: 9px 16px; font-size: 0.8125rem;
|
||||
font-weight: 600; cursor: pointer; background: var(--aurora-accent); color: #fff;
|
||||
}
|
||||
.module-card__install:disabled { opacity: 0.6; cursor: default; }
|
||||
.module-card__error { margin: 8px 0 0; font-size: 0.8125rem; color: #e5484d; }
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Aurora's standalone companion plugins, installable from the Modules page.
|
||||
* Single source of truth for both the card metadata and the REST endpoints
|
||||
* that back them (admin/src/Rest/CompanionPluginData.php) — a new companion
|
||||
* plugin only needs an entry here plus a matching PHP-side entry, no other
|
||||
* wiring.
|
||||
*/
|
||||
export const companionPlugins = [
|
||||
{
|
||||
slug: 'file-manager',
|
||||
icon: 'dashicons-portfolio',
|
||||
label: 'Aurora File Manager',
|
||||
description:
|
||||
'Browse, edit, and manage your site’s files (themes, plugins, uploads, and more) directly from wp-admin.',
|
||||
statusEndpoint: '/companion-plugins/file-manager/status',
|
||||
installEndpoint: '/companion-plugins/file-manager/install',
|
||||
manageUrl: 'admin.php?page=aurora-file-manager',
|
||||
},
|
||||
{
|
||||
slug: 'database-explorer',
|
||||
icon: 'dashicons-editor-table',
|
||||
label: 'Aurora Database Explorer',
|
||||
description:
|
||||
'A Supabase-style database browser inside wp-admin — see every table with row counts and sizes, browse paginated data, and run your own read-only queries.',
|
||||
statusEndpoint: '/companion-plugins/database-explorer/status',
|
||||
installEndpoint: '/companion-plugins/database-explorer/install',
|
||||
manageUrl: 'admin.php?page=aurora-database-explorer',
|
||||
},
|
||||
{
|
||||
slug: 'site-backup',
|
||||
icon: 'dashicons-migrate',
|
||||
label: 'Aurora Site Backup',
|
||||
description:
|
||||
'Full site backup (database + files) and restore-in-place, on demand or on a recurring schedule, stored locally or on your own S3-compatible or Google Drive remote storage.',
|
||||
statusEndpoint: '/companion-plugins/site-backup/status',
|
||||
installEndpoint: '/companion-plugins/site-backup/install',
|
||||
manageUrl: 'admin.php?page=aurora-site-backup',
|
||||
},
|
||||
];
|
||||
@@ -173,7 +173,7 @@ const save = async () => {
|
||||
.native-settings__saved { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.native-settings__save {
|
||||
background: var(--aurora-accent); color: #fff; border: none;
|
||||
border-radius: 9px; padding: 9px 20px; font-size: 0.875rem; font-weight: 500; cursor: pointer;
|
||||
border-radius: var(--aurora-radius-md); padding: 9px 20px; font-size: 0.875rem; font-weight: 500; cursor: pointer;
|
||||
}
|
||||
.native-settings__save:disabled { opacity: 0.6; cursor: default; }
|
||||
.native-settings__fields > :last-child { border-bottom: none; }
|
||||
@@ -210,6 +210,6 @@ const save = async () => {
|
||||
font-size: 0.8125rem;
|
||||
background: var(--aurora-bg-subtle);
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -361,21 +361,21 @@ const assignLocation = async (slug, menuId) => {
|
||||
}
|
||||
.nav-menus-page h1 { margin: 0; font-size: 1.6rem; font-weight: 700; color: var(--aurora-text); }
|
||||
|
||||
.nav-menus-notice { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.nav-menus-notice { padding: 10px 14px; border-radius: var(--aurora-radius-sm); margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.nav-menus-notice--success { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.nav-menus-notice--error { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
|
||||
.nav-menus-layout { display: grid; grid-template-columns: 260px 300px 1fr; gap: 20px; align-items: start; }
|
||||
.nav-menus-side, .nav-menus-add, .nav-menus-structure {
|
||||
background: var(--aurora-bg-subtle); border: 1px solid var(--aurora-border);
|
||||
border-radius: 14px; padding: 18px;
|
||||
border-radius: var(--aurora-radius-md); padding: 18px;
|
||||
}
|
||||
.nav-menus-side h2, .nav-menus-add h2, .nav-menus-structure h2 {
|
||||
font-size: 0.9375rem; margin: 0 0 10px; color: var(--aurora-text);
|
||||
}
|
||||
.nav-menus-list { list-style: none; margin: 0 0 14px; padding: 0; }
|
||||
.nav-menus-list__item {
|
||||
width: 100%; text-align: left; padding: 8px 10px; border-radius: 8px;
|
||||
width: 100%; text-align: left; padding: 8px 10px; border-radius: var(--aurora-radius-sm);
|
||||
border: none; background: none; color: var(--aurora-text-muted); cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -385,7 +385,7 @@ const assignLocation = async (slug, menuId) => {
|
||||
|
||||
.nav-menus-create { display: flex; gap: 6px; margin-bottom: 20px; }
|
||||
.nav-menus-create input {
|
||||
flex: 1; padding: 7px 10px; border-radius: 7px; border: 1px solid var(--aurora-border);
|
||||
flex: 1; padding: 7px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg); color: var(--aurora-text); font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
@@ -393,20 +393,20 @@ const assignLocation = async (slug, menuId) => {
|
||||
.nav-menus-location label { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.nav-menus-location select,
|
||||
.nav-menus-add__custom input {
|
||||
padding: 7px 10px; border-radius: 7px; border: 1px solid var(--aurora-border);
|
||||
padding: 7px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg); color: var(--aurora-text); font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.nav-menus-add__tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||
.nav-menus-add__tabs button {
|
||||
padding: 6px 10px; border-radius: 7px; border: 1px solid var(--aurora-border);
|
||||
padding: 6px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg); color: var(--aurora-text-muted); font-size: 0.75rem; cursor: pointer;
|
||||
}
|
||||
.nav-menus-add__tabs button.is-active { background: var(--aurora-accent-soft); color: var(--aurora-accent); border-color: transparent; }
|
||||
|
||||
.nav-menus-add__list { max-height: 320px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
|
||||
.nav-menus-add__row {
|
||||
display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px;
|
||||
display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: var(--aurora-radius-sm);
|
||||
font-size: 0.8125rem; color: var(--aurora-text);
|
||||
}
|
||||
.nav-menus-add__row:hover { background: var(--aurora-bg); }
|
||||
@@ -417,14 +417,14 @@ const assignLocation = async (slug, menuId) => {
|
||||
.nav-menus-structure__head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.nav-menus-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 8px 12px; border-radius: 8px; margin-bottom: 4px;
|
||||
padding: 8px 12px; border-radius: var(--aurora-radius-sm); margin-bottom: 4px;
|
||||
background: var(--aurora-bg); border: 1px solid var(--aurora-border);
|
||||
font-size: 0.8125rem; color: var(--aurora-text);
|
||||
}
|
||||
.nav-menus-item__actions { display: flex; gap: 2px; }
|
||||
.nav-menus-item__actions button {
|
||||
border: none; background: none; color: var(--aurora-text-muted); cursor: pointer;
|
||||
width: 26px; height: 26px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 26px; height: 26px; border-radius: var(--aurora-radius-sm); display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.nav-menus-item__actions button:hover { background: var(--aurora-bg-subtle); color: var(--aurora-text); }
|
||||
.nav-menus-item__actions button:disabled { opacity: 0.35; cursor: default; }
|
||||
|
||||
@@ -340,7 +340,7 @@ const rowRange = computed(() => {
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-accent);
|
||||
color: var(--aurora-accent-text);
|
||||
font-size: 0.84rem;
|
||||
@@ -390,7 +390,7 @@ const rowRange = computed(() => {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -415,7 +415,7 @@ const rowRange = computed(() => {
|
||||
width: 42px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
@@ -442,7 +442,7 @@ const rowRange = computed(() => {
|
||||
.pages-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
@@ -451,7 +451,7 @@ const rowRange = computed(() => {
|
||||
.pages-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.pages-table__head,
|
||||
@@ -559,7 +559,7 @@ const rowRange = computed(() => {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -81,6 +81,7 @@ const runAction = async (action, plugin) => {
|
||||
busyFile.value = plugin.file;
|
||||
error.value = '';
|
||||
notice.value = '';
|
||||
let redirecting = false;
|
||||
try {
|
||||
const data = await request(`/plugins/${action}`, {
|
||||
method: action === 'delete' ? 'DELETE' : 'POST',
|
||||
@@ -88,14 +89,21 @@ const runAction = async (action, plugin) => {
|
||||
});
|
||||
notice.value = data.message || 'Done';
|
||||
if (data.redirect) {
|
||||
redirecting = true;
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Action failed';
|
||||
} finally {
|
||||
busyFile.value = '';
|
||||
// Always re-sync the list with the real backend state, even after an
|
||||
// error: activate_plugin() can return a WP_Error for benign "unexpected
|
||||
// output" from a plugin's own activation while still leaving the plugin
|
||||
// active, which previously left the row showing its stale pre-click
|
||||
// state (you clicked Activate but it still read "deactivated"). Skipped
|
||||
// only when navigating away (deactivating Aurora itself redirects).
|
||||
if (!redirecting) await load();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -364,7 +372,7 @@ const versionLine = (plugin) => {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -388,7 +396,7 @@ const versionLine = (plugin) => {
|
||||
.primary-button,
|
||||
.icon-button {
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
min-height: 42px;
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
@@ -439,7 +447,7 @@ const versionLine = (plugin) => {
|
||||
.plugins-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
@@ -454,7 +462,7 @@ const versionLine = (plugin) => {
|
||||
.plugins-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -505,7 +513,7 @@ const versionLine = (plugin) => {
|
||||
.plugin-row__mark {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -342,7 +342,7 @@ const rowRange = computed(() => {
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-accent);
|
||||
color: var(--aurora-accent-text);
|
||||
font-size: 0.84rem;
|
||||
@@ -392,7 +392,7 @@ const rowRange = computed(() => {
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -417,7 +417,7 @@ const rowRange = computed(() => {
|
||||
width: 42px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
@@ -444,7 +444,7 @@ const rowRange = computed(() => {
|
||||
.posts-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
@@ -453,7 +453,7 @@ const rowRange = computed(() => {
|
||||
.posts-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.posts-table__head,
|
||||
@@ -561,7 +561,7 @@ const rowRange = computed(() => {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -172,7 +172,7 @@ const changePassword = async () => {
|
||||
.profile-page h1 { margin: 0; font-size: 1.6rem; font-weight: 700; color: var(--aurora-text); }
|
||||
.profile-empty { color: var(--aurora-text-muted); font-size: 0.8125rem; }
|
||||
|
||||
.profile-notice { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.profile-notice { padding: 10px 14px; border-radius: var(--aurora-radius-sm); margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.profile-notice--success { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.profile-notice--error { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
.profile-notice--info { background: var(--aurora-bg-subtle); color: var(--aurora-text-muted); }
|
||||
@@ -182,7 +182,7 @@ const changePassword = async () => {
|
||||
display: flex; flex-direction: column; gap: 18px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ const changePassword = async () => {
|
||||
.profile-field span { font-size: 0.8125rem; font-weight: 600; color: var(--aurora-text); }
|
||||
.profile-field input,
|
||||
.profile-field textarea {
|
||||
padding: 9px 12px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
padding: 9px 12px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg); color: var(--aurora-text); font-size: 0.875rem;
|
||||
font-family: inherit; resize: vertical;
|
||||
}
|
||||
@@ -199,7 +199,7 @@ const changePassword = async () => {
|
||||
.profile-password h2 { margin: 0 0 4px; font-size: 0.9375rem; color: var(--aurora-text); }
|
||||
.profile-password__row { display: flex; gap: 8px; }
|
||||
.profile-password__row input {
|
||||
flex: 1; padding: 9px 12px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
flex: 1; padding: 9px 12px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg); color: var(--aurora-text); font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -182,7 +182,7 @@ const remove = async () => {
|
||||
.role-editor { display: flex; flex-direction: column; gap: 18px; }
|
||||
.role-editor__row { display: flex; gap: 10px; }
|
||||
.role-editor__slug, .role-editor__name {
|
||||
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
padding: 8px 10px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text); font-size: 0.875rem;
|
||||
}
|
||||
.role-editor__name { flex: 1; font-weight: 700; }
|
||||
@@ -225,7 +225,7 @@ const remove = async () => {
|
||||
z-index: 20;
|
||||
width: 260px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
@@ -246,7 +246,7 @@ const remove = async () => {
|
||||
}
|
||||
.role-editor__actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.role-editor__actions button {
|
||||
border: none; border-radius: 8px; padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
||||
border: none; border-radius: var(--aurora-radius-sm); padding: 9px 18px; font-size: 0.875rem; cursor: pointer;
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text);
|
||||
}
|
||||
.role-editor__actions .is-primary { background: var(--aurora-accent); color: #fff; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { settingsCategories } from './settingsConfig.js';
|
||||
import { settingsGroups, settingsCategories } from './settingsConfig.js';
|
||||
import SettingsField from './components/SettingsField.vue';
|
||||
import { applyThemePalette, applyFontFamily } from './applyLiveTheme.js';
|
||||
|
||||
@@ -10,22 +10,6 @@ const props = defineProps({
|
||||
|
||||
const visibleCategories = settingsCategories;
|
||||
|
||||
// Shared REST fetcher for field types that need their own API calls beyond
|
||||
// the generic wp/v2/settings save flow (currently just 'companion-plugin').
|
||||
const api = (path, options = {}) =>
|
||||
fetch(`${props.serverData.restUrl}aurora-admin/v1${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'X-WP-Nonce': props.serverData.restNonce,
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
}).then(async (r) => {
|
||||
const body = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(body.message || 'Request failed');
|
||||
return body;
|
||||
});
|
||||
|
||||
// Working copy of the settings, seeded from the server + field defaults.
|
||||
const settings = reactive({ ...(props.serverData.settings || {}) });
|
||||
visibleCategories.forEach((cat) =>
|
||||
@@ -45,6 +29,29 @@ const activeCategory = computed(
|
||||
() => visibleCategories.find((c) => c.id === activeId.value) || visibleCategories[0]
|
||||
);
|
||||
|
||||
const groupOfCategory = (catId) =>
|
||||
settingsGroups.find((g) => g.categories.some((c) => c.id === catId));
|
||||
|
||||
// Collapsible nav groups. Default open; collapse state persisted per group.
|
||||
const OPEN_KEY = 'aurora-settings-groups-open';
|
||||
const loadOpen = () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(OPEN_KEY)) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
const openState = reactive(loadOpen());
|
||||
// Keep the active category's group open so its item is always reachable.
|
||||
const activeGroup = groupOfCategory(activeId.value);
|
||||
if (activeGroup) openState[activeGroup.id] = true;
|
||||
|
||||
const isGroupOpen = (id) => openState[id] !== false;
|
||||
const toggleGroup = (id) => {
|
||||
openState[id] = !isGroupOpen(id);
|
||||
localStorage.setItem(OPEN_KEY, JSON.stringify(openState));
|
||||
};
|
||||
|
||||
// The content panel scrolls independently of the nav (see .settings__content
|
||||
// below) so picking a category from a scrolled-down spot in the nav doesn't
|
||||
// require scrolling the whole page back up to see it. But the panel is one
|
||||
@@ -56,6 +63,8 @@ const activeCategory = computed(
|
||||
const contentEl = ref(null);
|
||||
const selectCategory = (id) => {
|
||||
activeId.value = id;
|
||||
const g = groupOfCategory(id);
|
||||
if (g) openState[g.id] = true;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('category', id);
|
||||
window.history.replaceState({}, '', url);
|
||||
@@ -64,15 +73,25 @@ const selectCategory = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Search filter over the nav.
|
||||
// Search filter over the nav — matches on group title, category title, or
|
||||
// subtitle; groups with no surviving categories drop out entirely.
|
||||
const query = ref('');
|
||||
const filteredCategories = computed(() => {
|
||||
const searching = computed(() => query.value.trim().length > 0);
|
||||
const filteredGroups = computed(() => {
|
||||
const q = query.value.trim().toLowerCase();
|
||||
if (!q) return visibleCategories;
|
||||
return visibleCategories.filter(
|
||||
(c) =>
|
||||
c.title.toLowerCase().includes(q) || c.subtitle.toLowerCase().includes(q)
|
||||
);
|
||||
if (!q) return settingsGroups;
|
||||
return settingsGroups
|
||||
.map((g) => {
|
||||
const groupMatch = g.title.toLowerCase().includes(q);
|
||||
const categories = g.categories.filter(
|
||||
(c) =>
|
||||
groupMatch ||
|
||||
c.title.toLowerCase().includes(q) ||
|
||||
c.subtitle.toLowerCase().includes(q)
|
||||
);
|
||||
return { ...g, categories };
|
||||
})
|
||||
.filter((g) => g.categories.length > 0);
|
||||
});
|
||||
|
||||
// Options for multiselect fields, resolved from server-provided lists.
|
||||
@@ -172,20 +191,37 @@ const importSettings = (e) => {
|
||||
<input v-model="query" type="text" placeholder="Search settings…" />
|
||||
</div>
|
||||
|
||||
<ul class="settings__list">
|
||||
<li v-for="cat in filteredCategories" :key="cat.id">
|
||||
<ul class="settings__groups">
|
||||
<li v-for="group in filteredGroups" :key="group.id" class="settings__group">
|
||||
<button
|
||||
type="button"
|
||||
class="settings__cat"
|
||||
:class="{ 'settings__cat--active': cat.id === activeId }"
|
||||
@click="selectCategory(cat.id)"
|
||||
class="settings__group-head"
|
||||
:aria-expanded="isGroupOpen(group.id) || searching"
|
||||
@click="toggleGroup(group.id)"
|
||||
>
|
||||
<span class="settings__cat-icon"><span class="dashicons" :class="cat.icon" /></span>
|
||||
<span class="settings__cat-text">
|
||||
<span class="settings__cat-title">{{ cat.title }}</span>
|
||||
<span class="settings__cat-sub">{{ cat.subtitle }}</span>
|
||||
</span>
|
||||
<span class="settings__group-icon dashicons" :class="group.icon" />
|
||||
<span class="settings__group-title">{{ group.title }}</span>
|
||||
<span
|
||||
class="settings__group-caret dashicons dashicons-arrow-down-alt2"
|
||||
:class="{ 'is-collapsed': !isGroupOpen(group.id) && !searching }"
|
||||
/>
|
||||
</button>
|
||||
<ul v-show="isGroupOpen(group.id) || searching" class="settings__list">
|
||||
<li v-for="cat in group.categories" :key="cat.id">
|
||||
<button
|
||||
type="button"
|
||||
class="settings__cat"
|
||||
:class="{ 'settings__cat--active': cat.id === activeId }"
|
||||
@click="selectCategory(cat.id)"
|
||||
>
|
||||
<span class="settings__cat-icon dashicons" :class="cat.icon" />
|
||||
<span class="settings__cat-title">{{ cat.title }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li v-if="!filteredGroups.length" class="settings__empty">
|
||||
No settings match “{{ query }}”.
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
@@ -212,7 +248,6 @@ const importSettings = (e) => {
|
||||
:field="f"
|
||||
:options="optionsFor(f)"
|
||||
:disabled="!!f.dependsOn && !settings[f.dependsOn]"
|
||||
:api="api"
|
||||
v-model="settings[f.key]"
|
||||
/>
|
||||
</div>
|
||||
@@ -250,7 +285,7 @@ const importSettings = (e) => {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
padding: 24px 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -262,7 +297,7 @@ const importSettings = (e) => {
|
||||
.settings__actions { display: flex; gap: 4px; }
|
||||
.settings__actions button {
|
||||
width: 30px; height: 30px; border: none; background: none;
|
||||
color: var(--aurora-text-muted); cursor: pointer; border-radius: 7px;
|
||||
color: var(--aurora-text-muted); cursor: pointer; border-radius: var(--aurora-radius-sm);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.settings__actions button:hover { background: var(--aurora-bg-subtle); color: var(--aurora-text); }
|
||||
@@ -272,7 +307,7 @@ const importSettings = (e) => {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px; padding: 8px 12px; margin-bottom: 16px;
|
||||
border-radius: var(--aurora-radius-md); padding: 8px 12px; margin-bottom: 16px;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.settings__search input {
|
||||
@@ -280,27 +315,47 @@ const importSettings = (e) => {
|
||||
font-size: 0.8125rem; width: 100%;
|
||||
}
|
||||
|
||||
.settings__list { list-style: none; margin: 0; padding: 0; }
|
||||
.settings__groups { list-style: none; margin: 0; padding: 0; }
|
||||
.settings__group { margin-bottom: 4px; }
|
||||
|
||||
.settings__group-head {
|
||||
display: flex; align-items: center; gap: 8px; width: 100%;
|
||||
padding: 6px 8px; border: none; background: none; cursor: pointer;
|
||||
border-radius: var(--aurora-radius-sm); color: var(--aurora-text-muted);
|
||||
}
|
||||
.settings__group-head:hover { color: var(--aurora-text); }
|
||||
.settings__group-icon { font-size: 15px; width: 16px; height: 16px; }
|
||||
.settings__group-title {
|
||||
font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.settings__group-caret {
|
||||
margin-left: auto; font-size: 16px; width: 16px; height: 16px;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.settings__group-caret.is-collapsed { transform: rotate(-90deg); }
|
||||
|
||||
/* Indent the category items so they read as a submenu nested under their
|
||||
group header, rather than sitting flush at the same level. */
|
||||
.settings__list { list-style: none; margin: 2px 0 8px; padding: 0 0 0 14px; }
|
||||
.settings__cat {
|
||||
display: flex; align-items: center; gap: 12px; width: 100%;
|
||||
padding: 10px; border: none; background: none; cursor: pointer;
|
||||
border-radius: 12px; text-align: left; margin-bottom: 2px;
|
||||
display: flex; align-items: center; gap: 10px; width: 100%;
|
||||
padding: 8px 10px; border: none; background: none; cursor: pointer;
|
||||
border-radius: var(--aurora-radius-md); text-align: left; margin-bottom: 1px;
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.settings__cat:hover { background: var(--aurora-bg-subtle); }
|
||||
.settings__cat--active { background: var(--aurora-accent-soft); }
|
||||
.settings__cat-icon {
|
||||
width: 34px; height: 34px; flex-shrink: 0;
|
||||
border-radius: 9px; background: var(--aurora-bg-subtle);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; width: 18px; height: 18px; flex-shrink: 0;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.settings__cat--active .settings__cat-icon { color: var(--aurora-accent); }
|
||||
.settings__cat-text { display: flex; flex-direction: column; min-width: 0; }
|
||||
.settings__cat-title { font-size: 0.875rem; font-weight: 600; color: var(--aurora-text); }
|
||||
.settings__cat--active .settings__cat-title { color: var(--aurora-accent); }
|
||||
.settings__cat-sub {
|
||||
font-size: 0.75rem; color: var(--aurora-text-muted);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
.settings__cat-title { font-size: 0.8125rem; font-weight: 500; }
|
||||
.settings__cat--active .settings__cat-title { color: var(--aurora-accent); font-weight: 600; }
|
||||
|
||||
.settings__empty {
|
||||
padding: 12px 10px; font-size: 0.8125rem; color: var(--aurora-text-muted);
|
||||
}
|
||||
|
||||
/* Right content — no background/border of its own, same reasoning.
|
||||
@@ -313,7 +368,7 @@ const importSettings = (e) => {
|
||||
flex: 1; min-width: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
padding: 28px 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -330,7 +385,7 @@ const importSettings = (e) => {
|
||||
.settings__saved { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.settings__save {
|
||||
background: var(--aurora-accent); color: #fff; border: none;
|
||||
border-radius: 9px; padding: 9px 20px; font-size: 0.875rem; font-weight: 500; cursor: pointer;
|
||||
border-radius: var(--aurora-radius-md); padding: 9px 20px; font-size: 0.875rem; font-weight: 500; cursor: pointer;
|
||||
}
|
||||
.settings__save:disabled { opacity: 0.6; cursor: default; }
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ const props = defineProps({
|
||||
// True when this field's `dependsOn` key is falsy — greys out the
|
||||
// control without hiding it, so the relationship stays visible.
|
||||
disabled: { type: Boolean, default: false },
|
||||
// Shared REST fetcher, only used by field types that need their own API
|
||||
// calls beyond the generic wp/v2/settings save flow (companion-plugin).
|
||||
api: { type: Function, default: null },
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
@@ -76,43 +73,14 @@ const pickFont = (name) => {
|
||||
fontOpen.value = false;
|
||||
};
|
||||
const openFontList = () => {
|
||||
fontQuery.value = props.modelValue || '';
|
||||
// Start with an empty query so the whole font list is shown on focus.
|
||||
// Seeding it with the current font filtered the list down to just that
|
||||
// one font, making it impossible to pick a different one without first
|
||||
// clearing the box. The active font stays highlighted via `val === name`.
|
||||
fontQuery.value = '';
|
||||
fontOpen.value = true;
|
||||
};
|
||||
|
||||
// Companion plugin (Aurora File Manager) install card -----------------------
|
||||
const companionStatus = ref(null); // { installed, active } | null while loading
|
||||
const companionBusy = ref(false);
|
||||
const companionError = ref('');
|
||||
|
||||
const loadCompanionStatus = async () => {
|
||||
if (props.field.type !== 'companion-plugin' || !props.api) return;
|
||||
try {
|
||||
companionStatus.value = await props.api(props.field.statusEndpoint);
|
||||
} catch {
|
||||
companionStatus.value = { installed: false, active: false };
|
||||
}
|
||||
};
|
||||
if (props.field.type === 'companion-plugin') {
|
||||
loadCompanionStatus();
|
||||
}
|
||||
|
||||
const installCompanion = async () => {
|
||||
if (!props.api) return;
|
||||
companionBusy.value = true;
|
||||
companionError.value = '';
|
||||
try {
|
||||
const result = await props.api(props.field.installEndpoint, { method: 'POST' });
|
||||
if (!result.success) {
|
||||
companionError.value = result.message || 'Installation failed.';
|
||||
}
|
||||
await loadCompanionStatus();
|
||||
} catch (e) {
|
||||
companionError.value = e.message || 'Installation failed.';
|
||||
} finally {
|
||||
companionBusy.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -138,6 +106,7 @@ const installCompanion = async () => {
|
||||
<textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
class="field__input field__textarea"
|
||||
:class="{ 'field__textarea--mono': field.mono }"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:rows="field.rows || 6"
|
||||
:value="val || ''"
|
||||
@@ -272,31 +241,6 @@ const installCompanion = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Companion plugin install card -->
|
||||
<div v-else-if="field.type === 'companion-plugin'" class="field__companion">
|
||||
<div v-if="!companionStatus" class="field__companion-status">Checking…</div>
|
||||
<template v-else>
|
||||
<a
|
||||
v-if="companionStatus.active"
|
||||
class="field__companion-manage"
|
||||
:href="field.manageUrl"
|
||||
>
|
||||
Installed & active — open File Manager →
|
||||
</a>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="field__companion-install"
|
||||
:disabled="companionBusy"
|
||||
@click="installCompanion"
|
||||
>
|
||||
{{ companionBusy ? 'Installing…' : (companionStatus.installed ? 'Activate' : 'Install & Activate') }}
|
||||
</button>
|
||||
<p v-if="companionError" class="field__companion-error">{{ companionError }}</p>
|
||||
</template>
|
||||
</template>
|
||||
</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">
|
||||
@@ -340,24 +284,29 @@ const installCompanion = async () => {
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
color: var(--aurora-text);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
padding: 9px 12px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.field__input:focus { outline: none; border-color: var(--aurora-accent); }
|
||||
.field__textarea { resize: vertical; font-family: inherit; line-height: 1.5; }
|
||||
.field__textarea--mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.field__color { display: flex; align-items: center; gap: 10px; }
|
||||
.field__color input[type='color'] {
|
||||
width: 40px; height: 32px; border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px; background: none; cursor: pointer; padding: 2px;
|
||||
border-radius: var(--aurora-radius-sm); background: none; cursor: pointer; padding: 2px;
|
||||
}
|
||||
.field__color-hex { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
|
||||
.field__image {
|
||||
width: 160px; height: 120px;
|
||||
border: 1px dashed var(--aurora-border);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 8px; cursor: pointer; color: var(--aurora-text-muted);
|
||||
@@ -376,13 +325,13 @@ const installCompanion = async () => {
|
||||
.field__repeater-remove {
|
||||
flex-shrink: 0; width: 34px; border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||||
border-radius: 8px; cursor: pointer; font-size: 1.1rem;
|
||||
border-radius: var(--aurora-radius-sm); cursor: pointer; font-size: 1.1rem;
|
||||
}
|
||||
.field__newitem {
|
||||
border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
border-radius: 8px; padding: 8px 16px; font-size: 0.8125rem; cursor: pointer;
|
||||
border-radius: var(--aurora-radius-sm); padding: 8px 16px; font-size: 0.8125rem; cursor: pointer;
|
||||
}
|
||||
.field__newitem:hover { border-color: var(--aurora-accent); color: var(--aurora-accent); }
|
||||
|
||||
@@ -400,13 +349,13 @@ const installCompanion = async () => {
|
||||
display: inline-flex;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 9px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
.field__seg button {
|
||||
border: none; background: none; cursor: pointer;
|
||||
padding: 6px 26px; border-radius: 7px;
|
||||
padding: 6px 26px; border-radius: var(--aurora-radius-sm);
|
||||
font-size: 0.8125rem; color: var(--aurora-text-muted);
|
||||
}
|
||||
.field__seg--active { background: var(--aurora-accent); color: #fff !important; }
|
||||
@@ -417,13 +366,13 @@ const installCompanion = async () => {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
width: 76px; padding: 10px 6px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
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;
|
||||
width: 36px; height: 36px; border-radius: var(--aurora-radius-md);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
display: flex; align-items: flex-end; justify-content: flex-end;
|
||||
padding: 4px;
|
||||
@@ -441,33 +390,19 @@ const installCompanion = async () => {
|
||||
max-height: 260px; overflow-y: auto; z-index: 20;
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
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;
|
||||
padding: 8px 10px; border-radius: var(--aurora-radius-sm);
|
||||
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); }
|
||||
|
||||
/* Companion plugin install card */
|
||||
.field__companion-status { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.field__companion-install {
|
||||
border: none; border-radius: 8px; background: var(--aurora-accent); color: #fff;
|
||||
padding: 8px 16px; font-size: 0.8125rem; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.field__companion-install:disabled { opacity: 0.6; cursor: default; }
|
||||
.field__companion-manage {
|
||||
display: inline-block; font-size: 0.8125rem; font-weight: 600;
|
||||
color: var(--aurora-accent); text-decoration: none;
|
||||
}
|
||||
.field__companion-manage:hover { text-decoration: underline; }
|
||||
.field__companion-error { margin: 8px 0 0; font-size: 0.8125rem; color: #e5484d; }
|
||||
|
||||
@media (max-width: 782px) {
|
||||
.field { grid-template-columns: 1fr; gap: 12px; }
|
||||
}
|
||||
|
||||
@@ -1,359 +1,560 @@
|
||||
/**
|
||||
* Declarative settings schema for the Aurora Admin settings page.
|
||||
* Each category renders as an item in the left nav; its fields render in
|
||||
* the right panel. Field `key`s map directly into the aurora_admin_settings
|
||||
* option, saved via the wp/v2/settings REST route.
|
||||
*
|
||||
* Categories are organized into top-level groups. Each group renders as a
|
||||
* collapsible section in the left nav; each category within it renders as a
|
||||
* nav item, and its fields render in the right panel. Field `key`s map
|
||||
* directly into the aurora_admin_settings option, saved via the
|
||||
* wp/v2/settings REST route.
|
||||
*/
|
||||
export const settingsCategories = [
|
||||
export const settingsGroups = [
|
||||
{
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
subtitle: 'Core plugin settings',
|
||||
icon: 'dashicons-admin-settings',
|
||||
fields: [
|
||||
categories: [
|
||||
{
|
||||
key: 'logo',
|
||||
type: 'image',
|
||||
label: 'Logo',
|
||||
description: 'Optional logo to be displayed in the menu.',
|
||||
},
|
||||
{
|
||||
key: 'dark_logo',
|
||||
type: 'image',
|
||||
label: 'Dark mode logo',
|
||||
description:
|
||||
"Set an alternative logo to use in dark mode. If one isn't set it will fall back to the standard logo.",
|
||||
},
|
||||
{
|
||||
key: 'disable_theme',
|
||||
type: 'multiselect',
|
||||
label: 'Disable theme',
|
||||
description: 'Choose roles to disable the theme for.',
|
||||
optionsSource: 'roles',
|
||||
},
|
||||
{
|
||||
key: 'search_post_types',
|
||||
type: 'multiselect',
|
||||
label: 'Search post types',
|
||||
description: 'Choose which post types are available in the global search.',
|
||||
optionsSource: 'postTypes',
|
||||
},
|
||||
{
|
||||
key: 'disable_global_search',
|
||||
type: 'segmented',
|
||||
label: 'Disable global search',
|
||||
description: 'Disable global search for all users.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'login',
|
||||
title: 'Login',
|
||||
subtitle: 'Customize login page appearance and security',
|
||||
icon: 'dashicons-lock',
|
||||
fields: [
|
||||
{
|
||||
key: 'style_login',
|
||||
type: 'toggle',
|
||||
label: 'Style login page',
|
||||
description: 'Apply the Aurora theme to the WordPress login screen.',
|
||||
},
|
||||
{
|
||||
key: 'login_logo',
|
||||
type: 'image',
|
||||
label: 'Login logo',
|
||||
description: 'Replace the WordPress logo on the login screen.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
title: 'Theme',
|
||||
subtitle: 'Colors, styling, and visual customization',
|
||||
icon: 'dashicons-admin-appearance',
|
||||
fields: [
|
||||
{
|
||||
key: 'theme_preset',
|
||||
type: 'theme-picker',
|
||||
label: 'Theme',
|
||||
description: 'Pick a color for the whole admin interface.',
|
||||
default: 'indigo',
|
||||
},
|
||||
{
|
||||
key: 'font_family',
|
||||
type: 'font-picker',
|
||||
label: 'Font family',
|
||||
description: 'Google Font used across the Aurora interface.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'whitelabel',
|
||||
title: 'White Label',
|
||||
subtitle: 'Rebrand and customize interface text and icons',
|
||||
icon: 'dashicons-tag',
|
||||
fields: [
|
||||
{
|
||||
key: 'admin_favicon',
|
||||
type: 'image',
|
||||
label: 'Admin favicon',
|
||||
description: 'Optional favicon to replace the WordPress favicon in the admin.',
|
||||
},
|
||||
{
|
||||
key: 'plugin_name',
|
||||
type: 'text',
|
||||
label: 'Rename Aurora Admin',
|
||||
description:
|
||||
"This will rename Aurora Admin in the plugins list as well as renaming its settings link.",
|
||||
placeholder: 'Aurora Admin',
|
||||
},
|
||||
{
|
||||
key: 'text_replacements',
|
||||
type: 'repeater',
|
||||
label: 'Text Replacement',
|
||||
description:
|
||||
'Customize the text displayed in the WordPress admin area. Enter original words or phrases and their replacements to white-label the interface.',
|
||||
columns: ['Find', 'Replace with'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'menu',
|
||||
title: 'Menu',
|
||||
subtitle: 'Admin menu behavior and interaction settings',
|
||||
icon: 'dashicons-menu',
|
||||
fields: [
|
||||
{
|
||||
key: 'menu_search',
|
||||
type: 'toggle',
|
||||
label: 'Menu search',
|
||||
description: 'Show a search box at the top of the admin menu.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'posts',
|
||||
title: 'Posts',
|
||||
subtitle: 'Post management and display preferences',
|
||||
icon: 'dashicons-admin-post',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_posts',
|
||||
type: 'toggle',
|
||||
label: 'Modern posts table',
|
||||
description: 'Use the Aurora posts management interface.',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pages',
|
||||
title: 'Pages',
|
||||
subtitle: 'Page management and display preferences',
|
||||
icon: 'dashicons-admin-page',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_pages',
|
||||
type: 'toggle',
|
||||
label: 'Modern pages table',
|
||||
description: 'Use the Aurora pages management interface.',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'plugins',
|
||||
title: 'Plugins',
|
||||
subtitle: 'Plugin management interface options',
|
||||
icon: 'dashicons-admin-plugins',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_plugins',
|
||||
type: 'toggle',
|
||||
label: 'Modern plugins page',
|
||||
description: 'Use the Aurora plugins management interface.',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: 'Dashboard',
|
||||
subtitle: 'Custom dashboard and admin interface settings',
|
||||
icon: 'dashicons-dashboard',
|
||||
fields: [
|
||||
{
|
||||
key: 'use_custom_dashboard',
|
||||
type: 'toggle',
|
||||
label: 'Custom dashboard',
|
||||
description: "Replace the WordPress dashboard with Aurora's dashboard.",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
title: 'Analytics',
|
||||
subtitle: 'Built-in analytics and tracking settings',
|
||||
icon: 'dashicons-chart-bar',
|
||||
fields: [
|
||||
{
|
||||
key: 'enable_analytics',
|
||||
type: 'toggle',
|
||||
label: 'Enable analytics',
|
||||
description: 'Track first-party visitor analytics for the dashboard.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'track_admins',
|
||||
type: 'toggle',
|
||||
label: 'Track logged-in admins',
|
||||
description: 'Include administrator visits in analytics (off by default).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'media',
|
||||
title: 'Media',
|
||||
subtitle: 'Media library and upload management settings',
|
||||
icon: 'dashicons-admin-media',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_media',
|
||||
type: 'toggle',
|
||||
label: 'Modern media library',
|
||||
description: 'Use the Aurora media library interface.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'enable_svg',
|
||||
type: 'toggle',
|
||||
label: 'Allow SVG uploads',
|
||||
description: 'Permit SVG files to be uploaded to the media library.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
title: 'Users',
|
||||
subtitle: 'User management and administration settings',
|
||||
icon: 'dashicons-admin-users',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_users',
|
||||
type: 'toggle',
|
||||
label: 'Modern users page',
|
||||
description: 'Use the Aurora users management interface.',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'comments',
|
||||
title: 'Comments',
|
||||
subtitle: 'Comment management and administration',
|
||||
icon: 'dashicons-admin-comments',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_comments',
|
||||
type: 'toggle',
|
||||
label: 'Modern comments page',
|
||||
description: 'Use the Aurora comments management interface.',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'image-formats',
|
||||
title: 'Modern Image Formats',
|
||||
subtitle: 'Automatically generate WebP/AVIF versions of uploaded images',
|
||||
icon: 'dashicons-format-image',
|
||||
fields: [
|
||||
{
|
||||
key: 'image_formats_enabled',
|
||||
type: 'toggle',
|
||||
label: 'Enable modern image formats',
|
||||
description:
|
||||
'Automatically generate a WebP or AVIF copy of every size of a newly uploaded JPEG or PNG image, and serve it to browsers that support it. Only affects new uploads — existing library images are unaffected until re-uploaded or regenerated with a tool like Regenerate Thumbnails.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'image_formats_format',
|
||||
type: 'select',
|
||||
label: 'Output format',
|
||||
description:
|
||||
'Automatic prefers AVIF (smaller files, wider quality range) when the server supports it, falling back to WebP. If your server supports neither, generation is silently skipped — ask your host about enabling GD or Imagick WebP/AVIF support.',
|
||||
dependsOn: 'image_formats_enabled',
|
||||
default: 'auto',
|
||||
options: [
|
||||
{ value: 'auto', label: 'Automatic (AVIF, falling back to WebP)' },
|
||||
{ value: 'webp', label: 'WebP only' },
|
||||
{ value: 'avif', label: 'AVIF only' },
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
subtitle: 'Core plugin settings',
|
||||
icon: 'dashicons-admin-settings',
|
||||
fields: [
|
||||
{
|
||||
key: 'logo',
|
||||
type: 'image',
|
||||
label: 'Logo',
|
||||
description: 'Optional logo to be displayed in the menu.',
|
||||
},
|
||||
{
|
||||
key: 'dark_logo',
|
||||
type: 'image',
|
||||
label: 'Dark mode logo',
|
||||
description:
|
||||
"Set an alternative logo to use in dark mode. If one isn't set it will fall back to the standard logo.",
|
||||
},
|
||||
{
|
||||
key: 'disable_theme',
|
||||
type: 'multiselect',
|
||||
label: 'Disable theme',
|
||||
description: 'Choose roles to disable the theme for.',
|
||||
optionsSource: 'roles',
|
||||
},
|
||||
{
|
||||
key: 'search_post_types',
|
||||
type: 'multiselect',
|
||||
label: 'Search post types',
|
||||
description: 'Choose which post types are available in the global search.',
|
||||
optionsSource: 'postTypes',
|
||||
},
|
||||
{
|
||||
key: 'disable_global_search',
|
||||
type: 'segmented',
|
||||
label: 'Disable global search',
|
||||
description: 'Disable global search for all users.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'image_formats_picture_element',
|
||||
type: 'toggle',
|
||||
label: 'Serve via <picture> element',
|
||||
description:
|
||||
'Rewrite <img> tags (post content and post thumbnails) into a <picture> element offering the modern format with the original JPEG/PNG as a fallback. Turn this off if it conflicts with your theme’s image styling, while still generating the files for other tools to use.',
|
||||
dependsOn: 'image_formats_enabled',
|
||||
default: true,
|
||||
id: 'analytics',
|
||||
title: 'Analytics',
|
||||
subtitle: 'Built-in analytics and tracking settings',
|
||||
icon: 'dashicons-chart-bar',
|
||||
fields: [
|
||||
{
|
||||
key: 'enable_analytics',
|
||||
type: 'toggle',
|
||||
label: 'Enable analytics',
|
||||
description: 'Track first-party visitor analytics for the dashboard.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'track_admins',
|
||||
type: 'toggle',
|
||||
label: 'Track logged-in admins',
|
||||
description: 'Include administrator visits in analytics (off by default).',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'add-ons',
|
||||
title: 'Add-ons',
|
||||
subtitle: 'Standalone companion plugins',
|
||||
icon: 'dashicons-admin-plugins',
|
||||
fields: [
|
||||
id: 'appearance',
|
||||
title: 'Appearance',
|
||||
icon: 'dashicons-admin-appearance',
|
||||
categories: [
|
||||
{
|
||||
key: 'file_manager_addon',
|
||||
type: 'companion-plugin',
|
||||
label: 'Aurora File Manager',
|
||||
description:
|
||||
'Browse, edit, and manage your site’s files (themes, plugins, uploads, and more) directly from wp-admin. Distributed as a separate plugin rather than bundled here, since WordPress.org’s Plugin Directory policy doesn’t allow this category of functionality in a directory-listed plugin — restrictions or not.',
|
||||
statusEndpoint: '/companion-plugins/file-manager/status',
|
||||
installEndpoint: '/companion-plugins/file-manager/install',
|
||||
manageUrl: 'admin.php?page=aurora-file-manager',
|
||||
id: 'theme',
|
||||
title: 'Theme',
|
||||
subtitle: 'Colors, styling, and visual customization',
|
||||
icon: 'dashicons-admin-appearance',
|
||||
fields: [
|
||||
{
|
||||
key: 'theme_preset',
|
||||
type: 'theme-picker',
|
||||
label: 'Theme',
|
||||
description: 'Pick a color for the whole admin interface.',
|
||||
default: 'indigo',
|
||||
},
|
||||
{
|
||||
key: 'font_family',
|
||||
type: 'font-picker',
|
||||
label: 'Font family',
|
||||
description:
|
||||
'Optional. Leave empty to use your system font (no external requests). ' +
|
||||
'Choosing a font loads it from Google Fonts (fonts.googleapis.com) on each admin page.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'database_explorer_addon',
|
||||
type: 'companion-plugin',
|
||||
label: 'Aurora Database Explorer',
|
||||
description:
|
||||
'A Supabase-style database browser inside wp-admin — see every table with row counts and sizes, browse paginated data, and run your own read-only SELECT/SHOW/DESCRIBE/EXPLAIN queries. Distributed as a separate plugin rather than bundled here, since WordPress.org’s Plugin Directory review treats direct database-query tooling the same way it treats file-management functionality.',
|
||||
statusEndpoint: '/companion-plugins/database-explorer/status',
|
||||
installEndpoint: '/companion-plugins/database-explorer/install',
|
||||
manageUrl: 'admin.php?page=aurora-database-explorer',
|
||||
id: 'whitelabel',
|
||||
title: 'White Label',
|
||||
subtitle: 'Rebrand and customize interface text and icons',
|
||||
icon: 'dashicons-tag',
|
||||
fields: [
|
||||
{
|
||||
key: 'admin_favicon',
|
||||
type: 'image',
|
||||
label: 'Admin favicon',
|
||||
description: 'Optional favicon to replace the WordPress favicon in the admin.',
|
||||
},
|
||||
{
|
||||
key: 'plugin_name',
|
||||
type: 'text',
|
||||
label: 'Rename Aurora Admin',
|
||||
description:
|
||||
'This will rename Aurora Admin in the plugins list as well as renaming its settings link.',
|
||||
placeholder: 'Aurora Admin',
|
||||
},
|
||||
{
|
||||
key: 'text_replacements',
|
||||
type: 'repeater',
|
||||
label: 'Text Replacement',
|
||||
description:
|
||||
'Customize the text displayed in the WordPress admin area. Enter original words or phrases and their replacements to white-label the interface.',
|
||||
columns: ['Find', 'Replace with'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'site_backup_addon',
|
||||
type: 'companion-plugin',
|
||||
label: 'Aurora Site Backup',
|
||||
description:
|
||||
'Full site backup (database + files) and restore-in-place, on demand or on a recurring schedule, stored locally or on your own S3-compatible or Google Drive remote storage. Distributed as a separate plugin rather than bundled here, to keep Aurora Admin’s own footprint away from arbitrary file-write functionality.',
|
||||
statusEndpoint: '/companion-plugins/site-backup/status',
|
||||
installEndpoint: '/companion-plugins/site-backup/install',
|
||||
manageUrl: 'admin.php?page=aurora-site-backup',
|
||||
id: 'login',
|
||||
title: 'Login',
|
||||
subtitle: 'Customize login page appearance and security',
|
||||
icon: 'dashicons-lock',
|
||||
fields: [
|
||||
{
|
||||
key: 'style_login',
|
||||
type: 'toggle',
|
||||
label: 'Style login page',
|
||||
description: 'Apply the Aurora theme to the WordPress login screen.',
|
||||
},
|
||||
{
|
||||
key: 'login_logo',
|
||||
type: 'image',
|
||||
label: 'Login logo',
|
||||
description: 'Replace the WordPress logo on the login screen.',
|
||||
dependsOn: 'style_login',
|
||||
},
|
||||
{
|
||||
key: 'login_bg_image',
|
||||
type: 'image',
|
||||
label: 'Background image',
|
||||
description: 'Full-screen background for the login page. Overrides the background color below.',
|
||||
dependsOn: 'style_login',
|
||||
},
|
||||
{
|
||||
key: 'login_bg_color',
|
||||
type: 'color',
|
||||
label: 'Background color',
|
||||
description: 'Solid background color for the login page (used when no background image is set).',
|
||||
default: '#0f1420',
|
||||
dependsOn: 'style_login',
|
||||
},
|
||||
{
|
||||
key: 'login_form_bg',
|
||||
type: 'color',
|
||||
label: 'Form background',
|
||||
description: 'Background color of the login form panel.',
|
||||
default: '#1c2333',
|
||||
dependsOn: 'style_login',
|
||||
},
|
||||
{
|
||||
key: 'login_button_color',
|
||||
type: 'color',
|
||||
label: 'Button color',
|
||||
description: 'Color of the primary “Log In” button and the field focus accent.',
|
||||
default: '#4f7cff',
|
||||
dependsOn: 'style_login',
|
||||
},
|
||||
{
|
||||
key: 'login_custom_css',
|
||||
type: 'textarea',
|
||||
mono: true,
|
||||
rows: 8,
|
||||
label: 'Custom login CSS',
|
||||
description: 'Extra CSS applied to the login screen, after Aurora’s own styles.',
|
||||
placeholder: 'body.login { … }',
|
||||
dependsOn: 'style_login',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'login-redirect',
|
||||
title: 'Login Redirect',
|
||||
subtitle: 'Control where visitors and users land, before and after login',
|
||||
icon: 'dashicons-redo',
|
||||
fields: [
|
||||
{
|
||||
key: 'login_redirect_enabled',
|
||||
type: 'toggle',
|
||||
label: 'Enable login redirects',
|
||||
description:
|
||||
'Turns on the redirects below. This never changes, hides, or blocks your login page — wp-login.php always stays reachable at its normal address, so this can’t lock you out of logging in.',
|
||||
},
|
||||
{
|
||||
key: 'redirect_unauthenticated_url',
|
||||
type: 'text',
|
||||
label: 'Redirect logged-out wp-admin visitors',
|
||||
description:
|
||||
'Send visitors who try to access wp-admin without being logged in to this URL instead of the login screen. Leave empty to keep the normal WordPress behavior.',
|
||||
placeholder: 'https://example.com/',
|
||||
dependsOn: 'login_redirect_enabled',
|
||||
},
|
||||
{
|
||||
key: 'redirect_roles',
|
||||
type: 'multiselect',
|
||||
label: 'Redirect these roles after login',
|
||||
description: 'Users with these roles are sent to the URL below after logging in, instead of the dashboard.',
|
||||
optionsSource: 'roles',
|
||||
dependsOn: 'login_redirect_enabled',
|
||||
},
|
||||
{
|
||||
key: 'redirect_after_login_url',
|
||||
type: 'text',
|
||||
label: 'Redirect to',
|
||||
description: 'Where the roles above land after logging in.',
|
||||
placeholder: 'https://example.com/welcome',
|
||||
dependsOn: 'login_redirect_enabled',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'menu',
|
||||
title: 'Menu',
|
||||
subtitle: 'Admin menu behavior and interaction settings',
|
||||
icon: 'dashicons-menu',
|
||||
fields: [
|
||||
{
|
||||
key: 'menu_search',
|
||||
type: 'toggle',
|
||||
label: 'Menu search',
|
||||
description: 'Show a search box at the top of the admin menu.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin-takeover',
|
||||
title: 'Admin Takeover',
|
||||
subtitle: 'Replace the remaining native WordPress admin screens with Aurora',
|
||||
id: 'screens',
|
||||
title: 'Screens',
|
||||
icon: 'dashicons-desktop',
|
||||
categories: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: 'Dashboard',
|
||||
subtitle: 'Custom dashboard and admin interface settings',
|
||||
icon: 'dashicons-dashboard',
|
||||
fields: [
|
||||
{
|
||||
key: 'use_custom_dashboard',
|
||||
type: 'toggle',
|
||||
label: 'Custom dashboard',
|
||||
description: "Replace the WordPress dashboard with Aurora's dashboard.",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'content-tables',
|
||||
title: 'Content tables',
|
||||
subtitle: 'Choose which admin list screens use the Aurora interface',
|
||||
icon: 'dashicons-editor-table',
|
||||
fields: [
|
||||
{
|
||||
key: 'modern_posts',
|
||||
type: 'toggle',
|
||||
label: 'Posts',
|
||||
description: 'Use the Aurora posts management interface.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'modern_pages',
|
||||
type: 'toggle',
|
||||
label: 'Pages',
|
||||
description: 'Use the Aurora pages management interface.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'modern_media',
|
||||
type: 'toggle',
|
||||
label: 'Media library',
|
||||
description: 'Use the Aurora media library interface.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'modern_users',
|
||||
type: 'toggle',
|
||||
label: 'Users',
|
||||
description: 'Use the Aurora users management interface.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'modern_comments',
|
||||
type: 'toggle',
|
||||
label: 'Comments',
|
||||
description: 'Use the Aurora comments management interface.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'modern_plugins',
|
||||
type: 'toggle',
|
||||
label: 'Plugins',
|
||||
description: 'Use the Aurora plugins management interface.',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'editor',
|
||||
title: 'Editor',
|
||||
subtitle: 'Choose the editor used for posts and pages',
|
||||
icon: 'dashicons-edit',
|
||||
fields: [
|
||||
{
|
||||
key: 'disable_gutenberg',
|
||||
type: 'toggle',
|
||||
label: 'Disable Gutenberg',
|
||||
description:
|
||||
'Use WordPress’s classic editor instead of the block editor for all post types. Off by default.',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin-takeover',
|
||||
title: 'Admin Takeover',
|
||||
subtitle: 'Replace the remaining native WordPress admin screens with Aurora',
|
||||
icon: 'dashicons-shield',
|
||||
fields: [
|
||||
{
|
||||
key: 'full_admin_takeover',
|
||||
type: 'toggle',
|
||||
label: 'Full admin takeover',
|
||||
description:
|
||||
'Replace Media Upload, Add User, Profile, Appearance, Tools, and native Settings screens with Aurora equivalents. Off by default — these cover more ground than Posts/Pages/Media/Users/Comments, so it’s opt-in rather than on by default.',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'productivity',
|
||||
title: 'Productivity',
|
||||
icon: 'dashicons-superhero-alt',
|
||||
categories: [
|
||||
{
|
||||
id: 'image-formats',
|
||||
title: 'Modern Image Formats',
|
||||
subtitle: 'Automatically generate WebP/AVIF versions of uploaded images',
|
||||
icon: 'dashicons-format-image',
|
||||
fields: [
|
||||
{
|
||||
key: 'image_formats_enabled',
|
||||
type: 'toggle',
|
||||
label: 'Enable modern image formats',
|
||||
description:
|
||||
'Automatically generate a WebP or AVIF copy of every size of a newly uploaded JPEG or PNG image, and serve it to browsers that support it. Only affects new uploads — existing library images are unaffected until re-uploaded or regenerated with a tool like Regenerate Thumbnails.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'image_formats_format',
|
||||
type: 'select',
|
||||
label: 'Output format',
|
||||
description:
|
||||
'Automatic prefers AVIF (smaller files, wider quality range) when the server supports it, falling back to WebP. If your server supports neither, generation is silently skipped — ask your host about enabling GD or Imagick WebP/AVIF support.',
|
||||
dependsOn: 'image_formats_enabled',
|
||||
default: 'auto',
|
||||
options: [
|
||||
{ value: 'auto', label: 'Automatic (AVIF, falling back to WebP)' },
|
||||
{ value: 'webp', label: 'WebP only' },
|
||||
{ value: 'avif', label: 'AVIF only' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'image_formats_picture_element',
|
||||
type: 'toggle',
|
||||
label: 'Serve via <picture> element',
|
||||
description:
|
||||
'Rewrite <img> tags (post content and post thumbnails) into a <picture> element offering the modern format with the original JPEG/PNG as a fallback. Turn this off if it conflicts with your theme’s image styling, while still generating the files for other tools to use.',
|
||||
dependsOn: 'image_formats_enabled',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'media',
|
||||
title: 'Media',
|
||||
subtitle: 'Media library and upload options',
|
||||
icon: 'dashicons-admin-media',
|
||||
fields: [
|
||||
{
|
||||
key: 'enable_svg',
|
||||
type: 'toggle',
|
||||
label: 'Allow SVG uploads',
|
||||
description: 'Permit SVG files to be uploaded to the media library.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
title: 'Security & performance',
|
||||
icon: 'dashicons-shield',
|
||||
fields: [
|
||||
categories: [
|
||||
{
|
||||
key: 'full_admin_takeover',
|
||||
type: 'toggle',
|
||||
label: 'Full admin takeover',
|
||||
description:
|
||||
'Replace Media Upload, Add User, Profile, Appearance, Tools, and native Settings screens with Aurora equivalents. Off by default — these cover more ground than Posts/Pages/Media/Users/Comments, so it’s opt-in rather than on by default.',
|
||||
default: false,
|
||||
id: 'security-hardening',
|
||||
title: 'Hardening',
|
||||
subtitle: 'Reduce your site’s attack surface and trim unnecessary output',
|
||||
icon: 'dashicons-shield-alt',
|
||||
fields: [
|
||||
{
|
||||
key: 'disable_xmlrpc',
|
||||
type: 'toggle',
|
||||
label: 'Disable XML-RPC',
|
||||
description:
|
||||
'Turn off the XML-RPC endpoint, a common brute-force and pingback-DDoS vector. Leave on only if you rely on the WordPress mobile app or remote publishing.',
|
||||
},
|
||||
{
|
||||
key: 'disable_comments',
|
||||
type: 'toggle',
|
||||
label: 'Disable comments',
|
||||
description:
|
||||
'Turn comments off site-wide — closes them on every post type, hides existing ones, and removes the Comments admin screen and dashboard widget.',
|
||||
},
|
||||
{
|
||||
key: 'remove_head_junk',
|
||||
type: 'toggle',
|
||||
label: 'Clean up <head>',
|
||||
description:
|
||||
'Remove the WordPress generator/version tag, RSD link, WLW manifest, and shortlink meta from your site’s <head>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'performance-tweaks',
|
||||
title: 'Performance',
|
||||
subtitle: 'Trim requests and scripts WordPress loads by default',
|
||||
icon: 'dashicons-performance',
|
||||
fields: [
|
||||
{
|
||||
key: 'disable_emojis',
|
||||
type: 'toggle',
|
||||
label: 'Disable emoji scripts',
|
||||
description:
|
||||
'Stop WordPress loading its emoji-detection script and styles on every front-end and admin page.',
|
||||
},
|
||||
{
|
||||
key: 'disable_oembed',
|
||||
type: 'toggle',
|
||||
label: 'Disable oEmbeds',
|
||||
description:
|
||||
'Remove oEmbed discovery links and the wp-embed script (stops auto-embedding of pasted URLs and the embed REST route).',
|
||||
},
|
||||
{
|
||||
key: 'heartbeat_mode',
|
||||
type: 'select',
|
||||
label: 'Heartbeat API',
|
||||
description:
|
||||
'The Heartbeat API polls the server on a timer (autosave, post-lock notices). Slow it down or disable it to cut admin-ajax load.',
|
||||
default: 'default',
|
||||
options: [
|
||||
{ value: 'default', label: 'Default (15–60s)' },
|
||||
{ value: 'slow', label: 'Slow (60s)' },
|
||||
{ value: 'disabled', label: 'Disabled' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
title: 'Code',
|
||||
icon: 'dashicons-editor-code',
|
||||
categories: [
|
||||
{
|
||||
id: 'custom-code',
|
||||
title: 'Custom CSS & JS',
|
||||
subtitle: 'Inject your own CSS and JavaScript into the WordPress admin',
|
||||
icon: 'dashicons-admin-customizer',
|
||||
fields: [
|
||||
{
|
||||
key: 'custom_admin_css',
|
||||
type: 'textarea',
|
||||
mono: true,
|
||||
rows: 10,
|
||||
label: 'Admin CSS',
|
||||
description:
|
||||
'CSS added to every wp-admin page, inside a <style> tag in the admin <head>. Enter CSS only — no <style> tags needed.',
|
||||
placeholder: '.wrap { max-width: 1200px; }',
|
||||
},
|
||||
{
|
||||
key: 'custom_admin_js',
|
||||
type: 'textarea',
|
||||
mono: true,
|
||||
rows: 10,
|
||||
label: 'Admin JavaScript',
|
||||
description:
|
||||
'JavaScript added to the admin footer, inside a <script> tag. Enter JS only — no <script> tags needed.',
|
||||
placeholder: "console.log('Hello from Aurora');",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'header-footer',
|
||||
title: 'Header & footer scripts',
|
||||
subtitle: 'Inject raw code into your site’s front-end <head> and footer',
|
||||
icon: 'dashicons-editor-code',
|
||||
fields: [
|
||||
{
|
||||
key: 'header_scripts',
|
||||
type: 'textarea',
|
||||
mono: true,
|
||||
rows: 8,
|
||||
label: 'Header scripts',
|
||||
description:
|
||||
'Raw HTML/JS printed in the front-end <head> — analytics snippets, verification meta tags, etc. Include the full <script>/<meta> tags.',
|
||||
placeholder: '<!-- e.g. analytics -->\n<script>…</script>',
|
||||
},
|
||||
{
|
||||
key: 'footer_scripts',
|
||||
type: 'textarea',
|
||||
mono: true,
|
||||
rows: 8,
|
||||
label: 'Footer scripts',
|
||||
description:
|
||||
'Raw HTML/JS printed just before </body> on the front end. Include the full <script> tags.',
|
||||
placeholder: '<script>…</script>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Flattened category list — every category across all groups, in order.
|
||||
* Used for default-seeding, active-category lookup, and URL deep-links.
|
||||
*/
|
||||
export const settingsCategories = settingsGroups.flatMap((g) => g.categories);
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||
import SettingsField from '@/pages/settings/components/SettingsField.vue';
|
||||
import { applyThemePalette, applyFontFamily } from '@/pages/settings/applyLiveTheme.js';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
// Step definitions. Welcome and Done are custom content; the middle three
|
||||
// render their `fields` through the shared SettingsField component (same
|
||||
// controls as the settings page — theme picker, font picker, media-library
|
||||
// image fields, toggles).
|
||||
const steps = [
|
||||
{ id: 'welcome', label: 'Welcome' },
|
||||
{
|
||||
id: 'appearance',
|
||||
label: 'Appearance',
|
||||
title: 'Choose your look',
|
||||
subtitle:
|
||||
'Pick a color theme and font for the whole admin. You can change these any time in Settings.',
|
||||
fields: [
|
||||
{
|
||||
key: 'theme_preset',
|
||||
type: 'theme-picker',
|
||||
label: 'Theme',
|
||||
description: 'The accent and surface colors used across the admin.',
|
||||
default: 'indigo',
|
||||
},
|
||||
{
|
||||
key: 'font_family',
|
||||
type: 'font-picker',
|
||||
label: 'Font family',
|
||||
description:
|
||||
'Optional. Leave empty for your system font (no external requests). ' +
|
||||
'Choosing a font loads it from Google Fonts.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'branding',
|
||||
label: 'Branding',
|
||||
title: 'Add your branding',
|
||||
subtitle:
|
||||
'All optional — drop in a logo and favicon, or rename Aurora for a fully white-labeled admin.',
|
||||
fields: [
|
||||
{ key: 'logo', type: 'image', label: 'Logo', description: 'Shown at the top of the sidebar menu.' },
|
||||
{
|
||||
key: 'dark_logo',
|
||||
type: 'image',
|
||||
label: 'Dark mode logo',
|
||||
description: 'Optional alternative logo; falls back to the standard one.',
|
||||
},
|
||||
{
|
||||
key: 'admin_favicon',
|
||||
type: 'image',
|
||||
label: 'Admin favicon',
|
||||
description: 'Replaces the WordPress favicon in the admin.',
|
||||
},
|
||||
{
|
||||
key: 'plugin_name',
|
||||
type: 'text',
|
||||
label: 'Rename Aurora Admin',
|
||||
description: 'White-labels the plugin name in menus and the plugins list.',
|
||||
placeholder: 'Aurora Admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
label: 'Features',
|
||||
title: 'Choose your features',
|
||||
subtitle:
|
||||
'Aurora replaces these WordPress admin screens with faster versions of its own. Turn off any you’d rather keep native.',
|
||||
fields: [
|
||||
{ key: 'use_custom_dashboard', type: 'toggle', label: 'Dashboard', description: 'Replace the WordPress dashboard with Aurora’s.', default: true },
|
||||
{ key: 'modern_posts', type: 'toggle', label: 'Posts', description: 'Aurora posts management screen.', default: true },
|
||||
{ key: 'modern_pages', type: 'toggle', label: 'Pages', description: 'Aurora pages management screen.', default: true },
|
||||
{ key: 'modern_media', type: 'toggle', label: 'Media library', description: 'Aurora media library screen.', default: true },
|
||||
{ key: 'modern_users', type: 'toggle', label: 'Users', description: 'Aurora users management screen.', default: true },
|
||||
{ key: 'modern_comments', type: 'toggle', label: 'Comments', description: 'Aurora comments management screen.', default: true },
|
||||
{ key: 'modern_plugins', type: 'toggle', label: 'Plugins', description: 'Aurora plugins management screen.', default: true },
|
||||
{
|
||||
key: 'full_admin_takeover',
|
||||
type: 'toggle',
|
||||
label: 'Full admin takeover',
|
||||
description: 'Also replace Appearance, Tools, and the native Settings screens. Off by default.',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 'done', label: 'Done', title: 'You’re all set' },
|
||||
];
|
||||
|
||||
// Working settings, seeded from the server + each field's default.
|
||||
const settings = reactive({ ...(props.serverData.settings || {}) });
|
||||
steps.forEach((s) =>
|
||||
(s.fields || []).forEach((f) => {
|
||||
if (settings[f.key] === undefined && f.default !== undefined) settings[f.key] = f.default;
|
||||
})
|
||||
);
|
||||
|
||||
const stepIndex = ref(0);
|
||||
const current = computed(() => steps[stepIndex.value]);
|
||||
const isFirst = computed(() => stepIndex.value === 0);
|
||||
const isDone = computed(() => current.value.id === 'done');
|
||||
|
||||
const goNext = () => {
|
||||
if (stepIndex.value < steps.length - 1) stepIndex.value++;
|
||||
};
|
||||
const goBack = () => {
|
||||
if (stepIndex.value > 0) stepIndex.value--;
|
||||
};
|
||||
|
||||
// Live preview of theme + font as they're picked (nothing is persisted until
|
||||
// the wizard is finished, but the whole admin previews immediately).
|
||||
watch(() => settings.theme_preset, (p) => applyThemePalette(p || 'indigo'), { immediate: true });
|
||||
watch(() => settings.font_family, (f) => applyFontFamily(f), { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
// The shell (which normally does this) is disabled on this page.
|
||||
document.documentElement.classList.add('aurora-dark');
|
||||
applyThemePalette(settings.theme_preset || 'indigo');
|
||||
applyFontFamily(settings.font_family);
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
const finish = async (target) => {
|
||||
if (saving.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await fetch(`${props.serverData.restUrl}wp/v2/settings`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': props.serverData.restNonce },
|
||||
body: JSON.stringify({ aurora_admin_settings: { ...settings } }),
|
||||
});
|
||||
await fetch(`${props.serverData.restUrl}aurora-admin/v1/setup/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': props.serverData.restNonce },
|
||||
});
|
||||
} catch {
|
||||
/* Even if persistence fails, don't trap the user in the wizard. */
|
||||
}
|
||||
window.location.href = target || props.serverData.dashboardUrl || '/wp-admin/';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wiz">
|
||||
<div class="wiz__inner">
|
||||
<div class="wiz__brand">
|
||||
<span class="wiz__mark">A</span>
|
||||
<span class="wiz__brandname">Aurora Admin</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<ol class="wiz__steps">
|
||||
<template v-for="(s, i) in steps" :key="s.id">
|
||||
<li
|
||||
class="wiz__step"
|
||||
:class="{ 'is-active': i === stepIndex, 'is-done': i < stepIndex }"
|
||||
>
|
||||
<span class="wiz__dot">
|
||||
<span v-if="i < stepIndex" class="dashicons dashicons-yes" />
|
||||
<span v-else>{{ i + 1 }}</span>
|
||||
</span>
|
||||
<span class="wiz__step-label">{{ s.label }}</span>
|
||||
</li>
|
||||
<li v-if="i < steps.length - 1" class="wiz__connector" :class="{ 'is-on': i < stepIndex }" />
|
||||
</template>
|
||||
</ol>
|
||||
|
||||
<!-- Card -->
|
||||
<section class="wiz__card">
|
||||
<!-- Welcome -->
|
||||
<div v-if="current.id === 'welcome'" class="wiz__welcome">
|
||||
<h1 class="wiz__h1">Welcome to Aurora Admin<span v-if="serverData.userName">, {{ serverData.userName }}</span></h1>
|
||||
<p class="wiz__lead">
|
||||
A modern, fast redesign of the WordPress admin. This quick setup takes about a
|
||||
minute — choose a look, add your branding, and pick which screens Aurora takes over.
|
||||
You can skip it and change everything later in Settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Done -->
|
||||
<div v-else-if="current.id === 'done'" class="wiz__welcome">
|
||||
<span class="wiz__done-icon dashicons dashicons-yes-alt" />
|
||||
<h1 class="wiz__h1">You’re all set</h1>
|
||||
<p class="wiz__lead">
|
||||
Aurora is configured and ready. Head to your dashboard, or fine-tune anything in
|
||||
Settings.
|
||||
</p>
|
||||
<div class="wiz__done-actions">
|
||||
<button type="button" class="wiz__btn wiz__btn--primary" :disabled="saving" @click="finish(serverData.dashboardUrl)">
|
||||
{{ saving ? 'Finishing…' : 'Go to dashboard' }}
|
||||
</button>
|
||||
<button type="button" class="wiz__btn" :disabled="saving" @click="finish(serverData.settingsUrl)">
|
||||
Open settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Field steps -->
|
||||
<div v-else>
|
||||
<h1 class="wiz__h1">{{ current.title }}</h1>
|
||||
<p v-if="current.subtitle" class="wiz__lead">{{ current.subtitle }}</p>
|
||||
<div class="wiz__fields">
|
||||
<SettingsField
|
||||
v-for="f in current.fields"
|
||||
:key="f.key"
|
||||
:field="f"
|
||||
v-model="settings[f.key]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer nav (hidden on the Done step, which has its own actions) -->
|
||||
<div v-if="!isDone" class="wiz__nav">
|
||||
<button type="button" class="wiz__btn wiz__btn--ghost" :disabled="saving" @click="finish(serverData.dashboardUrl)">
|
||||
Skip setup
|
||||
</button>
|
||||
<div class="wiz__nav-right">
|
||||
<button v-if="!isFirst" type="button" class="wiz__btn" @click="goBack">Back</button>
|
||||
<button type="button" class="wiz__btn wiz__btn--primary" @click="goNext">
|
||||
{{ isFirst ? 'Get started' : 'Continue' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wiz {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
overflow-y: auto;
|
||||
background:
|
||||
radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--aurora-accent) 12%, var(--aurora-app-bg-solid)) 0%, var(--aurora-app-bg-solid) 65%);
|
||||
color: var(--aurora-text);
|
||||
font-family: var(--aurora-font-family);
|
||||
}
|
||||
.wiz__inner {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 64px;
|
||||
}
|
||||
|
||||
.wiz__brand { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 30px; }
|
||||
.wiz__mark {
|
||||
width: 30px; height: 30px; border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-accent); color: #fff;
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 700;
|
||||
}
|
||||
.wiz__brandname { font-size: 1rem; font-weight: 500; }
|
||||
|
||||
/* Progress */
|
||||
.wiz__steps { display: flex; align-items: center; justify-content: center; list-style: none; margin: 0 0 30px; padding: 0; }
|
||||
.wiz__step { display: flex; flex-direction: column; align-items: center; gap: 7px; flex: 0 0 auto; }
|
||||
.wiz__dot {
|
||||
width: 28px; height: 28px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.75rem; font-weight: 500;
|
||||
border: 1.5px solid var(--aurora-border); background: var(--aurora-card-bg); color: var(--aurora-text-muted);
|
||||
}
|
||||
.wiz__dot .dashicons { font-size: 16px; width: 16px; height: 16px; }
|
||||
.wiz__step.is-done .wiz__dot { background: var(--aurora-accent); border-color: var(--aurora-accent); color: #fff; }
|
||||
.wiz__step.is-active .wiz__dot { border-color: var(--aurora-accent); color: var(--aurora-accent); }
|
||||
.wiz__step-label { font-size: 0.6875rem; color: var(--aurora-text-muted); }
|
||||
.wiz__step.is-active .wiz__step-label, .wiz__step.is-done .wiz__step-label { color: var(--aurora-text); }
|
||||
.wiz__connector { flex: 1 1 auto; height: 1.5px; min-width: 22px; background: var(--aurora-border); margin: 0 6px; position: relative; top: -10px; }
|
||||
.wiz__connector.is-on { background: var(--aurora-accent); }
|
||||
|
||||
/* Card */
|
||||
.wiz__card {
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: var(--aurora-radius-xl);
|
||||
padding: 30px 32px;
|
||||
}
|
||||
.wiz__h1 { font-size: 1.5rem; font-weight: 600; margin: 0; }
|
||||
.wiz__lead { margin: 10px 0 0; font-size: 0.9rem; color: var(--aurora-text-muted); line-height: 1.6; }
|
||||
|
||||
.wiz__welcome { text-align: center; padding: 20px 8px; }
|
||||
.wiz__welcome .wiz__lead { max-width: 520px; margin: 12px auto 0; }
|
||||
.wiz__done-icon { font-size: 52px; width: 52px; height: 52px; color: var(--aurora-accent); }
|
||||
.wiz__done-actions { display: flex; gap: 12px; justify-content: center; margin-top: 26px; }
|
||||
|
||||
/* Fields: SettingsField renders its own rows; drop the first row's top pad
|
||||
and the last row's divider so they sit cleanly inside the card. */
|
||||
.wiz__fields { margin-top: 8px; }
|
||||
.wiz__fields > :deep(.field):first-child { padding-top: 12px; }
|
||||
.wiz__fields > :deep(.field):last-child { border-bottom: none; padding-bottom: 4px; }
|
||||
|
||||
/* Nav */
|
||||
.wiz__nav { display: flex; align-items: center; justify-content: space-between; margin-top: 22px; }
|
||||
.wiz__nav-right { display: flex; gap: 10px; }
|
||||
.wiz__btn {
|
||||
font-size: 0.875rem; font-weight: 500;
|
||||
padding: 10px 20px; border-radius: var(--aurora-radius-md); cursor: pointer;
|
||||
border: 1px solid var(--aurora-border); background: var(--aurora-card-bg); color: var(--aurora-text);
|
||||
}
|
||||
.wiz__btn:hover { border-color: var(--aurora-accent); }
|
||||
.wiz__btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.wiz__btn--ghost { border-color: transparent; color: var(--aurora-text-muted); }
|
||||
.wiz__btn--ghost:hover { color: var(--aurora-text); border-color: transparent; }
|
||||
.wiz__btn--primary { background: var(--aurora-accent); color: #fff; border-color: var(--aurora-accent); }
|
||||
.wiz__btn--primary:hover { filter: brightness(1.08); }
|
||||
</style>
|
||||
@@ -131,13 +131,13 @@ const remove = async (theme) => {
|
||||
}
|
||||
.themes-page h1 { margin: 0; font-size: 1.6rem; font-weight: 700; color: var(--aurora-text); }
|
||||
.themes-empty { color: var(--aurora-text-muted); font-size: 0.8125rem; }
|
||||
.themes-notice { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.themes-notice { padding: 10px 14px; border-radius: var(--aurora-radius-sm); margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.themes-notice--error { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
|
||||
.themes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 18px; }
|
||||
.theme-card {
|
||||
background: var(--aurora-bg-subtle); border: 1px solid var(--aurora-border);
|
||||
border-radius: 14px; overflow: hidden; display: flex; flex-direction: column;
|
||||
border-radius: var(--aurora-radius-md); overflow: hidden; display: flex; flex-direction: column;
|
||||
}
|
||||
.theme-card--active { border-color: var(--aurora-accent); }
|
||||
.theme-card__shot {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="tools-page">
|
||||
<header class="tools-page__header">
|
||||
<p class="tools-page__eyebrow">Aurora Admin</p>
|
||||
<h1>Tools</h1>
|
||||
</header>
|
||||
|
||||
<div class="tools-page__grid">
|
||||
<!-- Core's Categories and Tags Converter card — not produced by a
|
||||
hook, so it's reproduced here directly (same visibility gate as
|
||||
native tools.php: the `import` capability plus category/tag
|
||||
manage_terms). -->
|
||||
<article v-if="serverData.showCategoriesConverter" class="card">
|
||||
<h2 class="title">Categories and Tags Converter</h2>
|
||||
<p>
|
||||
If you want to convert your categories to tags (or vice versa), use the
|
||||
<a :href="serverData.importUrl">Categories and Tags Converter</a> available from the Import screen.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<!-- Rendered from WordPress's own `tool_box` hook — whatever other
|
||||
active plugins add there (WooCommerce, migration tools, etc.).
|
||||
Not Aurora-authored content, only Aurora-styled, so nothing
|
||||
another plugin adds to this screen silently disappears. -->
|
||||
<div v-if="serverData.nativeToolsHtml" class="tools-page__native" v-html="serverData.nativeToolsHtml" />
|
||||
</div>
|
||||
<p v-if="!serverData.showCategoriesConverter && !serverData.nativeToolsHtml" class="tools-page__empty">
|
||||
No tools are available right now.
|
||||
</p>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tools-page { min-height: calc(100vh - 112px); color: var(--aurora-text); background: transparent; }
|
||||
.tools-page__eyebrow {
|
||||
margin: 0 0 7px; color: var(--aurora-text-muted); font-size: 0.72rem;
|
||||
font-weight: 800; text-transform: uppercase;
|
||||
}
|
||||
.tools-page__header h1 { margin: 0 0 24px; font-size: 1.45rem; line-height: 1.15; }
|
||||
.tools-page__empty { color: var(--aurora-text-muted); font-size: 0.875rem; }
|
||||
|
||||
.tools-page__grid {
|
||||
display: grid; gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
/* The v-html'd wrapper participates as if it weren't there, so cards inside
|
||||
it (there may be several, one per plugin hooked to tool_box) lay out in
|
||||
the same grid as Aurora's own card above instead of stacking inside one
|
||||
grid cell. */
|
||||
.tools-page__native { display: contents; }
|
||||
|
||||
/* Native tool_box markup is `.card > h2.title + p` (core's own Categories &
|
||||
Tags Converter shape), but other plugins' cards may not match exactly —
|
||||
styled broadly by element, with .card/.title as the primary targets. The
|
||||
plain selectors below cover Aurora's own card (real template markup, gets
|
||||
Vue's scoping attribute automatically); :deep() additionally reaches into
|
||||
the v-html'd content, which the scoping attribute can't be applied to. */
|
||||
.card, .tools-page__native :deep(.card) {
|
||||
background: var(--aurora-card-bg); border: 1px solid var(--aurora-card-border);
|
||||
border-radius: var(--aurora-radius-lg); padding: 20px 22px; margin: 0;
|
||||
}
|
||||
.card h2, .card h3, .card .title, .tools-page__native :deep(h2), .tools-page__native :deep(h3), .tools-page__native :deep(.title) {
|
||||
margin: 0 0 10px; font-size: 1rem; font-weight: 700; color: var(--aurora-text);
|
||||
}
|
||||
.card p, .tools-page__native :deep(p) {
|
||||
margin: 0; font-size: 0.8125rem; color: var(--aurora-text-muted); line-height: 1.6;
|
||||
}
|
||||
.card a, .tools-page__native :deep(a) { color: var(--aurora-accent); text-decoration: none; }
|
||||
.card a:hover, .tools-page__native :deep(a:hover) { text-decoration: underline; }
|
||||
</style>
|
||||
@@ -126,7 +126,7 @@ const submit = async () => {
|
||||
.add-user-page h1 { margin: 0; font-size: 1.6rem; font-weight: 700; color: var(--aurora-text); }
|
||||
|
||||
.add-user-notice {
|
||||
padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 0.875rem;
|
||||
padding: 10px 14px; border-radius: var(--aurora-radius-sm); margin-bottom: 16px; font-size: 0.875rem;
|
||||
}
|
||||
.add-user-notice--success { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.add-user-notice--error { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
@@ -135,7 +135,7 @@ const submit = async () => {
|
||||
display: flex; flex-direction: column; gap: 18px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
padding: 24px;
|
||||
}
|
||||
.add-user-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
@@ -143,7 +143,7 @@ const submit = async () => {
|
||||
.add-user-field em { font-weight: 400; font-style: normal; color: var(--aurora-text-muted); }
|
||||
.add-user-field input,
|
||||
.add-user-field select {
|
||||
padding: 9px 12px; border-radius: 8px; border: 1px solid var(--aurora-border);
|
||||
padding: 9px 12px; border-radius: var(--aurora-radius-sm); border: 1px solid var(--aurora-border);
|
||||
background: var(--aurora-bg); color: var(--aurora-text); font-size: 0.875rem;
|
||||
}
|
||||
.add-user-field__row { display: flex; gap: 8px; }
|
||||
|
||||
@@ -281,7 +281,7 @@ const initials = (name) =>
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-accent);
|
||||
color: var(--aurora-accent-text);
|
||||
font-size: 0.84rem;
|
||||
@@ -331,7 +331,7 @@ const initials = (name) =>
|
||||
width: min(320px, 100%);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -356,7 +356,7 @@ const initials = (name) =>
|
||||
width: 42px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
@@ -383,7 +383,7 @@ const initials = (name) =>
|
||||
.users-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.78rem;
|
||||
@@ -392,7 +392,7 @@ const initials = (name) =>
|
||||
.users-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.users-table__head,
|
||||
@@ -507,7 +507,7 @@ const initials = (name) =>
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--aurora-text-muted);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -111,20 +111,20 @@ const remove = async (widget) => {
|
||||
.widgets-page h1 { margin: 0 0 6px; font-size: 1.6rem; font-weight: 700; color: var(--aurora-text); }
|
||||
.widgets-page__sub { margin: 0; font-size: 0.8125rem; color: var(--aurora-text-muted); max-width: 640px; }
|
||||
|
||||
.widgets-notice { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.widgets-notice { padding: 10px 14px; border-radius: var(--aurora-radius-sm); margin-bottom: 16px; font-size: 0.875rem; }
|
||||
.widgets-notice--error { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
|
||||
.widgets-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }
|
||||
.widgets-sidebar {
|
||||
background: var(--aurora-bg-subtle); border: 1px solid var(--aurora-border);
|
||||
border-radius: 14px; padding: 18px;
|
||||
border-radius: var(--aurora-radius-md); padding: 18px;
|
||||
}
|
||||
.widgets-sidebar h2 { font-size: 0.9375rem; margin: 0 0 10px; color: var(--aurora-text); }
|
||||
.widgets-empty { color: var(--aurora-text-muted); font-size: 0.8125rem; }
|
||||
|
||||
.widgets-item {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
||||
padding: 8px 10px; border-radius: 8px; margin-bottom: 6px;
|
||||
padding: 8px 10px; border-radius: var(--aurora-radius-sm); margin-bottom: 6px;
|
||||
background: var(--aurora-bg); border: 1px solid var(--aurora-border);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
@@ -134,11 +134,11 @@ const remove = async (widget) => {
|
||||
.widgets-item__actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
||||
.widgets-item__actions button {
|
||||
border: none; background: none; color: var(--aurora-text-muted); cursor: pointer;
|
||||
width: 26px; height: 26px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 26px; height: 26px; border-radius: var(--aurora-radius-sm); display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.widgets-item__actions button:hover { background: var(--aurora-bg-subtle); color: var(--aurora-text); }
|
||||
.widgets-item__actions select {
|
||||
font-size: 0.75rem; padding: 4px 6px; border-radius: 6px;
|
||||
font-size: 0.75rem; padding: 4px 6px; border-radius: var(--aurora-radius-sm);
|
||||
border: 1px solid var(--aurora-border); background: var(--aurora-bg); color: var(--aurora-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,7 @@ 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 { getMinimizedWindows, removeMinimizedWindow } from './taskbar.js';
|
||||
import { initDashboardFrame } from './dashboardFrame.js';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: {
|
||||
@@ -25,21 +24,7 @@ const toggleMenu = () => {
|
||||
localStorage.setItem('aurora-menu-collapsed', collapsed.value ? '1' : '0');
|
||||
};
|
||||
|
||||
// Every minimized window (taskbar.js persists the list in sessionStorage
|
||||
// across the page loads minimizing triggers). Rendered as taskbar items at
|
||||
// the bottom of every page since SiteShell mounts on all of them — clicking
|
||||
// one navigates back to it and removes just that entry.
|
||||
const minimizedWindows = ref([]);
|
||||
const restoreWindow = (win) => {
|
||||
removeMinimizedWindow(win.id);
|
||||
window.location.href = win.url;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
minimizedWindows.value = getMinimizedWindows();
|
||||
// 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', minimizedWindows.value.length > 0);
|
||||
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
|
||||
@@ -49,7 +34,7 @@ onMounted(() => {
|
||||
applyThemePalette(settings.theme_preset || 'indigo');
|
||||
applyCollapsed();
|
||||
applyFontFamily(settings.font_family);
|
||||
initWindowChrome(props.serverData?.adminUrl);
|
||||
initDashboardFrame();
|
||||
// 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).
|
||||
@@ -73,74 +58,13 @@ onMounted(() => {
|
||||
:server-data="serverData"
|
||||
@toggle-menu="toggleMenu"
|
||||
/>
|
||||
<div v-if="minimizedWindows.length" class="aurora-taskbar">
|
||||
<button
|
||||
v-for="win in minimizedWindows"
|
||||
:key="win.id"
|
||||
type="button"
|
||||
class="aurora-taskbar__item"
|
||||
@click="restoreWindow(win)"
|
||||
>
|
||||
<span class="aurora-taskbar__dot" />
|
||||
{{ win.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Sidebar, Toolbar and the taskbar are all position:fixed, so this
|
||||
wrapper has no layout role — it just groups them under one mounted
|
||||
root. */
|
||||
/* Sidebar and Toolbar are both position:fixed, so this wrapper has no
|
||||
layout role — it just groups them under one mounted root. */
|
||||
.site-shell {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Full-width docked bar, but inset with the same gap the sidebar floats
|
||||
with (not flush against the browser edges or the window above it) and
|
||||
fully rounded + shadowed so it reads as its own separate surface rather
|
||||
than an extension of the content frame it sits under. Page content
|
||||
reserves space above it via html.aurora-has-taskbar in shell-frame.css. */
|
||||
.aurora-taskbar {
|
||||
position: fixed;
|
||||
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2);
|
||||
right: var(--aurora-sidebar-gap);
|
||||
bottom: var(--aurora-sidebar-gap);
|
||||
height: var(--aurora-taskbar-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||
box-sizing: border-box;
|
||||
z-index: 200001;
|
||||
overflow-x: auto;
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
.aurora-taskbar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
background: var(--aurora-bg-subtle);
|
||||
border: 1px solid var(--aurora-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>
|
||||
|
||||
@@ -16,6 +16,18 @@ const highlighted = ref(0);
|
||||
let debounceTimer;
|
||||
let requestSeq = 0;
|
||||
|
||||
// wp/v2/search returns titles with HTML entities (e.g. "Ben & Jerry").
|
||||
// Decode them for display WITHOUT rendering markup: a <textarea>'s innerHTML
|
||||
// is RCDATA, so tags stay literal text and only entities are decoded — then
|
||||
// we read them back as a plain string and render with text interpolation.
|
||||
// This replaces a v-html bind on item.title, which would have executed any
|
||||
// HTML an unfiltered_html user put in a post title (admin→admin XSS).
|
||||
const decodeEntities = (str) => {
|
||||
const el = document.createElement('textarea');
|
||||
el.innerHTML = String(str ?? '');
|
||||
return el.value;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
const q = query.value.trim();
|
||||
if (q.length < 2) {
|
||||
@@ -121,7 +133,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
@mouseenter="highlighted = i"
|
||||
@click="openResult(item)"
|
||||
>
|
||||
<span class="smodal__result-title" v-html="item.title" />
|
||||
<span class="smodal__result-title">{{ decodeEntities(item.title) }}</span>
|
||||
<span class="smodal__result-type">{{ item.subtype }}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -146,7 +158,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
align-self: flex-start;
|
||||
background: var(--aurora-frame-bg, #1c1c20);
|
||||
border: 1px solid var(--aurora-frame-border, #34343b);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
@@ -171,7 +183,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
font-size: 0.6875rem;
|
||||
color: var(--aurora-text-muted);
|
||||
border: 1px solid var(--aurora-border, #27272a);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
padding: 2px 6px;
|
||||
}
|
||||
.smodal__state {
|
||||
@@ -197,7 +209,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
}
|
||||
.smodal__result--active { background: var(--aurora-accent-soft); }
|
||||
.smodal__result-title {
|
||||
|
||||
@@ -276,7 +276,7 @@ const filteredAuroraNav = computed(() => {
|
||||
flush against the viewport edges. */
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 20px;
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
transition: width 0.2s ease;
|
||||
z-index: 99999;
|
||||
box-sizing: border-box;
|
||||
@@ -316,7 +316,7 @@ const filteredAuroraNav = computed(() => {
|
||||
gap: 8px;
|
||||
margin: 0 12px 10px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
@@ -361,7 +361,7 @@ const filteredAuroraNav = computed(() => {
|
||||
.sidebar__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ const filteredAuroraNav = computed(() => {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
transition: background 0.12s ease, color 0.12s ease;
|
||||
@@ -467,7 +467,7 @@ const filteredAuroraNav = computed(() => {
|
||||
text-decoration: none;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
padding: 0 10px;
|
||||
min-width: 240px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.8125rem;
|
||||
@@ -141,7 +141,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
.toolbar__search-kbd {
|
||||
font-size: 0.6875rem;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
padding: 1px 6px;
|
||||
background: var(--aurora-bg);
|
||||
}
|
||||
@@ -154,7 +154,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
height: 34px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
cursor: pointer;
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.user-menu__trigger:hover {
|
||||
@@ -125,7 +125,7 @@ onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
width: 240px;
|
||||
background: var(--aurora-bg);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--aurora-radius-md);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
z-index: 100000;
|
||||
@@ -170,7 +170,7 @@ onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--aurora-radius-sm);
|
||||
text-decoration: none;
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.8125rem;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Structural setup for the takeover Dashboard.
|
||||
*
|
||||
* DashboardTakeover.php renders the dashboard into a separate
|
||||
* #aurora-admin-dashboard-root sibling (above the hidden #wpbody), so unlike
|
||||
* every other page it never sits inside #wpbody-content and never got that
|
||||
* element's card frame. Wrap it in .aurora-dashboard-frame so it gets the
|
||||
* same card treatment, and pull the custom admin notices
|
||||
* (Notices::render_for_dashboard(), printed as a sibling just before the
|
||||
* mount point) into that frame so they render as part of the card.
|
||||
*
|
||||
* This is the only DOM restructuring the shell does — there is no window
|
||||
* chrome (title bars, traffic-light controls, minimize taskbar); that whole
|
||||
* macOS-window metaphor was removed.
|
||||
*/
|
||||
export function initDashboardFrame() {
|
||||
const dashboardRoot = document.getElementById('aurora-admin-dashboard-root');
|
||||
if (!dashboardRoot) {
|
||||
return;
|
||||
}
|
||||
// Dedupe: if SiteShell re-mounts, don't wrap a second time.
|
||||
if (dashboardRoot.parentNode.querySelector('.aurora-dashboard-frame')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'aurora-dashboard-frame';
|
||||
dashboardRoot.parentNode.insertBefore(frame, dashboardRoot);
|
||||
|
||||
const notices = document.getElementById('aurora-admin-dashboard-notices');
|
||||
if (notices) {
|
||||
frame.appendChild(notices);
|
||||
}
|
||||
frame.appendChild(dashboardRoot);
|
||||
}
|
||||
+109
-183
@@ -14,13 +14,6 @@
|
||||
: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;
|
||||
/* Height of the strip reserved at the top of the block editor's frame
|
||||
for .aurora-winbar--editor's traffic-light dots (a sibling element,
|
||||
not a child of the skeleton — see windowChrome.js). */
|
||||
--aurora-editor-winbar-h: 44px;
|
||||
/* 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. */
|
||||
@@ -28,8 +21,12 @@
|
||||
/* Breathing room between page content and the right browser edge. */
|
||||
--aurora-gutter-r: 44px;
|
||||
|
||||
--aurora-radius: 14px;
|
||||
--aurora-radius-lg: 24px;
|
||||
/* Corner-radius scale, one notch rounder across the UI — tune here and
|
||||
every surface follows (all radii are tokenized onto this scale). */
|
||||
--aurora-radius-sm: 10px; /* chips, badges, small/icon controls */
|
||||
--aurora-radius-md: 14px; /* buttons, inputs, search, list rows */
|
||||
--aurora-radius-lg: 20px; /* cards, panels, larger buttons */
|
||||
--aurora-radius-xl: 28px; /* page frames: content card, dashboard, sidebar, editor */
|
||||
|
||||
/* Light theme */
|
||||
--aurora-app-bg: #fafafa; /* zinc-50 */
|
||||
@@ -167,15 +164,9 @@ html.aurora-ready #site-editor {
|
||||
position produced a visible double-edge rendering artifact right at
|
||||
the rounded corners. The background/gradient contrast alone is
|
||||
enough to read as a distinct surface without it. */
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
border-radius: var(--aurora-radius-xl);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: left 0.2s ease;
|
||||
/* Reserves a strip at the top for .aurora-winbar--editor's dots (a
|
||||
sibling, not a child — see windowChrome.js) so they sit in their own
|
||||
clear space instead of overlapping the editor's own logo/toolbar row.
|
||||
border-box so the padding comes out of the top/left/right/bottom
|
||||
inset box already defined above, rather than adding to it. */
|
||||
padding-top: var(--aurora-editor-winbar-h);
|
||||
box-sizing: border-box;
|
||||
/* border-radius alone doesn't clip children — Gutenberg's own white
|
||||
canvas background ran straight past the rounded bottom corners as a
|
||||
@@ -203,8 +194,12 @@ html.aurora-ready #wpbody-content {
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: var(--aurora-radius-lg);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
border-radius: var(--aurora-radius-xl);
|
||||
/* A real soft shadow (not the old barely-there 0.06 one) so the rounded
|
||||
card edge reads against dense, full-bleed page content — otherwise the
|
||||
frame is technically rounded but visually invisible except on sparse
|
||||
pages. Follows the border-radius, defining the card shape. */
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
|
||||
margin: 16px var(--aurora-gutter-r) 24px 24px;
|
||||
padding: 32px;
|
||||
min-height: calc(100vh - var(--aurora-toolbar-h) - 40px);
|
||||
@@ -250,48 +245,6 @@ html.aurora-ready body.post-php #wpbody-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
html.aurora-ready body.block-editor-page #wpbody-content > .aurora-winbar,
|
||||
html.aurora-ready body.post-new-php #wpbody-content > .aurora-winbar,
|
||||
html.aurora-ready body.post-php #wpbody-content > .aurora-winbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* SiteShell.vue's taskbar floats along the bottom edge (inset by
|
||||
--aurora-sidebar-gap, same as the sidebar) only while a window is
|
||||
minimized. A plain margin-bottom only guarantees a gap once the page is
|
||||
scrolled all the way down — content taller than the viewport still
|
||||
renders underneath the fixed taskbar on first paint, since a fixed
|
||||
element doesn't reserve space from the rest of the page's flow. Instead,
|
||||
cap the content box's own height to whatever's actually left between the
|
||||
toolbar and the taskbar and let *it* scroll internally — the same way
|
||||
the toolbar's reserved space at the top never lets content start behind
|
||||
it, not just "eventually scroll clear of it". */
|
||||
html.aurora-has-taskbar #wpbody-content,
|
||||
html.aurora-has-taskbar .aurora-dashboard-frame {
|
||||
/* #wpbody-content's base rule sets min-height: calc(100vh - toolbar-h -
|
||||
40px) for the normal (no-taskbar) case — per spec, when min-height is
|
||||
larger than max-height, min-height always wins and max-height is
|
||||
silently ignored. Without this override the box kept sizing to that
|
||||
taller min-height regardless of the taskbar, which is why the
|
||||
max-height below appeared to do nothing. */
|
||||
min-height: 0;
|
||||
max-height: calc(
|
||||
100vh - var(--aurora-toolbar-h) - 16px - 24px - var(--aurora-taskbar-h) -
|
||||
var(--aurora-sidebar-gap) * 2
|
||||
);
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
/* The block editor's skeleton is already positioned with fixed insets
|
||||
(top/left/right/bottom) rather than margin, so reserving taskbar space
|
||||
is just moving its bottom inset in — no max-height/overflow trick
|
||||
needed there, unlike the margin-based elements above. */
|
||||
html.aurora-has-taskbar .interface-interface-skeleton,
|
||||
html.aurora-has-taskbar #site-editor {
|
||||
bottom: calc(24px + var(--aurora-taskbar-h) + var(--aurora-sidebar-gap) * 2) !important;
|
||||
}
|
||||
|
||||
/* Theme installer preview ("Add Themes" > click a theme > full "Preview &
|
||||
Install" overlay, theme-install.php). WP core's own .wp-full-overlay
|
||||
component (shared with the Customizer, but only .theme-install-overlay
|
||||
@@ -355,114 +308,53 @@ html.aurora-ready .theme-install-overlay.collapsed {
|
||||
|
||||
html.aurora-ready .theme-install-overlay .wp-full-overlay-sidebar {
|
||||
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2 + 24px) !important;
|
||||
top: calc(var(--aurora-toolbar-h) + 16px + var(--aurora-editor-winbar-h)) !important;
|
||||
top: calc(var(--aurora-toolbar-h) + 16px) !important;
|
||||
bottom: 24px !important;
|
||||
height: auto !important;
|
||||
border-top-left-radius: var(--aurora-radius-lg);
|
||||
border-top-left-radius: var(--aurora-radius-xl);
|
||||
}
|
||||
html.aurora-ready .theme-install-overlay .wp-full-overlay-main {
|
||||
top: var(--aurora-editor-winbar-h) !important;
|
||||
border-top-right-radius: var(--aurora-radius-lg);
|
||||
top: 0 !important;
|
||||
border-top-right-radius: var(--aurora-radius-xl);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
.aurora-winbar--editor {
|
||||
position: fixed;
|
||||
/* Centered vertically within the skeleton's reserved top strip
|
||||
(--aurora-editor-winbar-h), inset from its left edge to line up with
|
||||
the padding every other title bar uses. */
|
||||
top: calc(
|
||||
var(--aurora-toolbar-h) + 16px + (var(--aurora-editor-winbar-h) - 12px) / 2
|
||||
);
|
||||
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2 + 24px + 20px);
|
||||
height: 12px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
z-index: 100000;
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
#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__crumb {
|
||||
margin-left: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--aurora-text-muted);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.aurora-winbar__crumb:hover {
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.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. */
|
||||
it into a separate sibling — see dashboardFrame.js), so it never got the
|
||||
card frame every other page has. dashboardFrame.js wraps 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);
|
||||
border-radius: var(--aurora-radius-xl);
|
||||
/* Match #wpbody-content's shadow so the dashboard's rounded card edge
|
||||
reads the same way — the dashboard is dense and full-bleed, so the old
|
||||
0.06 shadow left it looking un-rounded. */
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
|
||||
margin: 16px var(--aurora-gutter-r) 24px 24px;
|
||||
box-sizing: border-box;
|
||||
/* Same unconditional height cap as #wpbody-content's base rule — this
|
||||
element never got it, since the Dashboard bypasses #wpbody-content
|
||||
entirely (a separate sibling — see windowChrome.js) and only had the
|
||||
taskbar-present variant below. Once html/body were locked to
|
||||
overflow:hidden (to kill a phantom document-level scrollbar on
|
||||
another page), the dashboard's content past the first ~1500px became
|
||||
completely unreachable: no internal scroll here, and no document
|
||||
entirely (a separate sibling — see dashboardFrame.js). Once html/body
|
||||
were locked to overflow:hidden (to kill a phantom document-level
|
||||
scrollbar on another page), the dashboard's content past the first
|
||||
~1500px became completely unreachable: no internal scroll here, and
|
||||
no document
|
||||
scroll left to fall back on either. */
|
||||
max-height: calc(100vh - var(--aurora-toolbar-h) - 40px);
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
.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);
|
||||
/* The frame itself clips (rounded corners) and lays out as a column; the
|
||||
actual scrolling happens on the inner app-root (see the shared scroll
|
||||
rule below), so the native scrollbar lives INSIDE the rounded card
|
||||
rather than squaring off its right corners. The 12px padding is the
|
||||
gutter that keeps the scrollbar clear of the corner radius. */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Custom admin notices (Notices::render_for_dashboard(), reparented in
|
||||
here by windowChrome.js) — native-dark.css already themes .notice
|
||||
here by dashboardFrame.js) — native-dark.css already themes .notice
|
||||
elsewhere via #wpbody-content, but that stylesheet isn't loaded on the
|
||||
Dashboard at all (Shell::is_own_page() treats index.php as one of
|
||||
Aurora's own pages), so it needs its own copy of the same treatment
|
||||
@@ -476,41 +368,6 @@ html.aurora-ready .theme-install-overlay .wp-full-overlay-main {
|
||||
margin: 16px 20px 0;
|
||||
}
|
||||
|
||||
/* 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,
|
||||
html.aurora-ready.aurora-maximized #site-editor {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: auto !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
html.aurora-maximized .aurora-winbar--editor {
|
||||
top: calc((var(--aurora-editor-winbar-h) - 12px) / 2);
|
||||
left: 20px;
|
||||
}
|
||||
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.
|
||||
No background here: #wpbody-content is inset from the sidebar/gutter,
|
||||
@@ -523,6 +380,75 @@ html.aurora-ready body.toplevel_page_aurora-admin #wpbody-content {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* --- Scroll INSIDE the rounded frame -------------------------------------
|
||||
A single element can't both scroll and keep its own rounded corners: the
|
||||
native scrollbar sits in the top/bottom-right corners and squares them
|
||||
off. So the frame clips (overflow:hidden + border-radius) and its Aurora
|
||||
app-root child becomes the real scroll container — the scrollbar then
|
||||
lives inside the rounded card, inset by the frame's 12px padding so it
|
||||
clears the corner radius.
|
||||
|
||||
`:has()` limits the #wpbody-content half to Aurora's own hijacked pages
|
||||
(their mount div is a direct #aurora-admin-*-root child); native WP pages
|
||||
keep their normal single-layer 32px-padded scroll, untouched. Content
|
||||
padding is split 12px (frame) + 20px (scroller) so it still totals the
|
||||
original 32px — pages that assume that (e.g. AppListShell's -32px bleed)
|
||||
keep working. */
|
||||
/* `body:not(.toplevel_page_aurora-admin)` excludes the Settings page — it
|
||||
deliberately drops the frame and manages its own -32px/scroll-as-a-whole
|
||||
layout (see Settings.vue), so the two-layer treatment must not touch it. */
|
||||
html.aurora-ready body:not(.toplevel_page_aurora-admin) #wpbody-content:has(> [id^="aurora-admin-"][id$="-root"]) {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
}
|
||||
.aurora-dashboard-frame > [id^="aurora-admin-"][id$="-root"],
|
||||
html.aurora-ready body:not(.toplevel_page_aurora-admin) #wpbody-content > [id^="aurora-admin-"][id$="-root"] {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
/* The hijacked/list pages need their content padding on the scroller (the
|
||||
dashboard's own content already pads itself). */
|
||||
html.aurora-ready body:not(.toplevel_page_aurora-admin) #wpbody-content > [id^="aurora-admin-"][id$="-root"] {
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- Themed scrollbars -----------------------------------------------------
|
||||
Skin every scrollbar inside the Aurora shell in a translucent shade of the
|
||||
active theme's accent (derived from --aurora-accent, so it tracks whatever
|
||||
preset is selected) instead of the OS default. The plain accent-soft token
|
||||
is a fallback for engines without color-mix; the 3px transparent border +
|
||||
background-clip:padding-box keeps the thumb slim with a little breathing
|
||||
room around it. */
|
||||
html.aurora-ready {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--aurora-accent-soft) transparent;
|
||||
scrollbar-color: color-mix(in srgb, var(--aurora-accent) 55%, transparent) transparent;
|
||||
}
|
||||
html.aurora-ready ::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
html.aurora-ready ::-webkit-scrollbar-track,
|
||||
html.aurora-ready ::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
html.aurora-ready ::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
border: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: var(--aurora-accent-soft);
|
||||
background-color: color-mix(in srgb, var(--aurora-accent) 45%, transparent);
|
||||
}
|
||||
html.aurora-ready ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--aurora-accent);
|
||||
background-color: color-mix(in srgb, var(--aurora-accent) 72%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Native WordPress pages are fully dark-themed by a dedicated stylesheet
|
||||
* (admin/assets/native-dark.css), enqueued only on native pages and gated
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Tracks every "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. Stored as a list, not a single slot —
|
||||
* minimizing a second page used to silently overwrite the first one's
|
||||
* entry, so only the most recent minimize ever showed up in the taskbar.
|
||||
*/
|
||||
const STORAGE_KEY = 'aurora-minimized-windows';
|
||||
|
||||
export function getMinimizedWindows() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveMinimizedWindows(list) {
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||||
} catch {
|
||||
/* sessionStorage unavailable (e.g. private mode) — minimize just won't persist */
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds (or, if the same page is already minimized, replaces) an entry. */
|
||||
export function addMinimizedWindow(title, url) {
|
||||
const list = getMinimizedWindows().filter((w) => w.url !== url);
|
||||
list.push({ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, title, url });
|
||||
saveMinimizedWindows(list);
|
||||
}
|
||||
|
||||
export function removeMinimizedWindow(id) {
|
||||
saveMinimizedWindows(getMinimizedWindows().filter((w) => w.id !== id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort readable label for the current page, used as the taskbar
|
||||
* item's title. Prefers the page's own on-screen <h1> over document.title —
|
||||
* WordPress's title-tag lookup walks the $submenu global to find a page's
|
||||
* title, which comes up empty for Aurora's sub-app pages (SubApps.php
|
||||
* registers each one normally, then immediately remove_submenu_page()s it
|
||||
* to keep it out of the native menu — that breaks WP's own title lookup,
|
||||
* not just this). document.title is still the fallback for pages where
|
||||
* that isn't an issue (Settings, native WP pages, the Dashboard).
|
||||
*/
|
||||
export function currentPageTitle() {
|
||||
const heading = document.querySelector('#wpbody-content h1, #aurora-admin-dashboard-root h1');
|
||||
if (heading && heading.textContent.trim()) {
|
||||
return heading.textContent.trim();
|
||||
}
|
||||
const raw = document.title || '';
|
||||
return raw.split('‹')[0].trim() || 'Window';
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
/**
|
||||
* Adds a macOS-style traffic-light title bar to the top of the page content.
|
||||
* Three cases, in priority order:
|
||||
* 1. The Dashboard — DashboardTakeover.php renders it into a separate
|
||||
* #aurora-admin-dashboard-root sibling, above #wpbody (which it hides),
|
||||
* to fully replace the native dashboard. Gets wrapped in a new
|
||||
* .aurora-dashboard-frame div to match the card treatment
|
||||
* #wpbody-content already has everywhere else.
|
||||
* 2. The block editor — gets only the traffic-light controls, overlaid on
|
||||
* the editor frame without a full-width titlebar surface.
|
||||
* 3. Everything else — native WP pages and all of Aurora's own Vue pages,
|
||||
* since each mounts its root div inside #wpbody-content. The bar is
|
||||
* inserted as its first child.
|
||||
*
|
||||
* 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. Minimized pages
|
||||
* are tracked as a list (taskbar.js), not a single slot, so minimizing
|
||||
* more than one shows up as separate taskbar items rather than the latest
|
||||
* silently replacing the previous one.
|
||||
*/
|
||||
import { addMinimizedWindow, currentPageTitle } from './taskbar.js';
|
||||
|
||||
function buildBar(adminUrl, isDashboard) {
|
||||
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;
|
||||
|
||||
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 {
|
||||
addMinimizedWindow(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');
|
||||
})
|
||||
);
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
function isBlockEditorPage() {
|
||||
const path = window.location.pathname;
|
||||
return (
|
||||
document.body.classList.contains('block-editor-page') ||
|
||||
path.endsWith('/post-new.php') ||
|
||||
path.endsWith('/post.php')
|
||||
);
|
||||
}
|
||||
|
||||
// WP core adds a "post-type-{slug}" class to <body> on every post edit
|
||||
// screen — the same signal used here to build a "back to the list" link,
|
||||
// since Posts ("edit.php") and Pages ("edit.php?post_type=page") are now
|
||||
// hijacked in place at their native URLs (see HijackedNativePages.php) and
|
||||
// there's no single admin.php?page=... URL to hardcode anymore.
|
||||
//
|
||||
// The Site Editor (site-editor.php) also carries the "block-editor-page"
|
||||
// body class isBlockEditorPage() matches on, but has no "post-type-*"
|
||||
// class (it edits templates/template parts, not a post) — returning null
|
||||
// here rather than guessing "Posts" is what keeps the crumb from showing
|
||||
// up somewhere it doesn't make sense.
|
||||
function editorBackLink(adminUrl) {
|
||||
const match = [...document.body.classList].find((c) => c.startsWith('post-type-'));
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const postType = match.replace('post-type-', '');
|
||||
const base = adminUrl || '/wp-admin/';
|
||||
if (postType === 'post') {
|
||||
return { label: 'Posts', href: base + 'edit.php' };
|
||||
}
|
||||
if (postType === 'page') {
|
||||
return { label: 'Pages', href: base + 'edit.php?post_type=page' };
|
||||
}
|
||||
return { label: postType, href: base + 'edit.php?post_type=' + encodeURIComponent(postType) };
|
||||
}
|
||||
|
||||
// theme-install.php's "Preview & Install" overlay (WP core's
|
||||
// .wp-full-overlay component) needs a permanently-running observer, not a
|
||||
// one-shot check, for two reasons: (1) core's own Backbone view creates it
|
||||
// lazily the first time a theme is clicked, so it's missing from the
|
||||
// initial DOM at initWindowChrome()'s call time; (2) every subsequent
|
||||
// theme selection re-renders the overlay's contents from scratch (its own
|
||||
// view.render() replaces all children to show the newly-selected theme's
|
||||
// details), wiping out anything inserted into it — confirmed live: the bar
|
||||
// was there after the very first click, gone after picking a second theme.
|
||||
// Kept running for the page's whole lifetime rather than disconnecting
|
||||
// after the first successful insert. Independent of the rest of this
|
||||
// file's dedupe guard (a normal page winbar already exists in
|
||||
// #wpbody-content here too; this is a second, additional bar for the
|
||||
// overlay specifically, not a replacement for it).
|
||||
function watchForThemeOverlay(adminUrl) {
|
||||
const ensureInserted = () => {
|
||||
const overlay = document.querySelector('.theme-install-overlay');
|
||||
if (overlay && !overlay.querySelector('.aurora-winbar')) {
|
||||
const bar = buildBar(adminUrl, false);
|
||||
bar.classList.add('aurora-winbar--editor');
|
||||
overlay.insertBefore(bar, overlay.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
ensureInserted();
|
||||
new MutationObserver(ensureInserted).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
export function initWindowChrome(adminUrl) {
|
||||
if (document.body.classList.contains('theme-install-php')) {
|
||||
watchForThemeOverlay(adminUrl);
|
||||
}
|
||||
|
||||
if (document.querySelector('.aurora-winbar')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardRoot = document.getElementById('aurora-admin-dashboard-root');
|
||||
if (dashboardRoot) {
|
||||
const bar = buildBar(adminUrl, true);
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'aurora-dashboard-frame';
|
||||
dashboardRoot.parentNode.insertBefore(frame, dashboardRoot);
|
||||
frame.appendChild(bar);
|
||||
// Custom admin notices (Notices::render_for_dashboard(), printed as a
|
||||
// sibling right before the mount point) — reparented into the frame
|
||||
// here too, so they render as part of the dashboard's own card
|
||||
// instead of sitting outside it unstyled. Normally these reach the
|
||||
// user via the plain admin_notices hook, but that renders inside
|
||||
// #wpbody, which the Dashboard hides entirely to hide the *native*
|
||||
// dashboard widgets — this is the replacement path for that one page.
|
||||
const notices = document.getElementById('aurora-admin-dashboard-notices');
|
||||
if (notices) frame.appendChild(notices);
|
||||
frame.appendChild(dashboardRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlockEditorPage()) {
|
||||
const bar = buildBar(adminUrl, false);
|
||||
bar.classList.add('aurora-winbar--editor');
|
||||
|
||||
const back = editorBackLink(adminUrl);
|
||||
if (back) {
|
||||
const crumb = document.createElement('a');
|
||||
crumb.className = 'aurora-winbar__crumb';
|
||||
crumb.href = back.href;
|
||||
crumb.textContent = '← ' + back.label;
|
||||
bar.appendChild(crumb);
|
||||
}
|
||||
|
||||
document.body.appendChild(bar);
|
||||
return;
|
||||
}
|
||||
|
||||
const wpbodyContent = document.getElementById('wpbody-content');
|
||||
if (wpbodyContent) {
|
||||
const bar = buildBar(adminUrl, false);
|
||||
wpbodyContent.insertBefore(bar, wpbodyContent.firstChild);
|
||||
}
|
||||
}
|
||||
+22
-4
@@ -2,9 +2,20 @@ import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
// Deliberately simple: one entry in this first slice (dashboard). Add more
|
||||
// entries here as additional pages get rebuilt — same flat pattern each
|
||||
// time, no shared scoping/theme-remap layer to keep in sync across files.
|
||||
// Multi-entry build: one entry per admin screen (see rollupOptions.input).
|
||||
// Each is enqueued as its own ES module by admin/src/Utility/Assets.php,
|
||||
// which also walks the manifest's `imports` chain for shared-chunk CSS.
|
||||
// Rollup automatically splits code shared by 2+ entries into its own chunk;
|
||||
// `vue` is pinned to a named `vendor-vue` chunk below so its hash stays
|
||||
// stable across app-code changes (returning users keep it cached across
|
||||
// plugin updates instead of re-downloading it every release).
|
||||
//
|
||||
// Sourcemaps are on by default for local debugging (the dev loop hot-patches
|
||||
// app/dist into a LocalWP install). The distribution build sets
|
||||
// AURORA_SOURCEMAP=false so the shipped zip doesn't carry ~1.2 MB of .map
|
||||
// files that are useless to end users.
|
||||
const emitSourcemaps = process.env.AURORA_SOURCEMAP !== 'false';
|
||||
|
||||
export default defineConfig({
|
||||
root: fileURLToPath(new URL('./src', import.meta.url)),
|
||||
plugins: [vue()],
|
||||
@@ -16,14 +27,16 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: fileURLToPath(new URL('./dist', import.meta.url)),
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
sourcemap: emitSourcemaps,
|
||||
manifest: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
shell: fileURLToPath(new URL('./src/entries/shell.js', import.meta.url)),
|
||||
dashboard: fileURLToPath(new URL('./src/entries/dashboard.js', import.meta.url)),
|
||||
settings: fileURLToPath(new URL('./src/entries/settings.js', import.meta.url)),
|
||||
'setup-wizard': fileURLToPath(new URL('./src/entries/setup-wizard.js', import.meta.url)),
|
||||
'menu-creator': fileURLToPath(new URL('./src/entries/menu-creator.js', import.meta.url)),
|
||||
modules: fileURLToPath(new URL('./src/entries/modules.js', import.meta.url)),
|
||||
'admin-notices': fileURLToPath(new URL('./src/entries/admin-notices.js', import.meta.url)),
|
||||
'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)),
|
||||
@@ -39,10 +52,15 @@ export default defineConfig({
|
||||
'nav-menus': fileURLToPath(new URL('./src/entries/nav-menus.js', import.meta.url)),
|
||||
'themes': fileURLToPath(new URL('./src/entries/themes.js', import.meta.url)),
|
||||
'widgets': fileURLToPath(new URL('./src/entries/widgets.js', import.meta.url)),
|
||||
'tools': fileURLToPath(new URL('./src/entries/tools.js', import.meta.url)),
|
||||
'export': fileURLToPath(new URL('./src/entries/export.js', import.meta.url)),
|
||||
'import': fileURLToPath(new URL('./src/entries/import.js', import.meta.url)),
|
||||
'profile': fileURLToPath(new URL('./src/entries/profile.js', import.meta.url)),
|
||||
'bug-report': fileURLToPath(new URL('./src/entries/bug-report.js', import.meta.url)),
|
||||
},
|
||||
output: {
|
||||
// Keep Vue in its own long-lived vendor chunk (see header note).
|
||||
manualChunks: { 'vendor-vue': ['vue'] },
|
||||
entryFileNames: '[name].[hash].js',
|
||||
chunkFileNames: 'assets/[name].[hash].js',
|
||||
assetFileNames: 'assets/[name].[hash][extname]',
|
||||
|
||||
+10
-2
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: Aurora Admin
|
||||
* Plugin URI: https://aurora.auroradragon.studio
|
||||
* Description: A modern, fast WordPress admin interface.
|
||||
* Version: 3.6.1
|
||||
* Version: 1.0
|
||||
* Author: Aurora Dragon Studio
|
||||
* Author URI: https://auroradragon.studio
|
||||
* Text Domain: aurora-admin
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
define("AURORA_ADMIN_VERSION", "3.6.1");
|
||||
define("AURORA_ADMIN_VERSION", "1.0");
|
||||
define("AURORA_ADMIN_PATH", plugin_dir_path(__FILE__));
|
||||
define("AURORA_ADMIN_URL", plugins_url("aurora-admin/"));
|
||||
|
||||
@@ -26,6 +26,7 @@ require AURORA_ADMIN_PATH . "admin/src/Shell/MenuSerializer.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Shell/Shell.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/DashboardTakeover.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/SettingsPage.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/SetupWizard.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";
|
||||
@@ -44,6 +45,9 @@ require AURORA_ADMIN_PATH . "admin/src/Pages/NativeSettingsPages.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/PermalinksData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/DiscussionData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/AppearancePages.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/ToolsPage.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/ExportPage.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Pages/ImportPage.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/NavMenusData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/ThemesData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/WidgetsData.php";
|
||||
@@ -56,9 +60,13 @@ require AURORA_ADMIN_PATH . "admin/src/Roles/RolesData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/ActivityLog/ActivityLog.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/ActivityLog/ActivityLogData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Login/LoginStyle.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Login/LoginRedirect.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Editor/DisableGutenberg.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Media/SvgUploads.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Media/MediaFolders.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/ImageFormats/ImageFormats.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Security/Security.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Code/CodeInjection.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/MediaFolderData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/BugReportData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Rest/CompanionPluginData.php";
|
||||
|
||||
+22
-62
@@ -1,10 +1,10 @@
|
||||
=== Aurora Admin ===
|
||||
Contributors: TODO-WPORG-USERNAME
|
||||
Contributors: Aurora Dragon Studio, KrakenRT
|
||||
Tags: admin, dashboard, admin theme, dark mode, admin redesign
|
||||
Requires at least: 5.5
|
||||
Tested up to: 7.0
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 3.6.1
|
||||
Stable tag: 1.0
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
@@ -32,20 +32,23 @@ Aurora Admin replaces the default WordPress admin with a faster, cleaner interfa
|
||||
**Look and feel**
|
||||
|
||||
* A real dark mode covering the entire admin, not just a few screens
|
||||
* 10 color presets plus 2 flat neutrals
|
||||
* macOS-style window chrome for the block editor and other full-screen views
|
||||
* 10 color presets plus 2 flat neutrals, applied across the whole interface
|
||||
* Rounded, framed content cards with theme-tinted scrollbars; the block editor and other full-screen views get the same treatment
|
||||
* Optional Google Font for the interface typeface (off by default — see Privacy below)
|
||||
|
||||
Everything in Aurora Admin is free — there's no license key, no locked features, and no upsell.
|
||||
|
||||
**Aurora File Manager, Aurora Database Explorer, and Aurora Site Backup (separate plugins)**
|
||||
|
||||
File Manager (browse/edit/manage your site's files from wp-admin), Database Explorer (browse your database tables and run read-only queries), and Site Backup (full site backup/restore, optionally to your own S3-compatible or Google Drive storage) previously shipped bundled inside Aurora Admin. All three are now distributed as their own standalone plugins — [Aurora File Manager](https://aurora.auroradragon.studio/file-manager), [Aurora Database Explorer](https://aurora.auroradragon.studio/database-explorer), and [Aurora Site Backup](https://aurora.auroradragon.studio/site-backup) — each installable with one click from Aurora's Settings → Add-ons screen. File-management/code-editing and direct database-query functionality of this kind isn't accepted into the WordPress.org Plugin Directory (see the [Plugin Developer FAQ](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/#are-there-plugins-you-dont-accept)); Site Backup was split out proactively for the same class of reason, since restoring a backup means writing arbitrary files back into the site. Aurora Admin itself never contains any of that functionality.
|
||||
File Manager (browse/edit/manage your site's files from wp-admin), Database Explorer (browse your database tables and run read-only queries), and Site Backup (full site backup/restore, optionally to your own S3-compatible or Google Drive storage) are distributed as their own standalone plugins — [Aurora File Manager](https://aurora.auroradragon.studio/file-manager), [Aurora Database Explorer](https://aurora.auroradragon.studio/database-explorer), and [Aurora Site Backup](https://aurora.auroradragon.studio/site-backup) — each installable with one click from Aurora's Modules screen. File-management/code-editing and direct database-query functionality of this kind isn't accepted into the WordPress.org Plugin Directory (see the [Plugin Developer FAQ](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/#are-there-plugins-you-dont-accept)); Site Backup is separate for the same class of reason, since restoring a backup means writing arbitrary files back into the site. Aurora Admin itself never contains any of that functionality.
|
||||
|
||||
= Privacy / external service =
|
||||
= Privacy / external services =
|
||||
|
||||
The optional "Report a Bug" screen sends your bug description and basic diagnostics (plugin version, WordPress version, PHP version, active theme name, and your site's domain) to a feedback endpoint on auroraplugins.com when you choose to submit it. Nothing is sent automatically or in the background — this only happens if you fill out and submit that form yourself.
|
||||
|
||||
Settings → Add-ons' "Install & Activate" buttons for Aurora File Manager, Aurora Database Explorer, and Aurora Site Backup download that plugin's zip file from aurora.auroradragon.studio using WordPress's own plugin-installer API, the same mechanism the native Plugins → Add New screen uses for WordPress.org-hosted plugins. This only happens when you click one of those buttons — nothing is downloaded automatically or in the background. (Aurora Site Backup's own remote-storage options, if you install and configure it, are documented in that plugin's own readme.)
|
||||
The interface font setting (Settings → Appearance) is empty by default, in which case Aurora uses your system font and makes no external requests. If you choose a Google Font, that font file is loaded from Google Fonts (fonts.googleapis.com) on admin pages, which sends a request to Google's servers. Leave the setting empty to avoid any external font requests.
|
||||
|
||||
The Modules page's "Install & Activate" buttons for Aurora File Manager, Aurora Database Explorer, and Aurora Site Backup download that plugin's zip file from aurora.auroradragon.studio using WordPress's own plugin-installer API, the same mechanism the native Plugins → Add New screen uses for WordPress.org-hosted plugins. This only happens when you click one of those buttons — nothing is downloaded automatically or in the background. (Aurora Site Backup's own remote-storage options, if you install and configure it, are documented in that plugin's own readme.)
|
||||
|
||||
== Installation ==
|
||||
|
||||
@@ -65,16 +68,16 @@ Yes. Every feature listed above is available with no license key and no restrict
|
||||
|
||||
= Will this work with my other plugins? =
|
||||
|
||||
Aurora Admin only replaces the built-in WordPress admin screens it explicitly targets (Posts, Pages, Media, Users, Comments, Nav Menus, Widgets, Themes, core Settings screens, Profile, the block editor frame). Screens added by other plugins are untouched. Each replacement can also be toggled off individually in Aurora's settings if you'd rather keep the native screen for something specific.
|
||||
Aurora Admin only replaces the built-in WordPress admin screens it explicitly targets (Posts, Pages, Media, Users, Comments, Nav Menus, Widgets, Themes, Tools, Export, Import, core Settings screens, Profile, the block editor frame). Running an already-active importer on the Import screen still opens that importer's own native screen, since its content is entirely owned by whichever plugin registered it. Screens added by other plugins are untouched. Each replacement can also be toggled off individually in Aurora's settings if you'd rather keep the native screen for something specific.
|
||||
|
||||
= Does Aurora load anything from external servers? =
|
||||
|
||||
Not by default. It only makes an external request if you opt in: choosing a Google Font loads it from Google Fonts, submitting the "Report a Bug" form sends it to auroraplugins.com, and the Modules page's install buttons download the companion plugins on click. See "Privacy / external services" above for details.
|
||||
|
||||
= Where do I report a bug? =
|
||||
|
||||
Use the "Report a Bug" item in the Aurora sidebar menu, or open a thread in this plugin's support forum.
|
||||
|
||||
= Where did File Manager, Database Explorer, and Site Backup go? =
|
||||
|
||||
File Manager is now Aurora File Manager, Database Explorer is now Aurora Database Explorer, and Site Backup is now Aurora Site Backup — three separate standalone plugins, each installable with one click from Settings → Add-ons in Aurora Admin, or downloadable directly from [aurora.auroradragon.studio/file-manager](https://aurora.auroradragon.studio/file-manager), [aurora.auroradragon.studio/database-explorer](https://aurora.auroradragon.studio/database-explorer), and [aurora.auroradragon.studio/site-backup](https://aurora.auroradragon.studio/site-backup). File Manager and Database Explorer are distributed separately because WordPress.org's Plugin Directory policy doesn't allow file-management/code-editing or direct database-query functionality in a directory-listed plugin; Site Backup was split out proactively for the same class of reason (restoring a backup writes arbitrary files back into the site).
|
||||
|
||||
== Screenshots ==
|
||||
|
||||
1. The Aurora dashboard.
|
||||
@@ -83,53 +86,10 @@ File Manager is now Aurora File Manager, Database Explorer is now Aurora Databas
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 3.6.1 =
|
||||
* Fixed another round of Plugin Check findings the 3.5.1 fix didn't fully cover: `WordPress.DB.DirectDatabaseQuery.DirectQuery`/`NoCaching` are attributed by PHPCS to the *outer* `$wpdb->get_results()`/`get_row()` call line, while `PreparedSQL.NotPrepared` is attributed to whichever *inner* line the interpolated table name appears on — two different lines in a multi-line call, so a single `phpcs:ignore` comment can only ever cover one of them. Collapsed the remaining multi-line calls in Menu Creator and Admin Notices to single physical lines so one ignore comment covers both sniffs' attributed lines at once. Re-verified with an automated scan plus live REST-dispatch round-trips (menu create/list/get/delete, notice create/render/delete).
|
||||
|
||||
= 3.6.0 =
|
||||
* Site Backup has moved to its own standalone plugin, Aurora Site Backup — install it with one click from the Settings → Add-ons screen, alongside Aurora File Manager and Aurora Database Explorer. Restoring a backup means writing arbitrary files back into the site, the same class of risk that's kept file-management functionality out of Aurora Admin proper, so it now lives in a separately-distributed companion plugin.
|
||||
|
||||
= 3.5.1 =
|
||||
* Fixed a fresh WordPress.org Plugin Check report: the previous compliance pass's `phpcs:ignore` annotations were placed above the *opening* line of several multi-line `$wpdb->get_results()`/`get_row()`/`get_var()` calls, but PHPCS attributes violations to the exact physical line where the interpolated tokens appear — often one or more lines further down inside the call — so those suppressions were never actually taking effect. Corrected across Menu Creator, Admin Notices, Site Backup, Media Folders, Analytics, and Media Library. Also fixed a real, unrelated bug found during verification: creating a menu in Menu Creator returned "Menu not found" instead of the newly created menu, because of how the internal follow-up request object was constructed.
|
||||
* Compressed files, hidden files, and non-standard markdown files flagged by Plugin Check are scanner noise from scanning the whole development repository (build zips, `.gitignore`, `.claude/`, `PROJECT_STATUS.md`) — confirmed none of these exist in the actual submission zip.
|
||||
|
||||
= 3.5.0 =
|
||||
* Database Explorer has moved to its own standalone plugin, Aurora Database Explorer — install it with one click from the Settings → Add-ons screen, alongside Aurora File Manager. Direct database-query tooling is treated by WordPress.org's Plugin Directory review the same way as file-management functionality, so rather than remove the capability outright, it now lives in a separately-distributed companion plugin, same as File Manager did in 3.4.0.
|
||||
|
||||
= 3.4.0 =
|
||||
* File Manager has moved to its own standalone plugin, Aurora File Manager — install it with one click from the new Settings → Add-ons screen. WordPress.org's Plugin Directory policy doesn't allow file-management/code-editing functionality in a directory-listed plugin regardless of the safeguards in place, so rather than remove the capability outright, it now lives in a separately-distributed companion plugin.
|
||||
|
||||
= 3.3.2 =
|
||||
* Compliance pass against the official WordPress.org Plugin Check tool: fixed a readme.txt version mismatch, a missing input-sanitization call, and annotated ~230 custom-database-table query warnings with the justification the review process expects (Aurora's backup/analytics/activity-log/menu/notices/media-folders features all use their own tables, which necessarily means direct, non-core-wrapped database access).
|
||||
|
||||
= 3.3.1 =
|
||||
* Fixed the Settings page: the category list and the settings panel now scroll independently, so picking a category from a scrolled-down spot in the list no longer leaves the panel showing stale, off-screen content above the fold.
|
||||
|
||||
= 3.3.0 =
|
||||
* Added Modern Image Formats: automatically generates a WebP or AVIF copy of every uploaded JPEG/PNG image and serves it via a <picture> element with the original as a fallback, configurable under Settings.
|
||||
|
||||
= 3.2.1 =
|
||||
* Security hardening: File Manager no longer allows opening or saving executable file types (PHP and similar) for a file that already exists — only creating, uploading, and renaming to those types were previously blocked. File Manager now also honors the DISALLOW_FILE_EDIT and DISALLOW_FILE_MODS constants, the same host-level lockdown WordPress core's own file-editing and update/install flows respect.
|
||||
|
||||
= 3.2.0 =
|
||||
* Added Google Drive as a second remote storage option for Site Backup, alongside S3-compatible storage. Connect your own Google Cloud OAuth client in Settings to store backups in your own Google Drive account.
|
||||
|
||||
= 3.1.0 =
|
||||
* Added Site Backup: full site backup (database + files) and restore-in-place, on demand or on a recurring daily/weekly/monthly schedule, stored locally or on your own S3-compatible remote storage (AWS S3, Backblaze B2, Cloudflare R2, DigitalOcean Spaces, and similar).
|
||||
|
||||
= 3.0.2 =
|
||||
* Fixed a plugin-header validation error: Plugin URI and Author URI were identical, which the WordPress.org submission checker requires to be distinct. Plugin URI now points to the plugin's own site (aurora.auroradragon.studio); Author URI stays on the studio's site.
|
||||
|
||||
= 3.0.1 =
|
||||
* Compliance and hardening pass ahead of the WordPress.org submission: replaced a forbidden core function in the Widgets screen, converted File Manager's file operations to use the WP_Filesystem API, added nonce/sanitization coverage to a few read paths, fixed missing translator comments, and annotated custom-table SQL queries for the plugin review process.
|
||||
|
||||
= 3.0.0 =
|
||||
* Aurora Admin is now completely free — the entire licensing system was removed, and every previously-paid feature (Role Editor, Activity Log, Database Explorer, File Manager, Analytics, White Label, Full Admin Takeover, Media Folders) is now available to everyone.
|
||||
|
||||
= 2.27.0 and earlier =
|
||||
* Pre-release beta versions, not distributed on WordPress.org.
|
||||
|
||||
== Upgrade Notice ==
|
||||
|
||||
= 3.0.0 =
|
||||
Aurora Admin is now 100% free. If you were on an earlier beta with a license key, it's no longer needed — all features are unlocked automatically.
|
||||
= 1.0 =
|
||||
* First public release of Aurora Admin.
|
||||
* Modern admin shell — fixed sidebar, toolbar, and global search — replacing the default WordPress admin chrome.
|
||||
* Real replacement screens for Dashboard, Posts, Pages, Media, Users, Comments, Plugins, Nav Menus, Widgets, Themes, the core Settings screens (General, Writing, Reading, Discussion, Permalinks), Tools, Export, and Import, plus Add User, Media Upload, and Profile.
|
||||
* Power tools: Role Editor, an Activity Log with Admin/User tabs and IP tracking, Media Folders, Menu Creator with drag-and-drop reordering, Admin Notices, White Label, Login Redirect, a Disable Gutenberg toggle, and a Modules page for one-click companion-plugin installs.
|
||||
* Full dark mode — including native screens Aurora frames rather than rebuilds (Site Health, Export/Erase Personal Data, Updates, the Theme/Plugin File Editor) — with 10 color presets plus 2 flat neutrals, rounded framed content cards, theme-tinted scrollbars, and an optional (off-by-default) Google Font for the interface.
|
||||
* Security & performance hardening, custom admin CSS/JS, and a login-screen customizer.
|
||||
|
||||
Reference in New Issue
Block a user