Add dark theme: CSS variables, prefers-color-scheme auto-detect, sun/moon toggle in nav with localStorage

This commit is contained in:
ask 2026-05-17 21:45:35 +05:00
parent 3165ff0f9b
commit e8be79edb0

View File

@ -7,25 +7,103 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
<script>
// Apply theme ASAP to avoid flash of wrong theme. Runs before <body> parses.
(function() {
try {
const saved = localStorage.getItem('vibe.theme');
if (saved === 'dark' || saved === 'light') {
document.documentElement.setAttribute('data-theme', saved);
}
} catch (e) {}
})();
</script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* Brand */
--blue: #0066cc;
--blue-hover: #0055b3;
--blue-link: #0058b3;
--bg: #efefef;
/* Surfaces */
--page-bg: #fff;
--bg: #efefef; /* nav / main background */
--surface: #fff; /* cards */
--card-neutral: #e3e3e3;
/* Text */
--text: #000;
--text-on-blue: #fff;
--text-65: rgba(0,0,0,0.65);
--text-55: rgba(0,0,0,0.55);
--text-45: rgba(0,0,0,0.45);
/* Lines + hovers */
--border: rgba(0,0,0,0.08);
--border-strong: rgba(0,0,0,0.12);
--hover: rgba(0,0,0,0.06);
--hover-light: rgba(0,0,0,0.03);
/* Status */
--positive: #059669;
--positive-bg: #dcfce7;
--negative: #dc2626;
/* Decorative */
--dot-color: rgba(0,0,0,0.05);
--code-bg: #1a1a1a; /* code blocks stay dark in both themes */
--code-text: #e5e5e5;
/* Misc */
--modal-scrim: rgba(0,0,0,0.4);
--shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
}
/* Dark theme — applied via [data-theme="dark"] OR auto when no explicit preference */
[data-theme="dark"] {
--page-bg: #16171c;
--bg: #1c1d22;
--surface: #212229;
--card-neutral: #2a2c33;
--text: #e8e8e8;
--text-65: rgba(255,255,255,0.72);
--text-55: rgba(255,255,255,0.55);
--text-45: rgba(255,255,255,0.40);
--border: rgba(255,255,255,0.08);
--border-strong: rgba(255,255,255,0.14);
--hover: rgba(255,255,255,0.06);
--hover-light: rgba(255,255,255,0.03);
--blue: #3b82f6;
--blue-hover: #60a5fa;
--blue-link: #60a5fa;
--positive: #34d399;
--positive-bg: rgba(52,211,153,0.15);
--negative: #f87171;
--dot-color: rgba(255,255,255,0.05);
--modal-scrim: rgba(0,0,0,0.65);
--shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.3);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--page-bg: #16171c;
--bg: #1c1d22;
--surface: #212229;
--card-neutral: #2a2c33;
--text: #e8e8e8;
--text-65: rgba(255,255,255,0.72);
--text-55: rgba(255,255,255,0.55);
--text-45: rgba(255,255,255,0.40);
--border: rgba(255,255,255,0.08);
--border-strong: rgba(255,255,255,0.14);
--hover: rgba(255,255,255,0.06);
--hover-light: rgba(255,255,255,0.03);
--blue: #3b82f6;
--blue-hover: #60a5fa;
--blue-link: #60a5fa;
--positive: #34d399;
--positive-bg: rgba(52,211,153,0.15);
--negative: #f87171;
--dot-color: rgba(255,255,255,0.05);
--modal-scrim: rgba(0,0,0,0.65);
--shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.3);
}
}
html { scroll-behavior: smooth; }
@ -39,12 +117,12 @@ html { scroll-behavior: smooth; }
.tool-card:hover, .create-card:hover, .plan-step:hover,
.gloss-item:hover, .prompt-card:hover, .eq-box:hover {
transform: translateY(-2px);
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
box-shadow: var(--shadow-card);
}
/* project-card already has its own hover (border + ring) — strengthen with lift */
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
box-shadow: var(--shadow-card);
}
}
@media (prefers-reduced-motion: reduce) {
@ -73,7 +151,7 @@ html { scroll-behavior: smooth; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #fff;
background: var(--page-bg);
min-height: 100dvh;
padding: 12px;
-webkit-font-smoothing: antialiased;
@ -182,6 +260,31 @@ nav {
/* ── AUTH (nav button + modal) ── */
.nav-cta-group { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.theme-toggle {
width: 36px; height: 36px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-65);
border-radius: 50%;
cursor: pointer;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
}
.theme-toggle:hover {
background: var(--hover);
color: var(--text);
border-color: var(--border-strong);
}
.theme-toggle svg { width: 16px; height: 16px; }
.theme-toggle .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
@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; }
}
.nav-cdn-chip {
font-size: 0.75rem;
color: var(--text-55);
@ -197,7 +300,7 @@ nav {
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.4);
background: var(--modal-scrim);
display: none; align-items: center; justify-content: center;
z-index: 100; padding: 16px;
animation: fadeIn 150ms ease;
@ -206,7 +309,7 @@ nav {
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal-card {
background: #fff;
background: var(--surface);
border-radius: 16px;
padding: 32px 28px 28px;
max-width: 420px; width: 100%;
@ -250,7 +353,7 @@ nav {
font-size: 0.9375rem;
font-family: inherit;
color: var(--text);
background: #fff;
background: var(--surface);
transition: border-color 150ms ease;
}
.modal-input:focus {
@ -319,7 +422,7 @@ nav {
}
.plan-step {
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 18px;
@ -368,7 +471,7 @@ nav {
}
.create-card-icon {
width: 44px; height: 44px;
background: #fff;
background: var(--surface);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
color: var(--blue);
@ -417,7 +520,7 @@ nav {
border: 1px solid rgba(0,102,204,0.2);
}
.eq-box.result {
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
}
.eq-box-label {
@ -453,7 +556,7 @@ nav {
.eq-note {
margin-top: 32px;
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--blue);
border-radius: 8px;
@ -492,7 +595,7 @@ nav {
}
.prompt-card {
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
@ -642,7 +745,7 @@ main {
text-align: center;
/* subtle dot-grid backdrop, fades to clean at the edges */
background-image:
radial-gradient(ellipse at center, rgba(0,0,0,0.05) 1px, transparent 1.5px);
radial-gradient(ellipse at center, var(--dot-color) 1px, transparent 1.5px);
background-size: 22px 22px;
background-position: center top;
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
@ -792,7 +895,7 @@ main {
}
.ba-card.after {
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
}
@ -877,7 +980,7 @@ main {
@media (min-width: 640px) { .tools-grid { grid-template-columns: repeat(3, 1fr); } }
.tool-card {
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
@ -1028,7 +1131,7 @@ main {
.gloss-item {
border: 1px solid var(--border);
border-radius: 12px;
background: #fff;
background: var(--surface);
display: flex;
flex-direction: column;
scroll-snap-align: start;
@ -1051,7 +1154,7 @@ main {
.gloss-nav-btn {
width: 40px; height: 40px;
border: 1px solid var(--border);
background: #fff;
background: var(--surface);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
@ -1136,7 +1239,7 @@ main {
@media (min-width: 640px) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
.project-card {
background: #fff;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
@ -1309,6 +1412,10 @@ footer {
<a href="#telegram" class="nav-link">Сообщество</a>
</div>
<div class="nav-cta-group">
<button id="theme-toggle" type="button" class="theme-toggle" aria-label="Переключить тему">
<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.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></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.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<span id="nav-cdn" class="nav-cdn-chip" hidden title="Выйти"></span>
<button id="nav-auth" type="button" class="nav-cta">Войти</button>
</div>
@ -2462,6 +2569,24 @@ const SCHEDULE = [
refreshHeader();
/* ── THEME TOGGLE ───────────────────────────────────────
Manual override stored in localStorage. Falls back to system
(prefers-color-scheme) when no explicit choice. */
(function initThemeToggle() {
const btn = document.getElementById('theme-toggle');
if (!btn) return;
function currentTheme() {
const explicit = document.documentElement.getAttribute('data-theme');
if (explicit) return explicit;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
btn.addEventListener('click', () => {
const next = currentTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('vibe.theme', next); } catch (e) {}
});
})();
/* ── POLISH: scroll-triggered fade-in ────────────────────
Marks all direct content blocks inside <section class="section">
with .reveal, then toggles .in on intersection.