Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/static/theme.js

Line attribution on main · 133 line(s)

0584bb70
Keenan Nemetz
23h ago
1 // Everforest and a few named palettes are built in, and the instance repository
2 // may add theme packs of its own. The account page also carries a text-size
3 // preset. All of it is a browser preference: a saved choice wins; with no saved
4 // theme, the medium Everforest variant follows the operating system. This runs
5 // before the stylesheet links in the document head so neither choice flashes.
796ac8c0
Keenan Nemetz
11d ago
6 (() => {
7 const root = document.documentElement;
8 const themeKey = 'vole-theme';
9 const scaleKey = 'vole-font-scale';
10 const dark = window.matchMedia('(prefers-color-scheme: dark)');
f7a34d87
Keenan Nemetz
1d ago
11 // The server writes the instance's theme ids to data-themes before this
12 // script runs, so a saved custom choice is accepted on the first paint.
796ac8c0
Keenan Nemetz
11d ago
13 const themes = new Set([
14 'everforest-dark-hard',
15 'everforest-dark-medium',
16 'everforest-dark-soft',
17 'everforest-light-hard',
18 'everforest-light-medium',
19 'everforest-light-soft',
0584bb70
Keenan Nemetz
23h ago
20 'catppuccin-mocha',
21 'catppuccin-latte',
22 'nord',
23 'solarized-light',
97b3b022
Keenan Nemetz
23h ago
24 'rose-pine-moon',
25 'rose-pine-dawn',
26 'kanagawa-wave',
27 'dracula',
28 'tokyo-night',
29 'ayu-light',
f7a34d87
Keenan Nemetz
1d ago
30 ...(root.dataset.themes || '').split(',').filter(Boolean),
796ac8c0
Keenan Nemetz
11d ago
31 ]);
32 const scales = new Set(['small', 'medium', 'large']);
33
34 const systemTheme = () => (dark.matches ? 'everforest-dark-medium' : 'everforest-light-medium');
35
36 const read = (key) => {
37 try {
38 return localStorage.getItem(key);
39 } catch (err) {
40 return null;
41 }
42 };
43 const save = (key, value) => {
44 try {
45 localStorage.setItem(key, value);
46 } catch (err) {
47 // The page still switches; only the remembered choice is lost.
48 }
49 };
50 const clear = (key) => {
51 try {
52 localStorage.removeItem(key);
53 } catch (err) {
54 // Nothing to clear when storage is unavailable.
55 }
56 };
57
58 // A valid saved variant is an explicit choice. Anything else (including the
59 // old letterpress key) is cleared so the system default can apply.
60 let chosen = false;
61 const savedTheme = read(themeKey);
62 if (themes.has(savedTheme)) {
63 chosen = true;
64 root.dataset.theme = savedTheme;
65 } else {
66 if (savedTheme) clear(themeKey);
67 root.dataset.theme = systemTheme();
68 }
69
70 const savedScale = read(scaleKey);
71 if (scales.has(savedScale)) {
72 root.dataset.fontScale = savedScale;
73 } else {
74 if (savedScale) clear(scaleKey);
75 root.dataset.fontScale = 'medium';
76 }
77
78 const sync = () => {
79 const currentTheme = chosen ? root.dataset.theme : 'system';
80 document.querySelectorAll('[data-theme-select]').forEach((select) => {
81 const system = select.querySelector('option[value="system"]');
82 if (system) system.textContent = `System · ${dark.matches ? 'Dark' : 'Light'}`;
83 select.value = currentTheme;
84 });
85 const currentScale = root.dataset.fontScale || 'medium';
86 document.querySelectorAll('[data-font-scale-select]').forEach((select) => {
87 select.value = currentScale;
88 });
89 };
90
91 document.addEventListener('change', (event) => {
92 const target = event.target instanceof Element ? event.target : null;
93
94 const themeSelect = target && target.closest('[data-theme-select]');
95 if (themeSelect) {
96 const value = themeSelect.value;
97 if (value === 'system') {
98 chosen = false;
99 clear(themeKey);
100 root.dataset.theme = systemTheme();
101 } else if (themes.has(value)) {
102 chosen = true;
103 root.dataset.theme = value;
104 save(themeKey, value);
105 }
106 sync();
107 return;
108 }
109
110 const scaleSelect = target && target.closest('[data-font-scale-select]');
111 if (scaleSelect) {
112 const value = scaleSelect.value;
113 if (!scales.has(value)) return;
114 root.dataset.fontScale = value;
115 save(scaleKey, value);
116 sync();
117 }
118 });
119
120 const followSystem = () => {
121 if (chosen) return;
122 root.dataset.theme = systemTheme();
123 sync();
124 };
125 if (dark.addEventListener) dark.addEventListener('change', followSystem);
126 else if (dark.addListener) dark.addListener(followSystem);
127
128 if (document.readyState === 'loading') {
129 document.addEventListener('DOMContentLoaded', sync, { once: true });
130 } else {
131 sync();
132 }
133 })();