Added theme switcher

This commit is contained in:
ayurishchev committed 2026-08-26 21:18:58 +03:00
1 parent 51402c590a
commit 6410273cf8
4 files changed
+98 -7

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
118e09f98944ac59593af603c04a1d5f0e7cafb6549b5a562300bc31d9c769da control-api 118e09f98944ac59593af603c04a1d5f0e7cafb6549b5a562300bc31d9c769da control-api
48c9b99fa88be751d9badfba8b7d80f894e326a743a90e85478f1d2251ce4ab6 validator-agent 48c9b99fa88be751d9badfba8b7d80f894e326a743a90e85478f1d2251ce4ab6 validator-agent
8877cf7c6d2bdd58faf70865be602b3db5f18508f4607c8942bc8899ce0fb1e6 prober 8877cf7c6d2bdd58faf70865be602b3db5f18508f4607c8942bc8899ce0fb1e6 prober
8d18a72276cdb1a7c14c919c9243446f467bdf9aa99f26d856aa954546031731 admin-dashboard 4c7cf560b4a7b87f8de0fcc5a4fe6200c45eadf5ae04b022d67c8a8e196f39cc admin-dashboard
Binary file not shown.
+63 -4
View File
@@ -10,8 +10,11 @@
(IBM Plex Mono, already vendored) carries headings/nav/labels/table (IBM Plex Mono, already vendored) carries headings/nav/labels/table
heads — an operations-console register — while --font-ui (Manrope) heads — an operations-console register — while --font-ui (Manrope)
stays on body copy and form microcopy for readability at length. stays on body copy and form microcopy for readability at length.
Both a light and a dark palette are defined; the dark one activates Both a light and a dark palette are defined. The bare :root (no
automatically from the OS/browser color-scheme (no in-app toggle). */ data-theme attribute) follows the OS/browser color-scheme; the sidebar
toggle (see layout.html) stamps documentElement with
data-theme="light"/"dark" to override that, storing the choice in
localStorage so it survives page navigation. */
:root { :root {
--bg: #EEF1F5; --bg: #EEF1F5;
@@ -58,7 +61,7 @@
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root:not([data-theme="light"]) {
--bg: #0A0D13; --bg: #0A0D13;
--surface: #10141C; --surface: #10141C;
--surface-alt: #161B25; --surface-alt: #161B25;
@@ -89,6 +92,37 @@
} }
} }
/* Explicit dark choice (toggle) wins even when the OS/browser is light. */
:root[data-theme="dark"] {
--bg: #0A0D13;
--surface: #10141C;
--surface-alt: #161B25;
--border: #262E3B;
--border-soft: #1D2430;
--text: #E7ECF5;
--text-muted: #8D97AC;
--text-faint: #626D82;
--accent: #5C8CFF;
--accent-strong: #82A6FF;
--accent-soft: #182140;
--accent-contrast: #071022;
--success: #3DDC8A; --success-soft: #10261C;
--warning: #F0B429; --warning-soft: #2B2210; --warning-border: #4A3B14;
--danger: #FF6B5E; --danger-soft: #2C1613; --danger-border: #5A2620;
--info: #5AC0FF; --info-soft: #10222E;
--neutral: #A3ACBE; --neutral-soft: #1B212B;
--cancel: #B79CFF; --cancel-soft: #221B3A;
--overlay: rgba(0, 0, 0, .55);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--shadow-md: 0 16px 40px rgba(0, 0, 0, .5);
color-scheme: dark;
}
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { height: 100%; } html, body { height: 100%; }
@@ -177,8 +211,9 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
font-family: var(--font-display); font-family: var(--font-display);
font-size: 11px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
display: flex; align-items: center; gap: 7px; display: flex; align-items: center; justify-content: space-between; gap: 7px;
} }
.sidebar-foot-status { display: flex; align-items: center; gap: 7px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pulse-dot { .pulse-dot {
width: 6px; height: 6px; border-radius: 1px; width: 6px; height: 6px; border-radius: 1px;
background: var(--success); background: var(--success);
@@ -186,6 +221,30 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
flex-shrink: 0; flex-shrink: 0;
} }
.theme-toggle {
display: grid; place-items: center;
width: 22px; height: 22px; flex-shrink: 0;
border-radius: var(--radius-xs);
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-muted);
cursor: pointer;
padding: 0;
}
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme-toggle svg { width: 13px; height: 13px; }
.theme-toggle .icon-moon { display: none; }
/* Icon reflects the *effective* theme (OS default, unless the toggle has
stamped an explicit choice) — same three-state precedence as the color
tokens above. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .theme-toggle .icon-sun { display: none; }
:root:not([data-theme="light"]) .theme-toggle .icon-moon { display: block; }
}
:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
.main { padding: 22px 26px 60px; max-width: 1180px; } .main { padding: 22px 26px 60px; max-width: 1180px; }
.topbar { .topbar {
display: flex; align-items: baseline; justify-content: space-between; display: flex; align-items: baseline; justify-content: space-between;
+34 -2
View File
@@ -14,6 +14,21 @@
<title>Cloud IP Validator — Admin</title> <title>Cloud IP Validator — Admin</title>
<link rel="stylesheet" href="/static/vendor/fonts.css"> <link rel="stylesheet" href="/static/vendor/fonts.css">
<link rel="stylesheet" href="/static/dashboard.css"> <link rel="stylesheet" href="/static/dashboard.css">
<script>
// Apply a saved theme choice before first paint, so a page load never
// flashes the OS-default theme before switching to the stored one. Runs
// synchronously, after the stylesheet link above (browsers block a
// same-document script on a preceding <link rel="stylesheet"> until it
// loads), and before body markup starts rendering.
(function () {
try {
var saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
document.documentElement.setAttribute('data-theme', saved);
}
} catch (e) {}
})();
</script>
<script src="/static/vendor/htmx.min.js"></script> <script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/alpine.min.js" defer></script> <script src="/static/vendor/alpine.min.js" defer></script>
{{end}} {{end}}
@@ -60,10 +75,27 @@
</nav> </nav>
<div class="sidebar-foot"> <div class="sidebar-foot">
<span class="pulse-dot"></span> <span class="sidebar-foot-status"><span class="pulse-dot"></span>control-api</span>
control-api <button type="button" class="theme-toggle" id="themeToggle" aria-label="Переключить тему" title="Переключить тему">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z"/></svg>
</button>
</div> </div>
</aside> </aside>
<script>
(function () {
var btn = document.getElementById('themeToggle');
if (!btn) return;
function effectiveTheme() {
return getComputedStyle(document.documentElement).colorScheme.indexOf('dark') !== -1 ? 'dark' : 'light';
}
btn.addEventListener('click', function () {
var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('theme', next); } catch (e) {}
});
})();
</script>
{{end}} {{end}}
{{define "topbar_mobile"}} {{define "topbar_mobile"}}