[Feat]: Add light/dark-mode toggle

INcluding localstorage token and quick-load functionality to prevent flashing on initial canvas paint.
This commit is contained in:
ClaraCrazy 2025-12-30 17:42:30 +01:00
parent cc4e3ddfbd
commit 89003f0d26
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
8 changed files with 189 additions and 9 deletions

View file

@ -1,5 +1,21 @@
// Consolidated utilities: date formatting and lock modals
// Load theme immediately to prevent flash
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.add('light-mode');
// Also add to body if it exists
if (document.body) {
document.body.classList.add('light-mode');
}
}
})();
document.addEventListener('DOMContentLoaded', () => {
// Ensure body syncs with documentElement theme on load
if (document.documentElement.classList.contains('light-mode') && !document.body.classList.contains('light-mode')) {
document.body.classList.add('light-mode');
}
function getExpiryMs(time, unit) {
switch (unit) {
case 'minutes':
@ -310,12 +326,34 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
function initThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) return;
// Sync body with documentElement if theme was loaded early
if (document.documentElement.classList.contains('light-mode')) {
document.body.classList.add('light-mode');
}
themeToggle.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
document.body.classList.toggle('light-mode');
document.documentElement.classList.toggle('light-mode');
// Save preference
const isLight = document.body.classList.contains('light-mode');
localStorage.setItem('theme', isLight ? 'light' : 'dark');
});
}
// Expose utilities and run them
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu };
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle };
formatEmailDates();
formatMailDate();
initLockModals();
initCopyAddress();
initQrModal();
initHamburgerMenu();
initThemeToggle();
});

View file

@ -52,6 +52,51 @@
--overlay-warning-10: rgba(255, 140, 0, 0.1);
}
/* Light mode theme */
body.light-mode {
--color-bg-dark: #f5f5f5;
--color-bg-medium: #e4e4e4;
--color-text-primary: #2c2c2c;
--color-text-light: #1a1a1a;
--color-text-dim: #666666;
--color-text-dimmer: #888888;
--color-text-gray: #555555;
--color-text-white: #2c2c2c;
--color-text-white-alt: #2c2c2c;
--color-text-muted: #666666;
--color-text-secondary: #444444;
--color-border-dark: #cccccc;
--color-button-cancel: #aaaaaa;
--overlay-white-15: rgba(0, 0, 0, 0.08);
--overlay-white-12: rgba(0, 0, 0, 0.06);
--overlay-white-10: rgba(0, 0, 0, 0.05);
--overlay-white-08: rgba(0, 0, 0, 0.04);
--overlay-white-06: rgba(0, 0, 0, 0.03);
--overlay-white-05: rgba(0, 0, 0, 0.025);
--overlay-white-04: rgba(0, 0, 0, 0.02);
--overlay-white-03: rgba(0, 0, 0, 0.015);
--overlay-white-02: rgba(0, 0, 0, 0.01);
--overlay-black-80: rgba(0, 0, 0, 0.15);
--overlay-black-45: rgba(0, 0, 0, 0.08);
--overlay-black-40: rgba(0, 0, 0, 0.07);
--overlay-black-35: rgba(0, 0, 0, 0.06);
--overlay-black-30: rgba(0, 0, 0, 0.05);
--overlay-purple-50: rgba(155, 77, 202, 0.25);
--overlay-purple-40: rgba(155, 77, 202, 0.2);
--overlay-purple-35: rgba(155, 77, 202, 0.18);
--overlay-purple-30: rgba(155, 77, 202, 0.15);
--overlay-purple-25: rgba(155, 77, 202, 0.12);
--overlay-purple-20: rgba(155, 77, 202, 0.1);
--overlay-purple-15: rgba(155, 77, 202, 0.08);
--overlay-purple-12: rgba(155, 77, 202, 0.06);
--overlay-purple-10: rgba(155, 77, 202, 0.05);
--overlay-purple-08: rgba(155, 77, 202, 0.04);
--overlay-purple-04: rgba(155, 77, 202, 0.02);
--overlay-warning-10: rgba(255, 140, 0, 0.05);
}
body {
margin: 0;
min-height: 100vh;
@ -65,6 +110,16 @@ body {
box-shadow: 0 10px 30px var(--overlay-black-45), inset 0 1px 0 var(--overlay-white-06);
}
button:focus {
background: var(--overlay-purple-20);
border-color: var(--overlay-purple-30);
}
button:focus,
button:hover {
color: var(--color-accent-purple-light);
}
body::-webkit-scrollbar {
display: none;
}
@ -194,6 +249,7 @@ text-muted {
}
.action-links a {
height: 42px;
display: inline-block;
padding: 12px 24px;
border-radius: 15px;
@ -422,15 +478,17 @@ label {
transform: translateY(-4px);
}
.email-card,
.mail-content,
.mail-attachments {
border-radius: 18px;
border: 1px solid var(--overlay-white-08);
border: 2px solid var(--overlay-purple-30);
border-radius: 15px;
box-shadow: 0 8px 30px var(--overlay-black-30);
}
.email-card {
border: 1px solid var(--overlay-purple-30);
border-radius: 15px;
box-shadow: 0 8px 30px var(--overlay-black-30);
padding: 24px;
transition: all 0.4s ease;
position: relative;
@ -564,6 +622,8 @@ label {
.mail-header {
padding: 30px;
margin-bottom: 30px;
border: 2px solid var(--overlay-purple-30);
border-radius: 15px;
box-shadow: 0 12px 40px var(--overlay-black-40);
}
@ -588,7 +648,6 @@ label {
.mail-content {
background: var(--overlay-white-02);
border: 1px solid var(--overlay-white-06);
padding: 30px;
margin-bottom: 30px;
}
@ -621,6 +680,8 @@ label {
}
.mail-attachments {
border: 2px solid var(--overlay-purple-30);
border-radius: 15px;
background: var(--overlay-white-03);
padding: 25px;
}
@ -931,6 +992,52 @@ body.loading-page .logo {
}
/* Theme Toggle Button */
.theme-toggle {
background: var(--overlay-purple-20);
border: 1px solid var(--overlay-purple-30);
border-radius: 15px;
height: 42px;
color: var(--color-accent-purple-light);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.theme-toggle:hover {
background: var(--overlay-purple-30);
border-color: var(--overlay-purple-40);
box-shadow: 0 4px 15px var(--overlay-purple-25);
transform: translateY(-2px);
}
.theme-toggle svg {
margin: auto;
width: 20px;
height: 20px;
}
.theme-icon {
display: none;
}
body:not(.light-mode) .theme-icon-dark {
display: block;
}
body.light-mode .theme-icon-light {
display: block;
}
/* Responsive Styles */
@media (max-width: 768px) {
@ -940,6 +1047,10 @@ body.loading-page .logo {
.hamburger-menu {
display: flex;
}
.action-links.mobile-open .theme-toggle {
justify-content: center;
width: 100%;
}
.action-links.mobile-hidden>a,
.action-links.mobile-hidden>button:not(.hamburger-menu) {
display: none;

View file

@ -6,6 +6,14 @@
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
{% endif %}
<a href="/" aria-label="Return to home">Logout</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
</button>
</div>
{% endblock %}

View file

@ -17,12 +17,19 @@
{% else %}
<a href="/logout" aria-label="Logout">Logout</a>
{% endif %}
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
</button>
</div>
{% endblock %}
{% block body %}
<script src="/javascripts/qrcode.js"></script>
<script src="/javascripts/utils.js" defer></script>
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}" data-expiry-time="{{ expiryTime }}" data-expiry-unit="{{ expiryUnit }}"></script>
<div class="inbox-container">
<div class="inbox-header">

View file

@ -71,6 +71,7 @@
</script>
<!-- Scripts -->
<script src="/javascripts/utils.js"></script>
<script src="/socket.io/socket.io.js" defer="true"></script>
<script src="/javascripts/notifications.js" defer="true"></script>

View file

@ -3,6 +3,14 @@
{% block header %}
<div class="action-links">
<a href="/inbox/{{ example }}">Example Inbox</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
</button>
</div>
{% endblock %}

View file

@ -10,11 +10,18 @@
{% else %}
<a href="/logout" aria-label="Logout">Logout</a>
{% endif %}
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
</button>
</div>
{% endblock %}
{% block body %}
<script src="/javascripts/utils.js" defer></script>
<div class="mail-container">
<div class="mail-header">
<h1 class="mail-subject">{{ mail.subject }}</h1>

View file

@ -1,6 +1,6 @@
{
"name": "48hr.email",
"version": "1.7.3",
"version": "1.7.4",
"private": false,
"description": "48hr.email is your favorite open-source tempmail client.",
"keywords": [