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 # 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 ## 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/settingsConfig.js`, rendered by
`app/src/pages/settings/components/SettingsField.vue` (types: text, toggle, `app/src/pages/settings/components/SettingsField.vue` (types: text, toggle,
color, image, repeater, multiselect, segmented, remote-sites). 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 each + one Vue page + one Vite entry each) — reachable only via the
sidebar's Aurora nav section, not the native WP menu: sidebar's Aurora nav section, not the native WP menu:
- Registered via `add_submenu_page('aurora-admin', ...)` then immediately - Registered via `add_submenu_page('aurora-admin', ...)` (or `'plugins.php'`
`remove_submenu_page()`'d on a late `admin_menu` hook — the for Plugins) and **left registered** in WP's native `$submenu` global —
register-but-hide trick: real page/capability/URL, but never appears in **no longer hidden via `remove_submenu_page()`**. An earlier version did
WP's native `$submenu` global (so `MenuSerializer` never sees them, call it (the standard register-but-hide trick) but that corrupts WP
nothing to filter there). 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 - All slugs prefixed `aurora-admin-*` so `Shell::is_own_page()`'s existing
`strpos($page, "aurora-admin") === 0` check covers them for free (full `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` - 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 (generalized from Settings.vue's box-pair CSS) + `AppListItem.vue` for
list rows. 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=`. `GET /activity-log?action_type=&object_type=&search=&page=`.
- **Plugins** (`admin/src/Rest/PluginsData.php` + `app/src/pages/plugins/Plugins.vue`): - **Plugins** (`admin/src/Rest/PluginsData.php` + `app/src/pages/plugins/Plugins.vue`):
custom plugin management list (activate/deactivate/delete), also using custom plugin management list (activate/deactivate/delete), also using
`AppListShell.vue`. **Known bug, unresolved**: unlike the other sub-apps, `AppListShell.vue`.
Menu Creator/Activity Log/Role Editor currently throw WordPress's native - **Comments** (`admin/src/Rest/CommentsData.php` + `app/src/pages/comments/`):
"Sorry, you are not allowed to access this page" for the logged-in admin newest sub-app, `edit_posts`-gated (not `manage_options` like the others).
user when navigated to directly, while Settings/Plugins/Dashboard work `SubApps::redirect_native_pages()` (`admin_init`) redirects the native
fine — likely a capability-check mismatch in how those three register `edit-comments.php` (when hit with no `?page=` param, i.e. a direct nav
their submenu pages vs. the working ones. Not yet root-caused; a or an old bookmark/menu link) to `admin.php?page=aurora-admin-comments`.
background investigation task was spawned for it but not completed this `MenuSerializer::resolve_url()` also maps the `edit-comments.php` slug
session — check for it before re-investigating from scratch. 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 — - **Database Explorer** (`admin/src/DatabaseExplorer/`): no custom table —
reads real `$wpdb` schema. `GET /db-tables` (SHOW TABLE STATUS), reads real `$wpdb` schema. `GET /db-tables` (SHOW TABLE STATUS),
`GET /db-tables/:table` (columns + paginated rows — table name validated `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>` 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 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. 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) ## 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 Every page gets a traffic-light title bar; minimizing persists across a full
page navigation via a taskbar. page navigation via a taskbar.
- `app/src/shell/windowChrome.js`: `initWindowChrome(adminUrl)`, called from - `app/src/shell/windowChrome.js`: `initWindowChrome(adminUrl)`, called
`SiteShell.vue::onMounted` (skipped on the block editor — see below). unconditionally from `SiteShell.vue::onMounted` (no longer skipped on the
Injects a `.aurora-winbar` div (3 dot buttons) as the first child of block editor — that special case was removed once the editor got its own
`#wpbody-content` — the one container shared by *every* page Aurora handling). Three cases, checked in priority order:
manages, native WP pages and all of Aurora's own Vue pages alike, since 1. **Dashboard** (`#aurora-admin-dashboard-root` present) — wraps the bar +
each Vue entry mounts its root div there. **Exception**: the Dashboard the dashboard root in a new `.aurora-dashboard-frame` div, giving it the
(`DashboardTakeover.php`) renders into a separate `#aurora-admin-dashboard-root` same card-frame treatment (`--aurora-frame-bg`, border, radius) every
sibling *above* `#wpbody` (which it hides) to fully replace the native other page gets from `#wpbody-content` itself.
dashboard — `#wpbody-content` is empty/collapsed there. `windowChrome.js` 2. **Block editor / Site Editor** (`isBlockEditorPage()`: body has
detects this and instead wraps the bar + `#aurora-admin-dashboard-root` in `block-editor-page`, or the path ends `/post.php` or `/post-new.php` —
a new `.aurora-dashboard-frame` div, giving the Dashboard the same `site-editor.php` also gets `block-editor-page` on its body, so this
card-frame treatment (`--aurora-frame-bg`, border, radius) every other already covers it with no JS change needed) — appends a
page already gets from `#wpbody-content` itself. `.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`. - Red (close): `window.location.href = adminUrl`.
- Green (maximize): toggles `html.aurora-maximized` — hides `.sidebar`/ - Green (maximize): toggles `html.aurora-maximized` — hides `.sidebar`/
`.toolbar`, zeroes `#wpcontent`'s `margin-left` and the block editor `.toolbar`, zeroes `#wpcontent`'s `margin-left` and the editor frame's
skeleton's `left`, so content fills the whole browser window. Toggle again inset, so content fills the whole browser window. Toggle again (or
(or minimize) to restore. Nothing persisted — resets on next page load. minimize) to restore. Nothing persisted — resets on next page load.
- Yellow (minimize): **on the Dashboard itself**, toggles `html.aurora-minimized` - Yellow (minimize): **on the Dashboard itself**, toggles `html.aurora-minimized`
which just collapses `#aurora-admin-dashboard-root` down to the bar in which just collapses `#aurora-admin-dashboard-root` down to the bar in
place (nowhere further "home" to send it). **On every other page**, calls place (nowhere further "home" to send it). **On every other page**, calls
`taskbar.js`'s `setMinimizedWindow(title, url)` (persists `{title, url}` in `taskbar.js`'s `addMinimizedWindow(title, url)` then navigates to the
`sessionStorage`, tab-scoped) then navigates to the dashboard. Only one dashboard.
minimized window is tracked at a time — this is "minimize sends you home - **Taskbar rewritten from a single sessionStorage slot to a list**
with a way back," not a real window manager. (`app/src/shell/taskbar.js`): minimizing a second page used to silently
- `SiteShell.vue` reads `taskbar.js`'s `getMinimizedWindow()` on every mount overwrite the first one's entry, so only the most recent minimize ever
(so the taskbar shows up on whatever page you're on after minimizing) and showed up. Now `getMinimizedWindows()`/`addMinimizedWindow()` (dedupes by
renders a **full-width docked bar** at the bottom with one pill-shaped URL)/`removeMinimizedWindow(id)` operate on an array under
item; clicking it clears the sessionStorage entry and navigates back to `sessionStorage['aurora-minimized-windows']`; `SiteShell.vue` `v-for`s over
the stored URL. `html.aurora-has-taskbar` (toggled alongside) reserves it, rendering one `.aurora-taskbar__item` pill per entry inside a
bottom margin on `#wpbody-content`/`.aurora-dashboard-frame` equal to transparent, floating `.aurora-taskbar` bar (inset by `--aurora-sidebar-gap`
`--aurora-taskbar-h` so page content stops above the bar instead of on all exposed edges, rounded, its own box-shadow — went through a design
running underneath it. Went through two design iterations (small centered iteration from a small centered pill back to full-length per explicit user
floating pill → back to a full-length bar per explicit user correction — correction, keeping only the pill shape on the individual items). Each item
the pill-shaped *item* stayed, just not as the whole bar). is independently restorable/closable. `currentPageTitle()` prefers the
- **Block editor exception**: Gutenberg (`post.php`/`post-new.php`) doesn't page's own on-screen `<h1>` (`#wpbody-content h1, #aurora-admin-dashboard-root h1`)
render inside `#wpcontent`'s normal box model at all — its over `document.title`, since WP's title-tag generation came up empty for
`.interface-interface-skeleton` is `position: fixed; inset: 0`, anchored to Aurora's sub-app pages back when they were still `remove_submenu_page()`'d
the viewport independent of any margin/padding on ancestors. `SiteShell.vue` (see the sub-apps permissions bug above) — `document.title` is still the
detects `document.body.classList.contains('block-editor-page')` and: (a) fallback for pages where that isn't an issue.
skips rendering Aurora's own `<Toolbar>` (the editor has its own, - **Content-behind-taskbar fix**: `html.aurora-has-taskbar` no longer just
equivalent one — stacking both left a gradient seam between them), (b) adds `margin-bottom` (that only guarantees clearance once scrolled to the
skips `initWindowChrome()` entirely (`#wpbody-content` is invisible/ very bottom — content taller than the viewport still rendered underneath
collapsed there, injecting into it would be dead clutter), (c) insets the the fixed taskbar on first paint). Instead it caps `#wpbody-content`'s/
skeleton itself via `left`/`top` in `shell-frame.css` so it doesn't render `.aurora-dashboard-frame`'s own height via `max-height` + `overflow-y: auto`,
underneath the sidebar. **If a future feature needs to reach the block same principle as the toolbar's reserved space at the top. This appeared to
editor's own chrome**, its real container is `.interface-interface-skeleton`, do nothing at first — `#wpbody-content`'s base rule sets `min-height:
not `#wpbody-content` — don't assume the normal page structure there. 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) ## 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 without first deleting the old one left old-hash asset filenames mixed
into the new zip (macOS `zip -r` appends/merges rather than replacing). into the new zip (macOS `zip -r` appends/merges rather than replacing).
Always `rm -f` the zip before rebuilding. 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) ## Not-yet-done (deferred, no active request)
@@ -389,22 +470,25 @@ page navigation via a taskbar.
text field). text field).
- Menu Creator: stored item `order` field isn't applied at runtime yet - Menu Creator: stored item `order` field isn't applied at runtime yet
(hide/relabel work; reordering doesn't). (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 - No automated tests anywhere in the project — all verification has been
manual, live-in-Chrome, per the established workflow rule above. manual, live-in-Chrome, per the established workflow rule above.
## Current state ## Current state
No task in progress. This session rebuilt the entire theming system (fixed No task in progress. Earlier this session rebuilt the entire theming system
color presets replacing the old light/dark toggle, gradient backgrounds, (fixed color presets, gradient backgrounds, "independently-painted gradient"
several "independently-painted gradient" seam bugs), added the macOS-style seam bugs) and added the macOS-style window chrome + taskbar feature
window chrome + taskbar feature end-to-end, and extended native-page dark end-to-end (pushed as commits `4a547d2`/`b0b0a99`). Later the same session:
styling coverage. All of it was live-verified in Chrome against the LocalWP root-caused and fixed the Menu Creator/Activity Log/Role Editor "Sorry, you
site and is pushed to `origin/main` (commit `4a547d2` at session end — a are not allowed to access this page" bug (was `remove_submenu_page()`
single large commit covering the whole session's work, since it wasn't corrupting WP's own capability lookup); added a 6th sub-app, Comments; fixed
broken into incremental commits along the way). The one known open issue is a white-patch bug in the block editor's dark-themed sidebar; rewrote the
the Menu Creator/Activity Log/Role Editor permissions bug noted above. 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); color: var(--aurora-text);
border-color: var(--aurora-card-border); 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\Analytics\Analytics;
use AuroraAdmin\Rest\AnalyticsData; use AuroraAdmin\Rest\AnalyticsData;
use AuroraAdmin\Rest\PluginsData; use AuroraAdmin\Rest\PluginsData;
use AuroraAdmin\Rest\CommentsData;
use AuroraAdmin\Posts\ScheduledDayFilter; use AuroraAdmin\Posts\ScheduledDayFilter;
use AuroraAdmin\WhiteLabel\WhiteLabel; use AuroraAdmin\WhiteLabel\WhiteLabel;
use AuroraAdmin\Pages\SubApps; use AuroraAdmin\Pages\SubApps;
@@ -41,6 +42,7 @@ class AuroraAdmin
new Analytics(); new Analytics();
new AnalyticsData(); new AnalyticsData();
new PluginsData(); new PluginsData();
new CommentsData();
new ScheduledDayFilter(); new ScheduledDayFilter();
new WhiteLabel(); new WhiteLabel();
new SubApps(); new SubApps();
+57 -15
View File
@@ -6,15 +6,28 @@ use AuroraAdmin\Utility\Assets;
defined("ABSPATH") || exit(); defined("ABSPATH") || exit();
/** /**
* Registers Aurora's 5 secondary full-page apps (Menu Creator, Admin * Registers Aurora's secondary full-page apps (Menu Creator, Admin Notices,
* Notices, Role Editor, Activity Log, Database Explorer). Each is a real WP * Role Editor, Activity Log, Database Explorer, Plugins, Comments). Each is
* admin page (proper capability check, working URL, its own hook_suffix for * a real WP admin page (proper capability check, working URL, its own
* asset gating) registered as a submenu of "aurora-admin" — then * hook_suffix for asset gating) registered as a submenu of "aurora-admin"
* immediately hidden from WordPress's own submenu via remove_submenu_page(), * (or "plugins.php" for the Plugins page).
* the standard register-but-hide trick. This keeps them out of the *
* WP-native menu list (MenuSerializer never sees them, nothing to filter) * These stay registered in WordPress's own $submenu — they are NOT hidden
* while still being reachable, permission-checked pages; they're presented * via remove_submenu_page(). An earlier version did call it (the standard
* instead in the sidebar's dedicated Aurora nav section (see Shell::aurora_nav()). * "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 class SubApps
{ {
@@ -25,6 +38,7 @@ class SubApps
"aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"], "aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"],
"aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"], "aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"],
"aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"], "aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"],
"aurora-admin-comments" => ["title" => "Comments", "entry" => "comments"],
]; ];
private static $hook_suffixes = []; private static $hook_suffixes = [];
@@ -32,19 +46,19 @@ class SubApps
public function __construct() public function __construct()
{ {
add_action("admin_menu", [self::class, "register_menus"]); 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_enqueue_scripts", [self::class, "maybe_enqueue"]);
add_action("admin_init", [self::class, "redirect_native_pages"]);
} }
public static function register_menus() public static function register_menus()
{ {
foreach (self::PAGES as $slug => $page) { foreach (self::PAGES as $slug => $page) {
$parent = $slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin"; $parent = self::parent_slug($slug);
$hook = add_submenu_page( $hook = add_submenu_page(
$parent, $parent,
__($page["title"], "aurora-admin"), __($page["title"], "aurora-admin"),
__($page["title"], "aurora-admin"), __($page["title"], "aurora-admin"),
$slug === "aurora-admin-plugins" ? "activate_plugins" : "manage_options", self::capability($slug),
$slug, $slug,
[self::class, "render"] [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) { if ($slug === "aurora-admin-plugins") {
remove_submenu_page($slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin", $slug); 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) 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"); 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() public static function render()
{ {
$page = isset($_GET["page"]) ? sanitize_text_field(wp_unslash($_GET["page"])) : ""; $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, "spam" => (int) $counts->spam,
"total" => (int) $counts->approved, "total" => (int) $counts->approved,
"latest" => $latest, "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_title = self::clean_title(isset($sub[0]) ? $sub[0] : "");
$sub_slug = isset($sub[2]) ? $sub[2] : ""; $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"][] = [ $entry["submenu"][] = [
"id" => $sub_slug, "id" => $sub_slug,
"title" => $sub_title["title"], "title" => $sub_title["title"],
@@ -136,6 +146,10 @@ class MenuSerializer
return admin_url("plugins.php?page=aurora-admin-plugins"); 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. // Already an absolute URL.
if (preg_match('#^https?://#', $slug)) { if (preg_match('#^https?://#', $slug)) {
return $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 * Whether the shell should run on the current request. Skipped on the
* full-screen native experiences that own the whole viewport. * 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() private static function is_enabled()
{ {
@@ -44,7 +53,7 @@ class Shell
} }
$screen = function_exists("get_current_screen") ? get_current_screen() : null; $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; 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 Toolbar from './components/Toolbar.vue';
import { applyThemePalette, applyFontFamily } from '@/pages/settings/applyLiveTheme.js'; import { applyThemePalette, applyFontFamily } from '@/pages/settings/applyLiveTheme.js';
import { initWindowChrome } from './windowChrome.js'; import { initWindowChrome } from './windowChrome.js';
import { getMinimizedWindow, clearMinimizedWindow } from './taskbar.js'; import { getMinimizedWindows, removeMinimizedWindow } from './taskbar.js';
const props = defineProps({ const props = defineProps({
serverData: { serverData: {
@@ -25,47 +25,31 @@ const toggleMenu = () => {
localStorage.setItem('aurora-menu-collapsed', collapsed.value ? '1' : '0'); localStorage.setItem('aurora-menu-collapsed', collapsed.value ? '1' : '0');
}; };
// The block editor (Gutenberg) renders its own toolbar (undo/redo, // Every minimized window (taskbar.js persists the list in sessionStorage
// title, save/publish) directly under Aurora's fixed toolbar — stacking // across the page loads minimizing triggers). Rendered as taskbar items at
// both left a sliver of page gradient visible as a seam between them. // the bottom of every page since SiteShell mounts on all of them — clicking
// Skip Aurora's toolbar there and let the editor's own bar sit flush // one navigates back to it and removes just that entry.
// against the top; shell-frame.css reads the aurora-no-toolbar class to const minimizedWindows = ref([]);
// also close up the top inset it reserves for the (now-absent) toolbar. const restoreWindow = (win) => {
const isBlockEditor = document.body.classList.contains('block-editor-page'); removeMinimizedWindow(win.id);
window.location.href = win.url;
// The one minimized window, if any (taskbar.js persists it in
// sessionStorage across the page load minimizing triggers). Rendered as a
// taskbar item at the bottom of every page since SiteShell mounts on all
// of them — clicking it navigates back and clears the entry.
const minimizedWindow = ref(null);
const restoreWindow = () => {
if (!minimizedWindow.value) return;
const url = minimizedWindow.value.url;
clearMinimizedWindow();
window.location.href = url;
}; };
onMounted(() => { onMounted(() => {
minimizedWindow.value = getMinimizedWindow(); minimizedWindows.value = getMinimizedWindows();
// Reserve bottom space for the taskbar (shell-frame.css) so page content // Reserve bottom space for the taskbar (shell-frame.css) so page content
// stops above it instead of running underneath. // 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 || {}; const settings = props.serverData?.settings || {};
// Every theme preset is its own self-contained dark-toned palette now — // 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 // 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 // (rather than renamed) because shell-frame.css and native-dark.css both
// still gate their rules on it. // still gate their rules on it.
document.documentElement.classList.add('aurora-dark'); document.documentElement.classList.add('aurora-dark');
document.documentElement.classList.toggle('aurora-no-toolbar', isBlockEditor);
applyThemePalette(settings.theme_preset || 'indigo'); applyThemePalette(settings.theme_preset || 'indigo');
applyCollapsed(); applyCollapsed();
applyFontFamily(settings.font_family); applyFontFamily(settings.font_family);
// Skipped on the block editor — #wpbody-content is bypassed by the initWindowChrome(props.serverData?.adminUrl);
// editor's own fixed skeleton there (see isBlockEditor above), so a
// bar injected into it would just be invisible clutter.
if (!isBlockEditor) {
initWindowChrome(props.serverData?.adminUrl);
}
// Signal the shell is live: reveals the framed content and commits to // Signal the shell is live: reveals the framed content and commits to
// keeping native chrome hidden (the PHP failsafe stops watching once // keeping native chrome hidden (the PHP failsafe stops watching once
// aurora-ready is present). // aurora-ready is present).
@@ -85,14 +69,19 @@ onMounted(() => {
:server-data="serverData" :server-data="serverData"
/> />
<Toolbar <Toolbar
v-if="!isBlockEditor"
:server-data="serverData" :server-data="serverData"
@toggle-menu="toggleMenu" @toggle-menu="toggleMenu"
/> />
<div v-if="minimizedWindow" class="aurora-taskbar"> <div v-if="minimizedWindows.length" class="aurora-taskbar">
<button type="button" class="aurora-taskbar__item" @click="restoreWindow"> <button
v-for="win in minimizedWindows"
:key="win.id"
type="button"
class="aurora-taskbar__item"
@click="restoreWindow(win)"
>
<span class="aurora-taskbar__dot" /> <span class="aurora-taskbar__dot" />
{{ minimizedWindow.title }} {{ win.title }}
</button> </button>
</div> </div>
</div> </div>
@@ -106,30 +95,37 @@ onMounted(() => {
display: contents; display: contents;
} }
/* Full-width docked bar (real taskbar, edge to edge) — page content /* Full-width docked bar, but inset with the same gap the sidebar floats
reserves space above it via html.aurora-has-taskbar in shell-frame.css with (not flush against the browser edges or the window above it) and
so nothing renders underneath. The item itself stays pill-shaped. */ 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 { .aurora-taskbar {
position: fixed; position: fixed;
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2); left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2);
right: 0; right: var(--aurora-sidebar-gap);
bottom: 0; bottom: var(--aurora-sidebar-gap);
height: var(--aurora-taskbar-h); height: var(--aurora-taskbar-h);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
padding: 0 16px; padding: 0 16px;
background: var(--aurora-frame-bg); background: var(--aurora-card-bg);
border-top: 1px solid var(--aurora-frame-border); 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; box-sizing: border-box;
z-index: 200001; z-index: 200001;
overflow-x: auto;
transition: left 0.2s ease; transition: left 0.2s ease;
} }
.aurora-taskbar__item { .aurora-taskbar__item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background: var(--aurora-card-bg); flex-shrink: 0;
border: 1px solid var(--aurora-card-border); background: var(--aurora-bg-subtle);
border: 1px solid var(--aurora-border);
border-radius: 999px; border-radius: 999px;
padding: 8px 18px 8px 10px; padding: 8px 18px 8px 10px;
color: var(--aurora-text); color: var(--aurora-text);
+158 -23
View File
@@ -17,6 +17,10 @@
/* SiteShell.vue's taskbar, shown only while a window is minimized /* SiteShell.vue's taskbar, shown only while a window is minimized
(html.aurora-has-taskbar toggles the bottom space reserved for it). */ (html.aurora-has-taskbar toggles the bottom space reserved for it). */
--aurora-taskbar-h: 56px; --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 /* Space between the sidebar card and the browser's left/top/bottom
edges (Sidebar.vue) — the toolbar and #wpcontent offsets below add edges (Sidebar.vue) — the toolbar and #wpcontent offsets below add
the same amount so nothing overlaps the floating sidebar. */ the same amount so nothing overlaps the floating sidebar. */
@@ -83,7 +87,14 @@ html.aurora-dark {
html.aurora-ready, html.aurora-ready,
html.aurora-ready body { 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 /* Fixed so the gradient stays pinned to the viewport instead of
scrolling with document height — the sidebar/toolbar are also scrolling with document height — the sidebar/toolbar are also
position:fixed and paint no background of their own (see 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 /* The block editor (Gutenberg) doesn't live inside #wpcontent's normal
box model — it renders its own skeleton as position:fixed, inset:0, box model — it renders its own fixed, full-viewport root, completely
completely independent of the margin-left above. Left unchecked it independent of the margin-left above. Left unchecked it spans the full
spans the full viewport and renders underneath the (higher z-index) viewport and renders underneath the (higher z-index) sidebar and
sidebar and toolbar. Inset it the same amount #wpcontent gets. */ toolbar. Framed the same way #wpbody-content is everywhere else — same
html.aurora-ready .interface-interface-skeleton { insets, same card background/border/radius — just expressed as
left: calc(var(--aurora-sidebar-w) + var(--aurora-sidebar-gap) * 2) !important; fixed-position offsets instead of margin, since this element can't
top: var(--aurora-toolbar-h) !important; participate in normal document flow.
transition: left 0.2s ease;
}
/* Aurora's own toolbar is skipped on the block editor (SiteShell.vue) — Two different pages, two different fixed roots, both matched here:
its own toolbar sits flush against the top instead, so no toolbar - post.php/post-new.php: .interface-interface-skeleton itself is
height needs to be reserved here; just the same top gap the sidebar position:fixed, inset:0.
gets, so the two line up. */ - site-editor.php: .interface-interface-skeleton is only
html.aurora-ready.aurora-no-toolbar .interface-interface-skeleton { position:relative, nested inside #site-editor, which is the actual
top: var(--aurora-sidebar-gap) !important; 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 { html.aurora-ready #wpbody-content {
@@ -144,14 +205,60 @@ html.aurora-ready #wpbody-content {
width: auto; width: auto;
} }
/* SiteShell.vue's taskbar docks along the bottom edge only while a window /* Gutenberg renders in its own fixed skeleton. The regular #wpbody-content
is minimized — push content up by its height so pages stop above it wrapper still exists on post-new.php/post.php, but it is inert; if framed
instead of rendering underneath. */ like normal native pages, it appears as a stray empty pill above the editor. */
html.aurora-has-taskbar #wpbody-content { html.aurora-ready body.block-editor-page #wpbody-content,
margin-bottom: calc(24px + var(--aurora-taskbar-h)); 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 { 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 /* 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; align-items: center;
gap: 8px; 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 { #wpbody-content > .aurora-winbar {
margin: -32px -32px 20px -32px; margin: -32px -32px 20px -32px;
padding: 14px 16px; padding: 14px 16px;
@@ -227,8 +352,18 @@ html.aurora-maximized .toolbar {
html.aurora-ready.aurora-maximized #wpcontent { html.aurora-ready.aurora-maximized #wpcontent {
margin-left: 0 !important; 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; 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 { html.aurora-maximized .aurora-taskbar {
left: 0 !important; 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 * 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 * 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 * tab closes) is enough for that. Stored as a list, not a single slot —
* is "minimize sends you home with a way back", not a full window manager. * 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 { try {
const raw = sessionStorage.getItem(STORAGE_KEY); const raw = sessionStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null; const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed : [];
} catch { } catch {
return null; return [];
} }
} }
export function setMinimizedWindow(title, url) { function saveMinimizedWindows(list) {
try { try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ title, url })); sessionStorage.setItem(STORAGE_KEY, JSON.stringify(list));
} catch { } catch {
/* sessionStorage unavailable (e.g. private mode) — minimize just won't persist */ /* sessionStorage unavailable (e.g. private mode) — minimize just won't persist */
} }
} }
export function clearMinimizedWindow() { /** Adds (or, if the same page is already minimized, replaces) an entry. */
try { export function addMinimizedWindow(title, url) {
sessionStorage.removeItem(STORAGE_KEY); const list = getMinimizedWindows().filter((w) => w.url !== url);
} catch { list.push({ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, title, url });
/* noop */ 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() { 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'; 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. * 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 * Three cases, in priority order:
* Aurora's own Vue pages, since each mounts its root div there). The * 1. The Dashboard — DashboardTakeover.php renders it into a separate
* Dashboard is the one exception — DashboardTakeover.php renders it into a * #aurora-admin-dashboard-root sibling, above #wpbody (which it hides),
* separate #aurora-admin-dashboard-root sibling, above #wpbody (which it * to fully replace the native dashboard. Gets wrapped in a new
* hides), to fully replace the native dashboard, so it gets wrapped in its * .aurora-dashboard-frame div to match the card treatment
* own frame to match the card treatment #wpbody-content already has. The * #wpbody-content already has everywhere else.
* block editor bypasses both (see SiteShell.vue isBlockEditor), so it's * 2. The block editor — gets only the traffic-light controls, overlaid on
* skipped there. * 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 — * Red closes (navigates back to the Aurora dashboard). Yellow minimizes —
* on any page other than the dashboard, that means "save where I am and * 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 * send me home", with SiteShell.vue's taskbar offering a way back; on the
* dashboard itself (already home) it just collapses in place instead, * dashboard itself (already home) it just collapses in place instead,
* since there's nowhere further back to send it. Green hides Aurora's own * 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 * sidebar/toolbar so the content fills the browser window. Minimized pages
* just classes on <html> plus one sessionStorage entry (taskbar.js) — no * are tracked as a list (taskbar.js), not a single slot, so minimizing
* server-side state. * more than one shows up as separate taskbar items rather than the latest
* silently replacing the previous one.
*/ */
import { setMinimizedWindow, currentPageTitle } from './taskbar.js'; import { addMinimizedWindow, currentPageTitle } from './taskbar.js';
export function initWindowChrome(adminUrl) {
if (document.querySelector('.aurora-winbar')) {
return;
}
function buildBar(adminUrl, isDashboard) {
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.className = 'aurora-winbar'; bar.className = 'aurora-winbar';
@@ -39,8 +39,6 @@ export function initWindowChrome(adminUrl) {
}; };
const root = document.documentElement; const root = document.documentElement;
const dashboardRoot = document.getElementById('aurora-admin-dashboard-root');
const isDashboard = !!dashboardRoot;
bar.appendChild( bar.appendChild(
makeDot('close', 'Close', () => { makeDot('close', 'Close', () => {
@@ -53,7 +51,7 @@ export function initWindowChrome(adminUrl) {
root.classList.toggle('aurora-minimized'); root.classList.toggle('aurora-minimized');
root.classList.remove('aurora-maximized'); root.classList.remove('aurora-maximized');
} else { } else {
setMinimizedWindow(currentPageTitle(), window.location.href); addMinimizedWindow(currentPageTitle(), window.location.href);
window.location.href = adminUrl || '/wp-admin/'; 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) { if (dashboardRoot) {
const bar = buildBar(adminUrl, true);
const frame = document.createElement('div'); const frame = document.createElement('div');
frame.className = 'aurora-dashboard-frame'; frame.className = 'aurora-dashboard-frame';
dashboardRoot.parentNode.insertBefore(frame, dashboardRoot); dashboardRoot.parentNode.insertBefore(frame, dashboardRoot);
@@ -74,8 +91,16 @@ export function initWindowChrome(adminUrl) {
return; 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'); const wpbodyContent = document.getElementById('wpbody-content');
if (wpbodyContent) { if (wpbodyContent) {
const bar = buildBar(adminUrl, false);
wpbodyContent.insertBefore(bar, wpbodyContent.firstChild); 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)), '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)), 'database-explorer': fileURLToPath(new URL('./src/entries/database-explorer.js', import.meta.url)),
plugins: fileURLToPath(new URL('./src/entries/plugins.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: { output: {
entryFileNames: '[name].[hash].js', 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/Analytics/Analytics.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/AnalyticsData.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/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/Posts/ScheduledDayFilter.php";
require AURORA_ADMIN_PATH . "admin/src/WhiteLabel/WhiteLabel.php"; require AURORA_ADMIN_PATH . "admin/src/WhiteLabel/WhiteLabel.php";
require AURORA_ADMIN_PATH . "admin/src/Pages/SubApps.php"; require AURORA_ADMIN_PATH . "admin/src/Pages/SubApps.php";