PageSourceSearch

https://relays.app/lib/theme.js

js relays.app collected 2026-10-02 00:14:15 UTC 3,359 bytes, 97 lines download raw bytes

1/*!
2 * nodality v1.3.12
3 * (c) 2026 Filip Vabrousek
4 * License: MIT
5 */
6
7// Nodality Theme — central dark/light mode + event bus.
8// No CSS. Components declare per-mode inline styles via .set({ theme: {...} }).
9
10const STORAGE_KEY = "nodality.theme.mode";
11const EVENT_NAME = "nodality:theme";
12
13function detectInitialMode() {
14    try {
15        const saved = localStorage.getItem(STORAGE_KEY);
16        if (saved === "light" || saved === "dark") return saved;
17    } catch (e) {}
18    if (typeof window !== "undefined" && window.matchMedia) {
19        if (window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark";
20    }
21    return "light";
22}
23
24const Theme = {
25    mode: detectInitialMode(),
26    tokens: { light: {}, dark: {} },
27
28    // Built-in defaults applied to every component that did NOT declare its own
29    // .theme({...}) map or an inline color. The LIGHT default is black (#111)
30    // so text renders black out of the box without each project calling
31    // setDefaults() — components that set their own color inline always win
32    // (see _applyTheme: it never overwrites a user-set key).
33    //
34    // DARK is intentionally left EMPTY: setting a built-in dark color would
35    // turn text white for visitors whose OS prefers-color-scheme is dark
36    // (getInitialMode() → "dark"), making it invisible on light-background
37    // sites that don't implement a dark background. Sites that DO implement
38    // dark mode opt in per-project via Theme.setDefaults({ dark: {...} }).
39    //
40    // Per-component theming via .set({ theme: {...} }) works regardless.
41    defaults: {
42        light: { color: "#111" },
43        dark:  {}
44    },
45
46    setDefaults(map) {
47        if (map && typeof map === "object") {
48            if (map.light) this.defaults.light = Object.assign({}, this.defaults.light, map.light);
49            if (map.dark)  this.defaults.dark  = Object.assign({}, this.defaults.dark,  map.dark);
50            this._broadcast();
51        }
52        return this;
53    },
54
55    setTokens(tokens) {
56        if (tokens && typeof tokens === "object") {
57            if (tokens.light) Object.assign(this.tokens.light, tokens.light);
58            if (tokens.dark) Object.assign(this.tokens.dark, tokens.dark);
59        }
60        return this;
61    },
62
63    get current() {
64        return this.tokens[this.mode] || {};
65    },
66
67    set(mode) {
68        if (mode !== "light" && mode !== "dark") return this;
69        if (mode === this.mode) return this;
70        this.mode = mode;
71        try { localStorage.setItem(STORAGE_KEY, mode); } catch (e) {}
72        this._broadcast();
73        return this;
74    },
75
76    toggle() {
77        return this.set(this.mode === "dark" ? "light" : "dark");
78    },
79
80    subscribe(fn) {
81        if (typeof document === "undefined") return () => {};
82        const handler = (e) => fn(e.detail.mode, e.detail.tokens);
83        document.addEventListener(EVENT_NAME, handler);
84        // Fire once with current state so late subscribers sync immediately.
85        try { fn(this.mode, this.current); } catch (e) {}
86        return () => document.removeEventListener(EVENT_NAME, handler);
87    },
88
89    _broadcast() {
90        if (typeof document === "undefined") return;
91        document.dispatchEvent(new CustomEvent(EVENT_NAME, {
92            detail: { mode: this.mode, tokens: this.current }
93        }));
94    }
95};
96
97export { Theme };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.