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.