mirror of https://github.com/requarks/wiki
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.
103 lines
3.6 KiB
103 lines
3.6 KiB
/**
|
|
* Dark mode, for blocks.
|
|
*
|
|
* The app keeps one source of truth for the theme: a `body--dark` / `body--light` class on <body>,
|
|
* set by `frontend/src/composables/dark.js`. CSS inside a shadow root cannot see it. `:host-context()`
|
|
* is exactly the selector for that job and every block here used to key its dark mode off it -- but
|
|
* only Chromium ever shipped it. MDN has it deprecated, Firefox and Safari have never implemented it,
|
|
* and in an unsupported browser the rule simply never matches, so a block stayed light on a dark page
|
|
* with nothing to show that anything had gone wrong.
|
|
*
|
|
* So the class is read in JS instead and pushed into the blocks that ask for it. One observer serves
|
|
* the whole page however many blocks are on it, and stops again once the last of them has gone.
|
|
*/
|
|
|
|
/** @type {Set<(dark: boolean) => void>} */
|
|
const watchers = new Set()
|
|
let observer = null
|
|
|
|
/**
|
|
* Whether the app is in dark mode at this moment.
|
|
*/
|
|
export function isDark() {
|
|
return document.body.classList.contains('body--dark')
|
|
}
|
|
|
|
/**
|
|
* Call `onChange` with the new value whenever the theme changes, until the returned function is
|
|
* called.
|
|
*
|
|
* @param {(dark: boolean) => void} onChange
|
|
* @returns {() => void} stops the watching
|
|
*/
|
|
export function watchTheme(onChange) {
|
|
watchers.add(onChange)
|
|
if (!observer) {
|
|
observer = new MutationObserver(() => {
|
|
const dark = isDark()
|
|
for (const watcher of watchers) {
|
|
watcher(dark)
|
|
}
|
|
})
|
|
// -> `body--light` is set in the same breath as `body--dark`, so one attribute is all to watch
|
|
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] })
|
|
}
|
|
return () => {
|
|
watchers.delete(onChange)
|
|
// -> Nothing left to tell, so nothing left to listen for
|
|
if (watchers.size < 1) {
|
|
observer?.disconnect()
|
|
observer = null
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A Lit reactive controller that keeps a `dark` attribute on its host in step with the app's theme.
|
|
*
|
|
* A block that only changes colour needs no more than to construct one -- `:host([dark])` in its
|
|
* styles then says everything `:host-context(body.body--dark)` used to, at a specificity one class
|
|
* lower, which is still above the `:host` rule holding the light values. One that has to act on the
|
|
* change rather than restyle for it -- redrawing in a library's own dark theme, say -- passes
|
|
* `onChange`, or reads `isDark` off the controller at the moment it needs it.
|
|
*
|
|
* The attribute is on the host and not on an element inside the shadow root because the host is the
|
|
* one element a block always has. Where the shadow tree is a library's to arrange, an attribute
|
|
* bound in `render()` risks being written over -- see the `data-theme` note in `block-map`.
|
|
*/
|
|
export class DarkMode {
|
|
/**
|
|
* @param {import('lit').ReactiveElement} host
|
|
* @param {{ attribute?: boolean, onChange?: (dark: boolean) => void }} [options]
|
|
* `attribute: false` for a block that resolves the theme itself and would find a second answer
|
|
* sitting on the host misleading. `onChange` is called after the host has been told to update.
|
|
*/
|
|
constructor(host, { attribute = true, onChange = null } = {}) {
|
|
this.host = host
|
|
this.isDark = isDark()
|
|
this._attribute = attribute
|
|
this._onChange = onChange
|
|
this._unwatch = null
|
|
host.addController(this)
|
|
}
|
|
|
|
hostConnected() {
|
|
this._apply(isDark())
|
|
this._unwatch = watchTheme((dark) => this._apply(dark))
|
|
}
|
|
|
|
hostDisconnected() {
|
|
this._unwatch?.()
|
|
this._unwatch = null
|
|
}
|
|
|
|
_apply(dark) {
|
|
this.isDark = dark
|
|
if (this._attribute) {
|
|
this.host.toggleAttribute('dark', dark)
|
|
}
|
|
this.host.requestUpdate()
|
|
this._onChange?.(dark)
|
|
}
|
|
}
|