diff --git a/site_static/dark-mode.css b/site_static/dark-mode.css new file mode 100644 index 0000000..8db627b --- /dev/null +++ b/site_static/dark-mode.css @@ -0,0 +1,378 @@ +/** + * Dark Mode Theme for Crypt Server + * + * This stylesheet provides dark mode support using CSS custom properties. + * Theme automatically respects system preferences via prefers-color-scheme. + * Users can also toggle manually via the theme switcher button. + */ + +/* Light theme (default) */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-tertiary: #e9ecef; + --text-primary: #212529; + --text-secondary: #6c757d; + --text-muted: #868e96; + --border-color: #dee2e6; + --link-color: #007bff; + --link-hover: #0056b3; + --navbar-bg: #f8f9fa; + --navbar-text: #000000; + --card-bg: #ffffff; + --table-bg: #ffffff; + --table-stripe: #f2f2f2; + --table-hover: #e9ecef; + --input-bg: #ffffff; + --input-border: #ced4da; + --code-bg: #444444; + --code-text: #ffffff; + --btn-primary-bg: #007bff; + --btn-primary-text: #ffffff; + --dropdown-bg: #ffffff; + --dropdown-hover: #f8f9fa; + --shadow: rgba(0, 0, 0, 0.1); +} + +/* Dark theme */ +[data-theme="dark"] { + --bg-primary: #1a1a2e; + --bg-secondary: #16213e; + --bg-tertiary: #0f3460; + --text-primary: #eaeaea; + --text-secondary: #a0a0a0; + --text-muted: #6c757d; + --border-color: #3a3a5c; + --link-color: #64b5f6; + --link-hover: #90caf9; + --navbar-bg: #16213e; + --navbar-text: #eaeaea; + --card-bg: #16213e; + --table-bg: #1a1a2e; + --table-stripe: #16213e; + --table-hover: #0f3460; + --input-bg: #16213e; + --input-border: #3a3a5c; + --code-bg: #0f3460; + --code-text: #e0e0e0; + --btn-primary-bg: #0f3460; + --btn-primary-text: #ffffff; + --dropdown-bg: #16213e; + --dropdown-hover: #0f3460; + --shadow: rgba(0, 0, 0, 0.3); +} + +/* Auto dark mode based on system preference */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg-primary: #1a1a2e; + --bg-secondary: #16213e; + --bg-tertiary: #0f3460; + --text-primary: #eaeaea; + --text-secondary: #a0a0a0; + --text-muted: #6c757d; + --border-color: #3a3a5c; + --link-color: #64b5f6; + --link-hover: #90caf9; + --navbar-bg: #16213e; + --navbar-text: #eaeaea; + --card-bg: #16213e; + --table-bg: #1a1a2e; + --table-stripe: #16213e; + --table-hover: #0f3460; + --input-bg: #16213e; + --input-border: #3a3a5c; + --code-bg: #0f3460; + --code-text: #e0e0e0; + --btn-primary-bg: #0f3460; + --btn-primary-text: #ffffff; + --dropdown-bg: #16213e; + --dropdown-hover: #0f3460; + --shadow: rgba(0, 0, 0, 0.3); + } +} + +/* Apply theme variables to elements */ +body { + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6, h3 a { + color: var(--text-primary); +} + +a { + color: var(--link-color); +} + +a:hover { + color: var(--link-hover); +} + +/* Navbar */ +.navbar { + background-color: var(--navbar-bg) !important; + border-bottom: 1px solid var(--border-color); +} + +.navbar-brand, +.navbar-nav .nav-link { + color: var(--navbar-text) !important; +} + +.navbar-light .navbar-toggler-icon { + filter: var(--text-primary) == #eaeaea ? invert(1) : none; +} + +[data-theme="dark"] .navbar-toggler-icon { + filter: invert(1); +} + +/* Dropdowns */ +.dropdown-menu { + background-color: var(--dropdown-bg); + border-color: var(--border-color); +} + +.dropdown-item { + color: var(--text-primary); +} + +.dropdown-item:hover, +.dropdown-item:focus { + background-color: var(--dropdown-hover); + color: var(--text-primary); +} + +/* Cards */ +.card { + background-color: var(--card-bg); + border-color: var(--border-color); +} + +.card-header { + background-color: var(--bg-secondary); + border-color: var(--border-color); + color: var(--text-primary); +} + +/* Tables */ +.table { + color: var(--text-primary); + background-color: var(--table-bg); +} + +.table thead th { + border-color: var(--border-color); + background-color: var(--bg-secondary); + color: var(--text-primary); +} + +.table td, +.table th { + border-color: var(--border-color); +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: var(--table-stripe); +} + +.table-hover tbody tr:hover { + background-color: var(--table-hover); + color: var(--text-primary); +} + +/* DataTables */ +.dataTables_wrapper { + color: var(--text-primary); +} + +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_filter, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_paginate { + color: var(--text-primary); +} + +.dataTables_wrapper .dataTables_filter input, +.dataTables_wrapper .dataTables_length select { + background-color: var(--input-bg); + color: var(--text-primary); + border-color: var(--input-border); +} + +.page-item .page-link { + background-color: var(--bg-secondary); + border-color: var(--border-color); + color: var(--text-primary); +} + +.page-item.active .page-link { + background-color: var(--btn-primary-bg); + border-color: var(--btn-primary-bg); +} + +.page-item.disabled .page-link { + background-color: var(--bg-tertiary); + color: var(--text-muted); +} + +/* Forms */ +.form-control { + background-color: var(--input-bg); + border-color: var(--input-border); + color: var(--text-primary); +} + +.form-control:focus { + background-color: var(--input-bg); + color: var(--text-primary); + border-color: var(--link-color); +} + +.form-control::placeholder { + color: var(--text-muted); +} + +/* Buttons */ +.btn-primary { + background-color: var(--btn-primary-bg); + border-color: var(--btn-primary-bg); + color: var(--btn-primary-text); +} + +.btn-secondary { + background-color: var(--bg-tertiary); + border-color: var(--border-color); + color: var(--text-primary); +} + +.btn-outline-secondary { + border-color: var(--border-color); + color: var(--text-primary); +} + +.btn-outline-secondary:hover { + background-color: var(--bg-secondary); + color: var(--text-primary); +} + +/* Code blocks */ +code { + background-color: var(--code-bg); + color: var(--code-text); +} + +pre { + background-color: var(--code-bg); + color: var(--code-text); + border-color: var(--border-color); +} + +/* Alerts */ +.alert { + border-color: var(--border-color); +} + +/* Badges */ +.badge-secondary { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +/* Modals */ +.modal-content { + background-color: var(--bg-primary); + border-color: var(--border-color); +} + +.modal-header { + border-color: var(--border-color); +} + +.modal-footer { + border-color: var(--border-color); +} + +.close { + color: var(--text-primary); +} + +/* Theme toggle button */ +.theme-toggle { + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + color: var(--navbar-text); + font-size: 1.2rem; + display: flex; + align-items: center; + justify-content: center; +} + +.theme-toggle:hover { + opacity: 0.8; +} + +.theme-toggle .icon-sun, +.theme-toggle .icon-moon { + display: none; +} + +/* Show sun icon in dark mode, moon in light mode */ +[data-theme="dark"] .theme-toggle .icon-sun { + display: inline; +} + +[data-theme="dark"] .theme-toggle .icon-moon { + display: none; +} + +:root:not([data-theme="dark"]) .theme-toggle .icon-sun { + display: none; +} + +:root:not([data-theme="dark"]) .theme-toggle .icon-moon { + display: inline; +} + +/* System preference dark mode icon visibility */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .theme-toggle .icon-sun { + display: inline; + } + :root:not([data-theme="light"]) .theme-toggle .icon-moon { + display: none; + } +} + +/* Auto mode indicator - shows "A" badge when following system */ +.theme-toggle.auto-mode::after { + content: 'A'; + font-size: 0.6rem; + position: absolute; + top: 2px; + right: 2px; + background: var(--link-color); + color: white; + border-radius: 50%; + width: 12px; + height: 12px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + +.theme-toggle { + position: relative; +} + +/* Tooltip for auto mode */ +.theme-toggle[title] { + cursor: help; +} diff --git a/site_static/theme-toggle.js b/site_static/theme-toggle.js new file mode 100644 index 0000000..3856248 --- /dev/null +++ b/site_static/theme-toggle.js @@ -0,0 +1,115 @@ +/** + * Theme Toggle Script for Crypt Server + * + * Handles dark/light mode switching with: + * - Automatic system preference detection (prefers-color-scheme) + * - Manual override with toggle button + * - localStorage persistence of user preference + * - "Auto" mode that follows system preference + */ +(function() { + 'use strict'; + + const THEME_KEY = 'crypt-theme'; + + /** + * Get system preference + */ + function getSystemTheme() { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + /** + * Get effective theme (what should be displayed) + */ + function getEffectiveTheme() { + const stored = localStorage.getItem(THEME_KEY); + if (stored === 'auto' || stored === null) { + return getSystemTheme(); + } + return stored; + } + + /** + * Get stored preference (auto, light, or dark) + */ + function getStoredPreference() { + return localStorage.getItem(THEME_KEY) || 'auto'; + } + + /** + * Apply theme to document + */ + function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + updateToggleButton(); + } + + /** + * Update toggle button icon and label + */ + function updateToggleButton() { + const toggleBtn = document.querySelector('.theme-toggle'); + if (!toggleBtn) return; + + const preference = getStoredPreference(); + const effective = getEffectiveTheme(); + + // Update aria-label based on current state + let label; + if (preference === 'auto') { + label = 'Theme: Auto (following system). Click to switch to ' + + (effective === 'dark' ? 'light' : 'dark') + ' mode'; + } else { + label = 'Theme: ' + preference + '. Click to switch to auto mode'; + } + toggleBtn.setAttribute('aria-label', label); + + // Update visual indicator for auto mode + toggleBtn.classList.toggle('auto-mode', preference === 'auto'); + } + + /** + * Cycle through themes: auto -> light -> dark -> auto + */ + function toggleTheme() { + const current = getStoredPreference(); + let next; + + if (current === 'auto') { + // If auto and showing dark, switch to light; if showing light, switch to dark + next = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; + } else if (current === 'light') { + next = 'dark'; + } else { + // dark -> auto + next = 'auto'; + } + + localStorage.setItem(THEME_KEY, next); + applyTheme(getEffectiveTheme()); + } + + // Apply theme immediately to prevent flash + applyTheme(getEffectiveTheme()); + + // Listen for system preference changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { + // Only auto-switch if user preference is 'auto' + if (getStoredPreference() === 'auto') { + applyTheme(e.matches ? 'dark' : 'light'); + } + }); + + // Initialize toggle button when DOM is ready + document.addEventListener('DOMContentLoaded', function() { + const toggleBtn = document.querySelector('.theme-toggle'); + if (toggleBtn) { + toggleBtn.addEventListener('click', toggleTheme); + updateToggleButton(); + } + }); + + // Expose toggle function globally + window.toggleTheme = toggleTheme; +})(); diff --git a/templates/base.html b/templates/base.html index 55bd4c2..22faa2c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,6 +18,9 @@ + {% bootstrap_javascript jquery='full' %}