|
4 | 4 | const SYSTEM_SNAPSHOT_KEY = "antora-theme-system-snapshot"; |
5 | 5 | const html = document.documentElement; |
6 | 6 | const darkThemeClass = "dark-theme"; |
| 7 | + /** Temporary class while the class-based theme crossfade runs (VT fallback). */ |
| 8 | + const themeAnimatingClass = "adt-theme-animating"; |
| 9 | + const themeAnimMs = 300; |
7 | 10 | const systemMq = window.matchMedia("(prefers-color-scheme: dark)"); |
| 11 | + const reducedMotionMq = window.matchMedia("(prefers-reduced-motion: reduce)"); |
| 12 | + let themeAnimTimer; |
8 | 13 |
|
9 | 14 | function systemPreferenceLabel() { |
10 | 15 | return systemMq.matches ? "dark" : "light"; |
11 | 16 | } |
12 | 17 |
|
| 18 | + function prefersReducedMotion() { |
| 19 | + return reducedMotionMq.matches; |
| 20 | + } |
| 21 | + |
13 | 22 | function captureSystemSnapshot() { |
14 | 23 | localStorage.setItem(SYSTEM_SNAPSHOT_KEY, systemPreferenceLabel()); |
15 | 24 | } |
|
31 | 40 | return "system"; |
32 | 41 | } |
33 | 42 |
|
34 | | - function applyVisibleTheme() { |
| 43 | + /** |
| 44 | + * Platform theme paint: View Transitions when available, else a short universal |
| 45 | + * CSS transition via html.adt-theme-animating. Skipped when reduced motion. |
| 46 | + */ |
| 47 | + function runThemePaint(updateFn, animate) { |
| 48 | + if (!animate || prefersReducedMotion()) { |
| 49 | + updateFn(); |
| 50 | + return; |
| 51 | + } |
| 52 | + if (typeof document.startViewTransition === "function") { |
| 53 | + document.startViewTransition(() => { |
| 54 | + updateFn(); |
| 55 | + }); |
| 56 | + return; |
| 57 | + } |
| 58 | + html.classList.add(themeAnimatingClass); |
| 59 | + // Ensure the browser applies the animating transition before the class flip. |
| 60 | + void html.offsetWidth; |
| 61 | + updateFn(); |
| 62 | + window.clearTimeout(themeAnimTimer); |
| 63 | + themeAnimTimer = window.setTimeout(() => { |
| 64 | + html.classList.remove(themeAnimatingClass); |
| 65 | + }, themeAnimMs); |
| 66 | + } |
| 67 | + |
| 68 | + function applyVisibleTheme(animate) { |
35 | 69 | const mode = getMode(); |
36 | 70 | let useDark; |
37 | 71 | if (mode === "system") { |
38 | 72 | useDark = systemMq.matches; |
39 | 73 | } else { |
40 | 74 | useDark = mode === "dark"; |
41 | 75 | } |
42 | | - if (useDark) { |
43 | | - html.classList.add(darkThemeClass); |
44 | | - } else { |
45 | | - html.classList.remove(darkThemeClass); |
46 | | - } |
47 | | - updateToggleLabel(); |
| 76 | + runThemePaint(() => { |
| 77 | + if (useDark) { |
| 78 | + html.classList.add(darkThemeClass); |
| 79 | + } else { |
| 80 | + html.classList.remove(darkThemeClass); |
| 81 | + } |
| 82 | + updateToggleLabel(); |
| 83 | + }, Boolean(animate)); |
48 | 84 | } |
49 | 85 |
|
50 | | - function setMode(next) { |
| 86 | + function setMode(next, animate) { |
51 | 87 | if (next === "system") { |
52 | 88 | localStorage.setItem(MODE_KEY, "system"); |
53 | 89 | clearSystemSnapshot(); |
54 | 90 | } else { |
55 | 91 | localStorage.setItem(MODE_KEY, next); |
56 | 92 | captureSystemSnapshot(); |
57 | 93 | } |
58 | | - applyVisibleTheme(); |
| 94 | + applyVisibleTheme(animate !== false); |
59 | 95 | } |
60 | 96 |
|
61 | 97 | function expireOverrideIfSystemChanged() { |
|
67 | 103 | return; |
68 | 104 | } |
69 | 105 | if (snapshot !== systemPreferenceLabel()) { |
70 | | - setMode("system"); |
| 106 | + setMode("system", true); |
71 | 107 | } |
72 | 108 | } |
73 | 109 |
|
74 | 110 | function onSystemThemeChange() { |
75 | 111 | const mode = getMode(); |
76 | 112 | if (mode === "system") { |
77 | | - applyVisibleTheme(); |
| 113 | + applyVisibleTheme(true); |
78 | 114 | return; |
79 | 115 | } |
80 | 116 | expireOverrideIfSystemChanged(); |
81 | 117 | } |
82 | 118 |
|
83 | 119 | function applyInitialTheme() { |
84 | 120 | expireOverrideIfSystemChanged(); |
85 | | - applyVisibleTheme(); |
| 121 | + applyVisibleTheme(false); |
86 | 122 | if (typeof systemMq.addEventListener === "function") { |
87 | 123 | systemMq.addEventListener("change", onSystemThemeChange); |
88 | 124 | } else { |
|
115 | 151 | } |
116 | 152 |
|
117 | 153 | function toggleTheme() { |
118 | | - setMode(isDark() ? "light" : "dark"); |
| 154 | + setMode(isDark() ? "light" : "dark", true); |
119 | 155 | const toggle = document.getElementById("theme-toggle"); |
120 | 156 | if (toggle) toggle.blur(); |
121 | 157 | } |
|
0 commit comments