Repository navigation
Expand file tree
/
Copy paththeme.js
More file actions
190 lines (169 loc) · 6.41 KB
/
Copy paththeme.js
File metadata and controls
190 lines (169 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* theme.js — Dark/Light Mode Manager + Scroll Elevation + Mobile Drawer Scrim
* Liquid Galaxy Docs
*
* NOTE: The no-FOUC inline script that runs BEFORE first paint lives in index.html <head>.
* This module runs after DOM is ready and wires up the toggle button.
*/
const STORAGE_KEY = 'lg-theme';
/**
* Returns current theme ('light' | 'dark') from the html element attribute.
*/
function getCurrentTheme() {
return document.documentElement.getAttribute('data-theme') || 'light';
}
/**
* Swaps the theme, cross-fading via View Transitions where supported.
*/
function applyTheme(theme, animated = false) {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Only a deliberate toggle animates: applyTheme runs more than once per
// load, and overlapping view transitions abort.
if (animated && document.startViewTransition && !prefersReduced) {
const vt = document.startViewTransition(() => _setTheme(theme));
// The callback still runs if the transition aborts, so swallow the rejection.
vt.ready?.catch(() => {});
vt.finished?.catch(() => {});
} else {
_setTheme(theme);
}
}
/**
* Sets data-theme on <html> and swaps the icon inside #theme-toggle.
*/
function _setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(STORAGE_KEY, theme);
const toggle = document.getElementById('theme-toggle');
if (!toggle) return;
const icon = toggle.querySelector('.material-icons');
if (icon) {
// Micro-animation: fade out → swap icon → fade in (no layout shift)
icon.style.opacity = '0';
setTimeout(() => {
icon.textContent = theme === 'dark' ? 'light_mode' : 'dark_mode';
icon.style.opacity = '1';
}, 150); // matches --md-sys-motion-duration-short-3
}
toggle.setAttribute(
'aria-label',
theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
);
}
/**
* Toggles between light and dark.
*/
function toggleTheme() {
const next = getCurrentTheme() === 'dark' ? 'light' : 'dark';
applyTheme(next, true);
}
/**
* Checks URL for iframe=true query param and attaches or removes token_dark.css dynamically.
* Does not pollute localStorage so normal viewing uses standard saved theme.
*/
function syncIframeTheme() {
const isIframeParam = window.location.href.indexOf('iframe=true') !== -1;
let link = document.getElementById('token-dark-stylesheet');
const toggle = document.getElementById('theme-toggle');
if (isIframeParam) {
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.classList.add('iframe-mode');
if (toggle) {
toggle.style.display = 'none';
}
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
link.id = 'token-dark-stylesheet';
link.href = 'token_dark.css';
document.head.appendChild(link);
}
} else {
if (toggle) {
toggle.style.display = '';
}
if (link) {
link.remove();
}
const saved = localStorage.getItem(STORAGE_KEY);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (prefersDark ? 'dark' : 'light');
applyTheme(theme);
}
}
// ── Wire up the theme toggle button ────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
syncIframeTheme();
const toggle = document.getElementById('theme-toggle');
const isIframeParam = window.location.href.indexOf('iframe=true') !== -1;
if (toggle) {
if (isIframeParam) {
toggle.style.display = 'none';
} else {
toggle.addEventListener('click', toggleTheme);
// Sync icon to current theme (may have been set by inline script)
applyTheme(getCurrentTheme());
}
}
});
window.addEventListener('hashchange', syncIframeTheme);
window.addEventListener('popstate', syncIframeTheme);
// ── Header scroll elevation ─────────────────────────────────────────────────
(function initScrollElevation() {
const header = document.querySelector('.app-bar');
if (!header) return;
// Determine which element to observe: the .content-area div (scrollable)
function getScrollTarget() {
return document.querySelector('.content-area');
}
function onScroll() {
const scrollTarget = getScrollTarget();
const scrolled = scrollTarget
? scrollTarget.scrollTop > 4
: window.scrollY > 4;
header.classList.toggle('scrolled', scrolled);
}
// Attach once DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const scrollTarget = getScrollTarget();
const el = scrollTarget || window;
el.addEventListener('scroll', onScroll, { passive: true });
});
})();
// ── Mobile Drawer Scrim ─────────────────────────────────────────────────────
(function initDrawerScrim() {
document.addEventListener('DOMContentLoaded', () => {
const scrim = document.getElementById('drawer-scrim');
const drawer = document.getElementById('drawer');
if (!scrim || !drawer) return;
// Close drawer when scrim is clicked
scrim.addEventListener('click', () => {
drawer.classList.add('collapsed');
scrim.classList.remove('visible');
});
// Observe drawer mutations to show/hide scrim
const observer = new MutationObserver(() => {
// On mobile viewports, show scrim when drawer is open
if (window.innerWidth < 960) {
const isOpen = !drawer.classList.contains('collapsed');
scrim.classList.toggle('visible', isOpen);
} else {
scrim.classList.remove('visible');
}
});
observer.observe(drawer, { attributes: true, attributeFilter: ['class'] });
// Also hide scrim when resizing past mobile breakpoint
window.addEventListener('resize', () => {
if (window.innerWidth >= 960) {
scrim.classList.remove('visible');
}
}, { passive: true });
// Close drawer on ESC key (accessibility)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !drawer.classList.contains('collapsed') && window.innerWidth < 960) {
drawer.classList.add('collapsed');
scrim.classList.remove('visible');
}
});
});
})();