Skip to content

Commit 32a4d9b

Browse files
fhirschmannclaude
andcommitted
feat: add Candy and Galaxy playful UI themes
Add two more playful themes alongside Kids: Candy (warm pink/purple) and Galaxy (cool blue/indigo). Refactor the shared playful styling (rounded corners, comic font, no neon/scanlines) behind a data-family="playful" marker so it is palette-driven and reused across all three themes; only the --cp-* palette differs per theme. A small theme registry in JS maps each theme to its family and pins the light mode for the playful ones. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ea427b8 commit 32a4d9b

5 files changed

Lines changed: 135 additions & 46 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +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` | [`6bb0097`](../../commit/6bb0097) |
105+
| **Selectable UI themes** (menu → 🎨): switch the whole interface between the default **Cyberpunk** look and three bright, rounded, playful alternatives — **Kids** (sunny), **Candy** (pink/purple) and **Galaxy** (blue/indigo). 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` | [`6bb0097`](../../commit/6bb0097) · [`THEMECOMMIT`](../../commit/THEMECOMMIT) |
106106
| **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) |
107107
| **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) |
108108
| **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
@@ -18,6 +18,8 @@
1818
"darkmode": "Dunkelmodus",
1919
"themeCyberpunk": "Cyberpunk",
2020
"themeKids": "Kinder",
21+
"themeCandy": "Bonbon",
22+
"themeGalaxy": "Galaxie",
2123
"restartinfo": "ESPuino wird jetzt neu gestartet...<br>Die Seite lädt sich nach dem Neustart automatisch neu.",
2224
"shutdowninfo": "ESPuino wird jetzt ausgeschaltet...<br>Die Seite lädt sich nach dem Wiedereinschalten automatisch neu.",
2325
"toast": {

‎html/locales/en.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"darkmode": "Dark Mode",
1919
"themeCyberpunk": "Cyberpunk",
2020
"themeKids": "Kids",
21+
"themeCandy": "Candy",
22+
"themeGalaxy": "Galaxy",
2123
"restartinfo": "ESPuino is being restarted...<br>Page reloads automatically after restart finished.",
2224
"shutdowninfo": "ESPuino is now switched off...<br>Page reloads automatically when the ESPuino is switched on again.",
2325
"toast": {

‎html/locales/fr.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"darkmode": "Mode sombre",
1919
"themeCyberpunk": "Cyberpunk",
2020
"themeKids": "Enfants",
21+
"themeCandy": "Bonbon",
22+
"themeGalaxy": "Galaxie",
2123
"restartinfo": "ESPuino est en cours de redémarrage...<br>La page se recharge automatiquement après la fin du redémarrage.",
2224
"shutdowninfo": "ESPuino est maintenant éteint...<br>La page se recharge automatiquement lorsque l'ESPuino est rallumé.",
2325
"toast": {

‎html/management.html‎

Lines changed: 128 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,47 @@
9898
--cp-dim: #7c83a8;
9999
}
100100

101+
/* "Candy" - warm sweet pink/purple playful palette. */
102+
body[data-theme="candy"] {
103+
--cp-cyan: #a25cf0;
104+
/* primary accent / links - grape purple */
105+
--cp-magenta: #ff4fa3;
106+
/* hot bubblegum pink (hover / secondary) */
107+
--cp-yellow: #ffba08;
108+
--cp-green: #59d98e;
109+
--cp-bg: #ffeef7;
110+
/* soft cotton-candy */
111+
--cp-panel: #ffffff;
112+
--cp-surface: #ffffff;
113+
--cp-surface-2: #ffe0f0;
114+
--cp-border: #ffc4e3;
115+
--cp-border-glow: rgba(162, 92, 240, .25);
116+
--cp-text: #5a2a55;
117+
/* deep plum */
118+
--cp-dim: #a06b95;
119+
}
120+
121+
/* "Galaxy" - cool blue/indigo adventurous playful palette. */
122+
body[data-theme="galaxy"] {
123+
--cp-cyan: #2f7bff;
124+
/* primary accent / links - azure */
125+
--cp-magenta: #7c4dff;
126+
/* indigo (hover / secondary) */
127+
--cp-yellow: #ffce47;
128+
/* star yellow */
129+
--cp-green: #2bd673;
130+
--cp-bg: #e8f1ff;
131+
/* icy sky */
132+
--cp-panel: #ffffff;
133+
--cp-surface: #ffffff;
134+
--cp-surface-2: #dde9ff;
135+
--cp-border: #b9d0ff;
136+
--cp-border-glow: rgba(47, 123, 255, .25);
137+
--cp-text: #1b2b54;
138+
/* deep navy */
139+
--cp-dim: #5f6f9a;
140+
}
141+
101142
body {
102143
/* remap Bootstrap theme variables onto the cyberpunk palette */
103144
--bs-body-bg: var(--cp-bg);
@@ -1656,28 +1697,27 @@
16561697
}
16571698

16581699
/* ============================================================
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.
1700+
"Playful" theme family - shared overrides for the kids/candy/
1701+
galaxy palettes. Strips the cyberpunk neon/CRT flourishes
1702+
(scanlines, glows, angled clip-paths, flicker) and rounds
1703+
everything off into a soft, friendly toy-like UI. Only the
1704+
colours differ per theme; this structural layer is palette-
1705+
driven (var(--cp-*)) so it works for every playful palette.
1706+
Selected via data-family="playful" (set alongside data-theme).
16631707
============================================================ */
1664-
body[data-theme="kids"] {
1708+
body[data-family="playful"] {
16651709
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;
16701710
}
16711711

16721712
/* no CRT scanline overlay */
1673-
body[data-theme="kids"]::after {
1713+
body[data-family="playful"]::after {
16741714
display: none;
16751715
}
16761716

16771717
/* 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 {
1718+
body[data-family="playful"] *,
1719+
body[data-family="playful"] *::before,
1720+
body[data-family="playful"] *::after {
16811721
text-shadow: none !important;
16821722
clip-path: none !important;
16831723
animation: none !important;
@@ -1686,62 +1726,88 @@
16861726
}
16871727

16881728
/* 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 {
1729+
body[data-family="playful"] .navbar,
1730+
body[data-family="playful"] fieldset,
1731+
body[data-family="playful"] .card,
1732+
body[data-family="playful"] .btn,
1733+
body[data-family="playful"] .form-control,
1734+
body[data-family="playful"] .form-select,
1735+
body[data-family="playful"] .modal-content,
1736+
body[data-family="playful"] .toast,
1737+
body[data-family="playful"] .dropdown-menu,
1738+
body[data-family="playful"] .list-group-item,
1739+
body[data-family="playful"] .nav-link {
17001740
border-radius: 18px !important;
17011741
}
17021742

1703-
body[data-theme="kids"] .btn {
1743+
body[data-family="playful"] .btn {
17041744
font-weight: 700;
17051745
box-shadow: 0 4px 0 rgba(0, 0, 0, .08);
17061746
}
17071747

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;
1748+
/* bright, friendly navbar (tinted per palette) instead of the dark neon bar */
1749+
body[data-family="playful"] .navbar.bg-primary {
1750+
background: linear-gradient(180deg, var(--cp-panel), var(--cp-surface-2)) !important;
17111751
border-bottom: 3px solid var(--cp-magenta);
1712-
box-shadow: 0 4px 16px rgba(255, 107, 157, .25);
1752+
box-shadow: 0 6px 16px var(--cp-border-glow);
17131753
}
17141754

1715-
body[data-theme="kids"] #navbar-brand-co {
1755+
body[data-family="playful"] #navbar-brand-co {
17161756
color: var(--cp-magenta) !important;
17171757
}
17181758

1719-
body[data-theme="kids"] #navbar-heading {
1759+
body[data-family="playful"] #navbar-heading {
17201760
color: var(--cp-cyan) !important;
17211761
}
17221762

1723-
body[data-theme="kids"] #menuToggle,
1724-
body[data-theme="kids"] .navbar .navbar-text {
1763+
body[data-family="playful"] #menuToggle,
1764+
body[data-family="playful"] .navbar .navbar-text {
17251765
color: var(--cp-text) !important;
17261766
}
17271767

1728-
body[data-theme="kids"] .nav-logo {
1768+
body[data-family="playful"] .nav-logo {
17291769
filter: none;
17301770
}
17311771

1732-
body[data-theme="kids"] fieldset {
1772+
body[data-family="playful"] fieldset {
17331773
box-shadow: 0 6px 18px rgba(0, 0, 0, .06);
17341774
}
1775+
1776+
/* per-palette background washes for a bit of flavour */
1777+
body[data-theme="kids"] {
1778+
background-image:
1779+
radial-gradient(900px 500px at 50% -10%, rgba(255, 183, 3, .18), transparent 70%),
1780+
radial-gradient(700px 500px at 100% 100%, rgba(255, 107, 157, .15), transparent 70%);
1781+
background-size: auto, auto;
1782+
}
1783+
1784+
body[data-theme="candy"] {
1785+
background-image:
1786+
radial-gradient(900px 500px at 50% -10%, rgba(255, 79, 163, .18), transparent 70%),
1787+
radial-gradient(700px 500px at 100% 100%, rgba(162, 92, 240, .16), transparent 70%);
1788+
background-size: auto, auto;
1789+
}
1790+
1791+
body[data-theme="galaxy"] {
1792+
background-image:
1793+
radial-gradient(900px 500px at 50% -10%, rgba(124, 77, 255, .16), transparent 70%),
1794+
radial-gradient(700px 500px at 100% 100%, rgba(47, 123, 255, .16), transparent 70%);
1795+
background-size: auto, auto;
1796+
}
17351797
</style>
17361798
</head>
17371799

17381800
<body data-bs-theme="dark">
17391801
<!-- Apply the stored theme before paint to avoid a flash of the wrong colours. -->
17401802
<script>
17411803
(function () {
1804+
var families = { cyberpunk: 'neon', kids: 'playful', candy: 'playful', galaxy: 'playful' };
17421805
var theme = localStorage.getItem('uiTheme') || 'cyberpunk';
1806+
if (!families[theme]) { theme = 'cyberpunk'; }
1807+
var playful = families[theme] === 'playful';
17431808
document.body.setAttribute('data-theme', theme);
1744-
var mode = theme === 'kids' ? 'light' : (localStorage.getItem('lightSwitch') || 'dark');
1809+
document.body.setAttribute('data-family', families[theme]);
1810+
var mode = playful ? 'light' : (localStorage.getItem('lightSwitch') || 'dark');
17451811
document.body.setAttribute('data-bs-theme', mode);
17461812
})();
17471813
</script>
@@ -1814,6 +1880,8 @@
18141880
<select class="form-control form-select" id="themeSel" name="themeSel">
18151881
<option value="cyberpunk" data-i18n="themeCyberpunk"></option>
18161882
<option value="kids" data-i18n="themeKids"></option>
1883+
<option value="candy" data-i18n="themeCandy"></option>
1884+
<option value="galaxy" data-i18n="themeGalaxy"></option>
18171885
</select>
18181886
</div>
18191887
</li>
@@ -7375,6 +7443,20 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
73757443
const lightSwitch = $('#lightSwitch');
73767444
const themeSel = $('#themeSel');
73777445

7446+
/* Theme registry: maps each theme to its visual family and whether it
7447+
* is part of the bright "playful" family (which pins the light
7448+
* Bootstrap mode, so the dark-mode toggle has no effect there). */
7449+
const THEMES = {
7450+
cyberpunk: { family: 'neon' },
7451+
kids: { family: 'playful' },
7452+
candy: { family: 'playful' },
7453+
galaxy: { family: 'playful' },
7454+
};
7455+
7456+
function isPlayful(theme) {
7457+
return (THEMES[theme] || THEMES.cyberpunk).family == 'playful';
7458+
}
7459+
73787460
/* Applies a Bootstrap color mode (dark/light) to body + file explorer. */
73797461
function applyColorMode(mode) {
73807462
$('body').attr("data-bs-theme", mode);
@@ -7389,21 +7471,22 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
73897471
localStorage.setItem('lightSwitch', mode);
73907472
}
73917473

7392-
/* The "kids" theme is intrinsically bright, so it pins the light
7393-
* Bootstrap mode and the dark-mode toggle has no effect there. */
73947474
function activeTheme() {
7395-
return localStorage.getItem('uiTheme') || 'cyberpunk';
7475+
const theme = localStorage.getItem('uiTheme') || 'cyberpunk';
7476+
return THEMES[theme] ? theme : 'cyberpunk';
73967477
}
73977478

7398-
/* Applies a named UI theme (palette) and reconciles the dark/light
7399-
* toggle, which only makes sense for the cyberpunk theme. */
7479+
/* Applies a named UI theme (palette + family) and reconciles the
7480+
* dark/light toggle, which only makes sense for the cyberpunk theme. */
74007481
function applyTheme(theme) {
7401-
$('body').attr('data-theme', theme);
7482+
const family = (THEMES[theme] || THEMES.cyberpunk).family;
7483+
$('body').attr('data-theme', theme).attr('data-family', family);
74027484
localStorage.setItem('uiTheme', theme);
74037485
themeSel.val(theme);
74047486

74057487
const darkToggle = lightSwitch.closest('li');
7406-
if (theme == 'kids') {
7488+
if (isPlayful(theme)) {
7489+
/* playful themes are intrinsically bright */
74077490
applyColorMode('light');
74087491
darkToggle.addClass('d-none');
74097492
} else {
@@ -7414,8 +7497,8 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
74147497

74157498
/* Triggers a newColorMode(..) call with either 'dark' or 'light' as the new color mode. */
74167499
function onSwitchDarkMode() {
7417-
/* The dark/light switch is inert while the kids theme is active. */
7418-
if (activeTheme() == 'kids') {
7500+
/* The dark/light switch is inert while a playful theme is active. */
7501+
if (isPlayful(activeTheme())) {
74197502
return;
74207503
}
74217504
newColorMode(lightSwitch.prop('checked') ? 'dark' : 'light');

0 commit comments

Comments
 (0)