Skip to content

Commit ba87c24

Browse files
committed
added dark mode persistence across pages
1 parent accf13b commit ba87c24

File tree

4 files changed

+500
-288
lines changed

4 files changed

+500
-288
lines changed

WFCalculator/script.js

Lines changed: 125 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -98,84 +98,137 @@ document.getElementById("goBackBtn").addEventListener("click", () => {
9898

9999
// Universal Dark Mode Toggle
100100
(function () {
101-
const STORAGE_KEY = "darkMode", body = document.body;
102-
103-
// Dark mode CSS
104-
const sheet = document.createElement("style");
105-
sheet.id = "universal-dark-mode-sheet";
106-
sheet.textContent = `
107-
.dark-mode {
108-
--dm-bg:#0f0f10;--dm-surface:#1e1e1e;--dm-text:#eaeaea;--dm-muted:#bdbdbd;--dm-accent:#eaeaea;
109-
}
110-
.dark-mode, .dark-mode body {background-color:var(--dm-bg)!important;color:var(--dm-text)!important;}
111-
.dark-mode .dm-btn {background:#2a2a2a!important;color:var(--dm-text)!important;border:1px solid rgba(200,200,200,0.45)!important;border-radius:8px!important;box-shadow:0 0 10px rgba(200,200,200,0.18)!important;}
112-
.dark-mode .dm-title {color:var(--dm-accent)!important;}
113-
.dark-mode .dm-note {color:var(--dm-muted)!important;font-style:italic;}
114-
.dark-mode .dm-panel {background:var(--dm-surface)!important;color:var(--dm-text)!important;border:1px solid rgba(255,255,255,0.03)!important;box-shadow:0 6px 18px rgba(0,0,0,0.35) inset!important;}
115-
.dark-mode * {transition:color 0.25s ease,background-color 0.25s ease,border-color 0.25s ease,box-shadow 0.25s ease!important;}
116-
`;
117-
document.head.appendChild(sheet);
118-
119-
// Toggle button
120-
const toggle = Object.assign(document.createElement("button"), {
121-
id: "dark-mode-toggle",
122-
textContent: "🌙",
123-
onclick() {
124-
const enabled = body.classList.toggle("dark-mode");
125-
toggle.textContent = enabled ? "☀️" : "🌙";
126-
localStorage.setItem(STORAGE_KEY, enabled ? "true" : "false");
127-
enabled ? applyDark() : restoreStyles();
128-
}
129-
});
130-
Object.assign(toggle.style, {
131-
position:"fixed",bottom:"15px",right:"15px",background:"transparent",
132-
border:"2px solid rgba(200,200,200,0.22)",borderRadius:"8px",
133-
fontSize:"1.4rem",cursor:"pointer",zIndex:2147483647,
134-
padding:"6px",lineHeight:"1",display:"flex",
135-
alignItems:"center",justifyContent:"center",
136-
transition:"transform 0.18s ease,border-color 0.18s ease,opacity 0.18s ease"
137-
});
138-
toggle.onmouseenter = () => { toggle.style.transform="scale(1.15)"; toggle.style.borderColor="rgba(200,200,200,0.7)"; };
139-
toggle.onmouseleave = () => { toggle.style.transform="scale(1)"; toggle.style.borderColor="rgba(200,200,200,0.22)"; };
140-
document.body.appendChild(toggle);
141-
142-
// Load preference
143-
const saved = localStorage.getItem(STORAGE_KEY);
144-
body.classList.toggle("dark-mode", saved === "true");
145-
toggle.textContent = saved === "true" ? "☀️" : "🌙";
146-
if (saved === "true") applyDark();
147-
148-
const brightness = rgb => (rgb[0]*299 + rgb[1]*587 + rgb[2]*114)/1000;
101+
const STORAGE_KEY = "darkMode", BODY = document.body;
102+
const TOGGLE_ID = "dark-mode-toggle", STYLE_ID = "universal-dark-mode-sheet";
103+
const SESSION_INIT = "dmInitialized", SESSION_USER_TOGGLED = "dmUserToggled";
104+
let applyTimer = 0, observer = null;
105+
106+
// insert stylesheet once
107+
if (!document.getElementById(STYLE_ID)) {
108+
const s = document.createElement("style");
109+
s.id = STYLE_ID;
110+
s.textContent = `
111+
.dark-mode {
112+
--dm-bg:#0f0f10;--dm-surface:#1e1e1e;--dm-text:#eaeaea;--dm-muted:#bdbdbd;--dm-accent:#eaeaea;
113+
}
114+
.dark-mode, .dark-mode body {background-color:var(--dm-bg) !important; color:var(--dm-text) !important;}
115+
.dark-mode .dm-btn {background:#2a2a2a !important; color:var(--dm-text) !important; border:1px solid rgba(200,200,200,0.45) !important; border-radius:8px !important; box-shadow:0 0 10px rgba(200,200,200,0.18) !important;}
116+
.dark-mode .dm-title {color:var(--dm-accent) !important;}
117+
.dark-mode .dm-note {color:var(--dm-muted) !important; font-style:italic;}
118+
.dark-mode .dm-panel {background:var(--dm-surface) !important; color:var(--dm-text) !important; border:1px solid rgba(255,255,255,0.03) !important; box-shadow:0 6px 18px rgba(0,0,0,0.35) inset !important;}
119+
.dm-transition * {transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease !important;}
120+
`;
121+
document.head.appendChild(s);
122+
}
123+
124+
function parseRgb(str) {
125+
if (!str) return null;
126+
const m = str.match(/rgba?\((\d+)[^\d]+(\d+)[^\d]+(\d+)/i);
127+
if (m) return [+m[1], +m[2], +m[3]];
128+
const h = (str.match(/^#([0-9a-f]{6})$/i) || [])[1];
129+
return h ? [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)] : null;
130+
}
131+
const brightness = rgb => rgb ? (rgb[0]*299 + rgb[1]*587 + rgb[2]*114)/1000 : 255;
149132

150133
function applyDark() {
151-
document.querySelectorAll("*").forEach(el => {
152-
if (el.dataset.dmProcessed) return;
153-
el.dataset.dmOriginalStyle = el.getAttribute("style")||"";
154-
const added = [];
155-
try {
156-
const cs = getComputedStyle(el), bg = cs.backgroundColor || "", color = cs.color || "", fs = parseFloat(cs.fontSize||0);
157-
if (el.tagName==="BUTTON" || el.getAttribute("role")==="button") added.push("dm-btn", el.classList.add("dm-btn"));
158-
else {
159-
if (/[\p{Emoji}]/u.test(el.textContent||"") || fs>=18) added.push("dm-title", el.classList.add("dm-title"));
160-
else if (fs<=12 || cs.fontStyle==="italic") added.push("dm-note", el.classList.add("dm-note"));
161-
if (bg && bg!=="rgba(0, 0, 0, 0)" && bg.match(/\d+/)) {
162-
const rgb = bg.match(/\d+/g).map(Number);
163-
if (brightness(rgb)>150) added.push("dm-panel", el.classList.add("dm-panel"));
134+
clearTimeout(applyTimer);
135+
applyTimer = setTimeout(() => requestAnimationFrame(() => {
136+
document.querySelectorAll("body *").forEach(el => {
137+
if (el.dataset.dmProcessed) return;
138+
try {
139+
el.dataset.dmOriginalStyle = el.getAttribute("style") || "";
140+
const cs = getComputedStyle(el), bg = cs.backgroundColor || "", fs = parseFloat(cs.fontSize || 0);
141+
const added = [];
142+
if (el.tagName === "BUTTON" || el.getAttribute("role") === "button" ||
143+
(el.tagName === "INPUT" && ["button","submit","reset"].includes((el.type||"").toLowerCase()))) {
144+
added.push("dm-btn"); el.classList.add("dm-btn");
145+
} else {
146+
const txt = (el.textContent || "").trim();
147+
if (/[\p{Emoji}]/u.test(txt) || fs >= 18) { added.push("dm-title"); el.classList.add("dm-title"); }
148+
else if (fs <= 12 || cs.fontStyle === "italic") { added.push("dm-note"); el.classList.add("dm-note"); }
149+
const rgb = parseRgb(bg);
150+
if ((rgb && brightness(rgb) > 150) || (cs.backgroundImage && cs.backgroundImage !== "none")) {
151+
added.push("dm-panel"); el.classList.add("dm-panel");
152+
}
164153
}
165-
if (el.tagName==="INPUT" && ["button","submit","reset"].includes((el.type||"").toLowerCase())) added.push("dm-btn", el.classList.add("dm-btn"));
166-
}
167-
if (added.length) el.dataset.dmAddedClasses = added.join(" ");
168-
} catch(e){}
169-
el.dataset.dmProcessed = "1";
170-
});
154+
if (added.length) el.dataset.dmAddedClasses = added.join(" ");
155+
el.dataset.dmProcessed = "1";
156+
} catch (e) {}
157+
});
158+
}), 50);
171159
}
172160

173161
function restoreStyles() {
174-
document.querySelectorAll("[data-dm-processed='1']").forEach(el=>{
175-
const orig = el.dataset.dmOriginalStyle||""; orig ? el.setAttribute("style",orig) : el.removeAttribute("style");
176-
(el.dataset.dmAddedClasses||"").split(/\s+/).forEach(c=>c&&el.classList.remove(c));
177-
delete el.dataset.dmProcessed; delete el.dataset.dmOriginalStyle; delete el.dataset.dmAddedClasses;
162+
clearTimeout(applyTimer);
163+
if (observer) observer.disconnect();
164+
document.querySelectorAll("[data-dm-processed='1']").forEach(el => {
165+
try {
166+
const orig = el.dataset.dmOriginalStyle || "";
167+
orig ? el.setAttribute("style", orig) : el.removeAttribute("style");
168+
(el.dataset.dmAddedClasses || "").split(/\s+/).forEach(c => c && el.classList.remove(c));
169+
delete el.dataset.dmProcessed; delete el.dataset.dmOriginalStyle; delete el.dataset.dmAddedClasses;
170+
} catch (e) {}
171+
});
172+
BODY.classList.remove("dark-mode");
173+
}
174+
175+
function ensureToggle() {
176+
let t = document.getElementById(TOGGLE_ID);
177+
if (!t) {
178+
t = Object.assign(document.createElement("button"), {
179+
id: TOGGLE_ID,
180+
textContent: "🌙",
181+
onclick() {
182+
const enabled = BODY.classList.toggle("dark-mode");
183+
this.textContent = enabled ? "☀️" : "🌙";
184+
sessionStorage.setItem(SESSION_USER_TOGGLED, "1");
185+
localStorage.setItem(STORAGE_KEY, enabled ? "true" : "false");
186+
if (enabled) {
187+
// one-time transition on first toggle
188+
BODY.classList.add("dm-transition");
189+
applyDark();
190+
startObserver();
191+
setTimeout(() => BODY.classList.remove("dm-transition"), 260);
192+
} else restoreStyles();
193+
}
194+
});
195+
Object.assign(t.style, { position:"fixed",bottom:"15px",right:"15px",background:"transparent",border:"2px solid rgba(200,200,200,0.22)",borderRadius:"8px",fontSize:"1.4rem",cursor:"pointer",zIndex:2147483647,padding:"6px",lineHeight:"1",display:"flex",alignItems:"center",justifyContent:"center",transition:"transform 0.18s ease,border-color 0.18s ease,opacity 0.18s ease" });
196+
t.onmouseenter = () => { t.style.transform="scale(1.15)"; t.style.borderColor="rgba(200,200,200,0.7)"; };
197+
t.onmouseleave = () => { t.style.transform="scale(1)"; t.style.borderColor="rgba(200,200,200,0.22)"; };
198+
document.body.appendChild(t);
199+
}
200+
return t;
201+
}
202+
203+
function startObserver() {
204+
observer && observer.disconnect();
205+
observer = new MutationObserver(muts => {
206+
if (!isEnabled()) return;
207+
for (const m of muts) if (m.addedNodes && m.addedNodes.length) { applyDark(); break; }
178208
});
179-
body.classList.remove("dark-mode");
209+
observer.observe(document.body, { childList: true, subtree: true });
180210
}
211+
212+
const isEnabled = () => BODY.classList.contains("dark-mode");
213+
214+
function init() {
215+
const toggle = ensureToggle();
216+
const saved = localStorage.getItem(STORAGE_KEY), inited = sessionStorage.getItem(SESSION_INIT);
217+
if (!inited) {
218+
sessionStorage.setItem(SESSION_INIT, "1");
219+
toggle.textContent = saved === "true" ? "☀️" : "🌙";
220+
return;
221+
}
222+
const userToggled = sessionStorage.getItem(SESSION_USER_TOGGLED);
223+
if (userToggled === "1" || saved === "true") {
224+
BODY.classList.toggle("dark-mode", saved === "true");
225+
toggle.textContent = saved === "true" ? "☀️" : "🌙";
226+
if (saved === "true") { applyDark(); startObserver(); }
227+
} else toggle.textContent = "🌙";
228+
}
229+
230+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init, { once: true });
231+
else init();
232+
233+
window.addEventListener("popstate", () => { clearTimeout(applyTimer); applyTimer = setTimeout(init, 40); });
181234
})();

0 commit comments

Comments
 (0)