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.

Refresh the web surface with Everforest, readable type, and clearer navigation

796ac8c0ce77461174cf520d5fad4dea37744662 · Keenan Nemetz · 11d ago · parent ac00929f

server/web/static/theme.js +119

@@ -0,0 +1,119 @@
1 // Everforest is Vole's permanent theme, and the account page also carries a
2 // text-size preset. Both are browser preferences: a saved choice wins; with no
3 // saved theme, the medium variant follows the operating system. This runs before
4 // the stylesheet links in the document head so neither choice flashes.
5 (() => {
6 const root = document.documentElement;
7 const themeKey = 'vole-theme';
8 const scaleKey = 'vole-font-scale';
9 const dark = window.matchMedia('(prefers-color-scheme: dark)');
10 const themes = new Set([
11 'everforest-dark-hard',
12 'everforest-dark-medium',
13 'everforest-dark-soft',
14 'everforest-light-hard',
15 'everforest-light-medium',
16 'everforest-light-soft',
17 ]);
18 const scales = new Set(['small', 'medium', 'large']);
19
20 const systemTheme = () => (dark.matches ? 'everforest-dark-medium' : 'everforest-light-medium');
21
22 const read = (key) => {
23 try {
24 return localStorage.getItem(key);
25 } catch (err) {
26 return null;
27 }
28 };
29 const save = (key, value) => {
30 try {
31 localStorage.setItem(key, value);
32 } catch (err) {
33 // The page still switches; only the remembered choice is lost.
34 }
35 };
36 const clear = (key) => {
37 try {
38 localStorage.removeItem(key);
39 } catch (err) {
40 // Nothing to clear when storage is unavailable.
41 }
42 };
43
44 // A valid saved variant is an explicit choice. Anything else (including the
45 // old letterpress key) is cleared so the system default can apply.
46 let chosen = false;
47 const savedTheme = read(themeKey);
48 if (themes.has(savedTheme)) {
49 chosen = true;
50 root.dataset.theme = savedTheme;
51 } else {
52 if (savedTheme) clear(themeKey);
53 root.dataset.theme = systemTheme();
54 }
55
56 const savedScale = read(scaleKey);
57 if (scales.has(savedScale)) {
58 root.dataset.fontScale = savedScale;
59 } else {
60 if (savedScale) clear(scaleKey);
61 root.dataset.fontScale = 'medium';
62 }
63
64 const sync = () => {
65 const currentTheme = chosen ? root.dataset.theme : 'system';
66 document.querySelectorAll('[data-theme-select]').forEach((select) => {
67 const system = select.querySelector('option[value="system"]');
68 if (system) system.textContent = `System · ${dark.matches ? 'Dark' : 'Light'}`;
69 select.value = currentTheme;
70 });
71 const currentScale = root.dataset.fontScale || 'medium';
72 document.querySelectorAll('[data-font-scale-select]').forEach((select) => {
73 select.value = currentScale;
74 });
75 };
76
77 document.addEventListener('change', (event) => {
78 const target = event.target instanceof Element ? event.target : null;
79
80 const themeSelect = target && target.closest('[data-theme-select]');
81 if (themeSelect) {
82 const value = themeSelect.value;
83 if (value === 'system') {
84 chosen = false;
85 clear(themeKey);
86 root.dataset.theme = systemTheme();
87 } else if (themes.has(value)) {
88 chosen = true;
89 root.dataset.theme = value;
90 save(themeKey, value);
91 }
92 sync();
93 return;
94 }
95
96 const scaleSelect = target && target.closest('[data-font-scale-select]');
97 if (scaleSelect) {
98 const value = scaleSelect.value;
99 if (!scales.has(value)) return;
100 root.dataset.fontScale = value;
101 save(scaleKey, value);
102 sync();
103 }
104 });
105
106 const followSystem = () => {
107 if (chosen) return;
108 root.dataset.theme = systemTheme();
109 sync();
110 };
111 if (dark.addEventListener) dark.addEventListener('change', followSystem);
112 else if (dark.addListener) dark.addListener(followSystem);
113
114 if (document.readyState === 'loading') {
115 document.addEventListener('DOMContentLoaded', sync, { once: true });
116 } else {
117 sync();
118 }
119 })();