← back to Rentv
Add site-wide user day/night toggle (sun/moon icon) with FOUC guard
b0e2eac5a1ad91be7744c320dcdde791e9f597ec · 2026-08-04 13:43:11 -0700 · steve
Files touched
A public/daynight.jsM server.js
Diff
commit b0e2eac5a1ad91be7744c320dcdde791e9f597ec
Author: steve <steve@designerwallcoverings.com>
Date: Tue Aug 4 13:43:11 2026 -0700
Add site-wide user day/night toggle (sun/moon icon) with FOUC guard
---
public/daynight.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 11 ++++++
2 files changed, 117 insertions(+)
diff --git a/public/daynight.js b/public/daynight.js
new file mode 100644
index 00000000..2d56771a
--- /dev/null
+++ b/public/daynight.js
@@ -0,0 +1,106 @@
+/* RENTV day/night toggle — a simple, user-facing light⇄dark switch with a
+ * sun/moon icon, available to EVERY visitor on EVERY page (injected site-wide
+ * by server.js, mirroring toggle.js). $0/local.
+ *
+ * It speaks the SAME contract as theme.js: it sets the CSS custom properties
+ * (--red/--ink/--sub/--line/--bg/--wash/--serif/--sans) on <html> and toggles
+ * body.theme-dark, so it renders correctly on any page whether or not the
+ * 10-theme power picker (theme.js) is also loaded. Two clean states only:
+ * DAY = RENTV Classic (light, the site default)
+ * NIGHT = Midnight Capital (a polished navy/gold dark theme)
+ *
+ * FOUC note: the pre-apply that runs before paint lives inline in <head>
+ * (injected by server.js). This file just mounts the button + wires clicks. */
+(function () {
+ if (window.__rentvDayNight) return; window.__rentvDayNight = 1;
+
+ var DAY = {
+ dark:false, accent:"#c8102e", ink:"#14171a", sub:"#5b636b", line:"#e4e7ea",
+ bg:"#ffffff", wash:"#f5f6f8",
+ serif:"'Playfair Display', Georgia, serif", sans:"'Inter', system-ui, sans-serif"
+ };
+ var NIGHT = {
+ dark:true, accent:"#c9a84c", ink:"#f0f2f5", sub:"#8fa3b8", line:"#1e2d40",
+ bg:"#0d1b2a", wash:"#132336",
+ serif:"'DM Serif Display', Georgia, serif", sans:"'Archivo', system-ui, sans-serif"
+ };
+ var KEY = 'rentv-daynight';
+
+ // Dark-surface overrides — same fix theme.js applies: the design system overloads
+ // var(--ink) as both text AND dark-surface bg (util bar / footer / subscribe box).
+ // In night mode --ink is light, so re-point those surfaces to --wash and their text
+ // to --sub/--ink. Guarded by an id so it isn't duplicated if theme.js is also present.
+ if (!document.getElementById('rentv-dark-surfaces')) {
+ var st = document.createElement('style'); st.id = 'rentv-dark-surfaces';
+ st.textContent =
+ "body.theme-dark .util,body.theme-dark footer,body.theme-dark .ns,body.theme-dark .mast{background:var(--wash)!important}" +
+ "body.theme-dark #rentv-footer{background:var(--wash)!important}" +
+ "body.theme-dark .util,body.theme-dark .util a,body.theme-dark footer,body.theme-dark footer a,body.theme-dark footer p{color:var(--sub)!important}" +
+ "body.theme-dark footer .logo,body.theme-dark footer h5,body.theme-dark #rentv-footer .rf-brand{color:var(--ink)!important}" +
+ "body.theme-dark .ns{color:var(--ink)!important}body.theme-dark .ns p{color:var(--sub)!important}" +
+ "body.theme-dark .card .img,body.theme-dark .lead .img{filter:brightness(.85)}";
+ document.head.appendChild(st);
+ }
+
+ // Button chrome. Fixed bottom-RIGHT so it never collides with the admin
+ // Front/Backend toggle (bottom-left, admin-only).
+ var css = document.createElement('style');
+ css.textContent =
+ '#rentv-daynight{position:fixed;right:16px;bottom:16px;z-index:2147482000;' +
+ 'width:42px;height:42px;display:flex;align-items:center;justify-content:center;' +
+ 'border-radius:999px;cursor:pointer;border:1px solid var(--line,#e4e7ea);' +
+ 'background:var(--bg,#fff);color:var(--ink,#14171a);' +
+ 'box-shadow:0 4px 16px rgba(0,0,0,.18);transition:transform .15s,box-shadow .15s,background .2s,color .2s;' +
+ '-webkit-tap-highlight-color:transparent;}' +
+ '#rentv-daynight:hover{transform:translateY(-2px);box-shadow:0 7px 22px rgba(0,0,0,.26);}' +
+ '#rentv-daynight:active{transform:translateY(0);}' +
+ '#rentv-daynight svg{width:20px;height:20px;display:block;}' +
+ '@media(max-width:640px){#rentv-daynight{width:38px;height:38px;right:12px;bottom:12px;}#rentv-daynight svg{width:18px;height:18px;}}';
+ document.head.appendChild(css);
+
+ var SUN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2v2.2M12 19.8V22M4.2 4.2l1.6 1.6M18.2 18.2l1.6 1.6M2 12h2.2M19.8 12H22M4.2 19.8l1.6-1.6M18.2 5.8l1.6-1.6"/></svg>';
+ var MOON = '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M20.5 14.2A8.3 8.3 0 0 1 9.8 3.5a.7.7 0 0 0-.94-.86A9.7 9.7 0 1 0 21.4 15.1a.7.7 0 0 0-.9-.9z"/></svg>';
+
+ function isNight() {
+ try {
+ var v = localStorage.getItem(KEY);
+ if (v === 'night') return true;
+ if (v === 'day') return false;
+ } catch (e) {}
+ // First visit with no stored preference → follow the OS setting.
+ return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ }
+
+ function apply(night) {
+ var t = night ? NIGHT : DAY, r = document.documentElement;
+ r.style.setProperty('--red', t.accent); r.style.setProperty('--ink', t.ink);
+ r.style.setProperty('--sub', t.sub); r.style.setProperty('--line', t.line);
+ r.style.setProperty('--bg', t.bg); r.style.setProperty('--wash', t.wash);
+ r.style.setProperty('--serif', t.serif); r.style.setProperty('--sans', t.sans);
+ document.body.classList.toggle('theme-dark', night);
+ var btn = document.getElementById('rentv-daynight');
+ if (btn) {
+ btn.innerHTML = night ? SUN : MOON; // show the action, not the state
+ btn.setAttribute('aria-label', night ? 'Switch to day mode' : 'Switch to night mode');
+ btn.setAttribute('title', night ? 'Day mode' : 'Night mode');
+ btn.setAttribute('aria-pressed', night ? 'true' : 'false');
+ }
+ }
+
+ function mount() {
+ if (document.getElementById('rentv-daynight')) return;
+ var btn = document.createElement('button');
+ btn.id = 'rentv-daynight'; btn.type = 'button';
+ document.body.appendChild(btn);
+ var night = isNight();
+ apply(night);
+ btn.addEventListener('click', function () {
+ night = !document.body.classList.contains('theme-dark');
+ apply(night);
+ try { localStorage.setItem(KEY, night ? 'night' : 'day'); } catch (e) {}
+ });
+ }
+
+ if (document.body) mount();
+ else document.addEventListener('DOMContentLoaded', mount);
+})();
diff --git a/server.js b/server.js
index d27a8c82..caafa010 100644
--- a/server.js
+++ b/server.js
@@ -53,6 +53,12 @@ function adminOnly(req, res, next) {
// </body>. Used by every explicit page route AND the static HTML injector below, so the
// toggle is present site-wide from a single include (the widget self-hides for user-tier).
const TOGGLE_TAG = '<script src="/toggle.js" defer></script>';
+// User-facing day/night toggle (sun/moon icon, bottom-right), shown site-wide to EVERY
+// visitor. DAYNIGHT_TAG mounts the button; DAYNIGHT_PREAPPLY runs inline in <head> to set
+// the dark CSS vars before first paint so night mode never flashes white (FOUC guard).
+const DAYNIGHT_TAG = '<script src="/daynight.js" defer></script>';
+const DAYNIGHT_PREAPPLY = `<style id="rentv-dn-fouc">html.rentv-night{background:#0d1b2a;color:#f0f2f5}</style>` +
+ `<script id="rentv-dn-pre">(function(){try{var k=localStorage.getItem('rentv-daynight');var n=k==='night'||(k==null&&window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches);if(!n)return;var r=document.documentElement,s=r.style;s.setProperty('--red','#c9a84c');s.setProperty('--ink','#f0f2f5');s.setProperty('--sub','#8fa3b8');s.setProperty('--line','#1e2d40');s.setProperty('--bg','#0d1b2a');s.setProperty('--wash','#132336');s.setProperty('--serif',"'DM Serif Display', Georgia, serif");s.setProperty('--sans',"'Archivo', system-ui, sans-serif");r.classList.add('rentv-night');}catch(e){}})();</script>`;
// Professional full-width site footer, injected on front-end pages (skipped on internal/admin
// shells). Hides any pre-existing per-page <footer> so there's exactly one, consistent footer.
const FOOTER_TAG = `<style id="rentv-footer-css">
@@ -88,6 +94,11 @@ function sendPage(res, absFile) {
if (html.indexOf('/toggle.js') === -1) {
html = closeBody() ? html.replace('</body>', TOGGLE_TAG + '</body>') : html + TOGGLE_TAG;
}
+ // Day/night toggle for all visitors: FOUC pre-apply in <head>, button before </body>.
+ if (html.indexOf('/daynight.js') === -1) {
+ html = html.indexOf('</head>') !== -1 ? html.replace('</head>', DAYNIGHT_PREAPPLY + '</head>') : DAYNIGHT_PREAPPLY + html;
+ html = closeBody() ? html.replace('</body>', DAYNIGHT_TAG + '</body>') : html + DAYNIGHT_TAG;
+ }
if (!INTERNAL_PAGE.test(absFile) && html.indexOf('rentv-footer') === -1) {
html = closeBody() ? html.replace('</body>', FOOTER_TAG + '</body>') : html + FOOTER_TAG;
}
← 15a1f9ba auto-save: 2026-08-04T13:33:48 (3 files) — data/markets.json
·
back to Rentv
·
auto-save: 2026-08-04T14:04:01 (3 files) — data/markets.json e32c9d29 →