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:
co-authored by
Claude Sonnet 5
parent
b0b0a9999a
commit
8153d4397c
+158
-74
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"])) : "";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user