feat: add 5 theme pairs (ocean, forest, sunset, rose, nord) with light/dark/auto modes\n\n- Override Tabler dark-mode surface/border CSS variables per theme to remove blue tint\n- Add theme accent colors for badges, buttons, links, forms\n- Make Ocean the default theme, auto-migrate legacy values (auto/light/dark)\n- Update settings dropdown with grouped theme options\n- Update user-guide docs with new theme descriptions"

This commit is contained in:
2026-04-07 22:14:56 +02:00
parent c4171e5b87
commit a63f3fb5ff
4 changed files with 342 additions and 29 deletions

View File

@@ -88,11 +88,23 @@ Navigate to **Settings** to manage your account:
### Theme ### Theme
Choose between: KeyWarden offers five color themes, each available in three modes:
- **Auto** — Follows your system/browser preference
| Theme | Description |
|-------|-------------|
| **Ocean** (default) | Cyan/teal accent |
| **Forest** | Green accent |
| **Sunset** | Amber/orange accent |
| **Rose** | Pink accent |
| **Nord** | Cool blue-gray accent |
Each theme supports:
- **System** — Follows your system/browser preference (light or dark)
- **Light** — Always light mode - **Light** — Always light mode
- **Dark** — Always dark mode - **Dark** — Always dark mode
> Existing installations using the previous theme values (`auto`, `light`, `dark`) are automatically migrated to the Ocean theme.
### Password Change ### Password Change
Change your password. The new password must comply with the configured password policy (displayed on the form). Change your password. The new password must comply with the configured password policy (displayed on the form).

View File

@@ -401,10 +401,26 @@ func (s *Service) DisableMFA(userID int64) error {
return err return err
} }
// UpdateTheme updates the user's theme preference (auto, light, dark) // UpdateTheme updates the user's theme preference
func (s *Service) UpdateTheme(id int64, theme string) error { func (s *Service) UpdateTheme(id int64, theme string) error {
if theme != "auto" && theme != "light" && theme != "dark" { // Map legacy default values to ocean
theme = "auto" switch theme {
case "auto", "":
theme = "ocean-auto"
case "light":
theme = "ocean-light"
case "dark":
theme = "ocean-dark"
}
validThemes := map[string]bool{
"ocean-auto": true, "ocean-light": true, "ocean-dark": true,
"forest-auto": true, "forest-light": true, "forest-dark": true,
"sunset-auto": true, "sunset-light": true, "sunset-dark": true,
"rose-auto": true, "rose-light": true, "rose-dark": true,
"nord-auto": true, "nord-light": true, "nord-dark": true,
}
if !validThemes[theme] {
theme = "ocean-auto"
} }
_, err := s.db.Exec( _, err := s.db.Exec(
`UPDATE users SET theme = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, `UPDATE users SET theme = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,

View File

@@ -12,21 +12,31 @@
<!-- Resolve theme BEFORE loading CSS to prevent FOUC (flash of unstyled content) --> <!-- Resolve theme BEFORE loading CSS to prevent FOUC (flash of unstyled content) -->
<script> <script>
(function() { (function() {
var theme = '{{with .User}}{{.Theme}}{{end}}' || 'auto'; var theme = '{{with .User}}{{.Theme}}{{end}}' || 'ocean-auto';
var resolved = theme; // Map legacy default values to ocean
if (theme === 'auto') { if (theme === 'auto' || theme === 'light' || theme === 'dark') {
resolved = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; theme = 'ocean-' + theme;
} }
document.documentElement.setAttribute('data-bs-theme', resolved); window.__kwThemeRaw = theme;
document.documentElement.style.colorScheme = resolved; var pair = 'default', mode = theme;
if (theme !== 'auto' && theme !== 'light' && theme !== 'dark') {
var idx = theme.lastIndexOf('-');
if (idx > 0) { pair = theme.substring(0, idx); mode = theme.substring(idx + 1); }
}
if (mode !== 'light' && mode !== 'dark') {
mode = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-bs-theme', mode);
document.documentElement.style.colorScheme = mode;
if (pair !== 'default') document.documentElement.setAttribute('data-theme-pair', pair);
})(); })();
</script> </script>
<style> <style>
/* Critical inline styles: prevent white flash between page navigations */ /* Critical inline styles: prevent white flash between page navigations */
html[data-bs-theme="dark"], html[data-bs-theme="dark"],
html[data-bs-theme="dark"] body { background-color: #0F1829; color-scheme: dark; } html[data-bs-theme="dark"] body { background-color: #0c1a2a; color-scheme: dark; }
html[data-bs-theme="light"], html[data-bs-theme="light"],
html[data-bs-theme="light"] body { background-color: #f1f5f9; color-scheme: light; } html[data-bs-theme="light"] body { background-color: #ecfeff; color-scheme: light; }
.navbar-brand-image { height: 2rem; } .navbar-brand-image { height: 2rem; }
.keywarden-brand { font-weight: 700; font-size: 1.25rem; color: #206bc4; } .keywarden-brand { font-weight: 700; font-size: 1.25rem; color: #206bc4; }
[data-bs-theme="dark"] .keywarden-brand { color: #4da3ff; } [data-bs-theme="dark"] .keywarden-brand { color: #4da3ff; }
@@ -118,6 +128,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
[data-bs-theme="dark"] .navbar-vertical { background: #0a1120 !important; }
[data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #0a1120 !important; border-bottom-color: rgba(255,255,255,0.04); }
.navbar-vertical > .container-fluid { .navbar-vertical > .container-fluid {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -322,6 +334,232 @@
object-fit: cover; object-fit: cover;
border-radius: 50%; border-radius: 50%;
} }
/* ═══════════════════════════════════════════════════════════ */
/* ADDITIONAL THEME PAIRS */
/* ═══════════════════════════════════════════════════════════ */
/* Shared themed overrides (active when any theme pair is set) */
html[data-theme-pair] .btn-primary {
--tblr-btn-bg: var(--kw-primary);
--tblr-btn-border-color: var(--kw-primary);
--tblr-btn-hover-bg: var(--kw-primary-hover);
--tblr-btn-hover-border-color: var(--kw-primary-hover);
--tblr-btn-active-bg: var(--kw-primary-active);
--tblr-btn-active-border-color: var(--kw-primary-active);
}
html[data-theme-pair] .btn-outline-primary {
--tblr-btn-color: var(--kw-primary);
--tblr-btn-border-color: var(--kw-primary);
--tblr-btn-hover-bg: var(--kw-primary);
--tblr-btn-hover-border-color: var(--kw-primary);
--tblr-btn-active-bg: var(--kw-primary-hover);
--tblr-btn-active-border-color: var(--kw-primary-hover);
}
html[data-theme-pair] {
--tblr-link-color: var(--kw-primary);
--tblr-link-hover-color: var(--kw-primary-hover);
}
html[data-theme-pair] .bg-primary-lt {
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
}
html[data-theme-pair] .alert-primary {
--tblr-alert-color: var(--kw-primary);
--tblr-alert-bg: rgba(var(--tblr-primary-rgb), 0.07);
--tblr-alert-border-color: rgba(var(--tblr-primary-rgb), 0.15);
}
html[data-theme-pair] .form-check-input:checked {
background-color: var(--kw-primary);
border-color: var(--kw-primary);
}
html[data-theme-pair] .form-select:focus,
html[data-theme-pair] .form-control:focus {
border-color: var(--kw-primary);
box-shadow: 0 0 0 0.25rem rgba(var(--tblr-primary-rgb), 0.25);
}
html[data-theme-pair] .nav-tabs .nav-link.active {
border-bottom-color: var(--kw-primary);
}
/* Override Tabler hardcoded blue badges to use theme accent */
html[data-theme-pair] .bg-blue-lt {
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
color: var(--kw-primary) !important;
}
html[data-theme-pair] .bg-azure-lt {
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
color: var(--kw-primary) !important;
}
html[data-theme-pair] .bg-cyan-lt {
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
color: var(--kw-primary) !important;
}
html[data-theme-pair] .text-primary {
color: var(--kw-primary) !important;
}
html[data-theme-pair] .text-blue {
color: var(--kw-primary) !important;
}
html[data-theme-pair] .text-azure {
color: var(--kw-primary) !important;
}
html[data-theme-pair] .text-cyan {
color: var(--kw-primary) !important;
}
/* ── Ocean Theme ── */
html[data-theme-pair="ocean"] {
--kw-primary: #0891b2; --kw-primary-hover: #0e7490; --kw-primary-active: #155e75;
--tblr-primary: #0891b2; --tblr-primary-rgb: 8,145,178;
}
html[data-theme-pair="ocean"][data-bs-theme="dark"] {
--kw-primary: #06b6d4; --kw-primary-hover: #0891b2; --kw-primary-active: #0e7490;
--tblr-primary: #06b6d4; --tblr-primary-rgb: 6,182,212;
--tblr-bg-surface: #0f2035; --tblr-bg-surface-secondary: #122840;
--tblr-bg-surface-tertiary: #0c1e30; --tblr-bg-surface-dark: #0c1a2a;
--tblr-bg-forms: #0c1e30; --tblr-body-bg: #0c1a2a; --tblr-body-bg-rgb: 12,26,42;
--tblr-border-color: #1a3555; --tblr-border-color-translucent: rgba(6, 182, 212, 0.12);
}
html[data-theme-pair="ocean"][data-bs-theme="light"],
html[data-theme-pair="ocean"][data-bs-theme="light"] body { background-color: #ecfeff !important; }
html[data-theme-pair="ocean"][data-bs-theme="dark"],
html[data-theme-pair="ocean"][data-bs-theme="dark"] body { background-color: #0c1a2a !important; }
html[data-theme-pair="ocean"][data-bs-theme="light"] .navbar-vertical,
html[data-theme-pair="ocean"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: #155e75 !important; }
html[data-theme-pair="ocean"][data-bs-theme="dark"] .navbar-vertical,
html[data-theme-pair="ocean"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #071220 !important; }
html[data-theme-pair="ocean"][data-bs-theme="light"] .page-wrapper { background: #ecfeff; }
html[data-theme-pair="ocean"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="ocean"][data-bs-theme="dark"] .page-body { background: #0c1a2a; }
html[data-theme-pair="ocean"] .keywarden-header-brand .keywarden-brand i.ti { color: #22d3ee; }
html[data-theme-pair="ocean"] .nav-category { color: rgba(160, 220, 230, 0.6); }
html[data-theme-pair="ocean"][data-bs-theme="light"] ::selection { background: #a5f3fc; color: #155e75; }
html[data-theme-pair="ocean"][data-bs-theme="light"] ::-moz-selection { background: #a5f3fc; color: #155e75; }
html[data-theme-pair="ocean"][data-bs-theme="dark"] ::selection { background: #164e63; color: #ecfeff; }
html[data-theme-pair="ocean"][data-bs-theme="dark"] ::-moz-selection { background: #164e63; color: #ecfeff; }
/* ── Forest Theme ── */
html[data-theme-pair="forest"] {
--kw-primary: #16a34a; --kw-primary-hover: #15803d; --kw-primary-active: #166534;
--tblr-primary: #16a34a; --tblr-primary-rgb: 22,163,74;
}
html[data-theme-pair="forest"][data-bs-theme="dark"] {
--kw-primary: #4ade80; --kw-primary-hover: #22c55e; --kw-primary-active: #16a34a;
--tblr-primary: #4ade80; --tblr-primary-rgb: 74,222,128;
--tblr-bg-surface: #0f2216; --tblr-bg-surface-secondary: #122a1b;
--tblr-bg-surface-tertiary: #0c1d12; --tblr-bg-surface-dark: #0a1a10;
--tblr-bg-forms: #0c1d12; --tblr-body-bg: #0a1a10; --tblr-body-bg-rgb: 10,26,16;
--tblr-border-color: #1a3524; --tblr-border-color-translucent: rgba(74, 222, 128, 0.10);
}
html[data-theme-pair="forest"][data-bs-theme="light"],
html[data-theme-pair="forest"][data-bs-theme="light"] body { background-color: #f0fdf4 !important; }
html[data-theme-pair="forest"][data-bs-theme="dark"],
html[data-theme-pair="forest"][data-bs-theme="dark"] body { background-color: #0a1a10 !important; }
html[data-theme-pair="forest"][data-bs-theme="light"] .navbar-vertical,
html[data-theme-pair="forest"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: #14532d !important; }
html[data-theme-pair="forest"][data-bs-theme="dark"] .navbar-vertical,
html[data-theme-pair="forest"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #061209 !important; }
html[data-theme-pair="forest"][data-bs-theme="light"] .page-wrapper { background: #f0fdf4; }
html[data-theme-pair="forest"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="forest"][data-bs-theme="dark"] .page-body { background: #0a1a10; }
html[data-theme-pair="forest"] .keywarden-header-brand .keywarden-brand i.ti { color: #4ade80; }
html[data-theme-pair="forest"] .nav-category { color: rgba(160, 210, 170, 0.6); }
html[data-theme-pair="forest"][data-bs-theme="light"] ::selection { background: #bbf7d0; color: #14532d; }
html[data-theme-pair="forest"][data-bs-theme="light"] ::-moz-selection { background: #bbf7d0; color: #14532d; }
html[data-theme-pair="forest"][data-bs-theme="dark"] ::selection { background: #166534; color: #f0fdf4; }
html[data-theme-pair="forest"][data-bs-theme="dark"] ::-moz-selection { background: #166534; color: #f0fdf4; }
/* ── Sunset Theme ── */
html[data-theme-pair="sunset"] {
--kw-primary: #d97706; --kw-primary-hover: #b45309; --kw-primary-active: #92400e;
--tblr-primary: #d97706; --tblr-primary-rgb: 217,119,6;
}
html[data-theme-pair="sunset"][data-bs-theme="dark"] {
--kw-primary: #f59e0b; --kw-primary-hover: #d97706; --kw-primary-active: #b45309;
--tblr-primary: #f59e0b; --tblr-primary-rgb: 245,158,11;
--tblr-bg-surface: #221a0e; --tblr-bg-surface-secondary: #281f12;
--tblr-bg-surface-tertiary: #1e170a; --tblr-bg-surface-dark: #1a1408;
--tblr-bg-forms: #1e170a; --tblr-body-bg: #1a1408; --tblr-body-bg-rgb: 26,20,8;
--tblr-border-color: #3a2c14; --tblr-border-color-translucent: rgba(245, 158, 11, 0.10);
}
html[data-theme-pair="sunset"][data-bs-theme="light"],
html[data-theme-pair="sunset"][data-bs-theme="light"] body { background-color: #fffbeb !important; }
html[data-theme-pair="sunset"][data-bs-theme="dark"],
html[data-theme-pair="sunset"][data-bs-theme="dark"] body { background-color: #1a1408 !important; }
html[data-theme-pair="sunset"][data-bs-theme="light"] .navbar-vertical,
html[data-theme-pair="sunset"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: #78350f !important; }
html[data-theme-pair="sunset"][data-bs-theme="dark"] .navbar-vertical,
html[data-theme-pair="sunset"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #110d04 !important; }
html[data-theme-pair="sunset"][data-bs-theme="light"] .page-wrapper { background: #fffbeb; }
html[data-theme-pair="sunset"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="sunset"][data-bs-theme="dark"] .page-body { background: #1a1408; }
html[data-theme-pair="sunset"] .keywarden-header-brand .keywarden-brand i.ti { color: #fbbf24; }
html[data-theme-pair="sunset"] .nav-category { color: rgba(220, 190, 150, 0.6); }
html[data-theme-pair="sunset"][data-bs-theme="light"] ::selection { background: #fde68a; color: #78350f; }
html[data-theme-pair="sunset"][data-bs-theme="light"] ::-moz-selection { background: #fde68a; color: #78350f; }
html[data-theme-pair="sunset"][data-bs-theme="dark"] ::selection { background: #92400e; color: #fffbeb; }
html[data-theme-pair="sunset"][data-bs-theme="dark"] ::-moz-selection { background: #92400e; color: #fffbeb; }
/* ── Rose Theme ── */
html[data-theme-pair="rose"] {
--kw-primary: #db2777; --kw-primary-hover: #be185d; --kw-primary-active: #9d174d;
--tblr-primary: #db2777; --tblr-primary-rgb: 219,39,119;
}
html[data-theme-pair="rose"][data-bs-theme="dark"] {
--kw-primary: #f472b6; --kw-primary-hover: #ec4899; --kw-primary-active: #db2777;
--tblr-primary: #f472b6; --tblr-primary-rgb: 244,114,182;
--tblr-bg-surface: #22101a; --tblr-bg-surface-secondary: #281420;
--tblr-bg-surface-tertiary: #1e0c16; --tblr-bg-surface-dark: #1a0a14;
--tblr-bg-forms: #1e0c16; --tblr-body-bg: #1a0a14; --tblr-body-bg-rgb: 26,10,20;
--tblr-border-color: #3a1a2c; --tblr-border-color-translucent: rgba(244, 114, 182, 0.10);
}
html[data-theme-pair="rose"][data-bs-theme="light"],
html[data-theme-pair="rose"][data-bs-theme="light"] body { background-color: #fdf2f8 !important; }
html[data-theme-pair="rose"][data-bs-theme="dark"],
html[data-theme-pair="rose"][data-bs-theme="dark"] body { background-color: #1a0a14 !important; }
html[data-theme-pair="rose"][data-bs-theme="light"] .navbar-vertical,
html[data-theme-pair="rose"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: #831843 !important; }
html[data-theme-pair="rose"][data-bs-theme="dark"] .navbar-vertical,
html[data-theme-pair="rose"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #12060e !important; }
html[data-theme-pair="rose"][data-bs-theme="light"] .page-wrapper { background: #fdf2f8; }
html[data-theme-pair="rose"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="rose"][data-bs-theme="dark"] .page-body { background: #1a0a14; }
html[data-theme-pair="rose"] .keywarden-header-brand .keywarden-brand i.ti { color: #f472b6; }
html[data-theme-pair="rose"] .nav-category { color: rgba(220, 160, 190, 0.6); }
html[data-theme-pair="rose"][data-bs-theme="light"] ::selection { background: #fbcfe8; color: #831843; }
html[data-theme-pair="rose"][data-bs-theme="light"] ::-moz-selection { background: #fbcfe8; color: #831843; }
html[data-theme-pair="rose"][data-bs-theme="dark"] ::selection { background: #9f1239; color: #fdf2f8; }
html[data-theme-pair="rose"][data-bs-theme="dark"] ::-moz-selection { background: #9f1239; color: #fdf2f8; }
/* ── Nord Theme ── */
html[data-theme-pair="nord"] {
--kw-primary: #5e81ac; --kw-primary-hover: #4c6e96; --kw-primary-active: #3b5b80;
--tblr-primary: #5e81ac; --tblr-primary-rgb: 94,129,172;
}
html[data-theme-pair="nord"][data-bs-theme="dark"] {
--kw-primary: #88c0d0; --kw-primary-hover: #6eb0c2; --kw-primary-active: #5e9fb4;
--tblr-primary: #88c0d0; --tblr-primary-rgb: 136,192,208;
--tblr-bg-surface: #242830; --tblr-bg-surface-secondary: #2a2e36;
--tblr-bg-surface-tertiary: #21252c; --tblr-bg-surface-dark: #1e2128;
--tblr-bg-forms: #21252c; --tblr-body-bg: #1e2128; --tblr-body-bg-rgb: 30,33,40;
--tblr-border-color: #353a44; --tblr-border-color-translucent: rgba(136, 192, 208, 0.10);
}
html[data-theme-pair="nord"][data-bs-theme="light"],
html[data-theme-pair="nord"][data-bs-theme="light"] body { background-color: #eceff4 !important; }
html[data-theme-pair="nord"][data-bs-theme="dark"],
html[data-theme-pair="nord"][data-bs-theme="dark"] body { background-color: #1e2128 !important; }
html[data-theme-pair="nord"][data-bs-theme="light"] .navbar-vertical,
html[data-theme-pair="nord"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: #2e3440 !important; }
html[data-theme-pair="nord"][data-bs-theme="dark"] .navbar-vertical,
html[data-theme-pair="nord"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #14171c !important; }
html[data-theme-pair="nord"][data-bs-theme="light"] .page-wrapper { background: #eceff4; }
html[data-theme-pair="nord"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="nord"][data-bs-theme="dark"] .page-body { background: #1e2128; }
html[data-theme-pair="nord"] .keywarden-header-brand .keywarden-brand i.ti { color: #88c0d0; }
html[data-theme-pair="nord"] .nav-category { color: rgba(160, 180, 200, 0.6); }
html[data-theme-pair="nord"][data-bs-theme="light"] ::selection { background: #d8dee9; color: #2e3440; }
html[data-theme-pair="nord"][data-bs-theme="light"] ::-moz-selection { background: #d8dee9; color: #2e3440; }
html[data-theme-pair="nord"][data-bs-theme="dark"] ::selection { background: #434c5e; color: #eceff4; }
html[data-theme-pair="nord"][data-bs-theme="dark"] ::-moz-selection { background: #434c5e; color: #eceff4; }
</style> </style>
<!-- Tabler CSS (self-hosted to prevent FOUC) --> <!-- Tabler CSS (self-hosted to prevent FOUC) -->
<link rel="stylesheet" href="/static/css/tabler.min.css"> <link rel="stylesheet" href="/static/css/tabler.min.css">
@@ -558,14 +796,36 @@
<script src="/static/js/tabler.min.js"></script> <script src="/static/js/tabler.min.js"></script>
<script> <script>
// --- Theme Toggle --- // --- Theme Toggle ---
function getResolvedTheme() { function parseTheme(theme) {
var stored = document.documentElement.getAttribute('data-bs-theme'); if (!theme || theme === 'auto' || theme === 'light' || theme === 'dark') {
return stored || 'light'; return { pair: 'default', mode: theme || 'auto' };
}
var idx = theme.lastIndexOf('-');
if (idx > 0) {
return { pair: theme.substring(0, idx), mode: theme.substring(idx + 1) };
}
return { pair: 'default', mode: 'auto' };
} }
function applyTheme(theme) { function resolveMode(mode) {
document.documentElement.setAttribute('data-bs-theme', theme); if (mode !== 'light' && mode !== 'dark') {
document.documentElement.style.colorScheme = theme; return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
updateThemeIcon(theme); }
return mode;
}
function getResolvedTheme() {
return document.documentElement.getAttribute('data-bs-theme') || 'light';
}
function applyFullTheme(themeStr) {
var parts = parseTheme(themeStr);
var resolved = resolveMode(parts.mode);
document.documentElement.setAttribute('data-bs-theme', resolved);
document.documentElement.style.colorScheme = resolved;
if (parts.pair !== 'default') {
document.documentElement.setAttribute('data-theme-pair', parts.pair);
} else {
document.documentElement.removeAttribute('data-theme-pair');
}
updateThemeIcon(resolved);
} }
function updateThemeIcon(theme) { function updateThemeIcon(theme) {
var icon = document.getElementById('theme-icon'); var icon = document.getElementById('theme-icon');
@@ -573,15 +833,18 @@
icon.className = theme === 'dark' ? 'ti ti-moon' : 'ti ti-sun'; icon.className = theme === 'dark' ? 'ti ti-moon' : 'ti ti-sun';
} }
function toggleTheme() { function toggleTheme() {
var current = getResolvedTheme(); var raw = window.__kwThemeRaw || 'auto';
var next = current === 'dark' ? 'light' : 'dark'; var parts = parseTheme(raw);
applyTheme(next); var currentMode = getResolvedTheme();
// Persist choice via API (fire-and-forget) var nextMode = currentMode === 'dark' ? 'light' : 'dark';
var nextTheme = parts.pair === 'default' ? nextMode : parts.pair + '-' + nextMode;
window.__kwThemeRaw = nextTheme;
applyFullTheme(nextTheme);
var csrf = (document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/) || [])[1] || ''; var csrf = (document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/) || [])[1] || '';
fetch('/settings/theme', { fetch('/settings/theme', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}, headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'theme=' + encodeURIComponent(next) + '&_csrf=' + encodeURIComponent(csrf) body: 'theme=' + encodeURIComponent(nextTheme) + '&_csrf=' + encodeURIComponent(csrf)
}); });
} }
// Set initial icon on page load // Set initial icon on page load

View File

@@ -11,10 +11,32 @@
<div class="row align-items-end"> <div class="row align-items-end">
<div class="col-auto"> <div class="col-auto">
<label class="form-label">Theme</label> <label class="form-label">Theme</label>
<select name="theme" class="form-select" style="width: 250px;"> <select name="theme" class="form-select" style="width: 280px;">
<option value="auto" {{if or (not .User) (eq .User.Theme "") (eq .User.Theme "auto")}}selected{{end}}>Automatic (System)</option> <optgroup label="🌊 Ocean (Standard)">
<option value="light" {{if and .User (eq .User.Theme "light")}}selected{{end}}>Light</option> <option value="ocean-auto" {{if or (not .User) (eq .User.Theme "") (eq .User.Theme "auto") (eq .User.Theme "ocean-auto")}}selected{{end}}>Ocean (System)</option>
<option value="dark" {{if and .User (eq .User.Theme "dark")}}selected{{end}}>Dark</option> <option value="ocean-light" {{if or (and .User (eq .User.Theme "ocean-light")) (and .User (eq .User.Theme "light"))}}selected{{end}}>Ocean Light</option>
<option value="ocean-dark" {{if or (and .User (eq .User.Theme "ocean-dark")) (and .User (eq .User.Theme "dark"))}}selected{{end}}>Ocean Dark</option>
</optgroup>
<optgroup label="🌲 Forest">
<option value="forest-auto" {{if and .User (eq .User.Theme "forest-auto")}}selected{{end}}>Forest (System)</option>
<option value="forest-light" {{if and .User (eq .User.Theme "forest-light")}}selected{{end}}>Forest Light</option>
<option value="forest-dark" {{if and .User (eq .User.Theme "forest-dark")}}selected{{end}}>Forest Dark</option>
</optgroup>
<optgroup label="🌅 Sunset">
<option value="sunset-auto" {{if and .User (eq .User.Theme "sunset-auto")}}selected{{end}}>Sunset (System)</option>
<option value="sunset-light" {{if and .User (eq .User.Theme "sunset-light")}}selected{{end}}>Sunset Light</option>
<option value="sunset-dark" {{if and .User (eq .User.Theme "sunset-dark")}}selected{{end}}>Sunset Dark</option>
</optgroup>
<optgroup label="🌹 Rose">
<option value="rose-auto" {{if and .User (eq .User.Theme "rose-auto")}}selected{{end}}>Rose (System)</option>
<option value="rose-light" {{if and .User (eq .User.Theme "rose-light")}}selected{{end}}>Rose Light</option>
<option value="rose-dark" {{if and .User (eq .User.Theme "rose-dark")}}selected{{end}}>Rose Dark</option>
</optgroup>
<optgroup label="❄️ Nord">
<option value="nord-auto" {{if and .User (eq .User.Theme "nord-auto")}}selected{{end}}>Nord (System)</option>
<option value="nord-light" {{if and .User (eq .User.Theme "nord-light")}}selected{{end}}>Nord Light</option>
<option value="nord-dark" {{if and .User (eq .User.Theme "nord-dark")}}selected{{end}}>Nord Dark</option>
</optgroup>
</select> </select>
</div> </div>
<div class="col-auto"> <div class="col-auto">