feat: add system/light/dark theme switcher
This commit is contained in:
@@ -5,30 +5,32 @@
|
||||
@source '../../internal/**/*.templ';
|
||||
|
||||
:root {
|
||||
--bg: #fafaf9;
|
||||
--panel: #f5f5f4;
|
||||
--panel-soft: #e7e5e4;
|
||||
--header: #ffffff;
|
||||
--text: #1c1917;
|
||||
--text-muted: #57534e;
|
||||
--text-faint: #a8a29e;
|
||||
--accent: #0c0a09;
|
||||
color-scheme: light dark;
|
||||
|
||||
--bg: light-dark(#fafaf9, #0c0a09);
|
||||
--panel: light-dark(#f5f5f4, #1c1917);
|
||||
--panel-soft: light-dark(#e7e5e4, #292524);
|
||||
--header: light-dark(#ffffff, #1c1917);
|
||||
--text: light-dark(#1c1917, #e7e5e4);
|
||||
--text-muted: light-dark(#57534e, #a8a29e);
|
||||
--text-faint: light-dark(#a8a29e, #78716c);
|
||||
--accent: light-dark(#0c0a09, #fafaf9);
|
||||
--danger: #dc2626;
|
||||
--surface-search: #f5f5f4;
|
||||
--surface-search-focus-border: rgba(12, 10, 9, 0.12);
|
||||
--surface-thumb: #e7e5e4;
|
||||
--surface-input: #ffffff;
|
||||
--surface-input-focus: #ffffff;
|
||||
--surface-tab-hover: #e7e5e4;
|
||||
--surface-tab-active: #1c1917;
|
||||
--text-tab-active: #fafaf9;
|
||||
--surface-select: #ffffff;
|
||||
--surface-search-view-all: #f5f5f4;
|
||||
--text-on-accent: #fafaf9;
|
||||
--overlay-subtle: rgba(0, 0, 0, 0.04);
|
||||
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-card: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-card-hover: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--surface-search: light-dark(#f5f5f4, #1c1917);
|
||||
--surface-search-focus-border: light-dark(rgba(12, 10, 9, 0.12), rgba(255, 255, 255, 0.12));
|
||||
--surface-thumb: light-dark(#e7e5e4, #44403c);
|
||||
--surface-input: light-dark(#ffffff, #1c1917);
|
||||
--surface-input-focus: light-dark(#ffffff, #1c1917);
|
||||
--surface-tab-hover: light-dark(#e7e5e4, #292524);
|
||||
--surface-tab-active: light-dark(#1c1917, #fafaf9);
|
||||
--text-tab-active: light-dark(#fafaf9, #0c0a09);
|
||||
--surface-select: light-dark(#ffffff, #1c1917);
|
||||
--surface-search-view-all: light-dark(#f5f5f4, #1c1917);
|
||||
--text-on-accent: light-dark(#fafaf9, #0c0a09);
|
||||
--overlay-subtle: light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.04));
|
||||
--shadow-subtle: light-dark(0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.2));
|
||||
--shadow-card: light-dark(0 4px 12px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.3));
|
||||
--shadow-card-hover: light-dark(0 8px 24px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(0, 0, 0, 0.4));
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
@@ -40,3 +42,11 @@
|
||||
--font: 'DM Sans', 'Segoe UI', system-ui, sans-serif;
|
||||
--radius: 6px;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
80
static/theme.ts
Normal file
80
static/theme.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
type Theme = 'system' | 'light' | 'dark'
|
||||
|
||||
const STORAGE_KEY = 'theme'
|
||||
|
||||
const getSavedTheme = (): Theme => {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw === 'light' || raw === 'dark') return raw
|
||||
return 'system'
|
||||
}
|
||||
|
||||
const getEffectiveTheme = (theme: Theme): 'light' | 'dark' => {
|
||||
if (theme !== 'system') return theme
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const applyTheme = (theme: Theme): void => {
|
||||
const html = document.documentElement
|
||||
if (theme === 'system') {
|
||||
html.removeAttribute('data-theme')
|
||||
} else {
|
||||
html.setAttribute('data-theme', theme)
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, theme)
|
||||
updateToggleButton(theme)
|
||||
}
|
||||
|
||||
const cycleTheme = (): void => {
|
||||
const current = getSavedTheme()
|
||||
const next: Theme = current === 'system' ? 'light' : current === 'light' ? 'dark' : 'system'
|
||||
applyTheme(next)
|
||||
}
|
||||
|
||||
const updateToggleButton = (theme: Theme): void => {
|
||||
const btn = document.getElementById('theme-toggle')
|
||||
if (!btn) return
|
||||
|
||||
const label = btn.querySelector('[data-theme-label]') as HTMLElement | null
|
||||
if (label) {
|
||||
label.textContent = theme
|
||||
}
|
||||
|
||||
const svg = btn.querySelector('svg')
|
||||
if (!svg) return
|
||||
|
||||
if (theme === 'light') {
|
||||
svg.innerHTML = '<circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>'
|
||||
svg.setAttribute('stroke', 'currentColor')
|
||||
svg.setAttribute('fill', 'none')
|
||||
} else if (theme === 'dark') {
|
||||
svg.innerHTML = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>'
|
||||
svg.setAttribute('stroke', 'currentColor')
|
||||
svg.setAttribute('fill', 'none')
|
||||
} else {
|
||||
svg.innerHTML = '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>'
|
||||
svg.setAttribute('stroke', 'currentColor')
|
||||
svg.setAttribute('fill', 'none')
|
||||
}
|
||||
}
|
||||
|
||||
const initTheme = (): void => {
|
||||
const saved = getSavedTheme()
|
||||
applyTheme(saved)
|
||||
|
||||
const btn = document.getElementById('theme-toggle')
|
||||
if (btn) {
|
||||
btn.addEventListener('click', cycleTheme)
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (getSavedTheme() === 'system') {
|
||||
applyTheme('system')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTheme)
|
||||
} else {
|
||||
initTheme()
|
||||
}
|
||||
@@ -12,6 +12,11 @@ templ Layout(title string, showHeader bool) {
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
<link rel="stylesheet" href="/dist/tailwind.css"/>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.11" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
var t = localStorage.getItem('theme');
|
||||
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||
</script>
|
||||
<script src="/dist/theme.js" defer></script>
|
||||
<script src="/dist/discover.js" defer></script>
|
||||
<script src="/dist/anime.js" defer></script>
|
||||
<script src="/dist/timezone.js" defer></script>
|
||||
@@ -77,6 +82,18 @@ templ Layout(title string, showHeader bool) {
|
||||
></div>
|
||||
</form>
|
||||
</div>
|
||||
<button
|
||||
id="theme-toggle"
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent text-(--text-muted) hover:text-(--text)"
|
||||
aria-label="Toggle theme"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||||
<path d="M8 21h8M12 17v4"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user