Skip to content

Commit 6bb0097

Browse files
fhirschmannclaude
andcommitted
feat: selectable UI themes with a playful Kids theme
Add a theme picker to the management UI (menu → palette icon) that switches the whole interface between the default Cyberpunk look and a new bright, rounded, playful Kids theme. Theming is a pure client-side CSS-variable swap layered on the existing --cp-* palette, so it puts zero extra load on the ESP32 - the device still serves one static, gzipped page. The Kids theme piggybacks on the light Bootstrap mode and strips the neon/CRT flourishes (scanlines, glows, flicker, angled clip-paths) for a soft, toy-like look with a rounded comic font. The choice is remembered per browser in localStorage and applied before paint to avoid a flash of wrong colours. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b805ac0 commit 6bb0097

5 files changed

Lines changed: 175 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
102102
| Change | Commit |
103103
| --- | --- |
104104
| **Cyberpunk web interface**: the management + access-point pages rebuilt with a neon palette, scanlines, `Orbitron`/`Rajdhani`/`Share Tech Mono` type, a custom login page, navbar branding, an SVG-favicon logo and a matching footer; the upstream Bluetooth scan UI restyled to fit | [`7be5254`](../../commit/7be5254) · [`b49f131`](../../commit/b49f131) |
105+
| **Selectable UI themes** (menu → 🎨): switch the whole interface between the default **Cyberpunk** look and a bright, rounded, playful **Kids** theme. Themes are a pure client-side CSS-variable swap (zero extra load on the ESP32 — the device still serves one static page) and the choice is remembered per browser in `localStorage` | [`6960166`](../../commit/6960166) |
105106
| **Fully offline web interface**: all third-party libs + fonts are vendored and served gzipped from flash (one JS + one CSS bundle), so the UI loads fast and works without internet (incl. AP mode) — fixes the ~2-min first-load hang and the browser stalls caused by too many parallel requests to the ESP32 | [`c618191`](../../commit/c618191) |
106107
| **PWA**: installable web-app manifest + icon ("add to home screen"), plus an offline fallback page with auto-reconnect when launched while the player is powered off | [`b4287b9`](../../commit/b4287b9) · [`bd07a7c`](../../commit/bd07a7c) |
107108
| **Consolidated, auto-saving Settings tab**: WiFi/MQTT/FTP/Bluetooth merged with the general settings into one tab; the sub-tabs are a **vertical UniFi-style sidebar** (icon-only on mobile; stays pinned while scrolling) with flat, consistent sections, split into General / **Buttons** / **LEDs** / **Power** / **RFID** / … . General/Buttons/LEDs/Power/RFID auto-save (debounced); the credential tabs keep an explicit submit on purpose | [`8b01876`](../../commit/8b01876) · [`1944cf4`](../../commit/1944cf4) · [`c284d0e`](../../commit/c284d0e) · [`32aabfd`](../../commit/32aabfd) |

‎html/locales/de.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"showhide": "Ein-/Ausblenden",
1717
"infos": "Informationen",
1818
"darkmode": "Dunkelmodus",
19+
"themeCyberpunk": "Cyberpunk",
20+
"themeKids": "Kinder",
1921
"restartinfo": "ESPuino wird jetzt neu gestartet...<br>Die Seite lädt sich nach dem Neustart automatisch neu.",
2022
"shutdowninfo": "ESPuino wird jetzt ausgeschaltet...<br>Die Seite lädt sich nach dem Wiedereinschalten automatisch neu.",
2123
"toast": {

‎html/locales/en.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"showhide": "Show/Hide",
1717
"infos": "Information",
1818
"darkmode": "Dark Mode",
19+
"themeCyberpunk": "Cyberpunk",
20+
"themeKids": "Kids",
1921
"restartinfo": "ESPuino is being restarted...<br>Page reloads automatically after restart finished.",
2022
"shutdowninfo": "ESPuino is now switched off...<br>Page reloads automatically when the ESPuino is switched on again.",
2123
"toast": {

‎html/locales/fr.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"showhide": "Afficher/Masquer",
1717
"infos": "Informations",
1818
"darkmode": "Mode sombre",
19+
"themeCyberpunk": "Cyberpunk",
20+
"themeKids": "Enfants",
1921
"restartinfo": "ESPuino est en cours de redémarrage...<br>La page se recharge automatiquement après la fin du redémarrage.",
2022
"shutdowninfo": "ESPuino est maintenant éteint...<br>La page se recharge automatiquement lorsque l'ESPuino est rallumé.",
2123
"toast": {

‎html/management.html‎

Lines changed: 168 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,36 @@
6868
--cp-dim: #5a6a8a;
6969
}
7070

71+
/* ============================================================
72+
"Kids" theme - a bright, friendly, rounded look for a
73+
children's player. It piggybacks on the light Bootstrap
74+
theme (set via JS) so all the light-mode structural fixes
75+
apply, then overrides the palette + neon flourishes below.
76+
Placed AFTER the light palette so it wins on source order
77+
(equal specificity).
78+
============================================================ */
79+
body[data-theme="kids"] {
80+
--cp-cyan: #3aa0ff;
81+
/* primary accent / links - cheerful blue */
82+
--cp-magenta: #ff6b9d;
83+
/* bubblegum pink (hover / secondary accent) */
84+
--cp-yellow: #ffb703;
85+
/* sunny */
86+
--cp-green: #38c172;
87+
/* grass */
88+
--cp-bg: #eaf4ff;
89+
/* soft sky */
90+
--cp-panel: #ffffff;
91+
--cp-surface: #ffffff;
92+
--cp-surface-2: #fff4d6;
93+
/* warm sunny cards */
94+
--cp-border: #ffd8a8;
95+
--cp-border-glow: rgba(58, 160, 255, .25);
96+
--cp-text: #2b2350;
97+
/* friendly deep purple-navy */
98+
--cp-dim: #7c83a8;
99+
}
100+
71101
body {
72102
/* remap Bootstrap theme variables onto the cyberpunk palette */
73103
--bs-body-bg: var(--cp-bg);
@@ -1624,10 +1654,97 @@
16241654
padding-right: 0;
16251655
padding-left: 5px;
16261656
}
1657+
1658+
/* ============================================================
1659+
"Kids" theme - playful overrides on top of the palette.
1660+
Strips the cyberpunk neon/CRT flourishes (scanlines, glows,
1661+
angled clip-paths, flicker) and rounds everything off into a
1662+
soft, friendly toy-like UI.
1663+
============================================================ */
1664+
body[data-theme="kids"] {
1665+
font-family: "Comic Sans MS", "Chalkboard SE", "Comic Neue", "Baloo 2", "Quicksand", "Segoe UI Rounded", system-ui, sans-serif;
1666+
background-image:
1667+
radial-gradient(900px 500px at 50% -10%, rgba(255, 183, 3, .18), transparent 70%),
1668+
radial-gradient(700px 500px at 100% 100%, rgba(255, 107, 157, .15), transparent 70%);
1669+
background-size: auto, auto;
1670+
}
1671+
1672+
/* no CRT scanline overlay */
1673+
body[data-theme="kids"]::after {
1674+
display: none;
1675+
}
1676+
1677+
/* kill neon glows, flicker/blink loops and angled corners everywhere */
1678+
body[data-theme="kids"] *,
1679+
body[data-theme="kids"] *::before,
1680+
body[data-theme="kids"] *::after {
1681+
text-shadow: none !important;
1682+
clip-path: none !important;
1683+
animation: none !important;
1684+
letter-spacing: normal !important;
1685+
text-transform: none !important;
1686+
}
1687+
1688+
/* round everything off for a soft, toy-like feel */
1689+
body[data-theme="kids"] .navbar,
1690+
body[data-theme="kids"] fieldset,
1691+
body[data-theme="kids"] .card,
1692+
body[data-theme="kids"] .btn,
1693+
body[data-theme="kids"] .form-control,
1694+
body[data-theme="kids"] .form-select,
1695+
body[data-theme="kids"] .modal-content,
1696+
body[data-theme="kids"] .toast,
1697+
body[data-theme="kids"] .dropdown-menu,
1698+
body[data-theme="kids"] .list-group-item,
1699+
body[data-theme="kids"] .nav-link {
1700+
border-radius: 18px !important;
1701+
}
1702+
1703+
body[data-theme="kids"] .btn {
1704+
font-weight: 700;
1705+
box-shadow: 0 4px 0 rgba(0, 0, 0, .08);
1706+
}
1707+
1708+
/* bright, friendly navbar instead of the dark neon bar */
1709+
body[data-theme="kids"] .navbar.bg-primary {
1710+
background: linear-gradient(180deg, #fff7e6, #ffe3ef) !important;
1711+
border-bottom: 3px solid var(--cp-magenta);
1712+
box-shadow: 0 4px 16px rgba(255, 107, 157, .25);
1713+
}
1714+
1715+
body[data-theme="kids"] #navbar-brand-co {
1716+
color: var(--cp-magenta) !important;
1717+
}
1718+
1719+
body[data-theme="kids"] #navbar-heading {
1720+
color: var(--cp-cyan) !important;
1721+
}
1722+
1723+
body[data-theme="kids"] #menuToggle,
1724+
body[data-theme="kids"] .navbar .navbar-text {
1725+
color: var(--cp-text) !important;
1726+
}
1727+
1728+
body[data-theme="kids"] .nav-logo {
1729+
filter: none;
1730+
}
1731+
1732+
body[data-theme="kids"] fieldset {
1733+
box-shadow: 0 6px 18px rgba(0, 0, 0, .06);
1734+
}
16271735
</style>
16281736
</head>
16291737

16301738
<body data-bs-theme="dark">
1739+
<!-- Apply the stored theme before paint to avoid a flash of the wrong colours. -->
1740+
<script>
1741+
(function () {
1742+
var theme = localStorage.getItem('uiTheme') || 'cyberpunk';
1743+
document.body.setAttribute('data-theme', theme);
1744+
var mode = theme === 'kids' ? 'light' : (localStorage.getItem('lightSwitch') || 'dark');
1745+
document.body.setAttribute('data-bs-theme', mode);
1746+
})();
1747+
</script>
16311748
<div class="toast-container position-fixed top-0 end-0 p-3 mt-5" id="toaster">
16321749
<div class="toast align-items-center border-0" id="toast-template" role="alert" aria-live="assertive"
16331750
aria-atomic="true">
@@ -1690,6 +1807,16 @@
16901807
</select>
16911808
</div>
16921809
</li>
1810+
<li>
1811+
<div class="dropdown-item d-flex">
1812+
<i class="fas fa-palette menu-icon"
1813+
style="margin-left: 0; margin-top: 10px; margin-right: 5px;"></i>
1814+
<select class="form-control form-select" id="themeSel" name="themeSel">
1815+
<option value="cyberpunk" data-i18n="themeCyberpunk"></option>
1816+
<option value="kids" data-i18n="themeKids"></option>
1817+
</select>
1818+
</div>
1819+
</li>
16931820
</ul>
16941821
</li>
16951822
</ul>
@@ -7246,27 +7373,64 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
72467373
});
72477374

72487375
const lightSwitch = $('#lightSwitch');
7249-
/* Changes the color mode to a new one and saves the value to local storage. */
7250-
function newColorMode(mode) {
7251-
console.log('new mode', mode);
7376+
const themeSel = $('#themeSel');
72527377

7378+
/* Applies a Bootstrap color mode (dark/light) to body + file explorer. */
7379+
function applyColorMode(mode) {
72537380
$('body').attr("data-bs-theme", mode);
72547381
// File explorer knows 'default' or 'default-dark' as themes
72557382
$('#explorerTree').jstree('set_theme', mode == 'dark' ? 'default-dark' : 'default');
7383+
}
72567384

7385+
/* Changes the color mode to a new one and saves the value to local storage. */
7386+
function newColorMode(mode) {
7387+
console.log('new mode', mode);
7388+
applyColorMode(mode);
72577389
localStorage.setItem('lightSwitch', mode);
72587390
}
72597391

7392+
/* The "kids" theme is intrinsically bright, so it pins the light
7393+
* Bootstrap mode and the dark-mode toggle has no effect there. */
7394+
function activeTheme() {
7395+
return localStorage.getItem('uiTheme') || 'cyberpunk';
7396+
}
7397+
7398+
/* Applies a named UI theme (palette) and reconciles the dark/light
7399+
* toggle, which only makes sense for the cyberpunk theme. */
7400+
function applyTheme(theme) {
7401+
$('body').attr('data-theme', theme);
7402+
localStorage.setItem('uiTheme', theme);
7403+
themeSel.val(theme);
7404+
7405+
const darkToggle = lightSwitch.closest('li');
7406+
if (theme == 'kids') {
7407+
applyColorMode('light');
7408+
darkToggle.addClass('d-none');
7409+
} else {
7410+
darkToggle.removeClass('d-none');
7411+
applyColorMode((localStorage.getItem('lightSwitch') || 'dark'));
7412+
}
7413+
}
7414+
72607415
/* Triggers a newColorMode(..) call with either 'dark' or 'light' as the new color mode. */
72617416
function onSwitchDarkMode() {
7417+
/* The dark/light switch is inert while the kids theme is active. */
7418+
if (activeTheme() == 'kids') {
7419+
return;
7420+
}
72627421
newColorMode(lightSwitch.prop('checked') ? 'dark' : 'light');
72637422
}
7264-
/* Fetches the settings for lightSwitch from storage, defaulting to dark. */
7423+
/* Fetches the settings for lightSwitch + theme from storage, defaulting
7424+
* to the dark cyberpunk look. */
72657425
function darkmodeSwitchSetup() {
72667426
/* The cyberpunk theme is dark by default, regardless of the system theme. */
72677427
lightSwitch.prop('checked', (localStorage.getItem('lightSwitch') || 'dark') == 'dark');
7428+
applyTheme(activeTheme());
72687429
onSwitchDarkMode();
72697430
lightSwitch.change(onSwitchDarkMode);
7431+
themeSel.change(function() {
7432+
applyTheme(themeSel.val());
7433+
});
72707434
}
72717435

72727436
function addOption(selectelement, value) {

0 commit comments

Comments
 (0)