From 399478c0a32ca97051f553fbdeb9629722116f1a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 20:14:47 +0000 Subject: [PATCH] =?UTF-8?q?Add=20multi-theme=20support=20(Settings=20?= =?UTF-8?q?=E2=86=92=20Appearance=20theme=20picker)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the binary light/dark toggle with a proper theme system per issue #91. - Tokenize theme-able colors as CSS custom properties in variables.css (brand/primary, surfaces, text, borders, danger, sidebar) and replace the scattered hardcoded brand hex values (#667eea/#764ba2 gradient and variants) across the component CSS with var(...) references. - Add styles/themes.css: a generic, token-driven surface layer (keyed on body.themed) plus theme token sets defined under :root[data-theme]. Ships light, dark, high-contrast, ocean, and forest themes. - Dark stays visually identical: it keeps the legacy body.dark-mode class and dark-mode.css overrides, with matching tokens so brand colors retint. - Settings → Appearance now shows a swatch-based theme picker with previews instead of a single dark toggle. - Persist the selection in localStorage ('theme'), respect the legacy 'darkMode' value for existing users, and apply the theme on before first paint via an inline head script (no flash of the wrong theme). - Update JS that hardcoded dark colors (email-service, contacts-service, utils) to read the active theme's tokens instead of the dark-mode class. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01QxyBudD1ouFYxsBpB2wy9M --- tauri-app/frontend/index.html | 47 +++- tauri-app/frontend/js/app.js | 88 +++++--- tauri-app/frontend/js/contacts-service.js | 6 +- tauri-app/frontend/js/email-service.js | 6 +- tauri-app/frontend/js/utils.js | 7 +- tauri-app/frontend/styles/buttons.css | 4 +- tauri-app/frontend/styles/contacts.css | 30 +-- tauri-app/frontend/styles/dark-mode.css | 4 +- tauri-app/frontend/styles/dm.css | 12 +- tauri-app/frontend/styles/email.css | 8 +- tauri-app/frontend/styles/forms.css | 14 +- tauri-app/frontend/styles/loading.css | 2 +- tauri-app/frontend/styles/modals.css | 2 +- tauri-app/frontend/styles/navigation.css | 4 +- tauri-app/frontend/styles/qr-scanner.css | 2 +- tauri-app/frontend/styles/relay.css | 2 +- tauri-app/frontend/styles/themes.css | 251 ++++++++++++++++++++++ tauri-app/frontend/styles/variables.css | 29 +++ 18 files changed, 433 insertions(+), 85 deletions(-) create mode 100644 tauri-app/frontend/styles/themes.css diff --git a/tauri-app/frontend/index.html b/tauri-app/frontend/index.html index 46a1e59..32fe4c7 100644 --- a/tauri-app/frontend/index.html +++ b/tauri-app/frontend/index.html @@ -20,7 +20,23 @@ + + + @@ -839,16 +855,29 @@

Appearance

-
-
- Dark Mode - -
+
+ + + + +
- Switch between light and dark theme + Choose a color theme. Your choice is saved and applied on startup.
diff --git a/tauri-app/frontend/js/app.js b/tauri-app/frontend/js/app.js index 518481f..2778109 100644 --- a/tauri-app/frontend/js/app.js +++ b/tauri-app/frontend/js/app.js @@ -1757,10 +1757,15 @@ NostrMailApp.prototype.setupEventListeners = function() { // Profile switcher this.setupProfileSwitcherListeners(); - // Dark mode toggle - const darkToggle = document.getElementById('dark-mode-toggle'); - if (darkToggle) { - darkToggle.addEventListener('click', () => this.toggleDarkMode()); + // Theme picker + const themePicker = document.getElementById('theme-picker'); + if (themePicker) { + themePicker.addEventListener('click', (e) => { + const swatch = e.target.closest('.theme-swatch'); + if (swatch) { + this.setTheme(swatch.getAttribute('data-theme-value')); + } + }); } // Profile form @@ -5694,28 +5699,58 @@ NostrMailApp.prototype.updatePublicKeyDisplay = async function() { } } -// Dark mode management -NostrMailApp.prototype.setDarkMode = function(enabled) { - document.body.classList.toggle('dark-mode', enabled); - const icon = document.getElementById('dark-mode-icon'); - if (icon) { - icon.className = enabled ? 'fas fa-sun' : 'fas fa-moon'; +// Theme management +NostrMailApp.AVAILABLE_THEMES = ['light', 'dark', 'high-contrast', 'ocean', 'forest']; + +// Resolve the active theme, honoring the legacy darkMode preference for +// users who set it before the multi-theme picker existed. +NostrMailApp.prototype.getSavedTheme = function() { + let theme = localStorage.getItem('theme'); + if (!theme) { + theme = localStorage.getItem('darkMode') === '1' ? 'dark' : 'light'; } - const text = document.getElementById('dark-mode-text'); - if (text) { - text.textContent = enabled ? 'Enable Light Mode' : 'Enable Dark Mode'; + if (NostrMailApp.AVAILABLE_THEMES.indexOf(theme) === -1) { + theme = 'light'; } - const label = document.getElementById('theme-label'); - if (label) { - label.textContent = enabled ? 'Dark Mode (Enabled)' : 'Light Mode (Enabled)'; + return theme; +}; + +NostrMailApp.prototype.setTheme = function(theme) { + if (NostrMailApp.AVAILABLE_THEMES.indexOf(theme) === -1) { + theme = 'light'; } - localStorage.setItem('darkMode', enabled ? '1' : '0'); -} + + // Token sets live on :root[data-theme]; keep html and body in sync. + document.documentElement.setAttribute('data-theme', theme); + document.body.setAttribute('data-theme', theme); + + // Dark keeps the legacy .dark-mode class (its visual overrides live in + // dark-mode.css); every other non-light theme uses the generic token + // layer driven by the .themed class. + document.body.classList.toggle('dark-mode', theme === 'dark'); + document.body.classList.toggle('themed', theme !== 'light' && theme !== 'dark'); + + // Persist, and keep darkMode in sync for any older code paths. + localStorage.setItem('theme', theme); + localStorage.setItem('darkMode', theme === 'dark' ? '1' : '0'); + + // Reflect the selection in the Settings picker if it is rendered. + const swatches = document.querySelectorAll('#theme-picker .theme-swatch'); + swatches.forEach((swatch) => { + const selected = swatch.getAttribute('data-theme-value') === theme; + swatch.setAttribute('aria-checked', selected ? 'true' : 'false'); + }); +}; + +// Backward-compatible shims for any remaining callers. +NostrMailApp.prototype.setDarkMode = function(enabled) { + this.setTheme(enabled ? 'dark' : 'light'); +}; NostrMailApp.prototype.toggleDarkMode = function() { - const enabled = !document.body.classList.contains('dark-mode'); - this.setDarkMode(enabled); -} + const isDark = this.getSavedTheme() === 'dark'; + this.setTheme(isDark ? 'light' : 'dark'); +}; // Add a method to render the email warning NostrMailApp.prototype.renderProfileEmailWarning = function() { @@ -6023,11 +6058,12 @@ document.addEventListener('DOMContentLoaded', () => { } - // Set initial dark mode from localStorage - const darkPref = localStorage.getItem('darkMode'); - window.app.setDarkMode(darkPref === '1'); - - console.log('🎨 Dark mode initialized:', darkPref === '1' ? 'enabled' : 'disabled'); + // Apply the saved theme (the inline script already set data-theme + // on pre-paint; this syncs the body classes and picker state). + const initialTheme = window.app.getSavedTheme(); + window.app.setTheme(initialTheme); + + console.log('🎨 Theme initialized:', initialTheme); // Initialize the application window.app.init(); diff --git a/tauri-app/frontend/js/contacts-service.js b/tauri-app/frontend/js/contacts-service.js index 7bbafa6..513bbbb 100644 --- a/tauri-app/frontend/js/contacts-service.js +++ b/tauri-app/frontend/js/contacts-service.js @@ -1980,9 +1980,9 @@ class ContactsService { // Update the UI to show it's an encrypted email if (toAddressInput) { - const isDarkMode = document.body.classList.contains('dark-mode'); - toAddressInput.style.borderColor = '#667eea'; - toAddressInput.style.backgroundColor = isDarkMode ? '#1a1f3a' : '#f8f9ff'; + const styles = getComputedStyle(document.body); + toAddressInput.style.borderColor = styles.getPropertyValue('--color-primary').trim() || '#667eea'; + toAddressInput.style.backgroundColor = styles.getPropertyValue('--color-input-bg').trim() || '#f8f9ff'; } window.notificationService.showSuccess(`Email address and recipient pubkey filled in for ${contact.name}`); diff --git a/tauri-app/frontend/js/email-service.js b/tauri-app/frontend/js/email-service.js index 478795a..082dc92 100644 --- a/tauri-app/frontend/js/email-service.js +++ b/tauri-app/frontend/js/email-service.js @@ -392,10 +392,10 @@ class EmailService { }); } - // Get appropriate background color for Nostr contact input based on dark mode + // Get appropriate background color for Nostr contact input from the active theme getNostrContactInputBackgroundColor() { - const isDarkMode = document.body.classList.contains('dark-mode'); - return isDarkMode ? '#1a1f3a' : '#f8f9ff'; + const inputBg = getComputedStyle(document.body).getPropertyValue('--color-input-bg').trim(); + return inputBg || '#f8f9ff'; } /** diff --git a/tauri-app/frontend/js/utils.js b/tauri-app/frontend/js/utils.js index 1842f7b..8eda9fe 100644 --- a/tauri-app/frontend/js/utils.js +++ b/tauri-app/frontend/js/utils.js @@ -26,8 +26,11 @@ class Utils { iframe.style.minHeight = '100px'; container.appendChild(iframe); const doc = iframe.contentDocument || iframe.contentWindow.document; - // Inject dark mode aware base styles + the email HTML - const isDark = document.body.classList.contains('dark-mode'); + // Inject dark mode aware base styles + the email HTML. + // Every theme except light uses a dark surface, so apply the dark + // email overrides whenever the active theme is not light. + const activeTheme = document.documentElement.getAttribute('data-theme') || 'light'; + const isDark = activeTheme !== 'light'; doc.open(); const darkOverrides = isDark ? ` h4 { color: #9ca3af !important; } diff --git a/tauri-app/frontend/styles/buttons.css b/tauri-app/frontend/styles/buttons.css index bd0c67a..b51ee81 100644 --- a/tauri-app/frontend/styles/buttons.css +++ b/tauri-app/frontend/styles/buttons.css @@ -14,7 +14,7 @@ } .btn-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--color-primary-gradient); color: white; } @@ -205,7 +205,7 @@ select.btn-select:hover:not(:disabled) { select.btn-select:focus { outline: none; - border-color: #667eea; + border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } diff --git a/tauri-app/frontend/styles/contacts.css b/tauri-app/frontend/styles/contacts.css index ea745ea..2e7e49a 100644 --- a/tauri-app/frontend/styles/contacts.css +++ b/tauri-app/frontend/styles/contacts.css @@ -28,7 +28,7 @@ } .contact-item.active { - background-color: #667eea; + background-color: var(--color-primary); color: white; } @@ -196,7 +196,7 @@ body.dark-mode .contact-section-content .section-empty-message { border: none; cursor: pointer; padding: 4px; - color: #667eea; + color: var(--color-primary); font-size: 14px; opacity: 0.8; transition: opacity 0.2s ease, color 0.2s ease; @@ -208,7 +208,7 @@ body.dark-mode .contact-section-content .section-empty-message { .contact-email-icon:hover { opacity: 1; - color: #5568d3; + color: var(--color-primary-hover); background-color: rgba(102, 126, 234, 0.1); } @@ -388,7 +388,7 @@ body.dark-mode .contact-section-content .section-empty-message { } .contact-detail-email { - color: #667eea; + color: var(--color-primary); text-decoration: none; font-weight: 500; } @@ -405,7 +405,7 @@ body.dark-mode .contact-section-content .section-empty-message { margin: 0 0 15px 0; font-size: 1.1rem; color: #333; - border-bottom: 2px solid #667eea; + border-bottom: 2px solid var(--color-primary); padding-bottom: 5px; } @@ -472,7 +472,7 @@ body.dark-mode .contact-section-content .section-empty-message { .contacts-search-input:focus { outline: none; - border-color: #667eea; + border-color: var(--color-primary); background-color: white; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -513,7 +513,7 @@ body.dark-mode .contact-detail-info h3 { body.dark-mode .contact-detail-section h4 { color: #e0e0e0; - border-bottom-color: #667eea; + border-bottom-color: var(--color-primary); } body.dark-mode .contact-detail-field label { @@ -551,7 +551,7 @@ body.dark-mode .contact-item:hover { } body.dark-mode .contact-item.active { - background-color: #667eea; + background-color: var(--color-primary); color: white; } @@ -573,7 +573,7 @@ body.dark-mode .contact-detail-pubkey-code { } body.dark-mode .contact-email-icon { - color: #667eea; + color: var(--color-primary); opacity: 0.8; } @@ -591,7 +591,7 @@ body.dark-mode .contacts-search-input { } body.dark-mode .contacts-search-input:focus { - border-color: #667eea; + border-color: var(--color-primary); background-color: #232946; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -654,7 +654,7 @@ body.dark-mode .contact-item.active .contact-delete-icon:hover { .privacy-toggle-icon { font-size: 1.2rem; - color: #667eea; + color: var(--color-primary); } .privacy-toggle-title { @@ -693,7 +693,7 @@ body.dark-mode .contact-item.active .contact-delete-icon:hover { } .privacy-toggle input[type="checkbox"]:checked + .privacy-toggle-slider { - background-color: #667eea; + background-color: var(--color-primary); } .privacy-toggle-slider::before { @@ -740,7 +740,7 @@ body.dark-mode .contact-item.active .contact-delete-icon:hover { z-index: 10; padding: 0; font-size: 0.95rem; - color: #667eea; + color: var(--color-primary); font-weight: 600; backdrop-filter: blur(2px); } @@ -755,7 +755,7 @@ body.dark-mode .contact-item.active .contact-delete-icon:hover { body.dark-mode .privacy-toggle-loading { background-color: rgba(24, 26, 32, 0.98); - color: #667eea; + color: var(--color-primary); } /* Dark mode support for privacy toggle */ @@ -777,7 +777,7 @@ body.dark-mode .privacy-toggle-slider { } body.dark-mode .privacy-toggle input[type="checkbox"]:checked + .privacy-toggle-slider { - background-color: #667eea; + background-color: var(--color-primary); } body.dark-mode .privacy-toggle-description { diff --git a/tauri-app/frontend/styles/dark-mode.css b/tauri-app/frontend/styles/dark-mode.css index b4024f4..20d5072 100644 --- a/tauri-app/frontend/styles/dark-mode.css +++ b/tauri-app/frontend/styles/dark-mode.css @@ -69,7 +69,7 @@ body.dark-mode .email-item { body.dark-mode .email-item.unread { background-color: #232946; - border-left: 4px solid #667eea; + border-left: 4px solid var(--color-primary); } body.dark-mode .modal { @@ -90,7 +90,7 @@ body.dark-mode .form-group textarea { body.dark-mode .form-group input:focus, body.dark-mode .form-group textarea:focus { - border-color: #667eea; + border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } diff --git a/tauri-app/frontend/styles/dm.css b/tauri-app/frontend/styles/dm.css index f389fca..0996eda 100644 --- a/tauri-app/frontend/styles/dm.css +++ b/tauri-app/frontend/styles/dm.css @@ -61,7 +61,7 @@ .dm-search-input:focus { outline: none; - border-color: #667eea; + border-color: var(--color-primary); background-color: white; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -84,7 +84,7 @@ body.dark-mode .dm-search-input { } body.dark-mode .dm-search-input:focus { - border-color: #667eea; + border-color: var(--color-primary); background-color: #232946; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -189,7 +189,7 @@ body.dark-mode .dm-search-icon { } .dm-contact-item.active { - background-color: #667eea; + background-color: var(--color-primary); color: white; } @@ -445,7 +445,7 @@ body.dark-mode .dm-back-to-list-btn:hover { } .message-sent .message-content { - background: #667eea; + background: var(--color-primary); color: white; } @@ -810,7 +810,7 @@ body.dark-mode .dm-debug-copy:hover { } .dm-send-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--color-primary-gradient); color: white; border: none; border-radius: 50%; @@ -825,7 +825,7 @@ body.dark-mode .dm-debug-copy:hover { } .dm-send-btn:hover:not(:disabled) { - background: linear-gradient(135deg, #5568d3 0%, #653a8f 100%); + background: linear-gradient(135deg, var(--color-primary-hover) 0%, #653a8f 100%); } .dm-send-btn:disabled { diff --git a/tauri-app/frontend/styles/email.css b/tauri-app/frontend/styles/email.css index cc970f5..9bcee42 100644 --- a/tauri-app/frontend/styles/email.css +++ b/tauri-app/frontend/styles/email.css @@ -785,7 +785,7 @@ body.dark-mode .email-item { body.dark-mode .email-item.unread { background-color: #232946; - border-left: 4px solid #667eea; + border-left: 4px solid var(--color-primary); } body.dark-mode .email-item:hover { @@ -1016,7 +1016,7 @@ body.dark-mode .email-attachment-icon { .email-search-input:focus { outline: none; - border-color: #667eea; + border-color: var(--color-primary); background-color: white; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -1039,7 +1039,7 @@ body.dark-mode .email-search-input { } body.dark-mode .email-search-input:focus { - border-color: #667eea; + border-color: var(--color-primary); background-color: #232946; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -1188,7 +1188,7 @@ body.dark-mode .email-body-content-text { min-width: 20px; height: 20px; border-radius: 10px; - background: #667eea; + background: var(--color-primary); color: white; font-size: 0.75rem; font-weight: 600; diff --git a/tauri-app/frontend/styles/forms.css b/tauri-app/frontend/styles/forms.css index e9bbda8..d3b3b5a 100644 --- a/tauri-app/frontend/styles/forms.css +++ b/tauri-app/frontend/styles/forms.css @@ -173,7 +173,7 @@ body.dark-mode .settings-section-toggle { .form-group textarea:focus, .form-group select:focus { outline: none; - border-color: #667eea; + border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } @@ -202,7 +202,7 @@ body.dark-mode .form-group select { } body.dark-mode .form-group select:focus { - border-color: #667eea; + border-color: var(--color-primary); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23667eea' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 6 7 7 7-7'/%3e%3c/svg%3e"); } @@ -315,7 +315,7 @@ body.dark-mode .profile-field-error { min-height: 1.5em; } .profile-field-value a { - color: #667eea; + color: var(--color-primary); text-decoration: none; } .profile-field-value a:hover { @@ -330,7 +330,7 @@ body.dark-mode .profile-field-value a { body.dark-mode .form-group input:focus, body.dark-mode .form-group textarea:focus { - border-color: #667eea; + border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); } @@ -631,7 +631,7 @@ body.dark-mode .toggle-setting > label:first-child { } .toggle-switch input[type="checkbox"]:checked { - background-color: #667eea; + background-color: var(--color-primary); } .toggle-switch input[type="checkbox"]:checked::before { @@ -643,7 +643,7 @@ body.dark-mode .toggle-switch input[type="checkbox"] { } body.dark-mode .toggle-switch input[type="checkbox"]:checked { - background-color: #667eea; + background-color: var(--color-primary); } /* Toggle Settings List */ @@ -714,7 +714,7 @@ body.dark-mode .toggle-settings-item:hover { } .selected-pubkey-display span { - color: #667eea; + color: var(--color-primary); font-family: 'Courier New', monospace; word-break: break-all; } diff --git a/tauri-app/frontend/styles/loading.css b/tauri-app/frontend/styles/loading.css index 51993bb..e0c9a72 100644 --- a/tauri-app/frontend/styles/loading.css +++ b/tauri-app/frontend/styles/loading.css @@ -6,7 +6,7 @@ width: 20px; height: 20px; border: 3px solid #f3f3f3; - border-top: 3px solid #667eea; + border-top: 3px solid var(--color-primary); border-radius: 50%; animation: spin 1s linear infinite; } diff --git a/tauri-app/frontend/styles/modals.css b/tauri-app/frontend/styles/modals.css index a16f86d..36f6fec 100644 --- a/tauri-app/frontend/styles/modals.css +++ b/tauri-app/frontend/styles/modals.css @@ -297,7 +297,7 @@ body.dark-mode .confirmation-modal .confirmation-dialog select { body.dark-mode .confirmation-modal .confirmation-dialog select:focus, body.dark-mode .confirmation-modal .confirmation-dialog input[type="text"]:focus { - border-color: #667eea !important; + border-color: var(--color-primary) !important; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; } diff --git a/tauri-app/frontend/styles/navigation.css b/tauri-app/frontend/styles/navigation.css index 8ed8afa..19339d9 100644 --- a/tauri-app/frontend/styles/navigation.css +++ b/tauri-app/frontend/styles/navigation.css @@ -8,7 +8,7 @@ /* Sidebar */ .sidebar { width: 250px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--color-primary-gradient); color: white; display: flex; flex-direction: column; @@ -154,7 +154,7 @@ body.dark-mode .nostr-mail-logo-dark { bottom: 100%; left: 0; right: 0; - background: #5a6fd6; + background: var(--color-primary-hover); border-radius: 8px; margin-bottom: 4px; box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.25); diff --git a/tauri-app/frontend/styles/qr-scanner.css b/tauri-app/frontend/styles/qr-scanner.css index 7e1f1e5..9339e93 100644 --- a/tauri-app/frontend/styles/qr-scanner.css +++ b/tauri-app/frontend/styles/qr-scanner.css @@ -28,7 +28,7 @@ } #qr-reader__scan_region { - border: 2px solid #667eea; + border: 2px solid var(--color-primary); border-radius: 8px; } diff --git a/tauri-app/frontend/styles/relay.css b/tauri-app/frontend/styles/relay.css index 49c10c2..dfd5583 100644 --- a/tauri-app/frontend/styles/relay.css +++ b/tauri-app/frontend/styles/relay.css @@ -193,7 +193,7 @@ body.dark-mode .relay-item-actions .relay-toggle-caption { } .relay-item-actions input[type="checkbox"]:checked { - background-color: #667eea; + background-color: var(--color-primary); } .relay-item-actions input[type="checkbox"]:checked::before { diff --git a/tauri-app/frontend/styles/themes.css b/tauri-app/frontend/styles/themes.css new file mode 100644 index 0000000..c2908b4 --- /dev/null +++ b/tauri-app/frontend/styles/themes.css @@ -0,0 +1,251 @@ +/* ============================================================ + Theme system + ------------------------------------------------------------ + Light : default tokens in variables.css (no data-theme / no class) + Dark : legacy body.dark-mode overrides in dark-mode.css (unchanged), + with matching tokens below so brand colors retint too. + Others : body.themed drives the generic token-based surface layer + below; each theme just supplies a token set. + ============================================================ */ + +/* Root paints the theme background before body JS runs (no flash). */ +html { + background-color: var(--color-bg); +} + +/* ---------- Generic surface layer (applies to body.themed) ---------- */ +/* Mirrors the element coverage of dark-mode.css, but purely token-driven, + so any new theme is defined by its custom-property values alone. */ + +body.themed { + background-color: var(--color-bg); + color: var(--color-text); +} + +body.themed .sidebar { + background: var(--color-sidebar); + color: var(--color-sidebar-text); + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.25); +} + +body.themed .sidebar-header, +body.themed .sidebar-nav { + color: var(--color-sidebar-text); +} + +body.themed .main-content { + background-color: var(--color-surface); + color: var(--color-text); +} + +body.themed .tab-header { + background-color: var(--color-surface-alt); + border-bottom: 1px solid var(--color-border); + color: var(--color-text); +} + +body.themed .tab-header h2 { + color: var(--color-text); +} + +body.themed .nav-item { + color: var(--color-sidebar-text); +} + +body.themed .nav-item.active { + background-color: rgba(255, 255, 255, 0.15); + border-left: 4px solid var(--color-sidebar-text); +} + +body.themed .nav-item:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +body.themed .btn-primary { + background: var(--color-primary-gradient); + color: var(--color-on-primary); +} + +body.themed .btn-secondary { + background-color: var(--color-surface-alt); + color: var(--color-text); + border: 2px solid var(--color-border); +} + +body.themed .btn-danger { + background-color: var(--color-danger); + color: #fff; +} + +body.themed .email-item { + background-color: var(--color-surface); + color: var(--color-text); + border-bottom: 1px solid var(--color-border); +} + +body.themed .email-item.unread { + background-color: var(--color-surface-alt); + border-left: 4px solid var(--color-primary); +} + +body.themed .modal { + background-color: var(--color-surface); + color: var(--color-text); +} + +body.themed .modal-header { + border-bottom: 1px solid var(--color-border); +} + +body.themed .form-group input, +body.themed .form-group textarea { + background-color: var(--color-input-bg); + color: var(--color-text); + border: 2px solid var(--color-border); +} + +body.themed .form-group input:focus, +body.themed .form-group textarea:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); +} + +body.themed .icon-button, +body.themed .sidebar .icon-button { + color: var(--color-sidebar-text); +} + +/* ============================================================ + Theme token sets + ============================================================ */ + +/* --- Dark (tokens only; visual overrides live in dark-mode.css) --- */ +:root[data-theme="dark"] { + --color-bg: #181a20; + --color-surface: #232946; + --color-surface-alt: #1e2138; + --color-text: #e0e0e0; + --color-text-muted: rgba(224, 224, 224, 0.6); + --color-border: #232946; + --color-input-bg: #181a20; + --color-sidebar: linear-gradient(135deg, #232946 0%, #121629 100%); + --color-sidebar-text: #e0e0e0; + --color-danger: #c82333; +} + +/* --- High contrast (accessibility) --- */ +:root[data-theme="high-contrast"] { + --color-primary: #ffd400; + --color-primary-dark: #ffaa00; + --color-primary-hover: #ffe14d; + --color-primary-gradient: linear-gradient(135deg, #ffd400 0%, #ffaa00 100%); + --color-on-primary: #000000; + --color-accent: #ffd400; + --color-bg: #000000; + --color-surface: #000000; + --color-surface-alt: #0a0a0a; + --color-text: #ffffff; + --color-text-muted: #cccccc; + --color-border: #ffffff; + --color-input-bg: #000000; + --color-sidebar: #000000; + --color-sidebar-text: #ffffff; + --color-danger: #ff5a5a; +} + +/* --- Ocean (cool blue) --- */ +:root[data-theme="ocean"] { + --color-primary: #2b9dc9; + --color-primary-dark: #1b6f9c; + --color-primary-hover: #37b0dd; + --color-primary-gradient: linear-gradient(135deg, #2b9dc9 0%, #1b6f9c 100%); + --color-on-primary: #ffffff; + --color-accent: #2b9dc9; + --color-bg: #0b1f2a; + --color-surface: #12303f; + --color-surface-alt: #0e2734; + --color-text: #e6f3f8; + --color-text-muted: rgba(230, 243, 248, 0.6); + --color-border: #1c4256; + --color-input-bg: #0b1f2a; + --color-sidebar: linear-gradient(135deg, #12303f 0%, #071620 100%); + --color-sidebar-text: #e6f3f8; + --color-danger: #e5484d; +} + +/* --- Forest (warm green) --- */ +:root[data-theme="forest"] { + --color-primary: #3fae6b; + --color-primary-dark: #2b7d4c; + --color-primary-hover: #4bc47c; + --color-primary-gradient: linear-gradient(135deg, #3fae6b 0%, #2b7d4c 100%); + --color-on-primary: #ffffff; + --color-accent: #3fae6b; + --color-bg: #14211a; + --color-surface: #1c3227; + --color-surface-alt: #172a20; + --color-text: #e7f4ec; + --color-text-muted: rgba(231, 244, 236, 0.6); + --color-border: #274838; + --color-input-bg: #14211a; + --color-sidebar: linear-gradient(135deg, #1c3227 0%, #0c160f 100%); + --color-sidebar-text: #e7f4ec; + --color-danger: #e5484d; +} + +/* ============================================================ + Settings → Appearance theme picker + ============================================================ */ +.theme-picker { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.theme-swatch { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 8px; + width: 92px; + background: transparent; + border: 2px solid var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + transition: border-color 0.15s ease, transform 0.15s ease; + color: inherit; +} + +.theme-swatch:hover { + transform: translateY(-2px); +} + +.theme-swatch[aria-checked="true"] { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15); +} + +.theme-swatch-preview { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + border-radius: var(--radius-sm); + border: 1px solid rgba(0, 0, 0, 0.1); +} + +.theme-swatch-dot { + width: 14px; + height: 14px; + border-radius: 50%; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); +} + +.theme-swatch-name { + font-size: 12px; + line-height: 1.2; + text-align: center; +} diff --git a/tauri-app/frontend/styles/variables.css b/tauri-app/frontend/styles/variables.css index 506e248..f901ebb 100644 --- a/tauri-app/frontend/styles/variables.css +++ b/tauri-app/frontend/styles/variables.css @@ -1,5 +1,34 @@ /* CSS Custom Properties */ :root { + /* ============================================================ + Color tokens (Light theme = defaults) + Themes override these under :root[data-theme="..."] in themes.css. + ============================================================ */ + + /* Brand / primary */ + --color-primary: #667eea; + --color-primary-dark: #764ba2; + --color-primary-hover: #5568d3; + --color-primary-gradient: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%); + --color-on-primary: #ffffff; + --color-accent: var(--color-primary); + + /* Surfaces & text */ + --color-bg: #f5f5f5; + --color-surface: #ffffff; + --color-surface-alt: #fafafa; + --color-text: #333333; + --color-text-muted: #888888; + --color-border: #e0e0e0; + --color-input-bg: #f8f9ff; + + /* Sidebar */ + --color-sidebar: var(--color-primary-gradient); + --color-sidebar-text: #ffffff; + + /* Status */ + --color-danger: #dc3545; + /* Spacing - Safe to add */ --spacing-xs: 4px; --spacing-sm: 8px;