Fix sub-app permissions bug, add Comments app, support Site Editor

- Root-cause fix for "Sorry, you are not allowed to access this page" on
  Menu Creator/Activity Log/Role Editor: remove_submenu_page() was
  corrupting WP core's user_can_access_admin_page() capability lookup.
  Sub-apps now stay registered in $submenu; MenuSerializer filters them
  out of the sidebar instead.
- Add a 6th sub-app, Comments, with a redirect from native edit-comments.php.
- Extend the window-chrome/taskbar framing to the Site Editor
  (site-editor.php), including a fix for a CSS position:fixed
  over-constraint bug that let its frame run past the viewport.
- Rewrite the taskbar from a single sessionStorage slot to a list so
  multiple minimized windows are each independently restorable.
- Extend native-dark.css coverage to the block editor's own chrome.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
nixpanel
2026-07-03 18:48:25 -05:00
co-authored by Claude Sonnet 5
parent b0b0a9999a
commit 8153d4397c
16 changed files with 1510 additions and 194 deletions
+158 -74
View File
@@ -1,6 +1,6 @@
# Aurora Admin v2 — Project Status
_Last updated: 2026-07-03 (evening session — theme system rebuild + window chrome)_
_Last updated: 2026-07-03 (late session — Site Editor support, permissions bug fix, Comments sub-app, taskbar rewrite)_
## What this project is
@@ -179,17 +179,29 @@ not a hue applied on top of a shared light/dark base.
`app/src/pages/settings/settingsConfig.js`, rendered by
`app/src/pages/settings/components/SettingsField.vue` (types: text, toggle,
color, image, repeater, multiselect, segmented, remote-sites).
- **5 sub-apps** (`admin/src/Pages/SubApps.php` + one PHP feature-class pair
- **6 sub-apps** (`admin/src/Pages/SubApps.php` + one PHP feature-class pair
each + one Vue page + one Vite entry each) — reachable only via the
sidebar's Aurora nav section, not the native WP menu:
- Registered via `add_submenu_page('aurora-admin', ...)` then immediately
`remove_submenu_page()`'d on a late `admin_menu` hook — the
register-but-hide trick: real page/capability/URL, but never appears in
WP's native `$submenu` global (so `MenuSerializer` never sees them,
nothing to filter there).
- Registered via `add_submenu_page('aurora-admin', ...)` (or `'plugins.php'`
for Plugins) and **left registered** in WP's native `$submenu` global —
**no longer hidden via `remove_submenu_page()`**. An earlier version did
call it (the standard register-but-hide trick) but that corrupts WP
core's own `user_can_access_admin_page()` capability lookup, which partly
depends on the page still being present in `$submenu` to resolve its
required capability — removing it caused "Sorry, you are not allowed to
access this page" for every user, including admins, on Menu Creator/
Activity Log/Role Editor/Comments. Root-caused and fixed this session by
deleting `hide_from_native_submenu()` entirely; duplication in the
sidebar's native-menu section is instead prevented in
`MenuSerializer::serialize()` (see below), which has no such side effect.
- All slugs prefixed `aurora-admin-*` so `Shell::is_own_page()`'s existing
`strpos($page, "aurora-admin") === 0` check covers them for free (full
custom dark theme, `native-dark.css` skipped).
custom dark theme, `native-dark.css` skipped). `MenuSerializer::serialize()`
skips any submenu slug starting `aurora-admin-` (in addition to already
skipping the `aurora-admin` top-level slug) so none of them are
duplicated in the sidebar's native-menu section — needed because most are
parented under `aurora-admin` (already skipped wholesale) but
`aurora-admin-plugins` is parented under the real `plugins.php` menu.
- Each page uses the shared two-pane layout `app/src/pages/_shared/AppListShell.vue`
(generalized from Settings.vue's box-pair CSS) + `AppListItem.vue` for
list rows.
@@ -219,14 +231,18 @@ not a hue applied on top of a shared light/dark base.
`GET /activity-log?action_type=&object_type=&search=&page=`.
- **Plugins** (`admin/src/Rest/PluginsData.php` + `app/src/pages/plugins/Plugins.vue`):
custom plugin management list (activate/deactivate/delete), also using
`AppListShell.vue`. **Known bug, unresolved**: unlike the other sub-apps,
Menu Creator/Activity Log/Role Editor currently throw WordPress's native
"Sorry, you are not allowed to access this page" for the logged-in admin
user when navigated to directly, while Settings/Plugins/Dashboard work
fine — likely a capability-check mismatch in how those three register
their submenu pages vs. the working ones. Not yet root-caused; a
background investigation task was spawned for it but not completed this
session — check for it before re-investigating from scratch.
`AppListShell.vue`.
- **Comments** (`admin/src/Rest/CommentsData.php` + `app/src/pages/comments/`):
newest sub-app, `edit_posts`-gated (not `manage_options` like the others).
`SubApps::redirect_native_pages()` (`admin_init`) redirects the native
`edit-comments.php` (when hit with no `?page=` param, i.e. a direct nav
or an old bookmark/menu link) to `admin.php?page=aurora-admin-comments`.
`MenuSerializer::resolve_url()` also maps the `edit-comments.php` slug
straight to the Aurora URL so the native-menu-derived sidebar entry (for
users who can `edit_posts` but not `manage_options`, so don't see the
Aurora nav section) still lands on the Aurora page. The Dashboard's
comments card (`DashboardData.php`'s `listUrl`) was updated to point at
the same Aurora URL instead of the native one.
- **Database Explorer** (`admin/src/DatabaseExplorer/`): no custom table —
reads real `$wpdb` schema. `GET /db-tables` (SHOW TABLE STATUS),
`GET /db-tables/:table` (columns + paginated rows — table name validated
@@ -273,61 +289,108 @@ not a hue applied on top of a shared light/dark base.
Known gap: the plugin "More Details" popup renders inside an `<iframe>`
pointing at a separate WP page — CSS can't reach into it; would need JS
injection into the iframe's own document if that's ever worth fixing.
This session also added dark-theme rules for the block editor's own chrome
(`.editor-header`/`.edit-post-header`, `.interface-interface-skeleton__sidebar`
and its nested `.components-panel`/`.editor-post-status`/
`.interface-complementary-area-header`) — found by inspecting the real
screen via `computer-use` and drilling into nested elements' computed
styles, since `.interface-complementary-area` (nested inside the sidebar)
had its own explicit white background the outer rule didn't reach, leaving
a white patch below the Tags panel until it was targeted directly. The
actual post content canvas is deliberately left untouched (WYSIWYG, matches
the front-end).
## Window chrome (macOS-style title bar + taskbar)
Added this session, user-requested after seeing a competing plugin's demo.
Added earlier this session, user-requested after seeing a competing plugin's
demo; extended later the same session to cover the block editor and the Site
Editor, and the taskbar was rewritten to support multiple minimized windows.
Every page gets a traffic-light title bar; minimizing persists across a full
page navigation via a taskbar.
- `app/src/shell/windowChrome.js`: `initWindowChrome(adminUrl)`, called from
`SiteShell.vue::onMounted` (skipped on the block editor — see below).
Injects a `.aurora-winbar` div (3 dot buttons) as the first child of
`#wpbody-content` — the one container shared by *every* page Aurora
manages, native WP pages and all of Aurora's own Vue pages alike, since
each Vue entry mounts its root div there. **Exception**: the Dashboard
(`DashboardTakeover.php`) renders into a separate `#aurora-admin-dashboard-root`
sibling *above* `#wpbody` (which it hides) to fully replace the native
dashboard — `#wpbody-content` is empty/collapsed there. `windowChrome.js`
detects this and instead wraps the bar + `#aurora-admin-dashboard-root` in
a new `.aurora-dashboard-frame` div, giving the Dashboard the same
card-frame treatment (`--aurora-frame-bg`, border, radius) every other
page already gets from `#wpbody-content` itself.
- `app/src/shell/windowChrome.js`: `initWindowChrome(adminUrl)`, called
unconditionally from `SiteShell.vue::onMounted` (no longer skipped on the
block editor — that special case was removed once the editor got its own
handling). Three cases, checked in priority order:
1. **Dashboard** (`#aurora-admin-dashboard-root` present) — wraps the bar +
the dashboard root in a new `.aurora-dashboard-frame` div, giving it the
same card-frame treatment (`--aurora-frame-bg`, border, radius) every
other page gets from `#wpbody-content` itself.
2. **Block editor / Site Editor** (`isBlockEditorPage()`: body has
`block-editor-page`, or the path ends `/post.php` or `/post-new.php` —
`site-editor.php` also gets `block-editor-page` on its body, so this
already covers it with no JS change needed) — appends a
`.aurora-winbar--editor` bar (dots only, no bar surface) directly to
`document.body`, since neither editor's real content container is
`#wpbody-content`.
3. **Everything else** — native WP pages and all of Aurora's own Vue pages,
inserted as the first child of `#wpbody-content`.
- Red (close): `window.location.href = adminUrl`.
- Green (maximize): toggles `html.aurora-maximized` — hides `.sidebar`/
`.toolbar`, zeroes `#wpcontent`'s `margin-left` and the block editor
skeleton's `left`, so content fills the whole browser window. Toggle again
(or minimize) to restore. Nothing persisted — resets on next page load.
`.toolbar`, zeroes `#wpcontent`'s `margin-left` and the editor frame's
inset, so content fills the whole browser window. Toggle again (or
minimize) to restore. Nothing persisted — resets on next page load.
- Yellow (minimize): **on the Dashboard itself**, toggles `html.aurora-minimized`
which just collapses `#aurora-admin-dashboard-root` down to the bar in
place (nowhere further "home" to send it). **On every other page**, calls
`taskbar.js`'s `setMinimizedWindow(title, url)` (persists `{title, url}` in
`sessionStorage`, tab-scoped) then navigates to the dashboard. Only one
minimized window is tracked at a time — this is "minimize sends you home
with a way back," not a real window manager.
- `SiteShell.vue` reads `taskbar.js`'s `getMinimizedWindow()` on every mount
(so the taskbar shows up on whatever page you're on after minimizing) and
renders a **full-width docked bar** at the bottom with one pill-shaped
item; clicking it clears the sessionStorage entry and navigates back to
the stored URL. `html.aurora-has-taskbar` (toggled alongside) reserves
bottom margin on `#wpbody-content`/`.aurora-dashboard-frame` equal to
`--aurora-taskbar-h` so page content stops above the bar instead of
running underneath it. Went through two design iterations (small centered
floating pill → back to a full-length bar per explicit user correction —
the pill-shaped *item* stayed, just not as the whole bar).
- **Block editor exception**: Gutenberg (`post.php`/`post-new.php`) doesn't
render inside `#wpcontent`'s normal box model at all — its
`.interface-interface-skeleton` is `position: fixed; inset: 0`, anchored to
the viewport independent of any margin/padding on ancestors. `SiteShell.vue`
detects `document.body.classList.contains('block-editor-page')` and: (a)
skips rendering Aurora's own `<Toolbar>` (the editor has its own,
equivalent one — stacking both left a gradient seam between them), (b)
skips `initWindowChrome()` entirely (`#wpbody-content` is invisible/
collapsed there, injecting into it would be dead clutter), (c) insets the
skeleton itself via `left`/`top` in `shell-frame.css` so it doesn't render
underneath the sidebar. **If a future feature needs to reach the block
editor's own chrome**, its real container is `.interface-interface-skeleton`,
not `#wpbody-content` — don't assume the normal page structure there.
`taskbar.js`'s `addMinimizedWindow(title, url)` then navigates to the
dashboard.
- **Taskbar rewritten from a single sessionStorage slot to a list**
(`app/src/shell/taskbar.js`): minimizing a second page used to silently
overwrite the first one's entry, so only the most recent minimize ever
showed up. Now `getMinimizedWindows()`/`addMinimizedWindow()` (dedupes by
URL)/`removeMinimizedWindow(id)` operate on an array under
`sessionStorage['aurora-minimized-windows']`; `SiteShell.vue` `v-for`s over
it, rendering one `.aurora-taskbar__item` pill per entry inside a
transparent, floating `.aurora-taskbar` bar (inset by `--aurora-sidebar-gap`
on all exposed edges, rounded, its own box-shadow — went through a design
iteration from a small centered pill back to full-length per explicit user
correction, keeping only the pill shape on the individual items). Each item
is independently restorable/closable. `currentPageTitle()` prefers the
page's own on-screen `<h1>` (`#wpbody-content h1, #aurora-admin-dashboard-root h1`)
over `document.title`, since WP's title-tag generation came up empty for
Aurora's sub-app pages back when they were still `remove_submenu_page()`'d
(see the sub-apps permissions bug above) — `document.title` is still the
fallback for pages where that isn't an issue.
- **Content-behind-taskbar fix**: `html.aurora-has-taskbar` no longer just
adds `margin-bottom` (that only guarantees clearance once scrolled to the
very bottom — content taller than the viewport still rendered underneath
the fixed taskbar on first paint). Instead it caps `#wpbody-content`'s/
`.aurora-dashboard-frame`'s own height via `max-height` + `overflow-y: auto`,
same principle as the toolbar's reserved space at the top. This appeared to
do nothing at first — `#wpbody-content`'s base rule sets `min-height:
calc(100vh - toolbar-h - 40px)`, and per spec `min-height` always wins over
a smaller `max-height`; fixed by adding `min-height: 0` to the
taskbar-specific override.
- **Block editor / Site Editor framing**: neither editor renders inside
`#wpcontent`'s normal box model — each has its own fixed, full-viewport
root (`.interface-interface-skeleton` itself on `post.php`/`post-new.php`;
`#site-editor` on `site-editor.php`, where `.interface-interface-skeleton`
is merely `position: relative`, nested inside it — a reset rule puts the
nested skeleton back to a plain unpositioned child so the two frames don't
stack). `html.aurora-ready .interface-interface-skeleton, html.aurora-ready
#site-editor` in `shell-frame.css` frames whichever is the real fixed root
with the same insets/background/radius/shadow treatment `#wpbody-content`
gets elsewhere, plus `padding-top: var(--aurora-editor-winbar-h)` to
reserve a strip for `.aurora-winbar--editor`'s dots and `overflow: hidden`
so the rounded corners actually clip the editor's own white canvas
background (border-radius alone doesn't clip children). **Known
CSS-over-constraint gotcha**: WP core's `.edit-site { height: 100vh; }`
class (present on `#site-editor`) means `top`, `height`, and `bottom` are
all specified on that box at once — per spec this is over-constrained and
`bottom` silently gets recomputed from `top + height` instead of honoring
an explicit value, which is why the frame initially ran past the intended
bottom inset despite `bottom: 24px !important` being set. Fixed by adding
`height: auto !important` to the same rule so `top`/`bottom` actually
determine the box size again. **If a future feature needs to reach either
editor's own chrome**, the real containers are
`.interface-interface-skeleton` (post editor) or `#site-editor` (Site
Editor) — don't assume the normal `#wpbody-content` page structure there.
- `Shell.php::is_enabled()` no longer excludes the Site Editor screen
(`site-editor` used to be in the skip list alongside `customize`) — it
turns out to be built on the same `@wordpress/interface` package as the
block editor (`body.block-editor-page` is present on both), so the
existing block-editor handling covers it once `#site-editor` is framed.
## Key bugs fixed (root causes, in case they resurface)
@@ -374,6 +437,24 @@ page navigation via a taskbar.
without first deleting the old one left old-hash asset filenames mixed
into the new zip (macOS `zip -r` appends/merges rather than replacing).
Always `rm -f` the zip before rebuilding.
- **`remove_submenu_page()` corrupting WP's own capability check**: root
cause of "Sorry, you are not allowed to access this page" on Menu Creator/
Activity Log/Role Editor/Comments — see the sub-apps bullet above. Also
broke `document.title` on those pages, which is why `taskbar.js`'s
`currentPageTitle()` prefers the on-page `<h1>` instead. **If a future
custom admin page needs to be kept out of the native menu while staying
reachable and capability-checked, filter it out at the menu-serialization
layer (like `MenuSerializer.php` does), never with `remove_submenu_page()`.**
- **`min-height`/`max-height` conflict silently no-oping a fix**: per spec,
when `min-height` is larger than `max-height`, `min-height` always wins and
`max-height` is ignored outright — this is why the taskbar-overlap fix
first appeared to do nothing (see Window chrome section above).
- **`position: fixed` over-constraint silently ignoring an explicit `bottom`**:
when `top`, `height`, and `bottom` are all specified on a fixed-position
box, the spec has `bottom` get silently recomputed from `top + height`
rather than honored — root cause of the Site Editor frame running past the
viewport despite an explicit `bottom: 24px !important` (see Window chrome
section above).
## Not-yet-done (deferred, no active request)
@@ -389,22 +470,25 @@ page navigation via a taskbar.
text field).
- Menu Creator: stored item `order` field isn't applied at runtime yet
(hide/relabel work; reordering doesn't).
- **Unresolved from this session**: Menu Creator/Activity Log/Role Editor
throw "Sorry, you are not allowed to access this page" for the admin user
— see the Plugins sub-app bullet above. A background investigation task
was spawned (`task_f8e56f60` at spawn time) but its outcome isn't recorded
here — check whether it landed before re-investigating.
- No automated tests anywhere in the project — all verification has been
manual, live-in-Chrome, per the established workflow rule above.
## Current state
No task in progress. This session rebuilt the entire theming system (fixed
color presets replacing the old light/dark toggle, gradient backgrounds,
several "independently-painted gradient" seam bugs), added the macOS-style
window chrome + taskbar feature end-to-end, and extended native-page dark
styling coverage. All of it was live-verified in Chrome against the LocalWP
site and is pushed to `origin/main` (commit `4a547d2` at session end — a
single large commit covering the whole session's work, since it wasn't
broken into incremental commits along the way). The one known open issue is
the Menu Creator/Activity Log/Role Editor permissions bug noted above.
No task in progress. Earlier this session rebuilt the entire theming system
(fixed color presets, gradient backgrounds, "independently-painted gradient"
seam bugs) and added the macOS-style window chrome + taskbar feature
end-to-end (pushed as commits `4a547d2`/`b0b0a99`). Later the same session:
root-caused and fixed the Menu Creator/Activity Log/Role Editor "Sorry, you
are not allowed to access this page" bug (was `remove_submenu_page()`
corrupting WP's own capability lookup); added a 6th sub-app, Comments; fixed
a white-patch bug in the block editor's dark-themed sidebar; rewrote the
taskbar from a single sessionStorage slot to a list supporting multiple
minimized windows; and extended the window-chrome/taskbar framing to the
Site Editor (`site-editor.php`), including tracking down a CSS
over-constraint bug (`top`+`height`+`bottom` all set on a fixed-position box
silently ignoring `bottom`) that was making its frame run past the viewport.
All of it was live-verified in Chrome against the LocalWP site. This batch
of changes was uncommitted at the point this doc was last updated — commit
and push it as one of the next steps if not already done by the time this is
read.
+70
View File
@@ -377,3 +377,73 @@ html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-actio
color: var(--aurora-text);
border-color: var(--aurora-card-border);
}
/* ---------------------------------------------------------------------------
* Block editor (Gutenberg) chrome — the top toolbar/title bar and the
* right-hand Post/Block settings sidebar. The actual editing canvas
* (.interface-interface-skeleton__content, the white "page" itself) is
* deliberately left native/white — it's a WYSIWYG preview of the live,
* light-themed page, not admin chrome, so it shouldn't follow the theme.
* Everything surrounding it does, matching every other page's framing
* (shell-frame.css gives .interface-interface-skeleton itself the card
* background/border/radius; this just covers what's inside it).
* ------------------------------------------------------------------------ */
html.aurora-dark.aurora-ready .editor-header {
background: var(--aurora-frame-bg);
border-bottom: 1px solid var(--aurora-frame-border);
color: var(--aurora-text);
}
html.aurora-dark.aurora-ready .editor-header .components-button,
html.aurora-dark.aurora-ready .editor-header .editor-post-title__input,
html.aurora-dark.aurora-ready .editor-header .editor-document-bar,
html.aurora-dark.aurora-ready .editor-header .editor-document-bar__title {
color: var(--aurora-text);
}
html.aurora-dark.aurora-ready .editor-header .components-button:hover {
background: var(--aurora-bg-subtle);
}
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar {
background: var(--aurora-frame-bg);
border-left: 1px solid var(--aurora-frame-border);
color: var(--aurora-text);
}
/* .interface-complementary-area (the actual Post/Block settings panel) is
a nested child with its own explicit white background — the outer
__sidebar rule above doesn't reach it, so without this the whole panel
still shows through white regardless of theme. */
html.aurora-dark.aurora-ready .interface-complementary-area {
background: var(--aurora-frame-bg);
color: var(--aurora-text);
}
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-panel,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-panel__body,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .editor-post-status,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .interface-complementary-area-header {
background: var(--aurora-frame-bg);
border-color: var(--aurora-frame-border);
color: var(--aurora-text);
}
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-panel__body-title,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar label {
color: var(--aurora-text);
}
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar input,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar select,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar textarea,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-select-control__input,
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-text-control__input {
background: var(--aurora-bg-subtle);
border-color: var(--aurora-border);
color: var(--aurora-text);
}
html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-button {
color: var(--aurora-text);
}
+2
View File
@@ -9,6 +9,7 @@ use AuroraAdmin\Rest\DashboardData;
use AuroraAdmin\Analytics\Analytics;
use AuroraAdmin\Rest\AnalyticsData;
use AuroraAdmin\Rest\PluginsData;
use AuroraAdmin\Rest\CommentsData;
use AuroraAdmin\Posts\ScheduledDayFilter;
use AuroraAdmin\WhiteLabel\WhiteLabel;
use AuroraAdmin\Pages\SubApps;
@@ -41,6 +42,7 @@ class AuroraAdmin
new Analytics();
new AnalyticsData();
new PluginsData();
new CommentsData();
new ScheduledDayFilter();
new WhiteLabel();
new SubApps();
+57 -15
View File
@@ -6,15 +6,28 @@ use AuroraAdmin\Utility\Assets;
defined("ABSPATH") || exit();
/**
* Registers Aurora's 5 secondary full-page apps (Menu Creator, Admin
* Notices, Role Editor, Activity Log, Database Explorer). Each is a real WP
* admin page (proper capability check, working URL, its own hook_suffix for
* asset gating) registered as a submenu of "aurora-admin" — then
* immediately hidden from WordPress's own submenu via remove_submenu_page(),
* the standard register-but-hide trick. This keeps them out of the
* WP-native menu list (MenuSerializer never sees them, nothing to filter)
* while still being reachable, permission-checked pages; they're presented
* instead in the sidebar's dedicated Aurora nav section (see Shell::aurora_nav()).
* Registers Aurora's secondary full-page apps (Menu Creator, Admin Notices,
* Role Editor, Activity Log, Database Explorer, Plugins, Comments). Each is
* a real WP admin page (proper capability check, working URL, its own
* hook_suffix for asset gating) registered as a submenu of "aurora-admin"
* (or "plugins.php" for the Plugins page).
*
* These stay registered in WordPress's own $submenu — they are NOT hidden
* via remove_submenu_page(). An earlier version did call it (the standard
* "register but hide" trick), but that turned out to corrupt WP core's own
* access-control bookkeeping: user_can_access_admin_page() partly relies on
* $submenu still containing the entry to resolve the page's required
* capability, so removing it caused every user (including admins) to be
* denied with "Sorry, you are not allowed to access this page" — confirmed
* by reproducing the bug, disabling the remove_submenu_page() call, and
* watching it resolve immediately. Since Aurora already hides WordPress's
* entire native admin menu chrome (replaced by its own Vue sidebar), a
* still-registered-but-native-menu-invisible entry has no visible effect on
* its own — MenuSerializer::serialize() is what keeps these out of the
* sidebar's own WP-native-menu section (skips slugs prefixed
* "aurora-admin-", plus the "aurora-admin" top-level slug itself), so they
* aren't duplicated there. They're presented instead in the sidebar's
* dedicated Aurora nav section (see Shell::aurora_nav()).
*/
class SubApps
{
@@ -25,6 +38,7 @@ class SubApps
"aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"],
"aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"],
"aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"],
"aurora-admin-comments" => ["title" => "Comments", "entry" => "comments"],
];
private static $hook_suffixes = [];
@@ -32,19 +46,19 @@ class SubApps
public function __construct()
{
add_action("admin_menu", [self::class, "register_menus"]);
add_action("admin_menu", [self::class, "hide_from_native_submenu"], 999);
add_action("admin_enqueue_scripts", [self::class, "maybe_enqueue"]);
add_action("admin_init", [self::class, "redirect_native_pages"]);
}
public static function register_menus()
{
foreach (self::PAGES as $slug => $page) {
$parent = $slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin";
$parent = self::parent_slug($slug);
$hook = add_submenu_page(
$parent,
__($page["title"], "aurora-admin"),
__($page["title"], "aurora-admin"),
$slug === "aurora-admin-plugins" ? "activate_plugins" : "manage_options",
self::capability($slug),
$slug,
[self::class, "render"]
);
@@ -52,11 +66,23 @@ class SubApps
}
}
public static function hide_from_native_submenu()
private static function parent_slug($slug)
{
foreach (array_keys(self::PAGES) as $slug) {
remove_submenu_page($slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin", $slug);
if ($slug === "aurora-admin-plugins") {
return "plugins.php";
}
return "aurora-admin";
}
private static function capability($slug)
{
if ($slug === "aurora-admin-plugins") {
return "activate_plugins";
}
if ($slug === "aurora-admin-comments") {
return "edit_posts";
}
return "manage_options";
}
public static function maybe_enqueue($hook_suffix)
@@ -69,6 +95,22 @@ class SubApps
Assets::enqueue("aurora-admin-" . self::PAGES[$slug]["entry"], "entries/" . self::PAGES[$slug]["entry"] . ".js");
}
public static function redirect_native_pages()
{
global $pagenow;
if ($pagenow !== "edit-comments.php" || !current_user_can("edit_posts")) {
return;
}
if (isset($_GET["page"]) || wp_doing_ajax()) {
return;
}
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-comments"));
exit;
}
public static function render()
{
$page = isset($_GET["page"]) ? sanitize_text_field(wp_unslash($_GET["page"])) : "";
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* REST endpoints for Aurora's custom Comments page.
*/
class CommentsData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/comments", [
"methods" => "GET",
"callback" => [self::class, "list_comments"],
"permission_callback" => function () {
return current_user_can("moderate_comments");
},
"args" => [
"status" => ["type" => "string", "required" => false],
"search" => ["type" => "string", "required" => false],
"page" => ["type" => "integer", "required" => false],
"perPage" => ["type" => "integer", "required" => false],
],
]);
register_rest_route("aurora-admin/v1", "/comments/status", [
"methods" => "POST",
"callback" => [self::class, "update_status"],
"permission_callback" => function () {
return current_user_can("moderate_comments");
},
"args" => [
"id" => ["type" => "integer", "required" => true],
"status" => ["type" => "string", "required" => true],
],
]);
register_rest_route("aurora-admin/v1", "/comments/(?P<id>\d+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_comment"],
"permission_callback" => function () {
return current_user_can("moderate_comments");
},
"args" => [
"id" => ["type" => "integer", "required" => true],
],
]);
}
public static function list_comments($request)
{
$status = self::query_status($request->get_param("status"));
$page = max(1, (int) ($request->get_param("page") ?: 1));
$per_page = min(100, max(10, (int) ($request->get_param("perPage") ?: 50)));
$search = sanitize_text_field((string) $request->get_param("search"));
$args = [
"status" => $status,
"number" => $per_page,
"offset" => ($page - 1) * $per_page,
"orderby" => "comment_date_gmt",
"order" => "DESC",
"type" => "comment",
];
if ($search !== "") {
$args["search"] = $search;
}
$query = new \WP_Comment_Query();
$comments = $query->query($args);
$total = (int) $query->found_comments;
return new \WP_REST_Response([
"items" => array_map([self::class, "serialize_comment"], $comments),
"stats" => self::stats(),
"pagination" => [
"page" => $page,
"perPage" => $per_page,
"total" => $total,
"totalPages" => max(1, (int) ceil($total / $per_page)),
],
], 200);
}
public static function update_status($request)
{
$id = (int) $request->get_param("id");
$status = self::action_status($request->get_param("status"));
if (!$id || !$status || !get_comment($id)) {
return new \WP_Error("aurora_comment_status_invalid", __("Invalid comment action.", "aurora-admin"), ["status" => 400]);
}
$result = wp_set_comment_status($id, $status);
if (!$result) {
return new \WP_Error("aurora_comment_status_failed", __("Could not update the comment.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true, "message" => __("Comment updated.", "aurora-admin")], 200);
}
public static function delete_comment($request)
{
$id = (int) $request->get_param("id");
if (!$id || !get_comment($id)) {
return new \WP_Error("aurora_comment_delete_invalid", __("Invalid comment.", "aurora-admin"), ["status" => 400]);
}
$result = wp_delete_comment($id, true);
if (!$result) {
return new \WP_Error("aurora_comment_delete_failed", __("Could not delete the comment.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true, "message" => __("Comment deleted.", "aurora-admin")], 200);
}
private static function serialize_comment($comment)
{
$post_id = (int) $comment->comment_post_ID;
$status = wp_get_comment_status($comment);
return [
"id" => (int) $comment->comment_ID,
"author" => $comment->comment_author ?: __("Anonymous", "aurora-admin"),
"email" => $comment->comment_author_email,
"avatar" => get_avatar_url($comment, ["size" => 64]),
"content" => wp_trim_words(wp_strip_all_tags($comment->comment_content), 28),
"rawContent" => $comment->comment_content,
"postId" => $post_id,
"postTitle" => get_the_title($post_id) ?: __("(No title)", "aurora-admin"),
"postEditUrl" => get_edit_post_link($post_id, "raw") ?: "",
"date" => mysql2date("M j, Y", $comment->comment_date),
"relativeDate" => sprintf(__("%s ago", "aurora-admin"), human_time_diff(strtotime($comment->comment_date_gmt), current_time("timestamp", true))),
"status" => $status,
"statusLabel" => self::status_label($status),
"editUrl" => admin_url("comment.php?action=editcomment&c=" . (int) $comment->comment_ID),
"canEdit" => current_user_can("edit_comment", (int) $comment->comment_ID),
];
}
private static function stats()
{
$counts = wp_count_comments();
$approved = isset($counts->approved) ? (int) $counts->approved : 0;
$pending = isset($counts->moderated) ? (int) $counts->moderated : 0;
$spam = isset($counts->spam) ? (int) $counts->spam : 0;
$trash = isset($counts->trash) ? (int) $counts->trash : 0;
return [
"all" => $approved + $pending + $spam,
"approved" => $approved,
"pending" => $pending,
"spam" => $spam,
"trash" => $trash,
];
}
private static function query_status($status)
{
switch ($status) {
case "approved":
return "approve";
case "pending":
return "hold";
case "spam":
case "trash":
return $status;
default:
return "all";
}
}
private static function action_status($status)
{
switch ($status) {
case "approve":
case "hold":
case "spam":
case "trash":
return $status;
default:
return "";
}
}
private static function status_label($status)
{
switch ($status) {
case "approved":
return __("Approved", "aurora-admin");
case "unapproved":
return __("Pending", "aurora-admin");
case "spam":
return __("Spam", "aurora-admin");
case "trash":
return __("Trash", "aurora-admin");
default:
return ucfirst($status);
}
}
}
+1 -1
View File
@@ -141,7 +141,7 @@ class DashboardData
"spam" => (int) $counts->spam,
"total" => (int) $counts->approved,
"latest" => $latest,
"listUrl" => admin_url("edit-comments.php"),
"listUrl" => admin_url("admin.php?page=aurora-admin-comments"),
];
}
+14
View File
@@ -78,6 +78,16 @@ class MenuSerializer
$sub_title = self::clean_title(isset($sub[0]) ? $sub[0] : "");
$sub_slug = isset($sub[2]) ? $sub[2] : "";
// Aurora's own sub-apps (Menu Creator, Comments, etc.) are
// presented in the sidebar's dedicated Aurora nav section
// instead — skip them here so they aren't duplicated. Most are
// parented under "aurora-admin" itself, already skipped above
// wholesale, but "aurora-admin-plugins" is parented under the
// real "plugins.php" menu and needs this explicit check.
if (strpos($sub_slug, "aurora-admin-") === 0) {
continue;
}
$entry["submenu"][] = [
"id" => $sub_slug,
"title" => $sub_title["title"],
@@ -136,6 +146,10 @@ class MenuSerializer
return admin_url("plugins.php?page=aurora-admin-plugins");
}
if ($slug === "edit-comments.php") {
return admin_url("admin.php?page=aurora-admin-comments");
}
// Already an absolute URL.
if (preg_match('#^https?://#', $slug)) {
return $slug;
+12 -3
View File
@@ -34,8 +34,17 @@ class Shell
}
/**
* Whether the shell should run on the current request. Skipped on a few
* full-screen native experiences that own the whole viewport.
* Whether the shell should run on the current request. Skipped on the
* Customizer (its own full-screen live-preview UI with a fundamentally
* different structure Aurora doesn't attempt to frame). The Site Editor
* used to be excluded here too — it's now handled instead: it turns out
* to be built on the same @wordpress/interface package as the block
* editor (post.php/post-new.php both get body.block-editor-page too),
* so shell-frame.css/windowChrome.js's existing block-editor handling
* covers it once #site-editor (its actual position:fixed, full-viewport
* root — unlike the post editor, its .interface-interface-skeleton is
* merely position:relative, nested inside #site-editor) is framed the
* same way .interface-interface-skeleton is everywhere else.
*/
private static function is_enabled()
{
@@ -44,7 +53,7 @@ class Shell
}
$screen = function_exists("get_current_screen") ? get_current_screen() : null;
if ($screen && in_array($screen->id, ["site-editor", "customize"], true)) {
if ($screen && in_array($screen->id, ["customize"], true)) {
return false;
}
+10
View File
@@ -0,0 +1,10 @@
import { createApp } from 'vue';
import '@/shell/shell-frame.css';
import Comments from '@/pages/comments/Comments.vue';
const mountEl = document.getElementById('aurora-admin-comments-root');
if (mountEl) {
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
createApp(Comments, { serverData: data }).mount(mountEl);
}
+701
View File
@@ -0,0 +1,701 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
const props = defineProps({
serverData: { type: Object, required: true },
});
const comments = ref([]);
const stats = ref({ all: 0, approved: 0, pending: 0, spam: 0, trash: 0 });
const pagination = ref({ page: 1, perPage: 50, total: 0, totalPages: 1 });
const loading = ref(true);
const busyId = ref(0);
const error = ref('');
const notice = ref('');
const search = ref('');
const status = ref('all');
const compact = ref(false);
const apiBase = computed(() => `${props.serverData.restUrl}aurora-admin/v1`);
const request = async (path, options = {}) => {
const response = await fetch(`${apiBase.value}${path}`, {
credentials: 'same-origin',
...options,
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': props.serverData.restNonce,
...(options.headers || {}),
},
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.message || data.error || 'Request failed');
}
return data;
};
const load = async () => {
loading.value = true;
error.value = '';
try {
const params = new URLSearchParams({
status: status.value,
search: search.value.trim(),
page: String(pagination.value.page),
perPage: String(pagination.value.perPage),
});
const data = await request(`/comments?${params.toString()}`);
comments.value = data.items || [];
stats.value = data.stats || stats.value;
pagination.value = data.pagination || pagination.value;
} catch (err) {
error.value = err.message || 'Could not load comments';
} finally {
loading.value = false;
}
};
onMounted(load);
let searchTimer = 0;
watch(search, () => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
pagination.value.page = 1;
load();
}, 250);
});
const setStatus = (next) => {
status.value = next;
pagination.value.page = 1;
load();
};
const filters = computed(() => [
{ key: 'all', label: 'All', count: stats.value.all },
{ key: 'approved', label: 'Approved', count: stats.value.approved },
{ key: 'pending', label: 'Pending', count: stats.value.pending },
{ key: 'spam', label: 'Spam', count: stats.value.spam },
{ key: 'trash', label: 'Trash', count: stats.value.trash },
]);
const runStatus = async (comment, nextStatus) => {
if (!comment || busyId.value) return;
busyId.value = comment.id;
error.value = '';
notice.value = '';
try {
const data = await request('/comments/status', {
method: 'POST',
body: JSON.stringify({ id: comment.id, status: nextStatus }),
});
notice.value = data.message || 'Comment updated.';
await load();
} catch (err) {
error.value = err.message || 'Action failed';
} finally {
busyId.value = 0;
}
};
const deleteComment = async (comment) => {
if (!comment || busyId.value) return;
busyId.value = comment.id;
error.value = '';
notice.value = '';
try {
const data = await request(`/comments/${comment.id}`, { method: 'DELETE' });
notice.value = data.message || 'Comment deleted.';
await load();
} catch (err) {
error.value = err.message || 'Action failed';
} finally {
busyId.value = 0;
}
};
const goPage = (page) => {
const next = Math.min(Math.max(1, page), pagination.value.totalPages || 1);
if (next === pagination.value.page) return;
pagination.value.page = next;
load();
};
const initials = (name) =>
(name || '?')
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part.charAt(0).toUpperCase())
.join('');
const rowRange = computed(() => {
if (!pagination.value.total) return '0 to 0';
const start = (pagination.value.page - 1) * pagination.value.perPage + 1;
const end = Math.min(pagination.value.total, pagination.value.page * pagination.value.perPage);
return `${start} to ${end}`;
});
</script>
<template>
<main class="comments-page">
<header class="comments-page__header">
<div>
<p class="comments-page__eyebrow">Discussion</p>
<h1>Comments</h1>
</div>
<div class="comments-page__actions">
<button
type="button"
class="icon-button"
:class="{ 'is-active': compact }"
title="Toggle compact view"
@click="compact = !compact"
>
<span class="dashicons dashicons-menu-alt3" />
</button>
<button type="button" class="icon-button" title="Refresh comments" :disabled="loading" @click="load">
<span class="dashicons dashicons-update" />
</button>
</div>
</header>
<section class="comments-page__toolbar" aria-label="Comment filters and search">
<div class="comments-page__filters">
<button
v-for="filter in filters"
:key="filter.key"
type="button"
class="filter-pill"
:class="{ 'is-active': status === filter.key }"
@click="setStatus(filter.key)"
>
<span>{{ filter.label }}</span>
<strong>{{ filter.count }}</strong>
</button>
</div>
<label class="comments-page__search">
<span class="dashicons dashicons-search" />
<input v-model="search" type="search" placeholder="Search comments..." />
</label>
</section>
<div v-if="notice" class="comments-notice comments-notice--success">{{ notice }}</div>
<div v-if="error" class="comments-notice comments-notice--error">{{ error }}</div>
<section class="comments-table" :class="{ 'is-compact': compact }" aria-label="Comments">
<div class="comments-table__head">
<div>Author <span class="dashicons dashicons-filter" /></div>
<div>Comment <span class="dashicons dashicons-filter" /></div>
<div>Post <span class="dashicons dashicons-filter" /></div>
<div>Date <span class="dashicons dashicons-filter" /></div>
<div>Status <span class="dashicons dashicons-filter" /></div>
<div>Actions</div>
</div>
<div v-if="loading" class="comments-table__empty">Loading comments...</div>
<div v-else-if="!comments.length" class="comments-table__empty">No comments match your filters.</div>
<article v-for="comment in comments" v-else :key="comment.id" class="comment-row">
<div class="comment-row__author">
<img v-if="comment.avatar" :src="comment.avatar" alt="" class="comment-row__avatar" />
<div v-else class="comment-row__initials">{{ initials(comment.author) }}</div>
<div class="comment-row__person">
<strong>{{ comment.author }}</strong>
<span>{{ comment.email || 'No email' }}</span>
</div>
</div>
<div class="comment-row__comment" data-label="Comment">
{{ comment.content || 'No comment text.' }}
</div>
<div class="comment-row__post" data-label="Post">
<a v-if="comment.postEditUrl" :href="comment.postEditUrl">{{ comment.postTitle }}</a>
<span v-else>{{ comment.postTitle }}</span>
</div>
<div class="comment-row__cell" data-label="Date">
<strong>{{ comment.relativeDate }}</strong>
<span>{{ comment.date }}</span>
</div>
<div class="comment-row__cell" data-label="Status">
<span class="status-badge" :class="`status-badge--${comment.status}`">
<i />
{{ comment.statusLabel }}
</span>
</div>
<div class="comment-row__actions">
<button
v-if="comment.status !== 'approved'"
type="button"
class="icon-button icon-button--inline"
title="Approve"
:disabled="busyId === comment.id"
@click="runStatus(comment, 'approve')"
>
<span class="dashicons dashicons-yes" />
</button>
<button
v-if="comment.status === 'approved'"
type="button"
class="icon-button icon-button--inline"
title="Mark pending"
:disabled="busyId === comment.id"
@click="runStatus(comment, 'hold')"
>
<span class="dashicons dashicons-hidden" />
</button>
<a class="icon-button icon-button--inline" title="Edit comment" :href="comment.editUrl">
<span class="dashicons dashicons-edit" />
</a>
<button
v-if="comment.status !== 'spam'"
type="button"
class="icon-button icon-button--inline"
title="Mark spam"
:disabled="busyId === comment.id"
@click="runStatus(comment, 'spam')"
>
<span class="dashicons dashicons-warning" />
</button>
<button
v-if="comment.status !== 'trash'"
type="button"
class="icon-button icon-button--inline"
title="Move to trash"
:disabled="busyId === comment.id"
@click="runStatus(comment, 'trash')"
>
<span class="dashicons dashicons-trash" />
</button>
<button
v-else
type="button"
class="icon-button icon-button--inline"
title="Delete permanently"
:disabled="busyId === comment.id"
@click="deleteComment(comment)"
>
<span class="dashicons dashicons-no-alt" />
</button>
</div>
</article>
<footer v-if="!loading && comments.length" class="comments-table__footer">
<div>Page Size: {{ pagination.perPage }}</div>
<div>{{ rowRange }} of {{ pagination.total }}</div>
<div class="comments-table__pager">
<button type="button" :disabled="pagination.page <= 1" @click="goPage(1)">
<span class="dashicons dashicons-controls-skipback" />
</button>
<button type="button" :disabled="pagination.page <= 1" @click="goPage(pagination.page - 1)">
<span class="dashicons dashicons-arrow-left-alt2" />
</button>
<span>Page {{ pagination.page }} of {{ pagination.totalPages }}</span>
<button
type="button"
:disabled="pagination.page >= pagination.totalPages"
@click="goPage(pagination.page + 1)"
>
<span class="dashicons dashicons-arrow-right-alt2" />
</button>
<button
type="button"
:disabled="pagination.page >= pagination.totalPages"
@click="goPage(pagination.totalPages)"
>
<span class="dashicons dashicons-controls-skipforward" />
</button>
</div>
</footer>
</section>
</main>
</template>
<style scoped>
.comments-page {
min-height: calc(100vh - 112px);
padding: 0;
color: var(--aurora-text);
background: transparent;
}
.comments-page__header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
margin-bottom: 24px;
}
.comments-page__eyebrow {
margin: 0 0 7px;
color: var(--aurora-text-muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.comments-page h1 {
margin: 0;
color: var(--aurora-text);
font-size: 1.45rem;
line-height: 1.15;
}
.comments-page__actions,
.comments-page__toolbar,
.comments-page__filters,
.comment-row__actions {
display: flex;
align-items: center;
}
.comments-page__actions {
gap: 10px;
}
.comments-page__toolbar {
justify-content: space-between;
gap: 18px;
margin-bottom: 12px;
}
.comments-page__filters {
flex-wrap: wrap;
gap: 8px;
}
.filter-pill {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
border: 1px solid var(--aurora-border);
border-radius: 999px;
background: var(--aurora-bg-subtle);
color: var(--aurora-text-muted);
padding: 0 12px;
font-size: 0.78rem;
font-weight: 800;
cursor: pointer;
}
.filter-pill strong {
min-width: 22px;
height: 22px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--aurora-frame-bg);
color: inherit;
font-size: 0.72rem;
}
.filter-pill.is-active {
border-color: var(--aurora-text-muted);
background: var(--aurora-frame-bg);
color: var(--aurora-text);
}
.comments-page__search {
width: min(320px, 100%);
min-height: 38px;
border: 1px solid var(--aurora-border);
border-radius: 8px;
background: var(--aurora-bg-subtle);
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
color: var(--aurora-text-muted);
}
.comments-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;
}
.comments-page__search input::placeholder {
color: var(--aurora-text-muted);
}
.icon-button {
width: 42px;
min-height: 42px;
border: 1px solid var(--aurora-border);
border-radius: 8px;
color: var(--aurora-text);
background: var(--aurora-bg-subtle);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
text-decoration: none;
}
.icon-button.is-active,
.icon-button:hover:not(:disabled) {
border-color: var(--aurora-text-muted);
color: var(--aurora-text);
}
.icon-button--inline {
width: 30px;
min-height: 30px;
border: 0;
background: transparent;
color: var(--aurora-text-muted);
}
.icon-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.comments-notice {
margin: 0 0 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--aurora-bg-subtle);
color: var(--aurora-text);
font-size: 0.78rem;
font-weight: 750;
}
.comments-table {
overflow: hidden;
border: 1px solid var(--aurora-frame-border);
border-radius: 8px;
background: var(--aurora-frame-bg);
}
.comments-table__head,
.comment-row {
display: grid;
grid-template-columns: minmax(190px, 0.9fr) minmax(320px, 1.6fr) minmax(190px, 1fr) 110px 150px 142px;
align-items: center;
}
.comments-table__head {
min-height: 48px;
border-bottom: 1px solid var(--aurora-border);
color: var(--aurora-text);
font-size: 0.79rem;
font-weight: 850;
}
.comments-table__head > div {
min-height: 48px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.comment-row {
min-height: 76px;
border-bottom: 1px solid var(--aurora-border);
}
.comment-row:hover {
background: var(--aurora-bg-subtle);
}
.comment-row__author {
display: grid;
grid-template-columns: 36px minmax(0, 1fr);
gap: 12px;
align-items: center;
min-width: 0;
padding: 14px 16px;
}
.comment-row__avatar,
.comment-row__initials {
width: 36px;
height: 36px;
border-radius: 50%;
}
.comment-row__avatar {
display: block;
}
.comment-row__initials {
background: var(--aurora-bg-subtle);
color: var(--aurora-text);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: 900;
}
.comment-row__person {
min-width: 0;
}
.comment-row__person strong {
display: block;
overflow: hidden;
color: var(--aurora-text);
font-size: 0.86rem;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-row__person span,
.comment-row__cell span {
display: block;
overflow: hidden;
color: var(--aurora-text-muted);
font-size: 0.74rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-row__comment,
.comment-row__post,
.comment-row__cell {
min-width: 0;
padding: 14px 16px;
color: var(--aurora-text-muted);
font-size: 0.8rem;
line-height: 1.45;
}
.comment-row__comment {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-row__post a,
.comment-row__post span {
display: block;
overflow: hidden;
color: var(--aurora-text);
font-weight: 800;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-row__cell strong {
display: block;
color: var(--aurora-text-muted);
font-size: 0.8rem;
}
.status-badge {
width: fit-content;
border-radius: 999px;
background: var(--aurora-bg-subtle);
color: var(--aurora-text-muted);
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
font-size: 0.74rem;
font-weight: 850;
}
.status-badge i {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
opacity: 0.8;
}
.status-badge--approved {
background: var(--aurora-frame-bg);
color: var(--aurora-text);
box-shadow: inset 0 0 0 1px var(--aurora-border);
}
.comment-row__actions {
gap: 8px;
padding: 14px 16px;
}
.comments-table.is-compact .comment-row {
min-height: 58px;
}
.comments-table.is-compact .comment-row__author,
.comments-table.is-compact .comment-row__comment,
.comments-table.is-compact .comment-row__post,
.comments-table.is-compact .comment-row__cell,
.comments-table.is-compact .comment-row__actions {
padding-top: 9px;
padding-bottom: 9px;
}
.comments-table.is-compact .comment-row__avatar,
.comments-table.is-compact .comment-row__initials {
width: 30px;
height: 30px;
}
.comments-table__empty {
padding: 38px 18px;
color: var(--aurora-text-muted);
font-size: 0.85rem;
text-align: center;
}
.comments-table__footer {
min-height: 54px;
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;
}
.comments-table__pager {
display: flex;
align-items: center;
gap: 8px;
}
.comments-table__pager button {
width: 28px;
height: 28px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--aurora-text-muted);
cursor: pointer;
}
.comments-table__pager button:not(:disabled):hover {
background: var(--aurora-bg-subtle);
color: var(--aurora-text);
}
.comments-table__pager button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: 1280px) {
.comments-table__head,
.comment-row {
grid-template-columns: minmax(170px, 0.9fr) minmax(260px, 1.4fr) minmax(160px, 0.9fr) 100px 130px 132px;
}
}
@media (max-width: 1080px) {
.comments-table__head {
display: none;
}
.comment-row {
grid-template-columns: 1fr;
align-items: stretch;
padding: 8px 0;
}
.comment-row__comment,
.comment-row__post,
.comment-row__cell,
.comment-row__actions {
padding: 8px 16px;
}
.comment-row__cell,
.comment-row__post {
display: flex;
justify-content: space-between;
gap: 16px;
}
.comment-row__cell::before,
.comment-row__post::before {
content: attr(data-label);
color: var(--aurora-text-muted);
font-weight: 800;
}
.comments-table__footer,
.comments-page__header,
.comments-page__toolbar {
align-items: stretch;
flex-direction: column;
}
.comments-table__footer {
padding: 14px 16px;
gap: 10px;
}
.comments-page__search {
width: 100%;
}
}
</style>
+37 -41
View File
@@ -4,7 +4,7 @@ import Sidebar from './components/Sidebar.vue';
import Toolbar from './components/Toolbar.vue';
import { applyThemePalette, applyFontFamily } from '@/pages/settings/applyLiveTheme.js';
import { initWindowChrome } from './windowChrome.js';
import { getMinimizedWindow, clearMinimizedWindow } from './taskbar.js';
import { getMinimizedWindows, removeMinimizedWindow } from './taskbar.js';
const props = defineProps({
serverData: {
@@ -25,47 +25,31 @@ const toggleMenu = () => {
localStorage.setItem('aurora-menu-collapsed', collapsed.value ? '1' : '0');
};
// The block editor (Gutenberg) renders its own toolbar (undo/redo,
// title, save/publish) directly under Aurora's fixed toolbar — stacking
// both left a sliver of page gradient visible as a seam between them.
// Skip Aurora's toolbar there and let the editor's own bar sit flush
// against the top; shell-frame.css reads the aurora-no-toolbar class to
// also close up the top inset it reserves for the (now-absent) toolbar.
const isBlockEditor = document.body.classList.contains('block-editor-page');
// The one minimized window, if any (taskbar.js persists it in
// sessionStorage across the page load minimizing triggers). Rendered as a
// taskbar item at the bottom of every page since SiteShell mounts on all
// of them — clicking it navigates back and clears the entry.
const minimizedWindow = ref(null);
const restoreWindow = () => {
if (!minimizedWindow.value) return;
const url = minimizedWindow.value.url;
clearMinimizedWindow();
window.location.href = url;
// 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(() => {
minimizedWindow.value = getMinimizedWindow();
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', !!minimizedWindow.value);
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
// (rather than renamed) because shell-frame.css and native-dark.css both
// still gate their rules on it.
document.documentElement.classList.add('aurora-dark');
document.documentElement.classList.toggle('aurora-no-toolbar', isBlockEditor);
applyThemePalette(settings.theme_preset || 'indigo');
applyCollapsed();
applyFontFamily(settings.font_family);
// Skipped on the block editor — #wpbody-content is bypassed by the
// editor's own fixed skeleton there (see isBlockEditor above), so a
// bar injected into it would just be invisible clutter.
if (!isBlockEditor) {
initWindowChrome(props.serverData?.adminUrl);
}
initWindowChrome(props.serverData?.adminUrl);
// Signal the shell is live: reveals the framed content and commits to
// keeping native chrome hidden (the PHP failsafe stops watching once
// aurora-ready is present).
@@ -85,14 +69,19 @@ onMounted(() => {
:server-data="serverData"
/>
<Toolbar
v-if="!isBlockEditor"
:server-data="serverData"
@toggle-menu="toggleMenu"
/>
<div v-if="minimizedWindow" class="aurora-taskbar">
<button type="button" class="aurora-taskbar__item" @click="restoreWindow">
<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" />
{{ minimizedWindow.title }}
{{ win.title }}
</button>
</div>
</div>
@@ -106,30 +95,37 @@ onMounted(() => {
display: contents;
}
/* Full-width docked bar (real taskbar, edge to edge) — page content
reserves space above it via html.aurora-has-taskbar in shell-frame.css
so nothing renders underneath. The item itself stays pill-shaped. */
/* 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: 0;
bottom: 0;
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-frame-bg);
border-top: 1px solid var(--aurora-frame-border);
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;
background: var(--aurora-card-bg);
border: 1px solid var(--aurora-card-border);
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);
+158 -23
View File
@@ -17,6 +17,10 @@
/* 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. */
@@ -83,7 +87,14 @@ html.aurora-dark {
html.aurora-ready,
html.aurora-ready body {
background: var(--aurora-app-bg);
/* !important: WP core ships `body.js.block-editor-page { background:
#fff }` on the block editor, which has higher specificity (two
classes) than this selector (one) despite this having more type
selectors — CSS specificity compares class count before type count.
Without !important that rule wins and paints body solid white,
hiding the gradient completely on that page (everywhere else this
conflict doesn't exist, so it was never needed until now). */
background: var(--aurora-app-bg) !important;
/* Fixed so the gradient stays pinned to the viewport instead of
scrolling with document height — the sidebar/toolbar are also
position:fixed and paint no background of their own (see
@@ -108,22 +119,72 @@ html.aurora-ready #wpcontent {
}
/* The block editor (Gutenberg) doesn't live inside #wpcontent's normal
box model — it renders its own skeleton as position:fixed, inset:0,
completely independent of the margin-left above. Left unchecked it
spans the full viewport and renders underneath the (higher z-index)
sidebar and toolbar. Inset it the same amount #wpcontent gets. */
html.aurora-ready .interface-interface-skeleton {
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2) !important;
top: var(--aurora-toolbar-h) !important;
transition: left 0.2s ease;
}
box model — it renders its own fixed, full-viewport root, completely
independent of the margin-left above. Left unchecked it spans the full
viewport and renders underneath the (higher z-index) sidebar and
toolbar. Framed the same way #wpbody-content is everywhere else — same
insets, same card background/border/radius — just expressed as
fixed-position offsets instead of margin, since this element can't
participate in normal document flow.
/* Aurora's own toolbar is skipped on the block editor (SiteShell.vue) —
its own toolbar sits flush against the top instead, so no toolbar
height needs to be reserved here; just the same top gap the sidebar
gets, so the two line up. */
html.aurora-ready.aurora-no-toolbar .interface-interface-skeleton {
top: var(--aurora-sidebar-gap) !important;
Two different pages, two different fixed roots, both matched here:
- post.php/post-new.php: .interface-interface-skeleton itself is
position:fixed, inset:0.
- site-editor.php: .interface-interface-skeleton is only
position:relative, nested inside #site-editor, which is the actual
position:fixed, inset:0 element there — the rule below with
"#site-editor .interface-interface-skeleton" resets the nested one
back to a plain unpositioned child so the two frames don't stack. */
html.aurora-ready .interface-interface-skeleton,
html.aurora-ready #site-editor {
top: calc(var(--aurora-toolbar-h) + 16px) !important;
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2 + 24px) !important;
right: var(--aurora-gutter-r) !important;
bottom: 24px !important;
/* WP core's own .edit-site class (also present on #site-editor) sets
height: 100vh. A position:fixed box with top, height, AND bottom all
specified is over-constrained — per spec, 'bottom' is then silently
recomputed from top+height instead of honoring the value here, which
is exactly why the frame ran past the bottom of the viewport instead
of stopping at the inset above. Forcing height back to auto lets
top+bottom actually determine the box's size again. */
height: auto !important;
background: var(--aurora-frame-bg);
/* No border — an asymmetric one (3 sides, one "none") combined with
border-radius and this element's sub-pixel calc()-derived fixed
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);
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
hard square edge without this. Floating UI (block toolbars, popovers)
portals to document.body rather than nesting here, so this doesn't
clip anything that needs to escape the frame. */
overflow: hidden;
}
#site-editor .interface-interface-skeleton {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
background: transparent !important;
border-radius: 0 !important;
box-shadow: none !important;
padding-top: 0 !important;
overflow: visible !important;
width: 100%;
height: 100%;
}
html.aurora-ready #wpbody-content {
@@ -144,14 +205,60 @@ html.aurora-ready #wpbody-content {
width: auto;
}
/* SiteShell.vue's taskbar docks along the bottom edge only while a window
is minimized — push content up by its height so pages stop above it
instead of rendering underneath. */
html.aurora-has-taskbar #wpbody-content {
margin-bottom: calc(24px + var(--aurora-taskbar-h));
/* Gutenberg renders in its own fixed skeleton. The regular #wpbody-content
wrapper still exists on post-new.php/post.php, but it is inert; if framed
like normal native pages, it appears as a stray empty pill above the editor. */
html.aurora-ready body.block-editor-page #wpbody-content,
html.aurora-ready body.post-new-php #wpbody-content,
html.aurora-ready body.post-php #wpbody-content {
background: transparent !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
margin: 0 !important;
min-height: 0 !important;
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 {
margin-bottom: calc(24px + var(--aurora-taskbar-h));
/* #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;
}
/* 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;
}
/* macOS-style title bar (windowChrome.js). Most pages get it injected as
@@ -162,6 +269,24 @@ html.aurora-has-taskbar .aurora-dashboard-frame {
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;
@@ -227,8 +352,18 @@ html.aurora-maximized .toolbar {
html.aurora-ready.aurora-maximized #wpcontent {
margin-left: 0 !important;
}
html.aurora-ready.aurora-maximized .interface-interface-skeleton {
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;
+35 -17
View File
@@ -1,39 +1,57 @@
/**
* Tracks the one "minimized" window across page loads. Aurora is a normal
* 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. Only one item is tracked at a time: this
* is "minimize sends you home with a way back", not a full window manager.
* 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-window';
const STORAGE_KEY = 'aurora-minimized-windows';
export function getMinimizedWindow() {
export function getMinimizedWindows() {
try {
const raw = sessionStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed : [];
} catch {
return null;
return [];
}
}
export function setMinimizedWindow(title, url) {
function saveMinimizedWindows(list) {
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ title, url }));
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(list));
} catch {
/* sessionStorage unavailable (e.g. private mode) — minimize just won't persist */
}
}
export function clearMinimizedWindow() {
try {
sessionStorage.removeItem(STORAGE_KEY);
} catch {
/* noop */
}
/** 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);
}
/** Best-effort readable label for the current page, used as the taskbar item's title. */
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 raw = document.title || 'Window';
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';
}
+45 -20
View File
@@ -1,30 +1,30 @@
/**
* Adds a macOS-style traffic-light title bar to the top of the page content.
* Most pages render inside #wpbody-content (native WP pages and all of
* Aurora's own Vue pages, since each mounts its root div there). The
* Dashboard is the one exception — DashboardTakeover.php renders it into a
* separate #aurora-admin-dashboard-root sibling, above #wpbody (which it
* hides), to fully replace the native dashboard, so it gets wrapped in its
* own frame to match the card treatment #wpbody-content already has. The
* block editor bypasses both (see SiteShell.vue isBlockEditor), so it's
* skipped there.
* 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. All of this is
* just classes on <html> plus one sessionStorage entry (taskbar.js) — no
* server-side state.
* 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 { setMinimizedWindow, currentPageTitle } from './taskbar.js';
export function initWindowChrome(adminUrl) {
if (document.querySelector('.aurora-winbar')) {
return;
}
import { addMinimizedWindow, currentPageTitle } from './taskbar.js';
function buildBar(adminUrl, isDashboard) {
const bar = document.createElement('div');
bar.className = 'aurora-winbar';
@@ -39,8 +39,6 @@ export function initWindowChrome(adminUrl) {
};
const root = document.documentElement;
const dashboardRoot = document.getElementById('aurora-admin-dashboard-root');
const isDashboard = !!dashboardRoot;
bar.appendChild(
makeDot('close', 'Close', () => {
@@ -53,7 +51,7 @@ export function initWindowChrome(adminUrl) {
root.classList.toggle('aurora-minimized');
root.classList.remove('aurora-maximized');
} else {
setMinimizedWindow(currentPageTitle(), window.location.href);
addMinimizedWindow(currentPageTitle(), window.location.href);
window.location.href = adminUrl || '/wp-admin/';
}
})
@@ -65,7 +63,26 @@ export function initWindowChrome(adminUrl) {
})
);
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')
);
}
export function initWindowChrome(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);
@@ -74,8 +91,16 @@ export function initWindowChrome(adminUrl) {
return;
}
if (isBlockEditorPage()) {
const bar = buildBar(adminUrl, false);
bar.classList.add('aurora-winbar--editor');
document.body.appendChild(bar);
return;
}
const wpbodyContent = document.getElementById('wpbody-content');
if (wpbodyContent) {
const bar = buildBar(adminUrl, false);
wpbodyContent.insertBefore(bar, wpbodyContent.firstChild);
}
}
+1
View File
@@ -29,6 +29,7 @@ export default defineConfig({
'activity-log': fileURLToPath(new URL('./src/entries/activity-log.js', import.meta.url)),
'database-explorer': fileURLToPath(new URL('./src/entries/database-explorer.js', import.meta.url)),
plugins: fileURLToPath(new URL('./src/entries/plugins.js', import.meta.url)),
comments: fileURLToPath(new URL('./src/entries/comments.js', import.meta.url)),
},
output: {
entryFileNames: '[name].[hash].js',
+1
View File
@@ -29,6 +29,7 @@ require AURORA_ADMIN_PATH . "admin/src/Rest/DashboardData.php";
require AURORA_ADMIN_PATH . "admin/src/Analytics/Analytics.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/AnalyticsData.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/PluginsData.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/CommentsData.php";
require AURORA_ADMIN_PATH . "admin/src/Posts/ScheduledDayFilter.php";
require AURORA_ADMIN_PATH . "admin/src/WhiteLabel/WhiteLabel.php";
require AURORA_ADMIN_PATH . "admin/src/Pages/SubApps.php";