From 70ecb5e4da3a8ef9f5fe69921e19c38a96804bfb Mon Sep 17 00:00:00 2001 From: Louis King Date: Tue, 10 Feb 2026 18:11:11 +0000 Subject: [PATCH] Add light mode theme with dark/light toggle - Add sun/moon toggle in navbar (top-right) using DaisyUI swap component - Store user theme preference in localStorage, default to server config - Add WEB_THEME env var to configure default theme (dark/light) - Add light mode color palette with adjusted section colors for contrast - Use CSS filter to invert white SVG logos in light mode - Add section-colored hover/active backgrounds for navbar items - Style hero buttons with thicker outlines and white text on hover - Soften hero heading color in light mode - Change member callsign badges from green to neutral - Update AGENTS.md, .env.example with WEB_THEME documentation Co-Authored-By: Claude Opus 4.6 --- .env.example | 5 +++ AGENTS.md | 1 + src/meshcore_hub/common/config.py | 6 +++ src/meshcore_hub/web/app.py | 5 +++ src/meshcore_hub/web/static/css/app.css | 45 +++++++++++++++++++ .../web/static/js/spa/pages/home.js | 6 +-- .../web/static/js/spa/pages/members.js | 2 +- src/meshcore_hub/web/templates/spa.html | 37 +++++++++++++-- 8 files changed, 100 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 3a47a5f..bfad546 100644 --- a/.env.example +++ b/.env.example @@ -187,6 +187,11 @@ API_ADMIN_KEY= # External web port WEB_PORT=8080 +# Default theme for the web dashboard (dark or light) +# Users can override via the theme toggle; their preference is saved in localStorage +# Default: dark +# WEB_THEME=dark + # Timezone for displaying dates/times on the web dashboard # Uses standard IANA timezone names (e.g., America/New_York, Europe/London) # Default: UTC diff --git a/AGENTS.md b/AGENTS.md index 97abf9e..33214a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -492,6 +492,7 @@ Key variables: - `MQTT_TLS` - Enable TLS/SSL for MQTT (default: `false`) - `API_READ_KEY`, `API_ADMIN_KEY` - API authentication keys - `WEB_ADMIN_ENABLED` - Enable admin interface at /a/ (default: `false`, requires auth proxy) +- `WEB_THEME` - Default theme for the web dashboard (default: `dark`, options: `dark`, `light`). Users can override via the theme toggle in the navbar, which persists their preference in browser localStorage. - `TZ` - Timezone for web dashboard date/time display (default: `UTC`, e.g., `America/New_York`, `Europe/London`) - `FEATURE_DASHBOARD`, `FEATURE_NODES`, `FEATURE_ADVERTISEMENTS`, `FEATURE_MESSAGES`, `FEATURE_MAP`, `FEATURE_MEMBERS`, `FEATURE_PAGES` - Feature flags to enable/disable specific web dashboard pages (default: all `true`). Dependencies: Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled. - `LOG_LEVEL` - Logging verbosity diff --git a/src/meshcore_hub/common/config.py b/src/meshcore_hub/common/config.py index ae5f1dd..fbc8816 100644 --- a/src/meshcore_hub/common/config.py +++ b/src/meshcore_hub/common/config.py @@ -256,6 +256,12 @@ class WebSettings(CommonSettings): # Timezone for date/time display (uses standard TZ environment variable) tz: str = Field(default="UTC", description="Timezone for displaying dates/times") + # Theme (dark or light, default dark) + web_theme: str = Field( + default="dark", + description="Default theme for the web dashboard (dark or light)", + ) + # Admin interface (disabled by default for security) web_admin_enabled: bool = Field( default=False, diff --git a/src/meshcore_hub/web/app.py b/src/meshcore_hub/web/app.py index 5d7cbbd..6c76815 100644 --- a/src/meshcore_hub/web/app.py +++ b/src/meshcore_hub/web/app.py @@ -113,6 +113,7 @@ def _build_config_json(app: FastAPI, request: Request) -> str: "timezone": app.state.timezone_abbr, "timezone_iana": app.state.timezone, "is_authenticated": bool(request.headers.get("X-Forwarded-User")), + "default_theme": app.state.web_theme, } return json.dumps(config) @@ -174,6 +175,9 @@ def create_app( app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*") # Store configuration in app state (use args if provided, else settings) + app.state.web_theme = ( + settings.web_theme if settings.web_theme in ("dark", "light") else "dark" + ) app.state.api_url = api_url or settings.api_base_url app.state.api_key = api_key or settings.api_key app.state.admin_enabled = ( @@ -638,6 +642,7 @@ def create_app( "custom_pages": custom_pages, "logo_url": request.app.state.logo_url, "version": __version__, + "default_theme": request.app.state.web_theme, "config_json": config_json, }, ) diff --git a/src/meshcore_hub/web/static/css/app.css b/src/meshcore_hub/web/static/css/app.css index f9df993..8ff1f31 100644 --- a/src/meshcore_hub/web/static/css/app.css +++ b/src/meshcore_hub/web/static/css/app.css @@ -27,6 +27,16 @@ --color-members: oklch(0.72 0.17 50); /* orange */ } +/* Light mode: darker section colors for contrast on light backgrounds */ +[data-theme="light"] { + --color-dashboard: oklch(0.55 0.15 210); + --color-nodes: oklch(0.50 0.24 265); + --color-adverts: oklch(0.55 0.17 330); + --color-messages: oklch(0.55 0.18 180); + --color-map: oklch(0.58 0.16 45); + --color-members: oklch(0.55 0.18 25); +} + /* ========================================================================== Navbar Styling ========================================================================== */ @@ -34,6 +44,16 @@ /* Spacing between horizontal nav items */ .menu-horizontal { gap: 0.125rem; } +/* Invert white logos/images to dark for light mode */ +[data-theme="light"] .theme-logo { + filter: brightness(0.15); +} + +/* Soften hero heading to dark grey in light mode */ +[data-theme="light"] .hero-title { + color: #6b7280; +} + /* Nav icon colors */ .nav-icon-dashboard { color: var(--color-dashboard); } .nav-icon-nodes { color: var(--color-nodes); } @@ -42,6 +62,31 @@ .nav-icon-map { color: var(--color-map); } .nav-icon-members { color: var(--color-members); } +/* Propagate section color to parent li for hover/active backgrounds */ +.navbar .menu li:has(.nav-icon-dashboard) { --nav-color: var(--color-dashboard); } +.navbar .menu li:has(.nav-icon-nodes) { --nav-color: var(--color-nodes); } +.navbar .menu li:has(.nav-icon-adverts) { --nav-color: var(--color-adverts); } +.navbar .menu li:has(.nav-icon-messages) { --nav-color: var(--color-messages); } +.navbar .menu li:has(.nav-icon-map) { --nav-color: var(--color-map); } +.navbar .menu li:has(.nav-icon-members) { --nav-color: var(--color-members); } + +/* Section-tinted hover and active backgrounds (!important to override DaisyUI CDN) */ +.navbar .menu li > a:hover { + background-color: color-mix(in oklch, var(--nav-color, oklch(var(--bc))) 12%, transparent) !important; +} +.navbar .menu li > a.active { + background-color: color-mix(in oklch, var(--nav-color, oklch(var(--bc))) 20%, transparent) !important; + color: inherit !important; +} + +/* Homepage hero buttons: slightly thicker outline, white text on hover */ +#app .btn-outline { + border-width: 2px; +} +#app .btn-outline:hover { + color: #fff !important; +} + /* ========================================================================== Scrollbar Styling ========================================================================== */ diff --git a/src/meshcore_hub/web/static/js/spa/pages/home.js b/src/meshcore_hub/web/static/js/spa/pages/home.js index b29d7f6..868c04f 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/home.js +++ b/src/meshcore_hub/web/static/js/spa/pages/home.js @@ -70,9 +70,9 @@ export async function render(container, params, router) {
- ${networkName} +
-

${networkName}

+

${networkName}

${cityCountry}
@@ -159,7 +159,7 @@ export async function render(container, params, router) {

Our local off-grid mesh network is made possible by

- MeshCore +

Connecting people and things, without using the internet

diff --git a/src/meshcore_hub/web/static/js/spa/pages/members.js b/src/meshcore_hub/web/static/js/spa/pages/members.js index e0a21d3..69cbf6b 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/members.js +++ b/src/meshcore_hub/web/static/js/spa/pages/members.js @@ -53,7 +53,7 @@ function renderMemberCard(member, nodes) { : nothing; const callsignBadge = member.callsign - ? html`${member.callsign}` + ? html`${member.callsign}` : nothing; const descBlock = member.description diff --git a/src/meshcore_hub/web/templates/spa.html b/src/meshcore_hub/web/templates/spa.html index 682fd3e..ad5a1a6 100644 --- a/src/meshcore_hub/web/templates/spa.html +++ b/src/meshcore_hub/web/templates/spa.html @@ -1,10 +1,18 @@ - + {{ network_name }} + + + @@ -79,7 +87,7 @@
- {{ network_name }} + {{ network_name }}
@@ -111,8 +119,15 @@ {% endif %}
- @@ -167,6 +182,22 @@ window.__APP_CONFIG__ = {{ config_json|safe }}; + + +