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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user