Added a theme selector

This commit is contained in:
2026-08-26 12:54:38 +02:00
parent fec4c8def5
commit 72a4741cac
19 changed files with 305 additions and 9 deletions
+34 -1
View File
@@ -11,6 +11,9 @@ const adminAuthNotice = document.querySelector('#admin-auth-notice');
const smtpForm = document.querySelector('#smtp-form');
const smtpTestButton = document.querySelector('#smtp-test-button');
const smtpStatus = document.querySelector('#smtp-status');
const themesForm = document.querySelector('#themes-form');
const themeOptions = document.querySelector('#theme-options');
const themeStatus = document.querySelector('#theme-status');
let smtpNextAllowedAt = null;
let smtpTimerHandle = null;
const accessToken = localStorage.getItem('linklogAccessToken');
@@ -113,6 +116,22 @@ async function loadSmtpSettings() {
}
}
async function loadThemes() {
const response = await fetch('/api/admin/themes', {headers: authHeaders()});
if (!response.ok) throw new Error('Could not load themes');
const result = await response.json();
themeOptions.replaceChildren(...Object.entries(result.themes).map(([id, theme]) => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.name = 'theme';
checkbox.value = id;
checkbox.checked = result.enabled.includes(id);
label.append(checkbox, document.createTextNode(` ${theme.label}`));
return label;
}));
}
function renderUsers(users) {
userList.replaceChildren(...users.map((user) => {
const row = document.createElement('div');
@@ -185,7 +204,7 @@ async function loadAdminState() {
return;
}
await Promise.all([loadUsers(), loadPlugins(), loadLabels(), loadSmtpSettings()]);
await Promise.all([loadUsers(), loadPlugins(), loadLabels(), loadSmtpSettings(), loadThemes()]);
showAdminState(true);
}
@@ -312,6 +331,19 @@ smtpTestButton.addEventListener('click', async () => {
updateSmtpTimer();
});
themesForm.addEventListener('submit', async (event) => {
event.preventDefault();
const themes = [...themesForm.querySelectorAll('input[name="theme"]:checked')].map((input) => input.value);
const response = await fetch('/api/admin/themes', {
method: 'PUT',
headers: authHeaders(true),
body: JSON.stringify({themes}),
});
const result = await response.json();
themeStatus.textContent = response.ok ? 'Themes saved.' : (result.detail || 'Could not save themes.');
themeStatus.style.color = response.ok ? '#94e2d5' : '#f38ba8';
});
loadAdminState().catch((error) => {
showAdminState(false);
adminAuthNotice.textContent = accessToken
@@ -322,5 +354,6 @@ loadAdminState().catch((error) => {
pluginList.textContent = '';
adminLabelList.textContent = '';
smtpForm.reset();
themesForm.reset();
});
})();
+99 -3
View File
@@ -23,6 +23,78 @@
--shadow: 0 18px 50px rgba(17, 17, 27, 0.28);
}
:root[data-theme='plain-day'] {
--base: #f4f1ea; --mantle: #e8e3d8; --crust: #d7d0c2;
--surface-0: #fffdf8; --surface-1: #e5ded1; --surface-2: #c8beae;
--text: #2f2b27; --subtext: #514a42; --muted: #6b6258;
--mauve: #7a3d6e; --lavender: #5d4b84; --blue: #315c78;
--teal: #397c72; --peach: #a15d3d; --red: #a44250;
--border: rgba(47, 43, 39, 0.16); --shadow: 0 18px 50px rgba(47, 43, 39, 0.16);
}
:root[data-theme='plain-night'] {
--base: #181818; --mantle: #111111; --crust: #090909;
--surface-0: #262626; --surface-1: #353535; --surface-2: #4b4b4b;
--text: #eeeeee; --subtext: #c1c1c1; --muted: #929292;
--mauve: #d59acb; --lavender: #b9b4e8; --blue: #8ebbd8;
--teal: #8bc9bd; --peach: #e2ae88; --red: #ec929f;
--border: rgba(238, 238, 238, 0.14); --shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
:root[data-theme='latte'] {
--base: #eff1f5; --mantle: #e6e9ef; --crust: #dce0e8;
--surface-0: #ccd0da; --surface-1: #bcc0cc; --surface-2: #acb0be;
--text: #3d4058; --subtext: #51546b; --muted: #65687c;
--mauve: #7627c7; --lavender: #5946b2; --blue: #1854c7;
--teal: #179299; --peach: #fe640b; --red: #d20f39;
--border: rgba(76, 79, 105, 0.16); --shadow: 0 18px 50px rgba(76, 79, 105, 0.16);
}
:root[data-theme='frappe'] {
--base: #303446; --mantle: #292c3c; --crust: #232634;
--surface-0: #414559; --surface-1: #51576d; --surface-2: #626880;
--text: #c6d0f5; --subtext: #b5bfe2; --muted: #838ba7;
--mauve: #ca9ee6; --lavender: #babbf1; --blue: #8caaee;
--teal: #81c8be; --peach: #ef9f76; --red: #e78284;
--border: rgba(198, 208, 245, 0.12); --shadow: 0 18px 50px rgba(35, 38, 52, 0.3);
}
:root[data-theme='macchiato'] {
--base: #24273a; --mantle: #1e2030; --crust: #181926;
--surface-0: #363a4f; --surface-1: #494d64; --surface-2: #5b6078;
--text: #cad3f5; --subtext: #b8c0e0; --muted: #8087a2;
--mauve: #c6a0f6; --lavender: #b7bdf8; --blue: #8aadf4;
--teal: #8bd5ca; --peach: #f5a97f; --red: #ed8796;
--border: rgba(202, 211, 245, 0.12); --shadow: 0 18px 50px rgba(24, 25, 38, 0.34);
}
:root[data-theme='dracula'] {
--base: #282a36; --mantle: #21222c; --crust: #191a21;
--surface-0: #44475a; --surface-1: #6272a4; --surface-2: #7886b5;
--text: #f8f8f2; --subtext: #d6d6d0; --muted: #a7a7a0;
--mauve: #ff79c6; --lavender: #bd93f9; --blue: #8be9fd;
--teal: #50fa7b; --peach: #ffb86c; --red: #ff5555;
--border: rgba(248, 248, 242, 0.14); --shadow: 0 18px 50px rgba(25, 26, 33, 0.35);
}
:root[data-theme='nord'] {
--base: #2e3440; --mantle: #272c36; --crust: #242933;
--surface-0: #3b4252; --surface-1: #434c5e; --surface-2: #4c566a;
--text: #eceff4; --subtext: #d8dee9; --muted: #aeb8c8;
--mauve: #b48ead; --lavender: #d8dee9; --blue: #88c0d0;
--teal: #a3be8c; --peach: #d08770; --red: #bf616a;
--border: rgba(236, 239, 244, 0.14); --shadow: 0 18px 50px rgba(36, 41, 51, 0.35);
}
:root[data-theme='solarized'] {
--base: #fdf6e3; --mantle: #eee8d5; --crust: #e3ddc9;
--surface-0: #eee8d5; --surface-1: #ddd6c1; --surface-2: #c9c1aa;
--text: #073642; --subtext: #586e75; --muted: #657b83;
--mauve: #6c71c4; --lavender: #268bd2; --blue: #268bd2;
--teal: #2aa198; --peach: #cb4b16; --red: #dc322f;
--border: rgba(7, 54, 66, 0.18); --shadow: 0 18px 50px rgba(7, 54, 66, 0.14);
}
*,
*::before,
*::after {
@@ -91,7 +163,7 @@ body::selection {
.site-logo {
display: block;
width: min(260px, 70vw);
height: 150px;
height: 80px;
margin-bottom: 12px;
object-fit: contain;
object-position: left center;
@@ -124,6 +196,17 @@ body::selection {
position: relative;
}
.theme-picker {
width: auto;
min-width: 132px;
padding: 8px 10px;
border: 1px solid var(--surface-2);
border-radius: 7px;
background: var(--surface-0);
color: var(--text);
font: inherit;
}
.menu-toggle {
min-width: 0;
padding: 10px 13px;
@@ -237,7 +320,7 @@ main.container {
gap: 14px;
margin: 0 0 24px;
padding: 16px;
background: rgba(24, 24, 37, 0.72);
background: var(--surface-0);
border: 1px solid var(--border);
border-radius: 12px;
}
@@ -302,6 +385,19 @@ button:focus-visible {
max-width: 560px;
}
.theme-options {
display: grid;
gap: 8px;
margin: 14px 0;
}
.theme-options label {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.settings-panel textarea {
min-height: 110px;
resize: vertical;
@@ -522,7 +618,7 @@ button:disabled {
.link-item {
padding: 11px;
background: rgba(49, 50, 68, 0.84);
background: var(--surface-0);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow);
+28
View File
@@ -0,0 +1,28 @@
// Copyright © 2026 Olaf Kolkman
// SPDX-License-Identifier: GPL-3.0-or-later
const themePreference = 'linklog-theme';
async function loadAvailableThemes() {
const response = await fetch('/api/public/themes');
if (!response.ok) return;
const themes = await response.json();
const selected = themes.some((theme) => theme.id === localStorage.getItem(themePreference))
? localStorage.getItem(themePreference)
: themes[0]?.id;
if (selected) document.documentElement.dataset.theme = selected;
const headerActions = document.querySelector('.header-actions');
if (!headerActions || !themes.length) return;
const picker = document.createElement('select');
picker.className = 'theme-picker';
picker.setAttribute('aria-label', 'Theme');
picker.replaceChildren(...themes.map((theme) => new Option(theme.label, theme.id)));
picker.value = selected || themes[0].id;
picker.addEventListener('change', () => {
localStorage.setItem(themePreference, picker.value);
document.documentElement.dataset.theme = picker.value;
});
headerActions.prepend(picker);
}
loadAvailableThemes();