release: v0.12.0 (#520)
Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
This commit is contained in:
@@ -1,14 +1,22 @@
|
||||
/**
|
||||
* ThemeToggle — dark/light mode switch.
|
||||
*
|
||||
* Persists preference in localStorage and sets data-theme on <html>.
|
||||
* Defaults to dark theme (matching existing design).
|
||||
* A visitor's own choice is kept in localStorage and always wins. Without one,
|
||||
* the starting mode comes from `mode:` in settings.yaml, which the layout has
|
||||
* already rendered onto <html> as data-default-theme — reading it from the DOM
|
||||
* rather than taking a prop keeps this component mountable anywhere without
|
||||
* threading config through the tree. `auto` follows the operating system.
|
||||
*
|
||||
* It used to hardcode dark, so an operator who preferred light had no way to
|
||||
* give visitors anything else (#512).
|
||||
*
|
||||
* Uses useSyncExternalStore to satisfy React strict mode lint rules.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useSyncExternalStore, useCallback } from 'react';
|
||||
import { useDictionary } from './I18nProvider';
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
|
||||
@@ -29,6 +37,22 @@ function getServerSnapshot(): Theme {
|
||||
return 'dark'; // SSR default
|
||||
}
|
||||
|
||||
/** What the OS asks for. Dark unless it says otherwise, as before. */
|
||||
function systemTheme(): Theme {
|
||||
return typeof window !== 'undefined' &&
|
||||
window.matchMedia?.('(prefers-color-scheme: light)').matches
|
||||
? 'light'
|
||||
: 'dark';
|
||||
}
|
||||
|
||||
/** The configured starting mode, read off the <html> the server rendered. */
|
||||
function configuredTheme(): Theme {
|
||||
const mode = document.documentElement.getAttribute('data-default-theme');
|
||||
if (mode === 'light' || mode === 'dark') return mode;
|
||||
// 'auto', or an older layout that renders no attribute at all.
|
||||
return systemTheme();
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
currentTheme = theme;
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -40,13 +64,15 @@ function applyTheme(theme: Theme) {
|
||||
|
||||
// Initialize from localStorage on module load (client-side only)
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem('theme') as Theme | null;
|
||||
currentTheme = stored === 'light' ? 'light' : 'dark';
|
||||
const stored = localStorage.getItem('theme');
|
||||
currentTheme = stored === 'light' || stored === 'dark' ? stored : configuredTheme();
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
const t = useDictionary();
|
||||
const label = theme === 'dark' ? t.theme.switchToLight : t.theme.switchToDark;
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
applyTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
@@ -56,9 +82,9 @@ export function ThemeToggle() {
|
||||
<button
|
||||
className="theme-toggle"
|
||||
onClick={toggle}
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
aria-label={label}
|
||||
type="button"
|
||||
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
title={label}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
// Sun icon
|
||||
|
||||
Reference in New Issue
Block a user