You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
116 lines
2.7 KiB
116 lines
2.7 KiB
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
|
|
type Theme = 'light' | 'dark' | 'system';
|
|
|
|
const STOR_KEY = 'active-docs-theme';
|
|
|
|
let theme = $state<Theme>('system');
|
|
let resolved = $state<'light' | 'dark'>('light');
|
|
|
|
function applyTheme(next: Theme) {
|
|
const isDark =
|
|
next === 'dark' ||
|
|
(next === 'system' &&
|
|
window.matchMedia &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
|
|
resolved = isDark ? 'dark' : 'light';
|
|
}
|
|
|
|
function setTheme(next: Theme) {
|
|
theme = next;
|
|
try {
|
|
localStorage.setItem(STOR_KEY, next);
|
|
} catch {
|
|
/* private mode or storage disabled */
|
|
}
|
|
applyTheme(next);
|
|
}
|
|
|
|
function cycle() {
|
|
const order: Theme[] = ['light', 'dark', 'system'];
|
|
const idx = order.indexOf(theme);
|
|
setTheme(order[(idx + 1) % order.length]);
|
|
}
|
|
|
|
onMount(() => {
|
|
try {
|
|
const saved = localStorage.getItem(STOR_KEY) as Theme | null;
|
|
if (saved === 'light' || saved === 'dark' || saved === 'system') theme = saved;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
applyTheme(theme);
|
|
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const handler = () => {
|
|
if (theme === 'system') applyTheme('system');
|
|
};
|
|
mq.addEventListener('change', handler);
|
|
return () => mq.removeEventListener('change', handler);
|
|
});
|
|
|
|
const label = $derived(
|
|
theme === 'system' ? `System (${resolved})` : theme === 'dark' ? 'Dark' : 'Light'
|
|
);
|
|
</script>
|
|
|
|
<button
|
|
class="toggle"
|
|
type="button"
|
|
aria-label="Toggle color theme — currently {label}"
|
|
title="Theme: {label}"
|
|
onclick={cycle}
|
|
>
|
|
{#if resolved === 'dark'}
|
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
|
<path
|
|
fill="currentColor"
|
|
d="M6.05 2.5a6 6 0 1 0 7.45 7.45 5 5 0 0 1-7.45-7.45Z"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
|
<circle cx="8" cy="8" r="3" fill="currentColor" />
|
|
<g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
|
<path d="M8 1.5v1.5" />
|
|
<path d="M8 13v1.5" />
|
|
<path d="M1.5 8h1.5" />
|
|
<path d="M13 8h1.5" />
|
|
<path d="m3.4 3.4 1.05 1.05" />
|
|
<path d="m11.55 11.55 1.05 1.05" />
|
|
<path d="m3.4 12.6 1.05-1.05" />
|
|
<path d="m11.55 4.45 1.05-1.05" />
|
|
</g>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<style>
|
|
.toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: var(--r-md);
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--c-fg-muted);
|
|
transition:
|
|
background-color 120ms var(--ease),
|
|
color 120ms var(--ease);
|
|
}
|
|
|
|
.toggle:hover {
|
|
background: var(--c-bg-muted);
|
|
color: var(--c-fg);
|
|
}
|
|
|
|
.toggle:focus-visible {
|
|
outline: 2px solid var(--c-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
</style>
|