Add theme presets, gradient backgrounds, and macOS-style window chrome
Replaces the old light/dark toggle with 10 fixed color themes (plus Black & Grey and White) that drive a coordinated gradient palette across the whole admin UI, a Google Fonts picker, and a native-page dark-mode stylesheet that now covers modals and plugin/theme card grids. Fixes several background/gradient seams where the sidebar, toolbar, and content frame were each painting their own independent copy of the page gradient instead of sharing one continuous plane. Also adds a macOS-style traffic-light title bar to every page (close, minimize, maximize) and a persistent bottom taskbar so a minimized page can be restored from anywhere, plus a Plugins management page. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
db07e21f9e
commit
4a547d2c50
@@ -2,6 +2,7 @@
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { settingsCategories } from './settingsConfig.js';
|
||||
import SettingsField from './components/SettingsField.vue';
|
||||
import { applyThemePalette, applyFontFamily } from './applyLiveTheme.js';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
@@ -51,10 +52,17 @@ const optionsFor = (field) => {
|
||||
return field.options || [];
|
||||
};
|
||||
|
||||
// Live dark-mode preview.
|
||||
// Live preview — applied as soon as a picker changes, not just after
|
||||
// Save + reload.
|
||||
watch(
|
||||
() => settings.dark_mode,
|
||||
(v) => document.documentElement.classList.toggle('aurora-dark', !!v)
|
||||
() => settings.theme_preset,
|
||||
(preset) => applyThemePalette(preset || 'indigo'),
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => settings.font_family,
|
||||
(family) => applyFontFamily(family),
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Save -------------------------------------------------------------------
|
||||
@@ -189,16 +197,16 @@ const importSettings = (e) => {
|
||||
margin: -32px; /* cancel the frame padding */
|
||||
padding: 22px; /* inset the two boxes; the frame's right gutter adds the rest */
|
||||
box-sizing: border-box;
|
||||
/* Darker backdrop so the two boxes read as distinct panels. */
|
||||
background: var(--aurora-app-bg);
|
||||
/* 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 — its own box */
|
||||
/* 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. */
|
||||
.settings__nav {
|
||||
width: 340px;
|
||||
flex-shrink: 0;
|
||||
background: var(--aurora-frame-bg);
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
box-sizing: border-box;
|
||||
@@ -252,11 +260,9 @@ const importSettings = (e) => {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Right content — its own box */
|
||||
/* Right content — no background/border of its own, same reasoning. */
|
||||
.settings__content {
|
||||
flex: 1; min-width: 0;
|
||||
background: var(--aurora-frame-bg);
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 16px;
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Shared logic for applying the theme/font settings to the live document —
|
||||
* used both for the settings page's live preview (Settings.vue) and the
|
||||
* real per-page-load application (SiteShell.vue::onMounted).
|
||||
*/
|
||||
import { resolvePreset, buildPalette } from './themePresets.js';
|
||||
|
||||
export function applyThemePalette(themePresetId) {
|
||||
const palette = buildPalette(resolvePreset(themePresetId));
|
||||
const root = document.documentElement.style;
|
||||
Object.entries(palette).forEach(([prop, value]) => root.setProperty(prop, value));
|
||||
}
|
||||
|
||||
export function applyFontFamily(family) {
|
||||
if (!family) return;
|
||||
const linkId = 'aurora-google-font';
|
||||
let link = document.getElementById(linkId);
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.id = linkId;
|
||||
link.rel = 'stylesheet';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400;500;600;700&display=swap`;
|
||||
document.documentElement.style.setProperty('--aurora-font-family', `'${family}', sans-serif`);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import Toggle from '@/components/Toggle.vue';
|
||||
import { themePresets, buildPalette } from '@/pages/settings/themePresets.js';
|
||||
import { googleFonts } from '@/pages/settings/googleFonts.js';
|
||||
|
||||
const swatchPalette = (preset) => buildPalette(preset);
|
||||
|
||||
const props = defineProps({
|
||||
field: { type: Object, required: true },
|
||||
@@ -66,6 +70,24 @@ const pickImage = () => {
|
||||
});
|
||||
frame.open();
|
||||
};
|
||||
|
||||
// Font picker (searchable Google Font dropdown) ----------------------------
|
||||
const fontQuery = ref('');
|
||||
const fontOpen = ref(false);
|
||||
const filteredFonts = computed(() => {
|
||||
const q = fontQuery.value.trim().toLowerCase();
|
||||
const list = q ? googleFonts.filter((f) => f.toLowerCase().includes(q)) : googleFonts;
|
||||
return list.slice(0, 40);
|
||||
});
|
||||
const pickFont = (name) => {
|
||||
emit('update:modelValue', name);
|
||||
fontQuery.value = name;
|
||||
fontOpen.value = false;
|
||||
};
|
||||
const openFontList = () => {
|
||||
fontQuery.value = props.modelValue || '';
|
||||
fontOpen.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -169,6 +191,59 @@ const pickImage = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme picker (10 fixed color presets, whole-UI preview) -->
|
||||
<div v-else-if="field.type === 'theme-picker'" class="field__theme">
|
||||
<button
|
||||
v-for="preset in themePresets"
|
||||
:key="preset.id"
|
||||
type="button"
|
||||
class="field__theme-card"
|
||||
:class="{ 'field__theme-card--active': (val || field.default) === preset.id }"
|
||||
@click="val = preset.id"
|
||||
>
|
||||
<span
|
||||
class="field__theme-swatch"
|
||||
:style="{
|
||||
background: swatchPalette(preset)['--aurora-app-bg'],
|
||||
borderColor: swatchPalette(preset)['--aurora-border'],
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="field__theme-swatch-accent"
|
||||
:style="{ background: swatchPalette(preset)['--aurora-accent'] }"
|
||||
/>
|
||||
</span>
|
||||
<span class="field__theme-name">{{ preset.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Font picker (searchable Google Font dropdown) -->
|
||||
<div v-else-if="field.type === 'font-picker'" class="field__font">
|
||||
<input
|
||||
type="text"
|
||||
class="field__input"
|
||||
placeholder="Search Google Fonts…"
|
||||
:value="fontOpen ? fontQuery : (val || 'Default')"
|
||||
@focus="openFontList"
|
||||
@input="fontQuery = $event.target.value"
|
||||
@blur="() => setTimeout(() => (fontOpen = false), 150)"
|
||||
/>
|
||||
<div v-if="fontOpen" class="field__font-list">
|
||||
<button
|
||||
v-for="name in filteredFonts"
|
||||
:key="name"
|
||||
type="button"
|
||||
class="field__font-item"
|
||||
:class="{ 'field__font-item--active': val === name }"
|
||||
:style="{ fontFamily: `'${name}', sans-serif` }"
|
||||
@mousedown.prevent="pickFont(name)"
|
||||
>
|
||||
{{ name }}
|
||||
</button>
|
||||
<p v-if="!filteredFonts.length" class="field__multi-empty">No fonts match.</p>
|
||||
</div>
|
||||
</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">
|
||||
@@ -309,6 +384,49 @@ const pickImage = () => {
|
||||
.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 {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
width: 76px; padding: 10px 6px;
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
cursor: pointer;
|
||||
}
|
||||
.field__theme-card--active { border-color: var(--aurora-accent); box-shadow: 0 0 0 1px var(--aurora-accent); }
|
||||
.field__theme-swatch {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
display: flex; align-items: flex-end; justify-content: flex-end;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.field__theme-swatch-accent {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
}
|
||||
.field__theme-name { font-size: 0.6875rem; color: var(--aurora-text-muted); }
|
||||
|
||||
/* Font picker */
|
||||
.field__font { position: relative; width: 100%; max-width: 320px; }
|
||||
.field__font-list {
|
||||
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
|
||||
max-height: 260px; overflow-y: auto; z-index: 20;
|
||||
background: var(--aurora-card-bg);
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.field__font-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
border: none; background: none; cursor: pointer;
|
||||
padding: 8px 10px; border-radius: 7px;
|
||||
font-size: 0.875rem; color: var(--aurora-text);
|
||||
}
|
||||
.field__font-item:hover { background: var(--aurora-bg-subtle); }
|
||||
.field__font-item--active { color: var(--aurora-accent); }
|
||||
|
||||
@media (max-width: 782px) {
|
||||
.field { grid-template-columns: 1fr; gap: 12px; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Curated list of popular Google Font family names for the font-picker
|
||||
* field. Not the full ~1700-family catalog — a representative set across
|
||||
* sans, serif, display, and monospace so the picker has something useful
|
||||
* to search without shipping/maintaining a giant metadata blob. Each
|
||||
* family is requested from Google's CSS2 API on demand (see
|
||||
* SiteShell.vue::applyFontFamily), never bundled as font files.
|
||||
*/
|
||||
export const googleFonts = [
|
||||
'Inter',
|
||||
'Roboto',
|
||||
'Open Sans',
|
||||
'Lato',
|
||||
'Montserrat',
|
||||
'Poppins',
|
||||
'Nunito',
|
||||
'Nunito Sans',
|
||||
'Source Sans 3',
|
||||
'Work Sans',
|
||||
'Rubik',
|
||||
'Manrope',
|
||||
'DM Sans',
|
||||
'Karla',
|
||||
'Mulish',
|
||||
'Figtree',
|
||||
'Plus Jakarta Sans',
|
||||
'Outfit',
|
||||
'Space Grotesk',
|
||||
'Sora',
|
||||
'Albert Sans',
|
||||
'Urbanist',
|
||||
'Public Sans',
|
||||
'IBM Plex Sans',
|
||||
'Barlow',
|
||||
'Cabin',
|
||||
'Hanken Grotesk',
|
||||
'Lexend',
|
||||
'Jost',
|
||||
'Red Hat Display',
|
||||
'Playfair Display',
|
||||
'Merriweather',
|
||||
'Lora',
|
||||
'PT Serif',
|
||||
'Crimson Text',
|
||||
'Source Serif 4',
|
||||
'Libre Baskerville',
|
||||
'Cormorant Garamond',
|
||||
'EB Garamond',
|
||||
'Bitter',
|
||||
'Domine',
|
||||
'Fraunces',
|
||||
'Newsreader',
|
||||
'Spectral',
|
||||
'Frank Ruhl Libre',
|
||||
'Josefin Sans',
|
||||
'Quicksand',
|
||||
'Comfortaa',
|
||||
'Raleway',
|
||||
'Oswald',
|
||||
'Bebas Neue',
|
||||
'Archivo',
|
||||
'Archivo Black',
|
||||
'Anton',
|
||||
'Righteous',
|
||||
'Fjalla One',
|
||||
'Abril Fatface',
|
||||
'Cormorant',
|
||||
'Zilla Slab',
|
||||
'IBM Plex Mono',
|
||||
'JetBrains Mono',
|
||||
'Space Mono',
|
||||
'Roboto Mono',
|
||||
'Fira Code',
|
||||
'Source Code Pro',
|
||||
'Courier Prime',
|
||||
'Instrument Sans',
|
||||
'Instrument Serif',
|
||||
'Bricolage Grotesque',
|
||||
];
|
||||
@@ -94,17 +94,17 @@ export const settingsCategories = [
|
||||
icon: 'dashicons-admin-appearance',
|
||||
fields: [
|
||||
{
|
||||
key: 'dark_mode',
|
||||
type: 'toggle',
|
||||
label: 'Dark mode',
|
||||
description: 'Use the dark interface theme across the admin.',
|
||||
key: 'theme_preset',
|
||||
type: 'theme-picker',
|
||||
label: 'Theme',
|
||||
description: 'Pick a color for the whole admin interface.',
|
||||
default: 'indigo',
|
||||
},
|
||||
{
|
||||
key: 'accent_color',
|
||||
type: 'color',
|
||||
label: 'Accent color',
|
||||
description: 'Primary accent color used throughout the interface.',
|
||||
default: '#4772fa',
|
||||
key: 'font_family',
|
||||
type: 'font-picker',
|
||||
label: 'Font family',
|
||||
description: 'Google Font used across the Aurora interface.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Fixed set of theme presets for the theme-picker field. No custom color
|
||||
* picker — exactly these 10 predetermined options. Each preset is a hue;
|
||||
* the main background gradient is mostly two vivid, saturated stops of
|
||||
* that hue (like FleekDash's "Fresh"/"Sunrise" palette bars), only
|
||||
* dipping to a near-black anchor in the last stretch — not a 50/50
|
||||
* dark-to-vivid split, which reads washed out rather than "poppy".
|
||||
* Cards/borders/text derive as solid, more muted shades of the primary
|
||||
* hue so the gradient stays a deliberate hero background rather than
|
||||
* making every surface busy.
|
||||
*/
|
||||
export const themePresets = [
|
||||
{ id: 'indigo', label: 'Indigo', hue: 235 },
|
||||
{ id: 'sky', label: 'Sky', hue: 205 },
|
||||
{ id: 'teal', label: 'Teal', hue: 175 },
|
||||
{ id: 'forest', label: 'Forest', hue: 140 },
|
||||
{ id: 'lime', label: 'Lime', hue: 95 },
|
||||
{ id: 'amber', label: 'Amber', hue: 45 },
|
||||
{ id: 'orange', label: 'Orange', hue: 22 },
|
||||
{ id: 'rose', label: 'Rose', hue: 345 },
|
||||
{ id: 'berry', label: 'Berry', hue: 320 },
|
||||
{ id: 'violet', label: 'Violet', hue: 265 },
|
||||
// Two flat neutrals, no gradient — for anyone who finds the color
|
||||
// themes above too much. Same values the shell originally shipped with
|
||||
// before theme presets existed (zinc dark / zinc light).
|
||||
{
|
||||
id: 'mono',
|
||||
label: 'Black & Grey',
|
||||
palette: {
|
||||
'--aurora-app-bg': '#09090b',
|
||||
'--aurora-app-bg-solid': '#09090b',
|
||||
'--aurora-bg': '#18181b',
|
||||
'--aurora-card-bg': '#1f1f23',
|
||||
'--aurora-card-bg-deep': '#131316',
|
||||
'--aurora-bg-subtle': '#27272a',
|
||||
'--aurora-text': '#f4f4f5',
|
||||
'--aurora-text-muted': '#a1a1aa',
|
||||
'--aurora-border': '#27272a',
|
||||
'--aurora-card-border': '#2e2e33',
|
||||
'--aurora-frame-bg': '#1c1c20',
|
||||
'--aurora-frame-border': '#34343b',
|
||||
'--aurora-accent': 'rgb(125 158 255)',
|
||||
'--aurora-accent-soft': 'rgb(125 158 255 / 0.14)',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'white',
|
||||
label: 'White',
|
||||
palette: {
|
||||
'--aurora-app-bg': '#fafafa',
|
||||
'--aurora-app-bg-solid': '#fafafa',
|
||||
'--aurora-bg': '#ffffff',
|
||||
'--aurora-card-bg': '#fafafa',
|
||||
'--aurora-card-bg-deep': '#f0f0f2',
|
||||
'--aurora-bg-subtle': '#f4f4f5',
|
||||
'--aurora-text': '#18181b',
|
||||
'--aurora-text-muted': '#71717a',
|
||||
'--aurora-border': '#e4e4e7',
|
||||
'--aurora-card-border': '#ececef',
|
||||
'--aurora-frame-bg': '#ffffff',
|
||||
'--aurora-frame-border': '#e0e0e3',
|
||||
'--aurora-accent': 'rgb(71 114 250)',
|
||||
'--aurora-accent-soft': 'rgb(71 114 250 / 0.1)',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const hsl = (h, s, l, a) =>
|
||||
a === undefined ? `hsl(${h} ${s}% ${l}%)` : `hsl(${h} ${s}% ${l}% / ${a})`;
|
||||
|
||||
/** The full set of --aurora-* color tokens for a preset. */
|
||||
export function buildPalette(preset) {
|
||||
if (preset.palette) return preset.palette;
|
||||
const { hue } = preset;
|
||||
// Two vivid, bright stops of the same hue family carry most of the bar
|
||||
// (0% → 55%); the near-black anchor only takes over in the final
|
||||
// stretch (100%). That's the "mostly color, dark tail" look the
|
||||
// FleekDash palette bars have — full saturation start to middle keeps
|
||||
// it feeling poppy instead of muddy.
|
||||
const hue2 = (hue + 35) % 360;
|
||||
const solidAppBg = hsl(hue, 45, 16);
|
||||
return {
|
||||
'--aurora-app-bg': `linear-gradient(135deg, ${hsl(hue, 78, 48)} 0%, ${hsl(hue2, 78, 45)} 55%, ${hsl(hue, 32, 9)} 100%)`,
|
||||
'--aurora-app-bg-solid': solidAppBg,
|
||||
'--aurora-bg': hsl(hue, 40, 20),
|
||||
'--aurora-card-bg': hsl(hue, 36, 24),
|
||||
'--aurora-card-bg-deep': hsl(hue, 38, 14),
|
||||
'--aurora-bg-subtle': hsl(hue, 32, 28),
|
||||
'--aurora-text': hsl(hue, 20, 97),
|
||||
'--aurora-text-muted': hsl(hue, 14, 74),
|
||||
'--aurora-border': hsl(hue, 28, 30),
|
||||
'--aurora-card-border': hsl(hue, 26, 33),
|
||||
'--aurora-frame-bg': hsl(hue, 34, 22),
|
||||
'--aurora-frame-border': hsl(hue, 26, 33),
|
||||
'--aurora-accent': hsl(hue, 85, 62),
|
||||
'--aurora-accent-soft': hsl(hue, 85, 62, 0.16),
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a theme_preset id to the preset object (falls back to the first preset). */
|
||||
export function resolvePreset(themePresetId) {
|
||||
return themePresets.find((p) => p.id === themePresetId) || themePresets[0];
|
||||
}
|
||||
Reference in New Issue
Block a user