From e11047f00367e3b32cbc966de0ad687276aa613b Mon Sep 17 00:00:00 2001 From: raldone01 Date: Sat, 5 Jul 2025 19:42:44 +0200 Subject: [PATCH] Add support for dark and light theme. --- .../webusb_serial/website/application.js | 55 +++++++++++++- .../device/webusb_serial/website/index.html | 3 +- .../device/webusb_serial/website/style.css | 72 +++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/examples/device/webusb_serial/website/application.js b/examples/device/webusb_serial/website/application.js index 5be082ca2..e9528a2ba 100644 --- a/examples/device/webusb_serial/website/application.js +++ b/examples/device/webusb_serial/website/application.js @@ -2,6 +2,9 @@ (async () => { // bind to the html + const uiBody = document.body; + const uiToggleThemeBtn = document.getElementById('theme-toggle'); + const uiConnectWebUsbSerialBtn = document.getElementById('connect_webusb_serial_btn'); const uiConnectSerialBtn = document.getElementById('connect_serial_btn'); const uiDisconnectBtn = document.getElementById('disconnect_btn'); @@ -27,7 +30,9 @@ const uiNearTheBottomThreshold = 100; // pixels from the bottom to trigger scroll const maxCommandHistoryLength = 123; // max number of command history entries - const maxReceivedDataLength = 8192/8; // max number of received data entries + const maxReceivedDataLength = 8192 / 8; // max number of received data entries + + const THEME_STATES = ['auto', 'light', 'dark']; class CommandHistoryEntry { constructor(text) { @@ -59,6 +64,16 @@ this.receivedData = []; // bind the UI elements + uiToggleThemeBtn.addEventListener('click', () => this.toggleTheme()); + // Listener for OS Theme Changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + const currentPreference = localStorage.getItem('theme') || 'auto'; + // Only act if the user is in automatic mode + if (currentPreference === 'auto') { + this.setTheme('auto'); + } + }); + uiConnectWebUsbSerialBtn.addEventListener('click', () => this.connectWebUsbSerialPort()); uiConnectSerialBtn.addEventListener('click', () => this.connectSerialPort()); uiDisconnectBtn.addEventListener('click', () => this.disconnectPort()); @@ -88,6 +103,12 @@ } restoreState() { + // Restore theme choice + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + this.setTheme(savedTheme); + } + // Restore command history let savedCommandHistory = JSON.parse(localStorage.getItem('commandHistory') || '[]'); for (const cmd of savedCommandHistory) { @@ -112,6 +133,38 @@ } } + setTheme(theme) { + const modeName = theme.charAt(0).toUpperCase() + theme.slice(1); + uiToggleThemeBtn.textContent = `Theme: ${modeName}`; + + if (theme === 'auto') { + // In auto mode, we rely on the OS preference. + // We check the media query and add/remove the class accordingly. + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (prefersDark) { + uiBody.classList.add('dark-mode'); + } else { + uiBody.classList.remove('dark-mode'); + } + } else if (theme === 'light') { + // Force light mode by removing the class. + uiBody.classList.remove('dark-mode'); + } else if (theme === 'dark') { + // Force dark mode by adding the class. + uiBody.classList.add('dark-mode'); + } + + // Save the theme to localStorage + localStorage.setItem('theme', theme); + } + + toggleTheme() { + const currentTheme = localStorage.getItem('theme') || 'auto'; + const nextThemeIndex = (THEME_STATES.indexOf(currentTheme) + 1) % THEME_STATES.length; + const nextTheme = THEME_STATES[nextThemeIndex]; + this.setTheme(nextTheme); + } + appendCommandToHistory(commandHistoryEntry) { const wasNearBottom = uiCommandHistoryScrollbox.scrollHeight - uiCommandHistoryScrollbox.scrollTop <= uiCommandHistoryScrollbox.clientHeight + uiNearTheBottomThreshold; diff --git a/examples/device/webusb_serial/website/index.html b/examples/device/webusb_serial/website/index.html index 87fcfd6aa..bf02d0294 100644 --- a/examples/device/webusb_serial/website/index.html +++ b/examples/device/webusb_serial/website/index.html @@ -13,7 +13,8 @@
-

TinyUSB - WebUSB Serial

+

TinyUSB - WebUSB Serial

+ Find my source on GitHub diff --git a/examples/device/webusb_serial/website/style.css b/examples/device/webusb_serial/website/style.css index ee9dc73ac..9530d3d55 100644 --- a/examples/device/webusb_serial/website/style.css +++ b/examples/device/webusb_serial/website/style.css @@ -25,6 +25,7 @@ body { justify-content: space-between; align-items: center; padding: 0.5em 1em; + gap: 1em; flex-shrink: 0; } @@ -33,6 +34,15 @@ h2 { margin: 0; } +.app-title { + flex-grow: 1; +} + +.btn-theme { + background-color: #6b6b6b; + color: #fff; +} + .github-link { font-weight: 600; } @@ -198,3 +208,65 @@ main { cursor: col-resize; height: 100%; } + +/* +================================ +Togglable Dark Mode +================================ +*/ +/* This class will be added to the body element by JavaScript */ +body.dark-mode { + /* Invert base background and text colors */ + background: #1e1e1e; + color: #d4d4d4; +} + +body.dark-mode .btn-theme { + background-color: #b0b0b0; + color: #000; +} + +body.dark-mode .github-link { + color: #58a6ff; +} + +body.dark-mode .resizer { + background-color: #444; +} + +body.dark-mode .input { + background-color: #3c3c3c; + color: #f0f0f0; + border: 2px solid #555; +} + +body.dark-mode .input::placeholder { + color: #888; +} + +body.dark-mode .input:focus { + background-color: #2a2d2e; + border-color: #0078d7; +} + +body.dark-mode .scrollbox { + background-color: #252526; + border: 1px solid #444; +} + +body.dark-mode .monospaced { + color: #d4d4d4; +} + +body.dark-mode .command-history-entry { + border-bottom: 1px solid #444; +} + +body.dark-mode .command-history-entry:hover { + background-color: #3c3c3c; +} + +body.dark-mode .send-mode-command { + background-color: #555; + color: #f5f5f5; +}