Add light/dark theme toggle to all pages

- Flash-prevention script in <head> reads radical_theme from localStorage and
  sets data-theme on <html> before first paint — no FOUC
- Fixed ☀️/🌙 button (top-right, z-index 9999) on index, admin, and login using
  CSS ::before so the icon is driven by data-theme with no JS initialization
- Light mode: #F5F3EF warm off-white body, #E8E6E2 cards, #CCC borders, #111 text
- Pink accent (#ff0099) unchanged in both modes
- Signed-in users: theme saved to radical_user localStorage object and applied
  on load so preference follows their session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
King Omar 2026-07-12 16:50:24 +10:00
parent ad058c1978
commit 942da4b6a5
4 changed files with 202 additions and 0 deletions

View file

@ -105,9 +105,37 @@ body{font-family:'Roboto Mono',monospace;background:var(--bg);color:var(--text);
.recent-time{font-size:11px;color:var(--muted);white-space:nowrap} .recent-time{font-size:11px;color:var(--muted);white-space:nowrap}
.loading{text-align:center;padding:40px;color:var(--muted)} .loading{text-align:center;padding:40px;color:var(--muted)}
/* ── Theme toggle ── */
#theme-toggle{position:fixed;top:14px;right:14px;z-index:9999;background:rgba(0,0,0,.55);border:1px solid #444;border-radius:50%;width:38px;height:38px;font-size:0;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s,border-color .2s;padding:0}
#theme-toggle::before{content:"☀️";font-size:17px;line-height:1}
#theme-toggle:hover{background:rgba(255,0,153,.18);border-color:#ff0099}
/* ── Light mode ── */
html[data-theme="light"]{--bg:#F5F3EF;--card:#E8E6E2;--border:#CCCCCC;--text:#333;--muted:#777}
html[data-theme="light"] body{background:var(--bg);color:var(--text)}
html[data-theme="light"] #theme-toggle{background:rgba(245,243,239,.88);border-color:#BBB}
html[data-theme="light"] #theme-toggle::before{content:"🌙"}
html[data-theme="light"] #theme-toggle:hover{background:rgba(255,0,153,.1);border-color:#ff0099}
html[data-theme="light"] .top-bar{background:#E0DED9}
html[data-theme="light"] .stat-value{color:#111}
html[data-theme="light"] .aec-track{background:#CCC}
html[data-theme="light"] .state-track{background:#CCC}
html[data-theme="light"] .data-table th{background:#DDD;color:#555}
html[data-theme="light"] .data-table td{border-bottom-color:#DDD}
html[data-theme="light"] .data-table tr:hover td{background:#DEDAD6}
html[data-theme="light"] .data-table .name-cell{color:#111}
html[data-theme="light"] #pw-input{background:#FFF;color:#111;border-color:#CCC}
html[data-theme="light"] .search-box{background:#FFF;color:#111;border-color:#CCC}
html[data-theme="light"] .state-filter{background:#FFF;color:#111;border-color:#CCC}
html[data-theme="light"] .petition-row{border-bottom-color:#DDD}
html[data-theme="light"] .petition-row:hover{background:#DEDAD6}
html[data-theme="light"] .recent-row{border-bottom-color:#DDD}
html[data-theme="light"] .recent-row:hover{background:#DEDAD6}
html[data-theme="light"] .recent-name{color:#111}
</style> </style>
<script>(function(){try{if(localStorage.getItem('radical_theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}})();</script>
</head> </head>
<body> <body>
<button id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark mode"></button>
<!-- Auth gate --> <!-- Auth gate -->
<div id="auth-gate"> <div id="auth-gate">
@ -394,6 +422,12 @@ function fmtDate(ts) {
function esc(s) { function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
} }
function toggleTheme() {
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('radical_theme', next); } catch(e) {}
}
</script> </script>
</body> </body>
</html> </html>

View file

@ -250,6 +250,59 @@
.postcode-badge span{color:#ff0099} .postcode-badge span{color:#ff0099}
/* ── Inline milestone strip on proposal cards ── */ /* ── Inline milestone strip on proposal cards ── */
.card-milestone{margin-top:8px} .card-milestone{margin-top:8px}
/* ── LIGHT MODE: index.html-specific overrides ── */
html[data-theme="light"] .stats-bar{background:#E8E6E2;border-color:#CCC;color:#555}
html[data-theme="light"] .stat-divider{color:#BBB}
html[data-theme="light"] .leaderboard-section{border-color:#ff0099}
html[data-theme="light"] .leaderboard-item{border-bottom-color:#DDD}
html[data-theme="light"] .leaderboard-item:hover{background:#DEDAD6}
html[data-theme="light"] .leaderboard-text{color:#111}
html[data-theme="light"] .leaderboard-rank{color:#777}
html[data-theme="light"] .leaderboard-postcode{color:#777}
html[data-theme="light"] .milestone-bar-track{background:#CCC}
html[data-theme="light"] .milestone-label{color:#666}
html[data-theme="light"] .membership-modal-overlay{background:rgba(0,0,0,.55)}
html[data-theme="light"] .membership-modal{background:#F5F3EF}
html[data-theme="light"] .membership-modal-intro{color:#555}
html[data-theme="light"] .mm-input{background:#FFF;border-color:#CCC;color:#111}
html[data-theme="light"] .mm-input option{background:#FFF;color:#111}
html[data-theme="light"] .mm-field label{color:#555}
html[data-theme="light"] .mm-check label{color:#555}
html[data-theme="light"] .mm-later{color:#777}
html[data-theme="light"] .mm-later:hover{color:#444}
html[data-theme="light"] .email-modal-overlay{background:rgba(0,0,0,.55)}
html[data-theme="light"] .email-modal{background:#F5F3EF}
html[data-theme="light"] .email-modal p{color:#555}
html[data-theme="light"] .email-modal-input{background:#FFF;border-color:#CCC;color:#111}
html[data-theme="light"] .email-modal-skip{color:#777}
html[data-theme="light"] .email-modal-skip:hover{color:#333}
html[data-theme="light"] .constitution-section{background:#EBEBEA;border-color:#CCC}
html[data-theme="light"] .constitution-header{background:#E0DED9}
html[data-theme="light"] .constitution-header:hover{background:#D8D6D2}
html[data-theme="light"] .constitution-title{color:#111}
html[data-theme="light"] .constitution-meta{color:#777}
html[data-theme="light"] .constitution-body{background:#F5F3EF}
html[data-theme="light"] .constitution-draft-notice{background:#FFF8EE;border-color:#E8CFA0;color:#555}
html[data-theme="light"] .constitution-adopted{color:#777}
html[data-theme="light"] .const-section{border-bottom-color:#DDD}
html[data-theme="light"] .const-clause{color:#333}
html[data-theme="light"] .const-clause strong{color:#111}
html[data-theme="light"] .const-clause em{color:#555}
html[data-theme="light"] .const-sub{color:#555}
html[data-theme="light"] .const-end{color:#777;border-top-color:#DDD}
html[data-theme="light"] .about-section{background:#EBEBEA;border-color:#CCC}
html[data-theme="light"] .about-body{color:#333}
html[data-theme="light"] .about-body strong{color:#111}
html[data-theme="light"] .about-lead{color:#222}
html[data-theme="light"] .about-divider{border-top-color:#DDD}
html[data-theme="light"] .about-switzerland{background:#E0DED9;border-color:#CCC}
html[data-theme="light"] .about-swiss-heading{color:#111}
html[data-theme="light"] .about-step{background:#E0DED9}
html[data-theme="light"] .about-footer-note{color:#777;border-top-color:#DDD}
html[data-theme="light"] .join-input{background:#FFF;border-color:#CCC;color:#111}
html[data-theme="light"] .join-input option{background:#FFF;color:#111}
html[data-theme="light"] .join-field label{color:#555}
html[data-theme="light"] .join-check label{color:#555}
</style> </style>
<!-- Crawling --> <!-- Crawling -->
@ -332,8 +385,10 @@
} }
</script> </script>
<script>(function(){try{if(localStorage.getItem('radical_theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}})();</script>
</head> </head>
<body> <body>
<button id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark mode"></button>
<div class="container"> <div class="container">
<header> <header>
<h1>RADICAL</h1> <h1>RADICAL</h1>
@ -765,9 +820,23 @@ function getMediaURL(path) {
}); });
// Initialize application // Initialize application
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem('radical_theme', t); } catch(e) {}
}
function toggleTheme() {
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
applyTheme(next);
if (typeof currentUser !== 'undefined' && currentUser?.id) {
currentUser.theme = next;
try { localStorage.setItem('radical_user', JSON.stringify(currentUser)); } catch(e) {}
}
}
async function initializeApp() { async function initializeApp() {
updateUIText(); updateUIText();
await loadUserData(); await loadUserData();
if (currentUser?.theme) applyTheme(currentUser.theme);
loadVerifiedUsers(); loadVerifiedUsers();
setupEventListeners(); setupEventListeners();
await Promise.allSettled([fetchProposals(), fetchLeaderboard(), fetchStats()]); await Promise.allSettled([fetchProposals(), fetchLeaderboard(), fetchStats()]);

View file

@ -583,9 +583,37 @@
color: #444; color: #444;
line-height: 1.5; line-height: 1.5;
} }
/* ── Theme toggle ── */
#theme-toggle{position:fixed;top:14px;right:14px;z-index:9999;background:rgba(0,0,0,.55);border:1px solid #444;border-radius:50%;width:38px;height:38px;font-size:0;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s,border-color .2s;padding:0}
#theme-toggle::before{content:"☀️";font-size:17px;line-height:1}
#theme-toggle:hover{background:rgba(255,0,153,.18);border-color:#ff0099}
/* ── Light mode ── */
html[data-theme="light"]{--light:#111;--border:#CCC;--dark:#F5F3EF}
html[data-theme="light"] body{background:#F5F3EF;color:#111}
html[data-theme="light"] #theme-toggle{background:rgba(245,243,239,.88);border-color:#BBB}
html[data-theme="light"] #theme-toggle::before{content:"🌙"}
html[data-theme="light"] #theme-toggle:hover{background:rgba(255,0,153,.1);border-color:#ff0099}
html[data-theme="light"] .login-container{background-color:rgba(235,234,232,0.92)}
html[data-theme="light"] .emoji-grid{background-color:rgba(255,255,255,0.9)}
html[data-theme="light"] .emoji-item:hover{background-color:rgba(0,0,0,.06)}
html[data-theme="light"] .dictator-card{background-color:rgba(255,255,255,0.7)}
html[data-theme="light"] .target-card{background-color:rgba(255,255,255,0.7)}
html[data-theme="light"] .target-card:hover{background-color:rgba(235,234,232,0.9)}
html[data-theme="light"] .target-card.selected{background-color:rgba(255,0,153,.08)}
html[data-theme="light"] .search-input{background-color:rgba(255,255,255,0.9);color:#111}
html[data-theme="light"] .how-it-works{background:#EBEBEA;border-color:#CCC}
html[data-theme="light"] .hiw-step{color:#555}
html[data-theme="light"] .hiw-step strong{color:#111}
html[data-theme="light"] .anon-option{background:#EBEBEA;border-color:#CCC}
html[data-theme="light"] .anon-note{color:#777}
html[data-theme="light"] .tagline{color:#333}
html[data-theme="light"] .tooltip{background-color:rgba(245,243,239,0.97);color:#111}
</style> </style>
<script>(function(){try{if(localStorage.getItem('radical_theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}})();</script>
</head> </head>
<body> <body>
<button id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark mode"></button>
<div class="container animate-fade-in"> <div class="container animate-fade-in">
<header> <header>
<h1>RADICAL</h1> <h1>RADICAL</h1>
@ -1349,6 +1377,12 @@ function attemptLogin() {
setTimeout(() => { window.location.href = '/'; }, 2500); setTimeout(() => { window.location.href = '/'; }, 2500);
}, 1000); }, 1000);
} }
function toggleTheme() {
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('radical_theme', next); } catch(e) {}
}
</script> </script>
</body> </body>
</html> </html>

View file

@ -1219,3 +1219,68 @@ a {
} }
} }
/* ─── THEME TOGGLE BUTTON ──────────────────────────────────────────────── */
#theme-toggle {
position: fixed;
top: 14px;
right: 14px;
z-index: 9999;
background: rgba(0,0,0,0.55);
border: 1px solid #444;
border-radius: 50%;
width: 38px;
height: 38px;
font-size: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background .2s, border-color .2s;
padding: 0;
}
#theme-toggle::before {
content: "☀️";
font-size: 17px;
line-height: 1;
}
#theme-toggle:hover {
background: rgba(255,0,153,0.18);
border-color: #ff0099;
}
/* ─── LIGHT MODE ───────────────────────────────────────────────────────── */
html[data-theme="light"] {
--secondary: #111111;
--darker: #E8E6E2;
--light: #111111;
--border: #CCCCCC;
--text: #111111;
--text-muted: #666666;
--card-bg: rgba(235,234,232,0.9);
}
html[data-theme="light"] body { background-color: #F5F3EF; }
html[data-theme="light"] #theme-toggle { background: rgba(245,243,239,0.88); border-color: #BBB; }
html[data-theme="light"] #theme-toggle::before { content: "🌙"; }
html[data-theme="light"] #theme-toggle:hover { background: rgba(255,0,153,0.1); border-color: #ff0099; }
html[data-theme="light"] .proposal-input { background-color: #FFFFFF; box-shadow: inset 0 0 5px rgba(0,0,0,.06); }
html[data-theme="light"] .sort-select { background-color: #FFFFFF; }
html[data-theme="light"] .form-input { background-color: #FFFFFF; }
html[data-theme="light"] .share-link-field { background-color: #FFFFFF; }
html[data-theme="light"] .comment-input { background-color: #FFFFFF; }
html[data-theme="light"] .meme-upload { background-color: rgba(255,255,255,.5); }
html[data-theme="light"] .meme-upload-label { background-color: #FFFFFF; }
html[data-theme="light"] .meme-preview { background-color: #FFFFFF; }
html[data-theme="light"] .proposal-meme { background-color: rgba(255,255,255,.4); }
html[data-theme="light"] .meme-remove { background-color: rgba(255,255,255,.9); color: #111; }
html[data-theme="light"] .meme-remove:hover { background-color: #ff0099; color: #000; }
html[data-theme="light"] .modal-overlay { background-color: rgba(0,0,0,.55); }
html[data-theme="light"] .modal-container { background: #EBEBEA; }
html[data-theme="light"] .post-button:disabled { background-color: #CCC; color: #999; }
html[data-theme="light"] .petition-submit-btn:disabled { background-color: #CCC; color: #999; }
html[data-theme="light"] .comment-submit:disabled { background-color: #CCC; color: #999; }
html[data-theme="light"] .comment-item { border-bottom-color: #DDD; }
html[data-theme="light"] .social-share-button { background-color: #FFFFFF; }
html[data-theme="light"] .share-button { background: #FFFFFF; }
html[data-theme="light"] .music-controls { background: rgba(235,234,232,.95); }
html[data-theme="light"] .trending-badge { background-color: #333; color: #F5F3EF; }