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
@@ -76,15 +76,15 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
margin: -32px;
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
background: var(--aurora-app-bg);
|
||||
/* No background — html/body already paint the page gradient; this was
|
||||
a redundant duplicate layer on top of it. */
|
||||
}
|
||||
|
||||
/* Left — list box */
|
||||
/* Left — no background/border of its own; the page gradient shows
|
||||
straight through so it reads as one surface instead of a boxed panel. */
|
||||
.app-list-shell__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;
|
||||
@@ -127,7 +127,9 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
background: var(--aurora-bg-subtle); color: var(--aurora-text-muted);
|
||||
}
|
||||
.app-list-shell__filters :deep(button.is-active) {
|
||||
background: var(--aurora-text); color: var(--aurora-app-bg);
|
||||
/* --aurora-app-bg can be a gradient (invalid for `color`), so this one
|
||||
inverted-contrast badge uses the flat fallback token instead. */
|
||||
background: var(--aurora-text); color: var(--aurora-app-bg-solid);
|
||||
}
|
||||
|
||||
.app-list-shell__meta {
|
||||
@@ -145,11 +147,9 @@ const search = defineModel('search', { type: String, default: '' });
|
||||
|
||||
.app-list-shell__list { display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
/* Right — content box */
|
||||
/* Right — no background/border of its own, same reasoning. */
|
||||
.app-list-shell__content {
|
||||
flex: 1; min-width: 0;
|
||||
background: var(--aurora-frame-bg);
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 16px;
|
||||
padding: 28px 32px;
|
||||
min-height: 480px;
|
||||
|
||||
@@ -30,7 +30,11 @@ defineProps({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--aurora-card-bg);
|
||||
/* Deeper than the generic --aurora-card-bg — the Dashboard is now
|
||||
wrapped in its own frame (windowChrome.js's .aurora-dashboard-frame,
|
||||
using --aurora-frame-bg), and that's close in tone to --aurora-card-bg
|
||||
for most presets, so cards would nearly disappear into it otherwise. */
|
||||
background: var(--aurora-card-bg-deep, var(--aurora-card-bg));
|
||||
border: 1px solid var(--aurora-card-border);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
serverData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const plugins = ref([]);
|
||||
const stats = ref({ total: 0, active: 0, inactive: 0, updates: 0 });
|
||||
const addUrl = ref('');
|
||||
const loading = ref(true);
|
||||
const busyFile = ref('');
|
||||
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 data = await request('/plugins');
|
||||
plugins.value = data.items || [];
|
||||
stats.value = data.stats || stats.value;
|
||||
addUrl.value = data.addUrl || '';
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Could not load plugins';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(load);
|
||||
|
||||
const filtered = computed(() => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
return plugins.value.filter((plugin) => {
|
||||
const matchesStatus =
|
||||
status.value === 'all' ||
|
||||
(status.value === 'active' && plugin.active) ||
|
||||
(status.value === 'inactive' && !plugin.active) ||
|
||||
(status.value === 'updates' && plugin.updateAvailable);
|
||||
if (!matchesStatus) return false;
|
||||
if (!query) return true;
|
||||
return [plugin.name, plugin.description, plugin.author, plugin.file]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
});
|
||||
});
|
||||
|
||||
const filters = computed(() => [
|
||||
{ key: 'all', label: 'All', count: stats.value.total },
|
||||
{ key: 'active', label: 'Active', count: stats.value.active },
|
||||
{ key: 'inactive', label: 'Inactive', count: stats.value.inactive },
|
||||
{ key: 'updates', label: 'Update Available', count: stats.value.updates },
|
||||
]);
|
||||
|
||||
const runAction = async (action, plugin) => {
|
||||
if (!plugin || busyFile.value) return;
|
||||
busyFile.value = plugin.file;
|
||||
error.value = '';
|
||||
notice.value = '';
|
||||
try {
|
||||
const data = await request(`/plugins/${action}`, {
|
||||
method: action === 'delete' ? 'DELETE' : 'POST',
|
||||
body: JSON.stringify({ file: plugin.file }),
|
||||
});
|
||||
notice.value = data.message || 'Done';
|
||||
if (data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Action failed';
|
||||
} finally {
|
||||
busyFile.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const openAdd = () => {
|
||||
if (addUrl.value) {
|
||||
window.location.href = addUrl.value;
|
||||
}
|
||||
};
|
||||
|
||||
const pluginInitial = (name) => (name || 'P').trim().charAt(0).toUpperCase();
|
||||
const statusLabel = (plugin) => {
|
||||
if (plugin.networkActive) return 'Network active';
|
||||
return plugin.active ? 'Active' : 'Inactive';
|
||||
};
|
||||
const versionLine = (plugin) => {
|
||||
if (plugin.updateAvailable && plugin.newVersion) {
|
||||
return `${plugin.version || 'Unknown'} -> ${plugin.newVersion}`;
|
||||
}
|
||||
return plugin.version || 'Unknown';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="plugins-page">
|
||||
<header class="plugins-page__header">
|
||||
<div>
|
||||
<p class="plugins-page__eyebrow">Aurora Admin</p>
|
||||
<h1>Plugins</h1>
|
||||
</div>
|
||||
<div class="plugins-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 plugins" :disabled="loading" @click="load">
|
||||
<span class="dashicons dashicons-update" />
|
||||
</button>
|
||||
<button type="button" class="primary-button" @click="openAdd">
|
||||
<span class="dashicons dashicons-plus-alt2" />
|
||||
Add New
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="plugins-page__toolbar" aria-label="Plugin filters and search">
|
||||
<div class="plugins-page__filters">
|
||||
<button
|
||||
v-for="filter in filters"
|
||||
:key="filter.key"
|
||||
type="button"
|
||||
class="filter-pill"
|
||||
:class="{ 'is-active': status === filter.key }"
|
||||
@click="status = filter.key"
|
||||
>
|
||||
<span>{{ filter.label }}</span>
|
||||
<strong>{{ filter.count }}</strong>
|
||||
</button>
|
||||
</div>
|
||||
<label class="plugins-page__search">
|
||||
<span class="dashicons dashicons-search" />
|
||||
<input v-model="search" type="search" placeholder="Search plugins..." />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div v-if="notice" class="plugins-notice plugins-notice--success">{{ notice }}</div>
|
||||
<div v-if="error" class="plugins-notice plugins-notice--error">{{ error }}</div>
|
||||
|
||||
<section class="plugins-table" :class="{ 'is-compact': compact }" aria-label="Installed plugins">
|
||||
<div class="plugins-table__head">
|
||||
<div>Plugin Info</div>
|
||||
<div>Version <span class="dashicons dashicons-filter" /></div>
|
||||
<div>Status <span class="dashicons dashicons-filter" /></div>
|
||||
<div>Author <span class="dashicons dashicons-filter" /></div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="plugins-table__empty">Loading plugins...</div>
|
||||
<div v-else-if="!filtered.length" class="plugins-table__empty">No plugins match your filters.</div>
|
||||
|
||||
<article
|
||||
v-for="plugin in filtered"
|
||||
v-else
|
||||
:key="plugin.file"
|
||||
class="plugin-row"
|
||||
:class="{ 'is-active': plugin.active }"
|
||||
>
|
||||
<div class="plugin-row__info">
|
||||
<div class="plugin-row__mark">
|
||||
<span>{{ pluginInitial(plugin.name) }}</span>
|
||||
</div>
|
||||
<div class="plugin-row__copy">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<p>{{ plugin.description || 'No description provided.' }}</p>
|
||||
<div class="plugin-row__links">
|
||||
<a v-if="plugin.pluginUri" :href="plugin.pluginUri" target="_blank" rel="noopener noreferrer">
|
||||
Plugin site
|
||||
</a>
|
||||
<a v-if="plugin.authorUri" :href="plugin.authorUri" target="_blank" rel="noopener noreferrer">
|
||||
Author site
|
||||
</a>
|
||||
<span>{{ plugin.file }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__cell" data-label="Version">
|
||||
<strong>{{ versionLine(plugin) }}</strong>
|
||||
<span v-if="plugin.updateAvailable">Update ready</span>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__cell" data-label="Status">
|
||||
<span class="status-badge" :class="{ 'status-badge--active': plugin.active }">
|
||||
<i />
|
||||
{{ statusLabel(plugin) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__cell" data-label="Author">
|
||||
<strong>{{ plugin.author || 'Unknown' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="plugin-row__actions">
|
||||
<button
|
||||
v-if="!plugin.active"
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
title="Activate plugin"
|
||||
:disabled="busyFile === plugin.file || !plugin.canActivate"
|
||||
@click="runAction('activate', plugin)"
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="toggle-button is-on"
|
||||
title="Deactivate plugin"
|
||||
:disabled="busyFile === plugin.file"
|
||||
@click="runAction('deactivate', plugin)"
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
<button
|
||||
v-if="plugin.updateAvailable"
|
||||
type="button"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Update plugin"
|
||||
:disabled="busyFile === plugin.file || !plugin.canUpdate"
|
||||
@click="runAction('update', plugin)"
|
||||
>
|
||||
<span class="dashicons dashicons-update-alt" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-button icon-button--inline icon-button--danger"
|
||||
title="Delete plugin"
|
||||
:disabled="busyFile === plugin.file || !plugin.canDelete"
|
||||
@click="runAction('delete', plugin)"
|
||||
>
|
||||
<span class="dashicons dashicons-trash" />
|
||||
</button>
|
||||
<a
|
||||
v-if="plugin.pluginUri"
|
||||
class="icon-button icon-button--inline"
|
||||
title="Open plugin site"
|
||||
:href="plugin.pluginUri"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="dashicons dashicons-ellipsis" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plugins-page {
|
||||
min-height: calc(100vh - 112px);
|
||||
padding: 0;
|
||||
color: var(--aurora-text);
|
||||
background: transparent;
|
||||
}
|
||||
.plugins-page__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.plugins-page__eyebrow {
|
||||
margin: 0 0 7px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.plugins-page h1 {
|
||||
margin: 0;
|
||||
color: var(--aurora-text);
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.plugins-page__actions,
|
||||
.plugins-page__toolbar,
|
||||
.plugins-page__filters,
|
||||
.plugin-row__actions,
|
||||
.plugin-row__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.plugins-page__actions {
|
||||
gap: 10px;
|
||||
}
|
||||
.plugins-page__toolbar {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.plugins-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);
|
||||
}
|
||||
.filter-pill.is-active strong {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.plugins-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);
|
||||
}
|
||||
.plugins-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;
|
||||
}
|
||||
.plugins-page__search input::placeholder {
|
||||
color: var(--aurora-text-muted);
|
||||
}
|
||||
.primary-button,
|
||||
.icon-button {
|
||||
border: 1px solid var(--aurora-border);
|
||||
border-radius: 8px;
|
||||
min-height: 42px;
|
||||
color: var(--aurora-text);
|
||||
background: var(--aurora-bg-subtle);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.primary-button {
|
||||
gap: 8px;
|
||||
border-color: var(--aurora-text-muted);
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
padding: 0 16px;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.icon-button {
|
||||
width: 42px;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.icon-button.is-active {
|
||||
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:hover:not(:disabled),
|
||||
.icon-button--inline:hover:not(:disabled) {
|
||||
color: var(--aurora-text);
|
||||
border-color: var(--aurora-text-muted);
|
||||
}
|
||||
.icon-button--danger:hover:not(:disabled) {
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.primary-button:disabled,
|
||||
.icon-button:disabled,
|
||||
.toggle-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.plugins-notice {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.plugins-notice--success {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.plugins-notice--error {
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
}
|
||||
.plugins-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--aurora-frame-border);
|
||||
border-radius: 8px;
|
||||
background: var(--aurora-frame-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
.plugins-table__head,
|
||||
.plugin-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1fr) 140px 140px 170px 150px;
|
||||
align-items: center;
|
||||
}
|
||||
.plugins-table__head {
|
||||
min-height: 48px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.79rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.plugins-table__head > * {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.plugins-table__head > div {
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.plugin-row {
|
||||
min-height: 78px;
|
||||
border-bottom: 1px solid var(--aurora-border);
|
||||
transition: background 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
.plugin-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.plugin-row:hover {
|
||||
background: var(--aurora-bg-subtle);
|
||||
}
|
||||
.plugin-row.is-active {
|
||||
background: var(--aurora-bg-subtle);
|
||||
}
|
||||
.plugin-row__info {
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
.plugin-row__mark {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--aurora-bg-subtle);
|
||||
color: var(--aurora-text);
|
||||
font-size: 1rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
.plugin-row__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
.plugin-row__copy h2 {
|
||||
margin: 0 0 5px;
|
||||
color: var(--aurora-text);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.plugin-row__copy p {
|
||||
margin: 0;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.plugin-row__links {
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.plugin-row__links a {
|
||||
color: var(--aurora-text);
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
.plugin-row__links span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.plugin-row__cell {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.plugin-row__cell strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 850;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.plugin-row__cell span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.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.75rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
.status-badge i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.status-badge--active {
|
||||
background: var(--aurora-frame-bg);
|
||||
color: var(--aurora-text);
|
||||
box-shadow: inset 0 0 0 1px var(--aurora-border);
|
||||
}
|
||||
.plugin-row__actions {
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
padding: 16px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row {
|
||||
min-height: 62px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row__info,
|
||||
.plugins-table.is-compact .plugin-row__cell,
|
||||
.plugins-table.is-compact .plugin-row__actions {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row__mark {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
.plugins-table.is-compact .plugin-row__links {
|
||||
display: none;
|
||||
}
|
||||
.toggle-button {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--aurora-bg-subtle);
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-button span {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--aurora-text-muted);
|
||||
transition: transform 160ms ease, background 160ms ease;
|
||||
}
|
||||
.toggle-button.is-on {
|
||||
background: var(--aurora-text-muted);
|
||||
}
|
||||
.toggle-button.is-on span {
|
||||
transform: translateX(16px);
|
||||
background: var(--aurora-frame-bg);
|
||||
}
|
||||
.plugins-table__empty {
|
||||
padding: 38px 18px;
|
||||
color: var(--aurora-text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.plugins-page {
|
||||
padding: 24px 18px 34px;
|
||||
}
|
||||
.plugins-table__head {
|
||||
display: none;
|
||||
}
|
||||
.plugin-row {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.plugin-row__cell,
|
||||
.plugin-row__actions {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.plugin-row__cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.plugin-row__cell::before {
|
||||
content: attr(data-label);
|
||||
color: var(--aurora-text-muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.plugins-page__header,
|
||||
.plugins-page__toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.plugins-page__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.plugins-page__search {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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