PageSourceSearch

https://micro.ink/js/theme_manager.js?20260521.4

js micro.ink collected 2026-10-07 04:38:26 UTC 4,455 bytes, 160 lines download raw bytes

1const DEFAULT_THEME_URL = "/themes/default.json?20260216.1";
2const DARK_THEME_URL = "/themes/dark.json?20260217.1";
3
4let default_theme = create_theme("Default", "default", {});
5let dark_theme = create_theme("Dark", "dark", {});
6let active_theme = null;
7let is_loading = false;
8let color_scheme_query = null;
9let applied_color_keys = new Set();
10
11export async function initThemes() {
12	if (is_loading) {
13		return;
14	}
15
16	is_loading = true;
17
18	const [loaded_default, loaded_dark] = await Promise.all([
19		load_theme(DEFAULT_THEME_URL, "Default", "default"),
20		load_theme(DARK_THEME_URL, "Dark", "dark")
21	]);
22
23	default_theme = loaded_default;
24	dark_theme = merge_themes(default_theme, loaded_dark, "Dark", "dark");
25	apply_preferred_theme();
26	start_color_scheme_listener();
27	is_loading = false;
28}
29
30function apply_preferred_theme() {
31	const next_theme = prefers_dark_mode() ? dark_theme : default_theme;
32	apply_theme(next_theme);
33}
34
35function start_color_scheme_listener() {
36	if (color_scheme_query || typeof window == "undefined" || typeof window.matchMedia != "function") {
37		return;
38	}
39
40	color_scheme_query = window.matchMedia("(prefers-color-scheme: dark)");
41	const handle_change = () => {
42		apply_preferred_theme();
43	};
44
45	if (typeof color_scheme_query.addEventListener == "function") {
46		color_scheme_query.addEventListener("change", handle_change);
47		return;
48	}
49
50	if (typeof color_scheme_query.addListener == "function") {
51		color_scheme_query.addListener(handle_change);
52	}
53}
54
55function prefers_dark_mode() {
56	if (typeof window == "undefined" || typeof window.matchMedia != "function") {
57		return false;
58	}
59	return window.matchMedia("(prefers-color-scheme: dark)").matches;
60}
61
62function apply_theme(theme) {
63	if (!theme || typeof document == "undefined") {
64		return;
65	}
66
67	active_theme = theme;
68	const colors = theme.colors || {};
69	const color_entries = Object.entries(colors).filter(([key, value]) => {
70		return Boolean(key) && typeof value == "string";
71	});
72	const next_color_keys = new Set(color_entries.map(([key]) => key));
73
74	applied_color_keys.forEach((key) => {
75		if (!next_color_keys.has(key)) {
76			document.documentElement.style.removeProperty(key);
77		}
78	});
79
80	color_entries.forEach(([key, value]) => {
81		document.documentElement.style.setProperty(key, value);
82	});
83
84	applied_color_keys = next_color_keys;
85
86	document.documentElement.dataset.theme = theme.slug || "default";
87	window.dispatchEvent(new CustomEvent("theme:applied", { detail: { theme: active_theme } }));
88}
89
90async function load_theme(url, fallback_name, fallback_slug) {
91	try {
92		const response = await fetch(url, { cache: "no-store" });
93		if (!response.ok) {
94			throw new Error(`Failed to load theme from ${url}`);
95		}
96		const payload = await response.json();
97		return normalize_theme(payload, fallback_name, fallback_slug);
98	}
99	catch (error) {
100		console.warn(`Unable to load theme from ${url}`, error);
101		return create_theme(fallback_name, fallback_slug, {});
102	}
103}
104
105function normalize_theme(theme, fallback_name, fallback_slug) {
106	if (!theme || typeof theme != "object") {
107		return create_theme(fallback_name, fallback_slug, {});
108	}
109
110	const raw_name = typeof theme.name == "string" ? theme.name.trim() : "";
111	const name = raw_name || fallback_name;
112	const raw_slug = typeof theme.slug == "string" ? theme.slug.trim() : "";
113	const computed_slug = raw_slug || slugify(name) || fallback_slug;
114	const colors = theme.colors && typeof theme.colors == "object" ? theme.colors : {};
115	const cleaned_colors = {};
116
117	Object.entries(colors).forEach(([key, value]) => {
118		if (typeof key != "string" || typeof value != "string") {
119			return;
120		}
121
122		const trimmed_key = key.trim();
123		const trimmed_value = value.trim();
124		if (!trimmed_key || !trimmed_value) {
125			return;
126		}
127
128		cleaned_colors[trimmed_key] = trimmed_value;
129	});
130
131	return create_theme(name, computed_slug, cleaned_colors);
132}
133
134function merge_themes(base_theme, override_theme, fallback_name, fallback_slug) {
135	const base_colors = base_theme?.colors || {};
136	const override_colors = override_theme?.colors || {};
137	const name = override_theme?.name || fallback_name;
138	const slug = override_theme?.slug || fallback_slug;
139
140	return create_theme(name, slug, {
141		...base_colors,
142		...override_colors
143	});
144}
145
146function create_theme(name, slug, colors) {
147	return {
148		name,
149		slug,
150		colors: colors || {}
151	};
152}
153
154function slugify(value) {
155	return String(value || "")
156		.toLowerCase()
157		.replace(/[^a-z0-9]+/g, "-")
158		.replace(/^-+|-+$/g, "")
159		trim();
160}

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.