Added theme switcher
This commit is contained in:
1 parent
51402c590a
commit
6410273cf8
4 files changed
+98
-7
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
118e09f98944ac59593af603c04a1d5f0e7cafb6549b5a562300bc31d9c769da control-api
|
||||
48c9b99fa88be751d9badfba8b7d80f894e326a743a90e85478f1d2251ce4ab6 validator-agent
|
||||
8877cf7c6d2bdd58faf70865be602b3db5f18508f4607c8942bc8899ce0fb1e6 prober
|
||||
8d18a72276cdb1a7c14c919c9243446f467bdf9aa99f26d856aa954546031731 admin-dashboard
|
||||
4c7cf560b4a7b87f8de0fcc5a4fe6200c45eadf5ae04b022d67c8a8e196f39cc admin-dashboard
|
||||
Binary file not shown.
@@ -10,8 +10,11 @@
|
||||
(IBM Plex Mono, already vendored) carries headings/nav/labels/table
|
||||
heads — an operations-console register — while --font-ui (Manrope)
|
||||
stays on body copy and form microcopy for readability at length.
|
||||
Both a light and a dark palette are defined; the dark one activates
|
||||
automatically from the OS/browser color-scheme (no in-app toggle). */
|
||||
Both a light and a dark palette are defined. The bare :root (no
|
||||
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 {
|
||||
--bg: #EEF1F5;
|
||||
@@ -58,7 +61,7 @@
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #0A0D13;
|
||||
--surface: #10141C;
|
||||
--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; }
|
||||
|
||||
html, body { height: 100%; }
|
||||
@@ -177,8 +211,9 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
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 {
|
||||
width: 6px; height: 6px; border-radius: 1px;
|
||||
background: var(--success);
|
||||
@@ -186,6 +221,30 @@ nav.nav-groups { display: flex; flex-direction: column; gap: 1px; }
|
||||
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; }
|
||||
.topbar {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
|
||||
@@ -14,6 +14,21 @@
|
||||
<title>Cloud IP Validator — Admin</title>
|
||||
<link rel="stylesheet" href="/static/vendor/fonts.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/alpine.min.js" defer></script>
|
||||
{{end}}
|
||||
@@ -60,10 +75,27 @@
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-foot">
|
||||
<span class="pulse-dot"></span>
|
||||
control-api
|
||||
<span class="sidebar-foot-status"><span class="pulse-dot"></span>control-api</span>
|
||||
<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>
|
||||
</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}}
|
||||
|
||||
{{define "topbar_mobile"}}
|
||||
|
||||
Reference in new issue
Block a user