Add first-run setup wizard

Full-screen onboarding at admin.php?page=aurora-admin-setup: Welcome →
Appearance (theme + font) → Branding (logo/favicon/rename) → Features
(replacement-screen toggles) → Done. Reuses SettingsField for every control
and applyLiveTheme for instant preview; finish/skip saves via wp/v2/settings
and marks completion.

- admin/src/Pages/SetupWizard.php: hidden submenu page, disables the shell on
  its own page (aurora_admin_shell_enabled filter) + hides native chrome,
  register_activation_hook sets a one-time redirect flag, admin_init redirects
  once, and a POST /aurora-admin/v1/setup/complete route marks it done.
- app/src/pages/setup-wizard/SetupWizard.vue + entries/setup-wizard.js + vite
  input.
- Wired into aurora-admin.php + AuroraAdmin.php.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
NixReaper
2026-07-12 13:55:03 -05:00
co-authored by Claude Opus 4.8
parent 2da3883d20
commit 2fec4cf34b
6 changed files with 475 additions and 0 deletions
+2
View File
@@ -13,6 +13,7 @@ use AuroraAdmin\Options\Settings;
use AuroraAdmin\Shell\Shell;
use AuroraAdmin\Pages\DashboardTakeover;
use AuroraAdmin\Pages\SettingsPage;
use AuroraAdmin\Pages\SetupWizard;
use AuroraAdmin\Rest\DashboardData;
use AuroraAdmin\Analytics\Analytics;
use AuroraAdmin\Rest\AnalyticsData;
@@ -63,6 +64,7 @@ class AuroraAdmin
new Shell();
new DashboardTakeover();
new SettingsPage();
new SetupWizard();
new DashboardData();
new Analytics();
new AnalyticsData();
+151
View File
@@ -0,0 +1,151 @@
<?php
namespace AuroraAdmin\Pages;
use AuroraAdmin\Utility\Assets;
use AuroraAdmin\Options\Settings;
defined("ABSPATH") || exit();
/**
* First-run onboarding wizard.
*
* A full-screen, self-contained page (no Aurora sidebar/toolbar shell, no
* native WP chrome) walking a new user through appearance, branding, and
* which replacement screens to enable. Registered as a hidden submenu of the
* "aurora-admin" menu — MenuSerializer skips that slug, so it never shows in
* the Aurora sidebar, and the native menu it does technically live under is
* hidden by the shell on every other page anyway.
*
* First run: on activation an option flags a one-time redirect here; the
* wizard clears it and sets a "complete" option when finished or skipped, so
* it never auto-opens again (but stays reachable at its URL).
*/
class SetupWizard
{
const PAGE = "aurora-admin-setup";
const OPT_COMPLETE = "aurora_admin_setup_complete";
const OPT_REDIRECT = "aurora_admin_setup_redirect";
private static $hook_suffix = null;
public function __construct()
{
register_activation_hook(AURORA_ADMIN_PATH . "aurora-admin.php", [self::class, "on_activate"]);
add_action("admin_menu", [self::class, "register_menu"]);
add_action("admin_enqueue_scripts", [self::class, "maybe_enqueue"]);
add_action("admin_init", [self::class, "maybe_redirect"]);
add_action("rest_api_init", [self::class, "register_rest"]);
// Turn the immersive shell off on this page so the wizard owns the whole
// viewport; native chrome is hidden separately (printed below).
add_filter("aurora_admin_shell_enabled", [self::class, "disable_shell_here"]);
add_action("admin_head", [self::class, "hide_native_chrome"]);
}
/** Flag a one-time redirect to the wizard, unless setup was already done. */
public static function on_activate()
{
if (!get_option(self::OPT_COMPLETE)) {
update_option(self::OPT_REDIRECT, 1);
}
}
private static function is_wizard_page()
{
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page match; no state change.
return isset($_GET["page"]) && sanitize_text_field(wp_unslash($_GET["page"])) === self::PAGE;
}
public static function disable_shell_here($enabled)
{
return self::is_wizard_page() ? false : $enabled;
}
public static function register_menu()
{
self::$hook_suffix = add_submenu_page(
"aurora-admin",
__("Setup", "aurora-admin"),
__("Setup", "aurora-admin"),
"manage_options",
self::PAGE,
[self::class, "render"]
);
}
public static function maybe_redirect()
{
if (!get_option(self::OPT_REDIRECT)) {
return;
}
if (wp_doing_ajax() || !current_user_can("manage_options") || self::is_wizard_page()) {
return;
}
delete_option(self::OPT_REDIRECT);
wp_safe_redirect(admin_url("admin.php?page=" . self::PAGE));
exit();
}
public static function maybe_enqueue($hook_suffix)
{
if ($hook_suffix !== self::$hook_suffix) {
return;
}
// Image fields (logo, dark logo, favicon) use the real media library.
wp_enqueue_media();
Assets::enqueue("aurora-admin-setup", "entries/setup-wizard.js");
}
/** Hide native WP chrome on the wizard page (the shell, which normally does
* this, is turned off here). The Vue app also renders as a full-screen
* overlay, but this keeps the admin bar/menu from flashing first. */
public static function hide_native_chrome()
{
if (!self::is_wizard_page()) {
return;
}
?>
<style id="aurora-setup-hide-chrome">
#adminmenumain, #wpadminbar, #wpfooter { display: none !important; }
html.wp-toolbar { padding-top: 0 !important; }
#wpcontent { margin-left: 0 !important; }
#wpbody-content { padding: 0 !important; float: none; width: auto; }
</style>
<?php
}
public static function register_rest()
{
register_rest_route("aurora-admin/v1", "/setup/complete", [
"methods" => "POST",
"callback" => [self::class, "rest_complete"],
"permission_callback" => function () {
return current_user_can("manage_options");
},
]);
}
public static function rest_complete()
{
update_option(self::OPT_COMPLETE, 1);
delete_option(self::OPT_REDIRECT);
return ["success" => true];
}
public static function render()
{
$data = [
"restUrl" => esc_url_raw(get_rest_url()),
"restNonce" => wp_create_nonce("wp_rest"),
"userName" => wp_get_current_user()->display_name,
"settings" => Settings::get(),
"dashboardUrl" => esc_url_raw(admin_url("index.php")),
"settingsUrl" => esc_url_raw(admin_url("admin.php?page=aurora-admin")),
];
?>
<div
id="aurora-admin-setup-root"
data-aurora-admin="<?php echo esc_attr(wp_json_encode($data)); ?>"
></div>
<?php
}
}
+10
View File
@@ -0,0 +1,10 @@
import { createApp } from 'vue';
import '@/shell/shell-frame.css';
import SetupWizard from '@/pages/setup-wizard/SetupWizard.vue';
const mountEl = document.getElementById('aurora-admin-setup-root');
if (mountEl) {
const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}');
createApp(SetupWizard, { serverData: data }).mount(mountEl);
}
+310
View File
@@ -0,0 +1,310 @@
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue';
import SettingsField from '@/pages/settings/components/SettingsField.vue';
import { applyThemePalette, applyFontFamily } from '@/pages/settings/applyLiveTheme.js';
const props = defineProps({
serverData: { type: Object, required: true },
});
// Step definitions. Welcome and Done are custom content; the middle three
// render their `fields` through the shared SettingsField component (same
// controls as the settings page — theme picker, font picker, media-library
// image fields, toggles).
const steps = [
{ id: 'welcome', label: 'Welcome' },
{
id: 'appearance',
label: 'Appearance',
title: 'Choose your look',
subtitle:
'Pick a color theme and font for the whole admin. You can change these any time in Settings.',
fields: [
{
key: 'theme_preset',
type: 'theme-picker',
label: 'Theme',
description: 'The accent and surface colors used across the admin.',
default: 'indigo',
},
{
key: 'font_family',
type: 'font-picker',
label: 'Font family',
description: 'A Google Font used across the Aurora interface.',
},
],
},
{
id: 'branding',
label: 'Branding',
title: 'Add your branding',
subtitle:
'All optional — drop in a logo and favicon, or rename Aurora for a fully white-labeled admin.',
fields: [
{ key: 'logo', type: 'image', label: 'Logo', description: 'Shown at the top of the sidebar menu.' },
{
key: 'dark_logo',
type: 'image',
label: 'Dark mode logo',
description: 'Optional alternative logo; falls back to the standard one.',
},
{
key: 'admin_favicon',
type: 'image',
label: 'Admin favicon',
description: 'Replaces the WordPress favicon in the admin.',
},
{
key: 'plugin_name',
type: 'text',
label: 'Rename Aurora Admin',
description: 'White-labels the plugin name in menus and the plugins list.',
placeholder: 'Aurora Admin',
},
],
},
{
id: 'features',
label: 'Features',
title: 'Choose your features',
subtitle:
'Aurora replaces these WordPress admin screens with faster versions of its own. Turn off any you’d rather keep native.',
fields: [
{ key: 'use_custom_dashboard', type: 'toggle', label: 'Dashboard', description: 'Replace the WordPress dashboard with Aurora’s.', default: true },
{ key: 'modern_posts', type: 'toggle', label: 'Posts', description: 'Aurora posts management screen.', default: true },
{ key: 'modern_pages', type: 'toggle', label: 'Pages', description: 'Aurora pages management screen.', default: true },
{ key: 'modern_media', type: 'toggle', label: 'Media library', description: 'Aurora media library screen.', default: true },
{ key: 'modern_users', type: 'toggle', label: 'Users', description: 'Aurora users management screen.', default: true },
{ key: 'modern_comments', type: 'toggle', label: 'Comments', description: 'Aurora comments management screen.', default: true },
{ key: 'modern_plugins', type: 'toggle', label: 'Plugins', description: 'Aurora plugins management screen.', default: true },
{
key: 'full_admin_takeover',
type: 'toggle',
label: 'Full admin takeover',
description: 'Also replace Appearance, Tools, and the native Settings screens. Off by default.',
default: false,
},
],
},
{ id: 'done', label: 'Done', title: 'You’re all set' },
];
// Working settings, seeded from the server + each field's default.
const settings = reactive({ ...(props.serverData.settings || {}) });
steps.forEach((s) =>
(s.fields || []).forEach((f) => {
if (settings[f.key] === undefined && f.default !== undefined) settings[f.key] = f.default;
})
);
const stepIndex = ref(0);
const current = computed(() => steps[stepIndex.value]);
const isFirst = computed(() => stepIndex.value === 0);
const isDone = computed(() => current.value.id === 'done');
const goNext = () => {
if (stepIndex.value < steps.length - 1) stepIndex.value++;
};
const goBack = () => {
if (stepIndex.value > 0) stepIndex.value--;
};
// Live preview of theme + font as they're picked (nothing is persisted until
// the wizard is finished, but the whole admin previews immediately).
watch(() => settings.theme_preset, (p) => applyThemePalette(p || 'indigo'), { immediate: true });
watch(() => settings.font_family, (f) => applyFontFamily(f), { immediate: true });
onMounted(() => {
// The shell (which normally does this) is disabled on this page.
document.documentElement.classList.add('aurora-dark');
applyThemePalette(settings.theme_preset || 'indigo');
applyFontFamily(settings.font_family);
});
const saving = ref(false);
const finish = async (target) => {
if (saving.value) return;
saving.value = true;
try {
await fetch(`${props.serverData.restUrl}wp/v2/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': props.serverData.restNonce },
body: JSON.stringify({ aurora_admin_settings: { ...settings } }),
});
await fetch(`${props.serverData.restUrl}aurora-admin/v1/setup/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': props.serverData.restNonce },
});
} catch {
/* Even if persistence fails, don't trap the user in the wizard. */
}
window.location.href = target || props.serverData.dashboardUrl || '/wp-admin/';
};
</script>
<template>
<div class="wiz">
<div class="wiz__inner">
<div class="wiz__brand">
<span class="wiz__mark">A</span>
<span class="wiz__brandname">Aurora Admin</span>
</div>
<!-- Progress -->
<ol class="wiz__steps">
<template v-for="(s, i) in steps" :key="s.id">
<li
class="wiz__step"
:class="{ 'is-active': i === stepIndex, 'is-done': i < stepIndex }"
>
<span class="wiz__dot">
<span v-if="i < stepIndex" class="dashicons dashicons-yes" />
<span v-else>{{ i + 1 }}</span>
</span>
<span class="wiz__step-label">{{ s.label }}</span>
</li>
<li v-if="i < steps.length - 1" class="wiz__connector" :class="{ 'is-on': i < stepIndex }" />
</template>
</ol>
<!-- Card -->
<section class="wiz__card">
<!-- Welcome -->
<div v-if="current.id === 'welcome'" class="wiz__welcome">
<h1 class="wiz__h1">Welcome to Aurora Admin<span v-if="serverData.userName">, {{ serverData.userName }}</span></h1>
<p class="wiz__lead">
A modern, fast redesign of the WordPress admin. This quick setup takes about a
minute — choose a look, add your branding, and pick which screens Aurora takes over.
You can skip it and change everything later in Settings.
</p>
</div>
<!-- Done -->
<div v-else-if="current.id === 'done'" class="wiz__welcome">
<span class="wiz__done-icon dashicons dashicons-yes-alt" />
<h1 class="wiz__h1">You’re all set</h1>
<p class="wiz__lead">
Aurora is configured and ready. Head to your dashboard, or fine-tune anything in
Settings.
</p>
<div class="wiz__done-actions">
<button type="button" class="wiz__btn wiz__btn--primary" :disabled="saving" @click="finish(serverData.dashboardUrl)">
{{ saving ? 'Finishing…' : 'Go to dashboard' }}
</button>
<button type="button" class="wiz__btn" :disabled="saving" @click="finish(serverData.settingsUrl)">
Open settings
</button>
</div>
</div>
<!-- Field steps -->
<div v-else>
<h1 class="wiz__h1">{{ current.title }}</h1>
<p v-if="current.subtitle" class="wiz__lead">{{ current.subtitle }}</p>
<div class="wiz__fields">
<SettingsField
v-for="f in current.fields"
:key="f.key"
:field="f"
:api="null"
v-model="settings[f.key]"
/>
</div>
</div>
</section>
<!-- Footer nav (hidden on the Done step, which has its own actions) -->
<div v-if="!isDone" class="wiz__nav">
<button type="button" class="wiz__btn wiz__btn--ghost" :disabled="saving" @click="finish(serverData.dashboardUrl)">
Skip setup
</button>
<div class="wiz__nav-right">
<button v-if="!isFirst" type="button" class="wiz__btn" @click="goBack">Back</button>
<button type="button" class="wiz__btn wiz__btn--primary" @click="goNext">
{{ isFirst ? 'Get started' : 'Continue' }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.wiz {
position: fixed;
inset: 0;
z-index: 100000;
overflow-y: auto;
background:
radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--aurora-accent) 12%, var(--aurora-app-bg-solid)) 0%, var(--aurora-app-bg-solid) 65%);
color: var(--aurora-text);
font-family: var(--aurora-font-family);
}
.wiz__inner {
max-width: 760px;
margin: 0 auto;
padding: 48px 24px 64px;
}
.wiz__brand { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 30px; }
.wiz__mark {
width: 30px; height: 30px; border-radius: var(--aurora-radius-md);
background: var(--aurora-accent); color: #fff;
display: flex; align-items: center; justify-content: center; font-weight: 700;
}
.wiz__brandname { font-size: 1rem; font-weight: 500; }
/* Progress */
.wiz__steps { display: flex; align-items: center; justify-content: center; list-style: none; margin: 0 0 30px; padding: 0; }
.wiz__step { display: flex; flex-direction: column; align-items: center; gap: 7px; flex: 0 0 auto; }
.wiz__dot {
width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 500;
border: 1.5px solid var(--aurora-border); background: var(--aurora-card-bg); color: var(--aurora-text-muted);
}
.wiz__dot .dashicons { font-size: 16px; width: 16px; height: 16px; }
.wiz__step.is-done .wiz__dot { background: var(--aurora-accent); border-color: var(--aurora-accent); color: #fff; }
.wiz__step.is-active .wiz__dot { border-color: var(--aurora-accent); color: var(--aurora-accent); }
.wiz__step-label { font-size: 0.6875rem; color: var(--aurora-text-muted); }
.wiz__step.is-active .wiz__step-label, .wiz__step.is-done .wiz__step-label { color: var(--aurora-text); }
.wiz__connector { flex: 1 1 auto; height: 1.5px; min-width: 22px; background: var(--aurora-border); margin: 0 6px; position: relative; top: -10px; }
.wiz__connector.is-on { background: var(--aurora-accent); }
/* Card */
.wiz__card {
background: var(--aurora-card-bg);
border: 1px solid var(--aurora-border);
border-radius: var(--aurora-radius-xl);
padding: 30px 32px;
}
.wiz__h1 { font-size: 1.5rem; font-weight: 600; margin: 0; }
.wiz__lead { margin: 10px 0 0; font-size: 0.9rem; color: var(--aurora-text-muted); line-height: 1.6; }
.wiz__welcome { text-align: center; padding: 20px 8px; }
.wiz__welcome .wiz__lead { max-width: 520px; margin: 12px auto 0; }
.wiz__done-icon { font-size: 52px; width: 52px; height: 52px; color: var(--aurora-accent); }
.wiz__done-actions { display: flex; gap: 12px; justify-content: center; margin-top: 26px; }
/* Fields: SettingsField renders its own rows; drop the first row's top pad
and the last row's divider so they sit cleanly inside the card. */
.wiz__fields { margin-top: 8px; }
.wiz__fields > :deep(.field):first-child { padding-top: 12px; }
.wiz__fields > :deep(.field):last-child { border-bottom: none; padding-bottom: 4px; }
/* Nav */
.wiz__nav { display: flex; align-items: center; justify-content: space-between; margin-top: 22px; }
.wiz__nav-right { display: flex; gap: 10px; }
.wiz__btn {
font-size: 0.875rem; font-weight: 500;
padding: 10px 20px; border-radius: var(--aurora-radius-md); cursor: pointer;
border: 1px solid var(--aurora-border); background: var(--aurora-card-bg); color: var(--aurora-text);
}
.wiz__btn:hover { border-color: var(--aurora-accent); }
.wiz__btn:disabled { opacity: 0.6; cursor: default; }
.wiz__btn--ghost { border-color: transparent; color: var(--aurora-text-muted); }
.wiz__btn--ghost:hover { color: var(--aurora-text); border-color: transparent; }
.wiz__btn--primary { background: var(--aurora-accent); color: #fff; border-color: var(--aurora-accent); }
.wiz__btn--primary:hover { filter: brightness(1.08); }
</style>
+1
View File
@@ -34,6 +34,7 @@ export default defineConfig({
shell: fileURLToPath(new URL('./src/entries/shell.js', import.meta.url)),
dashboard: fileURLToPath(new URL('./src/entries/dashboard.js', import.meta.url)),
settings: fileURLToPath(new URL('./src/entries/settings.js', import.meta.url)),
'setup-wizard': fileURLToPath(new URL('./src/entries/setup-wizard.js', import.meta.url)),
'menu-creator': fileURLToPath(new URL('./src/entries/menu-creator.js', import.meta.url)),
'admin-notices': fileURLToPath(new URL('./src/entries/admin-notices.js', import.meta.url)),
'role-editor': fileURLToPath(new URL('./src/entries/role-editor.js', import.meta.url)),
+1
View File
@@ -26,6 +26,7 @@ require AURORA_ADMIN_PATH . "admin/src/Shell/MenuSerializer.php";
require AURORA_ADMIN_PATH . "admin/src/Shell/Shell.php";
require AURORA_ADMIN_PATH . "admin/src/Pages/DashboardTakeover.php";
require AURORA_ADMIN_PATH . "admin/src/Pages/SettingsPage.php";
require AURORA_ADMIN_PATH . "admin/src/Pages/SetupWizard.php";
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";