Snapshot synced v3.6.1 working state as baseline
Machine-sync snapshot: the working tree carried the full 3.0.0-3.6.1 arc (licensing/FileManager/DatabaseExplorer removal, all new replacement screens and REST controllers) that had not been committed on this machine. Captured here as a safety baseline before starting the Adminify-parity revamp. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
07d77bda57
commit
e2db2ad4b5
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { settingsCategories } from './settingsConfig.js';
|
||||
import SettingsField from './components/SettingsField.vue';
|
||||
import { applyThemePalette, applyFontFamily } from './applyLiveTheme.js';
|
||||
@@ -8,9 +8,27 @@ const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const visibleCategories = settingsCategories;
|
||||
|
||||
// Shared REST fetcher for field types that need their own API calls beyond
|
||||
// the generic wp/v2/settings save flow (currently just 'companion-plugin').
|
||||
const api = (path, options = {}) =>
|
||||
fetch(`${props.serverData.restUrl}aurora-admin/v1${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'X-WP-Nonce': props.serverData.restNonce,
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
}).then(async (r) => {
|
||||
const body = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(body.message || 'Request failed');
|
||||
return body;
|
||||
});
|
||||
|
||||
// Working copy of the settings, seeded from the server + field defaults.
|
||||
const settings = reactive({ ...(props.serverData.settings || {}) });
|
||||
settingsCategories.forEach((cat) =>
|
||||
visibleCategories.forEach((cat) =>
|
||||
cat.fields.forEach((f) => {
|
||||
if (settings[f.key] === undefined && f.default !== undefined) {
|
||||
settings[f.key] = f.default;
|
||||
@@ -21,25 +39,37 @@ settingsCategories.forEach((cat) =>
|
||||
// Active category, synced with the URL ?category= param.
|
||||
const initialCat = new URLSearchParams(window.location.search).get('category');
|
||||
const activeId = ref(
|
||||
settingsCategories.some((c) => c.id === initialCat) ? initialCat : settingsCategories[0].id
|
||||
visibleCategories.some((c) => c.id === initialCat) ? initialCat : visibleCategories[0].id
|
||||
);
|
||||
const activeCategory = computed(
|
||||
() => settingsCategories.find((c) => c.id === activeId.value) || settingsCategories[0]
|
||||
() => visibleCategories.find((c) => c.id === activeId.value) || visibleCategories[0]
|
||||
);
|
||||
|
||||
// The content panel scrolls independently of the nav (see .settings__content
|
||||
// below) so picking a category from a scrolled-down spot in the nav doesn't
|
||||
// require scrolling the whole page back up to see it. But the panel is one
|
||||
// persistent DOM node across category switches — its scrollTop doesn't reset
|
||||
// on its own unless the new content happens to be shorter, so without this
|
||||
// a long category could inherit a scrolled-down position from the previous
|
||||
// one and land on the same "content is off-screen" problem this was meant
|
||||
// to fix.
|
||||
const contentEl = ref(null);
|
||||
const selectCategory = (id) => {
|
||||
activeId.value = id;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('category', id);
|
||||
window.history.replaceState({}, '', url);
|
||||
nextTick(() => {
|
||||
if (contentEl.value) contentEl.value.scrollTop = 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Search filter over the nav.
|
||||
const query = ref('');
|
||||
const filteredCategories = computed(() => {
|
||||
const q = query.value.trim().toLowerCase();
|
||||
if (!q) return settingsCategories;
|
||||
return settingsCategories.filter(
|
||||
if (!q) return visibleCategories;
|
||||
return visibleCategories.filter(
|
||||
(c) =>
|
||||
c.title.toLowerCase().includes(q) || c.subtitle.toLowerCase().includes(q)
|
||||
);
|
||||
@@ -161,7 +191,7 @@ const importSettings = (e) => {
|
||||
</aside>
|
||||
|
||||
<!-- Right content panel -->
|
||||
<section class="settings__content">
|
||||
<section ref="contentEl" class="settings__content">
|
||||
<header class="settings__content-head">
|
||||
<div>
|
||||
<h2 class="settings__content-title">{{ activeCategory.title }}</h2>
|
||||
@@ -181,7 +211,8 @@ const importSettings = (e) => {
|
||||
:key="f.key"
|
||||
:field="f"
|
||||
:options="optionsFor(f)"
|
||||
:license-status="f.key === 'license_key' ? serverData.licenseStatus : null"
|
||||
:disabled="!!f.dependsOn && !settings[f.dependsOn]"
|
||||
:api="api"
|
||||
v-model="settings[f.key]"
|
||||
/>
|
||||
</div>
|
||||
@@ -194,20 +225,31 @@ const importSettings = (e) => {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
min-height: calc(100vh - var(--aurora-toolbar-h) - 60px);
|
||||
/* height, not min-height — the outer #wpbody-content card (shell-
|
||||
frame.css) already caps itself to the viewport and scrolls as a
|
||||
whole. Filling it exactly (rather than growing taller than it) is
|
||||
what lets .settings__nav and .settings__content become their own
|
||||
independent scroll regions below instead of both scrolling together
|
||||
as one long page. */
|
||||
height: calc(100vh - var(--aurora-toolbar-h) - 60px);
|
||||
margin: -32px; /* cancel the frame padding */
|
||||
padding: 22px; /* inset the two boxes; the frame's right gutter adds the rest */
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* No background here — html/body already paint the page gradient
|
||||
(shell-frame.css); repainting the same var(--aurora-app-bg) here was
|
||||
just a redundant duplicate layer on top of it. */
|
||||
}
|
||||
|
||||
/* Left nav — no background/border of its own; the page gradient shows
|
||||
straight through so it reads as one surface instead of a boxed panel. */
|
||||
straight through so it reads as one surface instead of a boxed panel.
|
||||
Scrolls independently of the content panel (own height + overflow)
|
||||
so its scroll position is preserved when you switch categories. */
|
||||
.settings__nav {
|
||||
width: 340px;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
box-sizing: border-box;
|
||||
@@ -261,11 +303,19 @@ const importSettings = (e) => {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Right content — no background/border of its own, same reasoning. */
|
||||
/* Right content — no background/border of its own, same reasoning.
|
||||
Own height + overflow (independent of .settings__nav's) is the actual
|
||||
fix: clicking a category always shows its content from the top,
|
||||
whatever the nav's own scroll position is — resetScrollTop on
|
||||
category switch (see selectCategory() above) handles the case where
|
||||
this panel itself was already scrolled down. */
|
||||
.settings__content {
|
||||
flex: 1; min-width: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
padding: 28px 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.settings__content-head {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
@@ -287,7 +337,11 @@ const importSettings = (e) => {
|
||||
.settings__fields > :last-child { border-bottom: none; }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.settings { flex-direction: column; }
|
||||
.settings__nav { width: auto; }
|
||||
/* Independent-scroll panes only make sense side-by-side — stacked on a
|
||||
narrow screen, revert to one natural-height column so the page
|
||||
scrolls as a whole the normal way, same as before this change. */
|
||||
.settings { flex-direction: column; height: auto; overflow: visible; }
|
||||
.settings__nav { width: auto; height: auto; overflow: visible; }
|
||||
.settings__content { height: auto; overflow: visible; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import Toggle from '@/components/Toggle.vue';
|
||||
import { themePresets, buildPalette } from '@/pages/settings/themePresets.js';
|
||||
import { googleFonts } from '@/pages/settings/googleFonts.js';
|
||||
@@ -11,24 +11,15 @@ const props = defineProps({
|
||||
modelValue: { default: undefined },
|
||||
// Options for multiselect fields (resolved by the page from optionsSource).
|
||||
options: { type: Array, default: () => [] },
|
||||
// License::status()'s cached result, only passed for the license_key field.
|
||||
licenseStatus: { type: Object, default: null },
|
||||
// True when this field's `dependsOn` key is falsy — greys out the
|
||||
// control without hiding it, so the relationship stays visible.
|
||||
disabled: { type: Boolean, default: false },
|
||||
// Shared REST fetcher, only used by field types that need their own API
|
||||
// calls beyond the generic wp/v2/settings save flow (companion-plugin).
|
||||
api: { type: Function, default: null },
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const licenseStatusText = computed(() => {
|
||||
const s = props.licenseStatus;
|
||||
if (!s || s.status === 'none') return null;
|
||||
if (s.status === 'active') {
|
||||
const plan = s.plan ? ` (${s.plan} plan)` : '';
|
||||
return { tone: 'ok', text: `License active${plan}${s.expires_at ? ` — renews/expires ${s.expires_at}` : ''}` };
|
||||
}
|
||||
if (s.status === 'unreachable') {
|
||||
return { tone: 'warn', text: s.message || 'Could not reach the license server — will retry automatically.' };
|
||||
}
|
||||
return { tone: 'error', text: s.message || 'License invalid.' };
|
||||
});
|
||||
|
||||
const val = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v),
|
||||
@@ -54,21 +45,6 @@ const toggleValue = (value) => {
|
||||
emit('update:modelValue', next);
|
||||
};
|
||||
|
||||
// Remote sites helpers -------------------------------------------------------
|
||||
const sites = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []));
|
||||
const newSite = reactive({ url: '', username: '', app_password: '' });
|
||||
const addSite = () => {
|
||||
if (!newSite.url.trim()) return;
|
||||
emit('update:modelValue', [
|
||||
...sites.value,
|
||||
{ url: newSite.url.trim(), username: newSite.username.trim(), app_password: newSite.app_password },
|
||||
]);
|
||||
newSite.url = '';
|
||||
newSite.username = '';
|
||||
newSite.app_password = '';
|
||||
};
|
||||
const removeSite = (i) => emit('update:modelValue', sites.value.filter((_, idx) => idx !== i));
|
||||
|
||||
// Media picker (image fields) ---------------------------------------------
|
||||
const pickImage = () => {
|
||||
const wp = window.wp;
|
||||
@@ -103,16 +79,47 @@ const openFontList = () => {
|
||||
fontQuery.value = props.modelValue || '';
|
||||
fontOpen.value = true;
|
||||
};
|
||||
|
||||
// Companion plugin (Aurora File Manager) install card -----------------------
|
||||
const companionStatus = ref(null); // { installed, active } | null while loading
|
||||
const companionBusy = ref(false);
|
||||
const companionError = ref('');
|
||||
|
||||
const loadCompanionStatus = async () => {
|
||||
if (props.field.type !== 'companion-plugin' || !props.api) return;
|
||||
try {
|
||||
companionStatus.value = await props.api(props.field.statusEndpoint);
|
||||
} catch {
|
||||
companionStatus.value = { installed: false, active: false };
|
||||
}
|
||||
};
|
||||
if (props.field.type === 'companion-plugin') {
|
||||
loadCompanionStatus();
|
||||
}
|
||||
|
||||
const installCompanion = async () => {
|
||||
if (!props.api) return;
|
||||
companionBusy.value = true;
|
||||
companionError.value = '';
|
||||
try {
|
||||
const result = await props.api(props.field.installEndpoint, { method: 'POST' });
|
||||
if (!result.success) {
|
||||
companionError.value = result.message || 'Installation failed.';
|
||||
}
|
||||
await loadCompanionStatus();
|
||||
} catch (e) {
|
||||
companionError.value = e.message || 'Installation failed.';
|
||||
} finally {
|
||||
companionBusy.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field" :id="field.key === 'license_key' ? 'license' : null">
|
||||
<div class="field">
|
||||
<div class="field__info">
|
||||
<div class="field__label">{{ field.label }}</div>
|
||||
<p v-if="field.description" class="field__desc">{{ field.description }}</p>
|
||||
<p v-if="licenseStatusText" class="field__license-status" :class="`field__license-status--${licenseStatusText.tone}`">
|
||||
{{ licenseStatusText.text }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field__control">
|
||||
@@ -122,14 +129,39 @@ const openFontList = () => {
|
||||
type="text"
|
||||
class="field__input"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:readonly="!!field.readonly"
|
||||
:value="val || ''"
|
||||
@input="val = $event.target.value"
|
||||
/>
|
||||
|
||||
<!-- Textarea (multi-line plain text — moderation/blocklist fields) -->
|
||||
<textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
class="field__input field__textarea"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:rows="field.rows || 6"
|
||||
:value="val || ''"
|
||||
@input="val = $event.target.value"
|
||||
></textarea>
|
||||
|
||||
<!-- Select (plain dropdown, distinct from the multiselect checkbox list below) -->
|
||||
<select
|
||||
v-else-if="field.type === 'select'"
|
||||
class="field__input"
|
||||
:value="val ?? ''"
|
||||
:disabled="disabled"
|
||||
@change="val = $event.target.value"
|
||||
>
|
||||
<option v-for="opt in (field.options || options)" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Toggle -->
|
||||
<Toggle
|
||||
v-else-if="field.type === 'toggle'"
|
||||
:model-value="!!val"
|
||||
:disabled="disabled"
|
||||
@update:model-value="val = $event"
|
||||
/>
|
||||
|
||||
@@ -186,28 +218,6 @@ const openFontList = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Remote sites -->
|
||||
<div v-else-if="field.type === 'remote-sites'" class="field__sites">
|
||||
<div class="field__sites-add">
|
||||
<input v-model="newSite.url" type="text" class="field__input" placeholder="Site URL (e.g., https://example.com)" />
|
||||
<div class="field__sites-row">
|
||||
<input v-model="newSite.username" type="text" class="field__input" placeholder="Username" />
|
||||
<input v-model="newSite.app_password" type="password" class="field__input" placeholder="Application Password" />
|
||||
<button type="button" class="field__newitem" @click="addSite">Add Site</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field__sites-list">
|
||||
<div v-if="!sites.length" class="field__sites-empty">No remote sites configured yet</div>
|
||||
<div v-for="(site, i) in sites" :key="i" class="field__sites-item">
|
||||
<span class="field__sites-url">{{ site.url }}</span>
|
||||
<span class="field__sites-user">{{ site.username }}</span>
|
||||
<button type="button" class="field__repeater-remove" @click="removeSite(i)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="field__sites-help">
|
||||
Application passwords can be created in WordPress under Users > Profile > Application Passwords
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme picker (10 fixed color presets, whole-UI preview) -->
|
||||
<div v-else-if="field.type === 'theme-picker'" class="field__theme">
|
||||
@@ -262,6 +272,31 @@ const openFontList = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Companion plugin install card -->
|
||||
<div v-else-if="field.type === 'companion-plugin'" class="field__companion">
|
||||
<div v-if="!companionStatus" class="field__companion-status">Checking…</div>
|
||||
<template v-else>
|
||||
<a
|
||||
v-if="companionStatus.active"
|
||||
class="field__companion-manage"
|
||||
:href="field.manageUrl"
|
||||
>
|
||||
Installed & active — open File Manager →
|
||||
</a>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="field__companion-install"
|
||||
:disabled="companionBusy"
|
||||
@click="installCompanion"
|
||||
>
|
||||
{{ companionBusy ? 'Installing…' : (companionStatus.installed ? 'Activate' : 'Install & Activate') }}
|
||||
</button>
|
||||
<p v-if="companionError" class="field__companion-error">{{ companionError }}</p>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Repeater -->
|
||||
<div v-else-if="field.type === 'repeater'" class="field__repeater">
|
||||
<div v-for="(row, i) in rows" :key="i" class="field__repeater-row">
|
||||
@@ -298,10 +333,6 @@ const openFontList = () => {
|
||||
}
|
||||
.field__label { font-size: 0.95rem; font-weight: 600; color: var(--aurora-text); }
|
||||
.field__desc { margin: 6px 0 0; font-size: 0.8125rem; color: var(--aurora-text-muted); line-height: 1.5; }
|
||||
.field__license-status { margin: 8px 0 0; font-size: 0.8125rem; font-weight: 600; }
|
||||
.field__license-status--ok { color: #34d399; }
|
||||
.field__license-status--warn { color: #fbbf24; }
|
||||
.field__license-status--error { color: #f87171; }
|
||||
.field__control { display: flex; justify-content: flex-start; }
|
||||
|
||||
.field__input {
|
||||
@@ -314,6 +345,7 @@ const openFontList = () => {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.field__input:focus { outline: none; border-color: var(--aurora-accent); }
|
||||
.field__textarea { resize: vertical; font-family: inherit; line-height: 1.5; }
|
||||
|
||||
.field__color { display: flex; align-items: center; gap: 10px; }
|
||||
.field__color input[type='color'] {
|
||||
@@ -379,33 +411,6 @@ const openFontList = () => {
|
||||
}
|
||||
.field__seg--active { background: var(--aurora-accent); color: #fff !important; }
|
||||
|
||||
/* Remote sites */
|
||||
.field__sites { width: 100%; }
|
||||
.field__sites-add .field__input { margin-bottom: 8px; }
|
||||
.field__sites-row { display: flex; gap: 8px; }
|
||||
.field__sites-row .field__input { margin-bottom: 0; }
|
||||
.field__sites-row .field__newitem { flex-shrink: 0; white-space: nowrap; }
|
||||
.field__sites-list {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.field__sites-empty {
|
||||
padding: 18px; text-align: center;
|
||||
font-size: 0.8125rem; color: var(--aurora-text-muted);
|
||||
}
|
||||
.field__sites-item {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.field__sites-item:last-child { border-bottom: none; }
|
||||
.field__sites-url { color: var(--aurora-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.field__sites-user { color: var(--aurora-text-muted); }
|
||||
.field__sites-help { margin: 10px 0 0; font-size: 0.75rem; color: var(--aurora-text-muted); }
|
||||
|
||||
/* Theme picker */
|
||||
.field__theme { display: flex; flex-wrap: wrap; gap: 10px; width: 100%; }
|
||||
.field__theme-card {
|
||||
@@ -449,6 +454,20 @@ const openFontList = () => {
|
||||
.field__font-item:hover { background: var(--aurora-bg-subtle); }
|
||||
.field__font-item--active { color: var(--aurora-accent); }
|
||||
|
||||
/* Companion plugin install card */
|
||||
.field__companion-status { font-size: 0.8125rem; color: var(--aurora-text-muted); }
|
||||
.field__companion-install {
|
||||
border: none; border-radius: 8px; background: var(--aurora-accent); color: #fff;
|
||||
padding: 8px 16px; font-size: 0.8125rem; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.field__companion-install:disabled { opacity: 0.6; cursor: default; }
|
||||
.field__companion-manage {
|
||||
display: inline-block; font-size: 0.8125rem; font-weight: 600;
|
||||
color: var(--aurora-accent); text-decoration: none;
|
||||
}
|
||||
.field__companion-manage:hover { text-decoration: underline; }
|
||||
.field__companion-error { margin: 8px 0 0; font-size: 0.8125rem; color: #e5484d; }
|
||||
|
||||
@media (max-width: 782px) {
|
||||
.field { grid-template-columns: 1fr; gap: 12px; }
|
||||
}
|
||||
|
||||
@@ -8,16 +8,9 @@ export const settingsCategories = [
|
||||
{
|
||||
id: 'general',
|
||||
title: 'General',
|
||||
subtitle: 'Core plugin settings and license management',
|
||||
subtitle: 'Core plugin settings',
|
||||
icon: 'dashicons-admin-settings',
|
||||
fields: [
|
||||
{
|
||||
key: 'license_key',
|
||||
type: 'text',
|
||||
label: 'License key',
|
||||
description: 'Enter your license key to enable automatic updates.',
|
||||
placeholder: 'AA-XXXX-XXXX-XXXX',
|
||||
},
|
||||
{
|
||||
key: 'logo',
|
||||
type: 'image',
|
||||
@@ -31,20 +24,6 @@ export const settingsCategories = [
|
||||
description:
|
||||
"Set an alternative logo to use in dark mode. If one isn't set it will fall back to the standard logo.",
|
||||
},
|
||||
{
|
||||
key: 'remote_sites',
|
||||
type: 'remote-sites',
|
||||
label: 'Remote Sites',
|
||||
description: 'Configure remote WordPress sites for cross-site operations.',
|
||||
},
|
||||
{
|
||||
key: 'remote_site_switcher_capability',
|
||||
type: 'text',
|
||||
label: 'Remote Site Switcher Capability',
|
||||
description:
|
||||
'Pro feature — requires a license. WordPress capability required to see the remote site switcher in the toolbar. Defaults to "manage_options" (admin users). Users with this capability or below will see the switcher.',
|
||||
placeholder: 'manage_options',
|
||||
},
|
||||
{
|
||||
key: 'disable_theme',
|
||||
type: 'multiselect',
|
||||
@@ -111,7 +90,7 @@ export const settingsCategories = [
|
||||
{
|
||||
id: 'whitelabel',
|
||||
title: 'White Label',
|
||||
subtitle: 'Rebrand and customize interface text and icons (Pro feature — requires a license)',
|
||||
subtitle: 'Rebrand and customize interface text and icons',
|
||||
icon: 'dashicons-tag',
|
||||
fields: [
|
||||
{
|
||||
@@ -215,7 +194,7 @@ export const settingsCategories = [
|
||||
{
|
||||
id: 'analytics',
|
||||
title: 'Analytics',
|
||||
subtitle: 'Built-in analytics and tracking settings (Pro feature — requires a license)',
|
||||
subtitle: 'Built-in analytics and tracking settings',
|
||||
icon: 'dashicons-chart-bar',
|
||||
fields: [
|
||||
{
|
||||
@@ -284,4 +263,97 @@ export const settingsCategories = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'image-formats',
|
||||
title: 'Modern Image Formats',
|
||||
subtitle: 'Automatically generate WebP/AVIF versions of uploaded images',
|
||||
icon: 'dashicons-format-image',
|
||||
fields: [
|
||||
{
|
||||
key: 'image_formats_enabled',
|
||||
type: 'toggle',
|
||||
label: 'Enable modern image formats',
|
||||
description:
|
||||
'Automatically generate a WebP or AVIF copy of every size of a newly uploaded JPEG or PNG image, and serve it to browsers that support it. Only affects new uploads — existing library images are unaffected until re-uploaded or regenerated with a tool like Regenerate Thumbnails.',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: 'image_formats_format',
|
||||
type: 'select',
|
||||
label: 'Output format',
|
||||
description:
|
||||
'Automatic prefers AVIF (smaller files, wider quality range) when the server supports it, falling back to WebP. If your server supports neither, generation is silently skipped — ask your host about enabling GD or Imagick WebP/AVIF support.',
|
||||
dependsOn: 'image_formats_enabled',
|
||||
default: 'auto',
|
||||
options: [
|
||||
{ value: 'auto', label: 'Automatic (AVIF, falling back to WebP)' },
|
||||
{ value: 'webp', label: 'WebP only' },
|
||||
{ value: 'avif', label: 'AVIF only' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'image_formats_picture_element',
|
||||
type: 'toggle',
|
||||
label: 'Serve via <picture> element',
|
||||
description:
|
||||
'Rewrite <img> tags (post content and post thumbnails) into a <picture> element offering the modern format with the original JPEG/PNG as a fallback. Turn this off if it conflicts with your theme’s image styling, while still generating the files for other tools to use.',
|
||||
dependsOn: 'image_formats_enabled',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'add-ons',
|
||||
title: 'Add-ons',
|
||||
subtitle: 'Standalone companion plugins',
|
||||
icon: 'dashicons-admin-plugins',
|
||||
fields: [
|
||||
{
|
||||
key: 'file_manager_addon',
|
||||
type: 'companion-plugin',
|
||||
label: 'Aurora File Manager',
|
||||
description:
|
||||
'Browse, edit, and manage your site’s files (themes, plugins, uploads, and more) directly from wp-admin. Distributed as a separate plugin rather than bundled here, since WordPress.org’s Plugin Directory policy doesn’t allow this category of functionality in a directory-listed plugin — restrictions or not.',
|
||||
statusEndpoint: '/companion-plugins/file-manager/status',
|
||||
installEndpoint: '/companion-plugins/file-manager/install',
|
||||
manageUrl: 'admin.php?page=aurora-file-manager',
|
||||
},
|
||||
{
|
||||
key: 'database_explorer_addon',
|
||||
type: 'companion-plugin',
|
||||
label: 'Aurora Database Explorer',
|
||||
description:
|
||||
'A Supabase-style database browser inside wp-admin — see every table with row counts and sizes, browse paginated data, and run your own read-only SELECT/SHOW/DESCRIBE/EXPLAIN queries. Distributed as a separate plugin rather than bundled here, since WordPress.org’s Plugin Directory review treats direct database-query tooling the same way it treats file-management functionality.',
|
||||
statusEndpoint: '/companion-plugins/database-explorer/status',
|
||||
installEndpoint: '/companion-plugins/database-explorer/install',
|
||||
manageUrl: 'admin.php?page=aurora-database-explorer',
|
||||
},
|
||||
{
|
||||
key: 'site_backup_addon',
|
||||
type: 'companion-plugin',
|
||||
label: 'Aurora Site Backup',
|
||||
description:
|
||||
'Full site backup (database + files) and restore-in-place, on demand or on a recurring schedule, stored locally or on your own S3-compatible or Google Drive remote storage. Distributed as a separate plugin rather than bundled here, to keep Aurora Admin’s own footprint away from arbitrary file-write functionality.',
|
||||
statusEndpoint: '/companion-plugins/site-backup/status',
|
||||
installEndpoint: '/companion-plugins/site-backup/install',
|
||||
manageUrl: 'admin.php?page=aurora-site-backup',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin-takeover',
|
||||
title: 'Admin Takeover',
|
||||
subtitle: 'Replace the remaining native WordPress admin screens with Aurora',
|
||||
icon: 'dashicons-shield',
|
||||
fields: [
|
||||
{
|
||||
key: 'full_admin_takeover',
|
||||
type: 'toggle',
|
||||
label: 'Full admin takeover',
|
||||
description:
|
||||
'Replace Media Upload, Add User, Profile, Appearance, Tools, and native Settings screens with Aurora equivalents. Off by default — these cover more ground than Posts/Pages/Media/Users/Comments, so it’s opt-in rather than on by default.',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user