diff --git a/test/DARK_MODE_README.md b/test/DARK_MODE_README.md new file mode 100644 index 0000000000..1858e15011 --- /dev/null +++ b/test/DARK_MODE_README.md @@ -0,0 +1,73 @@ +# Dark/Light Mode Toggle Feature + +This feature adds a dark/light mode toggle to the p5.js test interfaces, addressing [Issue #8276](https://github.com/processing/p5.js/issues/8276). + +## Features + +- **One-click toggle**: Simple button to switch between dark and light themes +- **Persistent preferences**: User's theme choice is saved in localStorage +- **Works for all users**: Functions for both anonymous and logged-in users +- **Comprehensive theming**: Applies to background, sidebar, code panels, and all UI elements +- **Accessible**: Includes proper ARIA labels and keyboard navigation support +- **Smooth transitions**: CSS transitions for a polished user experience + +## Files Added + +- `test/dark-mode-toggle.js` - JavaScript module that handles theme switching logic +- `test/dark-mode.css` - CSS styles for both dark and light themes + +## Files Modified + +- `test/test.html` - Added dark mode toggle support +- `test/test-reference.html` - Added dark mode toggle support +- `test/test-minified.html` - Added dark mode toggle support + +## Usage + +The dark mode toggle is automatically initialized when the test pages load. Users can: + +1. Click the theme toggle button (🌙/☀️) in the top-right corner +2. The theme preference is automatically saved +3. The preference persists across page reloads and browser sessions + +## Implementation Details + +### Theme Storage +- Uses `localStorage` with key `p5js-theme-preference` +- Defaults to 'light' mode if no preference is stored +- Gracefully handles cases where localStorage is unavailable + +### Theme Application +- Applies `dark-mode` or `light-mode` classes to `` and `` elements +- Uses CSS custom properties (CSS variables) for easy theme customization +- All colors and styles are defined in `dark-mode.css` + +### Button Placement +- Positioned in the top-right corner, near the mocha stats +- Fixed position for easy access +- Responsive and accessible + +## Browser Support + +Works in all modern browsers that support: +- localStorage +- CSS custom properties (CSS variables) +- classList API + +## Testing + +To test the feature: + +1. Open any test HTML file (e.g., `test/test.html`) in a browser +2. Look for the theme toggle button in the top-right corner +3. Click to switch between dark and light modes +4. Reload the page to verify the preference persists + +## Future Enhancements + +Potential improvements for the web editor implementation: +- System preference detection (prefers-color-scheme media query) +- More granular theme controls +- Custom theme colors +- Theme synchronization across tabs + diff --git a/test/dark-mode-toggle.js b/test/dark-mode-toggle.js new file mode 100644 index 0000000000..00cfefb7a1 --- /dev/null +++ b/test/dark-mode-toggle.js @@ -0,0 +1,142 @@ +/** + * Dark/Light Mode Toggle for p5.js Test Interfaces + * + * This module provides a dark/light mode toggle functionality that: + * - Allows one-click theme switching + * - Persists user preference in localStorage + * - Works for both anonymous and logged-in users + * - Applies theme to background, sidebar, and code panels + */ + +(function() { + 'use strict'; + + // Theme configuration + var THEME_STORAGE_KEY = 'p5js-theme-preference'; + var DARK_THEME_CLASS = 'dark-mode'; + var LIGHT_THEME_CLASS = 'light-mode'; + + // Get current theme from localStorage or default to light + function getStoredTheme() { + try { + return localStorage.getItem(THEME_STORAGE_KEY) || 'light'; + } catch (e) { + return 'light'; + } + } + + // Save theme preference to localStorage + function saveTheme(theme) { + try { + localStorage.setItem(THEME_STORAGE_KEY, theme); + } catch (e) { + // localStorage might not be available, silently fail + console.warn('Could not save theme preference:', e); + } + } + + // Apply theme to document + function applyTheme(theme) { + var html = document.documentElement; + var body = document.body; + + // Remove existing theme classes + html.classList.remove(DARK_THEME_CLASS, LIGHT_THEME_CLASS); + body.classList.remove(DARK_THEME_CLASS, LIGHT_THEME_CLASS); + + // Add new theme class + html.classList.add(theme === 'dark' ? DARK_THEME_CLASS : LIGHT_THEME_CLASS); + body.classList.add(theme === 'dark' ? DARK_THEME_CLASS : LIGHT_THEME_CLASS); + + // Save preference + saveTheme(theme); + } + + // Toggle between dark and light themes + function toggleTheme() { + var currentTheme = getStoredTheme(); + var newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + applyTheme(newTheme); + updateToggleButton(newTheme); + return newTheme; + } + + // Create and insert toggle button + function createToggleButton() { + var button = document.createElement('button'); + button.id = 'theme-toggle'; + button.className = 'theme-toggle-button'; + button.setAttribute('aria-label', 'Toggle dark/light mode'); + button.setAttribute('title', 'Toggle dark/light mode'); + + // Set initial icon based on current theme + var currentTheme = getStoredTheme(); + updateToggleButton(currentTheme, button); + + // Add click handler + button.addEventListener('click', function() { + toggleTheme(); + }); + + // Insert button into page + // Try to find a good location (header, stats area, etc.) + var stats = document.getElementById('mocha-stats'); + if (stats) { + stats.appendChild(button); + } else { + // Fallback: insert at top of body + var header = document.querySelector('header') || document.body; + if (header) { + header.style.position = 'relative'; + header.appendChild(button); + } else { + document.body.insertBefore(button, document.body.firstChild); + } + } + + return button; + } + + // Update toggle button icon and aria-label + function updateToggleButton(theme, button) { + button = button || document.getElementById('theme-toggle'); + if (!button) return; + + // Update icon (using Unicode symbols for simplicity) + if (theme === 'dark') { + button.textContent = '☀️'; + button.setAttribute('aria-label', 'Switch to light mode'); + button.setAttribute('title', 'Switch to light mode'); + } else { + button.textContent = '🌙'; + button.setAttribute('aria-label', 'Switch to dark mode'); + button.setAttribute('title', 'Switch to dark mode'); + } + } + + // Initialize dark mode on page load + function initDarkMode() { + // Apply stored theme or default + var theme = getStoredTheme(); + applyTheme(theme); + + // Create toggle button + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', createToggleButton); + } else { + createToggleButton(); + } + } + + // Export for external use if needed + window.p5DarkMode = { + toggle: toggleTheme, + setTheme: applyTheme, + getTheme: getStoredTheme, + init: initDarkMode + }; + + // Auto-initialize + initDarkMode(); +})(); + diff --git a/test/dark-mode.css b/test/dark-mode.css new file mode 100644 index 0000000000..7cf3c21d2f --- /dev/null +++ b/test/dark-mode.css @@ -0,0 +1,214 @@ +/** + * Dark/Light Mode Styles for p5.js Test Interfaces + * + * Provides comprehensive theming for: + * - Background colors + * - Sidebar/navigation + * - Code panels + * - Text colors + * - Borders and accents + */ + +/* Light mode (default) */ +:root.light-mode, +.light-mode { + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-tertiary: #eeeeee; + --text-primary: #333333; + --text-secondary: #666666; + --text-tertiary: #999999; + --border-color: #dddddd; + --accent-color: #ed225d; + --code-bg: #ffffff; + --code-border: #eee; + --link-color: #2d7bb6; + --link-hover: #ed225d; +} + +/* Dark mode */ +:root.dark-mode, +.dark-mode { + --bg-primary: #1e1e1e; + --bg-secondary: #252525; + --bg-tertiary: #2d2d2d; + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-tertiary: #808080; + --border-color: #404040; + --accent-color: #ed225d; + --code-bg: #1e1e1e; + --code-border: #404040; + --link-color: #5da8d8; + --link-hover: #ed225d; +} + +/* Theme toggle button */ +.theme-toggle-button { + position: fixed; + top: 15px; + right: 60px; + z-index: 1000; + background: var(--bg-secondary, #f5f5f5); + border: 1px solid var(--border-color, #ddd); + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.theme-toggle-button:hover { + background: var(--accent-color, #ed225d); + border-color: var(--accent-color, #ed225d); + transform: scale(1.1); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.theme-toggle-button:active { + transform: scale(0.95); +} + +.dark-mode .theme-toggle-button { + background: var(--bg-secondary, #252525); + border-color: var(--border-color, #404040); + color: var(--text-primary, #e0e0e0); +} + +.dark-mode .theme-toggle-button:hover { + background: var(--accent-color, #ed225d); + border-color: var(--accent-color, #ed225d); +} + +/* Body and background */ +body.dark-mode { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +/* Mocha test interface */ +#mocha.dark-mode { + color: var(--text-primary); +} + +#mocha.dark-mode h1, +#mocha.dark-mode h2 { + color: var(--text-primary); +} + +#mocha.dark-mode .test { + color: var(--text-primary); +} + +#mocha.dark-mode .test.pass::before { + color: #00d6b2; +} + +#mocha.dark-mode .test.fail { + color: #ff6b6b; +} + +#mocha.dark-mode .test.fail::before { + color: #ff6b6b; +} + +#mocha.dark-mode .test.fail pre { + background-color: var(--bg-secondary); + border-color: var(--border-color); + color: var(--text-primary); +} + +#mocha.dark-mode .test pre { + background-color: var(--bg-secondary); + border-color: var(--border-color); + color: var(--text-primary); +} + +#mocha.dark-mode .test pre.error { + background-color: rgba(255, 107, 107, 0.1); + border-color: #ff6b6b; +} + +/* Mocha stats */ +#mocha-stats.dark-mode { + color: var(--text-secondary); +} + +#mocha-stats.dark-mode em { + color: var(--text-primary); +} + +#mocha-stats.dark-mode a { + color: var(--text-secondary); +} + +#mocha-stats.dark-mode a:hover { + border-bottom-color: var(--border-color); +} + +/* Code syntax highlighting adjustments for dark mode */ +.dark-mode code .comment { + color: #6a9955; +} + +.dark-mode code .init { + color: #569cd6; +} + +.dark-mode code .string { + color: #ce9178; +} + +.dark-mode code .keyword { + color: #c586c0; +} + +.dark-mode code .number { + color: #b5cea8; +} + +/* Links */ +.dark-mode a { + color: var(--link-color); +} + +.dark-mode a:hover, +.dark-mode a:active { + color: var(--link-hover); +} + +/* Smooth transitions */ +body, +#mocha, +.theme-toggle-button { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +/* Ensure proper contrast in dark mode */ +.dark-mode * { + scrollbar-color: var(--border-color) var(--bg-secondary); +} + +.dark-mode *::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +.dark-mode *::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +.dark-mode *::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 6px; +} + +.dark-mode *::-webkit-scrollbar-thumb:hover { + background: var(--text-tertiary); +} + diff --git a/test/test-minified.html b/test/test-minified.html index c60829b8d3..d546a95aeb 100644 --- a/test/test-minified.html +++ b/test/test-minified.html @@ -3,6 +3,7 @@ + Example Mocha Test @@ -31,6 +32,9 @@ + + + + + + + + +