Skip to content

Commit 64b7283

Browse files
committed
feat!: v1.0.0 system color-scheme preference and user override reset
BREAKING CHANGE: theme mode uses antora-theme-mode (system|dark|light). Legacy antora-theme is migrated once. First visit no longer pins a resolved theme in localStorage as a fixed dark/light. Made-with: Cursor
1 parent c439867 commit 64b7283

5 files changed

Lines changed: 161 additions & 67 deletions

File tree

CHANGELOG.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ Entries below are in **chronological order** by date: prior release, then the **
1414
* `package.json` `packageManager`: bump pnpm to **10.33.0** (matches `pnpm/action-setup` in CI, including link:https://github.com/antora-supplemental/antora-build-action[antora-build-action], so the action and Corepack no longer disagree).
1515
* GitHub Pages deploy workflow: use link:https://github.com/antora-supplemental/antora-build-action[antora-build-action] **v2** with `setup_pnpm: false`, `setup_node: false`, and `install_dependencies: false` after the job’s own `pnpm` / `setup-node` steps (no duplicate toolchain setup).
1616

17+
== [1.0.0] - 2026-04-26
18+
19+
Major release: *system preference* is the default (no more locking the first paint into `localStorage` as an explicit `dark`/`light` that ignored future OS changes).
20+
21+
* *Default mode* is *system*: the site follows `prefers-color-scheme` and **updates** when the OS/browser scheme changes.
22+
* *User override* (toggle): stores an explicit *dark* or *light* until the *next* `prefers-color-scheme` *change* event, at which point the mode resets to *system* and follows the new system value. (Migrated: legacy `antora-theme` in `localStorage` is read once and written to the new `antora-theme-mode` key.)
23+
* *First paint* (`head-meta` inline): matches the same `antora-theme-mode` / `antora-theme` and *system* rules to reduce FOUC.
24+
* *aria-label* on the theme toggle uses “Switch to …” wording.
25+
26+
link:changelog-details/2026-04-26%20-%201.0.0%20system%20theme%20preference.adoc[Detailed changelog — 2026-04-26]
27+
1728
== [0.1.6] - 2026-04-12
1829

1930
Bugfix: restore default Antora UI spacing between navbar dropdown labels and the caret (`navbar-link` padding no longer overridden).
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
= 1.0.0 — system theme preference (2026-04-26)
2+
3+
== Summary
4+
5+
`supplemental-ui/js/site-dark-mode.js` and the inline script in `supplemental-ui/partials/head-meta.hbs` now use a three-state model:
6+
7+
* `antora-theme-mode` in `localStorage`: `system` | `dark` | `light`
8+
* Legacy `antora-theme` (`dark` / `light` only) is migrated on first read
9+
10+
Default is *system*: `matchMedia('(prefers-color-scheme: dark)')` drives the `html.dark-theme` class. A `change` listener on that media query:
11+
12+
* If mode is `system`, applies the new scheme immediately.
13+
* If the user had chosen an explicit `dark` or `light`, the next OS scheme change clears the override and sets mode back to `system`, then applies the new system preference.
14+
15+
This matches the product rule: follow the OS until the user toggles; keep that override until the system color scheme changes again, then re-align with the system.
16+
17+
== Files
18+
19+
* `supplemental-ui/js/site-dark-mode.js` — mode keys, `applyVisibleTheme`, `onSystemThemeChange`, toggle sets explicit `dark`/`light`
20+
* `supplemental-ui/partials/head-meta.hbs` — inline pre-paint script aligned with the same rules
21+
22+
== Semver
23+
24+
Bumped to **1.0.0** because default storage/behavior changes (no longer persisting the first resolved theme as a fixed `dark`/`light` in `antora-theme` on every load).

package.json

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
1-
{
2-
"name": "antora-dark-theme",
3-
"version": "0.1.6",
4-
"description": "Dark mode supplemental UI theme for Antora documentation sites",
5-
"keywords": [
6-
"antora",
7-
"dark-mode",
8-
"theme",
9-
"documentation",
10-
"asciidoc"
11-
],
12-
"homepage": "https://antora-supplemental.github.io/antora-dark-theme",
13-
"repository": {
14-
"type": "git",
15-
"url": "git+https://github.com/antora-supplemental/antora-dark-theme.git"
16-
},
17-
"bugs": {
18-
"url": "https://github.com/antora-supplemental/antora-dark-theme/issues"
19-
},
20-
"license": "MIT",
21-
"author": "Ryan Johnson (https://github.com/AMDphreak)",
22-
"files": [
23-
"supplemental-ui/**/*"
24-
],
25-
"scripts": {
26-
"build": "antora antora-playbook.yml",
27-
"build:local": "antora antora-playbook-local.yml",
28-
"preview": "antora antora-playbook.yml",
29-
"serve": "pnpm build && npx http-server build/site -p 8080 -o",
30-
"clean": "rimraf build .cache"
31-
},
32-
"devDependencies": {
33-
"@antora/lunr-extension": "1.0.0-alpha.12",
34-
"@asciidoctor/tabs": "^1.0.0-beta.6",
35-
"antora": "^3.1.14",
36-
"biome": "^0.3.3",
37-
"http-server": "^14.1.1",
38-
"rimraf": "^6.0.1"
39-
},
40-
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
41-
}
1+
{
2+
"name": "antora-dark-theme",
3+
"version": "1.0.0",
4+
"description": "Dark mode supplemental UI theme for Antora documentation sites",
5+
"keywords": [
6+
"antora",
7+
"dark-mode",
8+
"theme",
9+
"documentation",
10+
"asciidoc"
11+
],
12+
"homepage": "https://antora-supplemental.github.io/antora-dark-theme",
13+
"repository": {
14+
"type": "git",
15+
"url": "git+https://github.com/antora-supplemental/antora-dark-theme.git"
16+
},
17+
"bugs": {
18+
"url": "https://github.com/antora-supplemental/antora-dark-theme/issues"
19+
},
20+
"license": "MIT",
21+
"author": "Ryan Johnson (https://github.com/AMDphreak)",
22+
"files": [
23+
"supplemental-ui/**/*"
24+
],
25+
"scripts": {
26+
"build": "antora antora-playbook.yml",
27+
"build:local": "antora antora-playbook-local.yml",
28+
"preview": "antora antora-playbook.yml",
29+
"serve": "pnpm build && npx http-server build/site -p 8080 -o",
30+
"clean": "rimraf build .cache"
31+
},
32+
"devDependencies": {
33+
"@antora/lunr-extension": "1.0.0-alpha.12",
34+
"@asciidoctor/tabs": "^1.0.0-beta.6",
35+
"antora": "^3.1.14",
36+
"biome": "^0.3.3",
37+
"http-server": "^14.1.1",
38+
"rimraf": "^6.0.1"
39+
},
40+
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
41+
}

supplemental-ui/js/site-dark-mode.js

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
11
(function () {
2-
const themeKey = "antora-theme";
2+
const MODE_KEY = "antora-theme-mode";
3+
const LEGACY_KEY = "antora-theme";
34
const html = document.documentElement;
45
const darkThemeClass = "dark-theme";
56

6-
function setTheme(theme) {
7-
if (theme === "dark") {
7+
function getMode() {
8+
const m = localStorage.getItem(MODE_KEY);
9+
if (m === "system" || m === "dark" || m === "light") return m;
10+
const leg = localStorage.getItem(LEGACY_KEY);
11+
if (leg === "dark" || leg === "light") {
12+
localStorage.setItem(MODE_KEY, leg);
13+
localStorage.removeItem(LEGACY_KEY);
14+
return leg;
15+
}
16+
return "system";
17+
}
18+
19+
function applyVisibleTheme() {
20+
const mode = getMode();
21+
let useDark;
22+
if (mode === "system") {
23+
useDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
24+
} else {
25+
useDark = mode === "dark";
26+
}
27+
if (useDark) {
828
html.classList.add(darkThemeClass);
929
} else {
1030
html.classList.remove(darkThemeClass);
1131
}
12-
localStorage.setItem(themeKey, theme);
1332
updateToggleLabel();
1433
}
1534

35+
function setMode(next) {
36+
if (next === "system") {
37+
localStorage.setItem(MODE_KEY, "system");
38+
} else {
39+
localStorage.setItem(MODE_KEY, next);
40+
}
41+
applyVisibleTheme();
42+
}
43+
1644
function isDark() {
1745
return html.classList.contains(darkThemeClass);
1846
}
@@ -23,28 +51,36 @@
2351
if (isDark()) {
2452
toggle.innerHTML =
2553
'<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';
26-
toggle.setAttribute("aria-label", "Light mode");
54+
toggle.setAttribute("aria-label", "Switch to light mode");
2755
} else {
2856
toggle.innerHTML =
2957
'<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>';
30-
toggle.setAttribute("aria-label", "Dark mode");
58+
toggle.setAttribute("aria-label", "Switch to dark mode");
3159
}
3260
}
3361

3462
function toggleTheme() {
35-
setTheme(isDark() ? "light" : "dark");
63+
setMode(isDark() ? "light" : "dark");
64+
const toggle = document.getElementById("theme-toggle");
65+
if (toggle) toggle.blur();
3666
}
3767

38-
function applyInitialTheme() {
39-
const savedTheme = localStorage.getItem(themeKey);
40-
if (savedTheme === "dark" || savedTheme === "light") {
41-
setTheme(savedTheme);
42-
return;
68+
function onSystemThemeChange() {
69+
const mode = getMode();
70+
if (mode === "system") {
71+
applyVisibleTheme();
72+
} else {
73+
setMode("system");
4374
}
44-
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
45-
setTheme("dark");
75+
}
76+
77+
function applyInitialTheme() {
78+
applyVisibleTheme();
79+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
80+
if (typeof mq.addEventListener === "function") {
81+
mq.addEventListener("change", onSystemThemeChange);
4682
} else {
47-
setTheme("light");
83+
mq.addListener(onSystemThemeChange);
4884
}
4985
}
5086

@@ -89,16 +125,26 @@
89125
function getRepoUrl() {
90126
const meta = document.querySelector('meta[name="antora-repo-url"]');
91127
if (meta && meta.content) return meta.content;
92-
const editLink = document.querySelector('.navbar-end a[href*="/edit/"], .navbar-end a[href*="/-/edit/"], .navbar-end a[href*="/blob/"]');
128+
const editLink = document.querySelector(
129+
'.navbar-end a[href*="/edit/"], .navbar-end a[href*="/-/edit/"], .navbar-end a[href*="/blob/"]'
130+
);
93131
if (editLink && editLink.href) {
94132
try {
95133
const u = new URL(editLink.href);
96134
const pathParts = u.pathname.split("/").filter(Boolean);
97-
if (u.hostname.includes("github") && pathParts.length >= 2) return u.origin + "/" + pathParts.slice(0, 2).join("/");
98-
if (u.hostname.includes("gitlab") && pathParts.length >= 2) return u.origin + "/" + pathParts.slice(0, 2).join("/");
99-
if (u.hostname.includes("bitbucket") && pathParts.length >= 2) return u.origin + "/" + pathParts.slice(0, 2).join("/");
135+
if (u.hostname.includes("github") && pathParts.length >= 2) {
136+
return u.origin + "/" + pathParts.slice(0, 2).join("/");
137+
}
138+
if (u.hostname.includes("gitlab") && pathParts.length >= 2) {
139+
return u.origin + "/" + pathParts.slice(0, 2).join("/");
140+
}
141+
if (u.hostname.includes("bitbucket") && pathParts.length >= 2) {
142+
return u.origin + "/" + pathParts.slice(0, 2).join("/");
143+
}
100144
if (pathParts.length >= 2) return u.origin + "/" + pathParts.slice(0, 2).join("/");
101-
} catch {}
145+
} catch {
146+
// ignore
147+
}
102148
}
103149
return null;
104150
}
@@ -112,7 +158,9 @@
112158
const u = new URL(script.src);
113159
u.pathname = u.pathname.replace(/\/[^/]*$/, "/");
114160
return u.pathname + u.search || ".";
115-
} catch (_e) {}
161+
} catch (_e) {
162+
// ignore
163+
}
116164
}
117165
return ".";
118166
}

supplemental-ui/partials/head-meta.hbs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@
44
{{/if}}
55
<script>
66
(function () {
7-
const themeKey = 'antora-theme';
8-
const savedTheme = localStorage.getItem(themeKey);
9-
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
10-
document.documentElement.classList.add('dark-theme');
7+
const MODE = 'antora-theme-mode'
8+
const LEGACY = 'antora-theme'
9+
function mode() {
10+
const m = localStorage.getItem(MODE)
11+
if (m === 'system' || m === 'dark' || m === 'light') return m
12+
const o = localStorage.getItem(LEGACY)
13+
if (o === 'dark' || o === 'light') return o
14+
return 'system'
1115
}
12-
})();
16+
function isDark() {
17+
const m = mode()
18+
if (m === 'dark') return true
19+
if (m === 'light') return false
20+
return window.matchMedia('(prefers-color-scheme: dark)').matches
21+
}
22+
if (isDark()) document.documentElement.classList.add('dark-theme')
23+
})()
1324
</script>

0 commit comments

Comments
 (0)