feat: add dark mode with light/dark/auto theme setting
Theme preference stored in localStorage (auto follows the OS setting). The chosen data-theme attribute is applied synchronously in <head> to avoid any flash of unstyled content. CSS custom properties handle all surface, text, border and input colours across every page. The Appearance section on the profile page lets each user switch modes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,56 @@
|
||||
.btn-sm-del { background: transparent; color: #c62828; border: 1px solid #e0e0e0; border-radius: 4px; padding: 2px 7px; font-size: .78em; cursor: pointer; }
|
||||
.btn-sm-del:hover { background: #fce4ec; }
|
||||
|
||||
/* ---- Theme picker ---- */
|
||||
.theme-btns { display: flex; gap: 6px; }
|
||||
.theme-btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--border, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
background: var(--surface-3, #f5f5f5);
|
||||
color: var(--text-sec, #666);
|
||||
cursor: pointer;
|
||||
font-size: .88em;
|
||||
font-family: inherit;
|
||||
}
|
||||
.theme-btn:hover { border-color: var(--link, #0066cc); color: var(--link, #0066cc); }
|
||||
.theme-btn.active { background: var(--link, #0066cc); color: #fff; border-color: var(--link, #0066cc); }
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .profile-card { background: var(--surface); box-shadow: 0 1px 6px var(--shadow); }
|
||||
html[data-theme="dark"] .profile-name { color: var(--text); }
|
||||
html[data-theme="dark"] .profile-username { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .badge-admin { background: #1a3255; color: #7aa8f0; }
|
||||
html[data-theme="dark"] .badge-user { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .section { background: var(--surface); box-shadow: 0 1px 6px var(--shadow); }
|
||||
html[data-theme="dark"] .section h2 { color: var(--text); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .settings-row { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .settings-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .settings-value { color: var(--text); }
|
||||
html[data-theme="dark"] .settings-empty { color: var(--text-dim); }
|
||||
html[data-theme="dark"] .edit-section h4 { color: var(--text); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .edit-field label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .edit-input { background: var(--input-bg); border-color: var(--input-border); color: var(--text); }
|
||||
html[data-theme="dark"] .channel-row { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .channel-name { color: var(--text); }
|
||||
html[data-theme="dark"] .ch-picker-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-chip.selected { background: #1a3255; color: #60a5fa; }
|
||||
html[data-theme="dark"] .ch-chip.available { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-chip.available:hover { background: var(--border); color: var(--link); }
|
||||
html[data-theme="dark"] .my-ch-card { border-color: var(--border); }
|
||||
html[data-theme="dark"] .my-ch-header { background: var(--surface-2); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .my-ch-name { color: var(--text); }
|
||||
html[data-theme="dark"] .host-chip.owner { background: #0d2e17; color: #66bb6a; }
|
||||
html[data-theme="dark"] .host-chip.manager { background: #0d1f40; color: #64b5f6; }
|
||||
html[data-theme="dark"] .host-chip.monitor { background: #1e0d30; color: #ba68c8; }
|
||||
html[data-theme="dark"] .no-hosts { color: var(--text-dim); }
|
||||
html[data-theme="dark"] .ch-modal-box { background: var(--surface); color: var(--text); }
|
||||
html[data-theme="dark"] .ch-modal-box h3 { color: var(--text); }
|
||||
html[data-theme="dark"] .ch-form-row label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-form-divider { color: var(--text-muted); border-top-color: var(--border); }
|
||||
|
||||
/* ---- Channel modal (for My Channels CRUD) ---- */
|
||||
.ch-modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.4);
|
||||
@@ -477,6 +527,19 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Appearance -->
|
||||
<div class="section">
|
||||
<h2>Appearance</h2>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Theme</span>
|
||||
<div class="theme-btns">
|
||||
<button class="theme-btn" data-theme-val="auto" onclick="setTheme('auto')">Auto</button>
|
||||
<button class="theme-btn" data-theme-val="light" onclick="setTheme('light')">Light</button>
|
||||
<button class="theme-btn" data-theme-val="dark" onclick="setTheme('dark')">Dark</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Host access -->
|
||||
<div class="section">
|
||||
<h2>Host Access</h2>
|
||||
@@ -523,6 +586,28 @@
|
||||
|
||||
</div>
|
||||
<script>
|
||||
// ---- Theme ----
|
||||
function applyTheme(pref) {
|
||||
var dark = pref === 'dark' ||
|
||||
(pref === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (dark) { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||||
else { document.documentElement.removeAttribute('data-theme'); }
|
||||
}
|
||||
function setTheme(pref) {
|
||||
try { localStorage.setItem('hbd_theme', pref); } catch(e) {}
|
||||
applyTheme(pref);
|
||||
document.querySelectorAll('.theme-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.themeVal === pref);
|
||||
});
|
||||
}
|
||||
(function() {
|
||||
var pref = 'auto';
|
||||
try { pref = localStorage.getItem('hbd_theme') || 'auto'; } catch(e) {}
|
||||
document.querySelectorAll('.theme-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.themeVal === pref);
|
||||
});
|
||||
})();
|
||||
|
||||
// ---- Identity ----
|
||||
async function saveIdentity() {
|
||||
const full_name = document.getElementById('profile-fullname').value;
|
||||
|
||||
Reference in New Issue
Block a user