|
/** Callback to inform of a value updates. */ |
|
declare type Subscriber<T> = (value: T) => void |
|
|
|
/** Unsubscribes from value updates. */ |
|
declare type Unsubscriber = () => void; |
|
|
|
/** Callback to update a value. */ |
|
declare type Updater<T> = (value: T) => T; |
|
|
|
/** Readable interface for subscribing. */ |
|
export interface Readable<T> { |
|
/** |
|
* Subscribe on value changes. |
|
* @param run subscription callback |
|
* @param invalidate cleanup callback |
|
*/ |
|
subscribe(run: Subscriber<T>): Unsubscriber; |
|
} |
|
|
|
/** Writable interface for both updating and subscribing. */ |
|
export interface Writable<T> extends Readable<T> { |
|
/** |
|
* Set value and inform subscribers. |
|
* @param value to set |
|
*/ |
|
set(value: T): void; |
|
/** |
|
* Update value using callback and inform subscribers. |
|
* @param updater callback |
|
*/ |
|
update(updater: Updater<T>): void; |
|
} |
|
|
|
type ColorMode = 'light' | 'dark' |
|
type ColorModeSwitcher = Writable<ColorMode> |
|
|
|
const createColorModeSwitcher = (): ColorModeSwitcher => { |
|
let hasSavedMode = false |
|
const colorModeKey = 'color-mode' |
|
const colorModes = ['light', 'dark'] |
|
const parseColorMode = (mode: string): ColorMode => |
|
colorModes.includes(mode) ? (mode as ColorMode) : 'light' |
|
|
|
const matchesToMode = (matches: boolean): ColorMode => |
|
matches ? 'dark' : 'light' |
|
const mediaQuery = matchMedia('(prefers-color-scheme: dark)') |
|
const colorModeOS = matchesToMode(mediaQuery.matches) |
|
|
|
const colorModeDefault = 'light' |
|
const colorModeSaved: ColorMode | null = localStorage.getItem( |
|
colorModeKey, |
|
) as ColorMode |
|
|
|
if (colorModeSaved !== null) { |
|
hasSavedMode = true |
|
} |
|
|
|
let colorMode: ColorMode = colorModeDefault |
|
if (colorModeSaved) { |
|
colorMode = parseColorMode(colorModeSaved) |
|
} else if (colorModeOS) { |
|
colorMode = colorModeOS |
|
} |
|
|
|
let m: ColorMode |
|
const internalSet = (mode: ColorMode, save: boolean) => { |
|
document.body.classList.remove(m) |
|
document.body.classList.add(mode) |
|
if (save) { |
|
localStorage.setItem(colorModeKey, mode) |
|
hasSavedMode = true |
|
} |
|
m = mode |
|
listeners.forEach((l) => l(mode)) |
|
} |
|
|
|
const listeners = new Set<Function>() |
|
|
|
const onChange = (e) => { |
|
if (!hasSavedMode) { |
|
internalSet(matchesToMode(e.matches), false) |
|
} |
|
} |
|
|
|
if (mediaQuery.addEventListener) { |
|
mediaQuery.addEventListener('change', onChange) |
|
} else { |
|
mediaQuery.addListener(onChange) |
|
} |
|
|
|
document.addEventListener('DOMContentLoaded', () => |
|
internalSet(colorMode, false), |
|
) |
|
|
|
return { |
|
subscribe: (listener) => { |
|
listener(m) |
|
listeners.add(listener) |
|
return () => listeners.delete(listener) |
|
}, |
|
set: (mode: ColorMode) => { |
|
internalSet(mode, true) |
|
}, |
|
update: (updater) => { |
|
internalSet(updater(m), true) |
|
}, |
|
} |
|
} |
|
|
|
const colorModeSwitcher = createColorModeSwitcher() |
|
|
|
declare global { |
|
interface Window { |
|
__color_mode__: ColorModeSwitcher |
|
} |
|
} |
|
|
|
window.__color_mode__ = colorModeSwitcher |