From 42fd49f172b6b386e32f4b8b89fed4c7eedce7f8 Mon Sep 17 00:00:00 2001 From: Jonathon Leight Date: Wed, 1 Jul 2026 19:31:09 -0400 Subject: [PATCH] Light mode map toggle --- internal/web/static/app.css | 12 ++++++++++ internal/web/static/meshmap.js | 41 +++++++++++++++++++++++++++----- internal/web/static/regionmap.js | 38 +++++++++++++++++++++++------ 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index f424f61..56f1960 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -153,6 +153,18 @@ code.pubkey { border-bottom-color: var(--tblr-border-color, #2b2f36); } .leaflet-container .leaflet-bar a:hover { background: var(--tblr-bg-surface-secondary, #22262e); } +/* Base-layer (Dark/Light) switcher: match the dark UI so the native radios render + correctly. On the default white box + dark color-scheme, an unselected radio + shows as a black dot; a dark box fixes that, and accent-color tints the selected + one. Scoped under .leaflet-container to beat leaflet.css (loaded after app.css). */ +.leaflet-container .leaflet-control-layers { + background: var(--tblr-bg-surface, #1a1d24); + color: var(--tblr-body-color, #e6edf3); + border-color: var(--tblr-border-color, #2b2f36); + box-shadow: var(--tblr-box-shadow, 0 1px 2px rgba(0, 0, 0, 0.4)); +} +.leaflet-container .leaflet-control-layers-list label { margin: 0; color: var(--tblr-body-color, #e6edf3); } +.leaflet-container .leaflet-control-layers-selector { accent-color: var(--tblr-primary, #4dabf7); } /* Active region block in the config editor: highlight the block currently being drawn on the shared region map. */ diff --git a/internal/web/static/meshmap.js b/internal/web/static/meshmap.js index 8b575c0..187730c 100644 --- a/internal/web/static/meshmap.js +++ b/internal/web/static/meshmap.js @@ -1,3 +1,37 @@ +// meshBaseLayers adds the dark (default) and a light basemap to a map, with a +// layers control to toggle between them. The choice is remembered in localStorage +// so it carries across maps and pages. Dark matches the UI; the light layer (CARTO +// Voyager) reads better in bright conditions and for some eyes. The control is +// left expanded — Leaflet's collapsed toggle needs an icon asset we don't bundle. +function meshBaseLayers(map) { + var attribution = "© OpenStreetMap © CARTO"; + var dark = L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { + maxZoom: 19, + subdomains: "abcd", + attribution: attribution, + }); + var light = L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", { + maxZoom: 19, + subdomains: "abcd", + attribution: attribution, + }); + var pref = null; + try { + pref = localStorage.getItem("mt_map_base"); + } catch (e) { + /* storage unavailable (private mode); fall back to dark */ + } + (pref === "light" ? light : dark).addTo(map); + L.control.layers({ Dark: dark, Light: light }, null, { position: "topright", collapsed: false }).addTo(map); + map.on("baselayerchange", function (e) { + try { + localStorage.setItem("mt_map_base", e.name === "Light" ? "light" : "dark"); + } catch (e) { + /* ignore */ + } + }); +} + // meshMap renders a dark-mode Leaflet map of points into the element with the // given id. pts is an array of {name, lat, lon}. Does nothing if pts is empty. function meshMap(elId, pts) { @@ -11,12 +45,7 @@ function meshMap(elId, pts) { fadeAnimation: false, markerZoomAnimation: false, }); - // CARTO "dark matter" basemap (OpenStreetMap data) for a dark UI. - L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { - maxZoom: 19, - subdomains: "abcd", - attribution: "© OpenStreetMap © CARTO", - }).addTo(map); + meshBaseLayers(map); var group = L.featureGroup( pts.map(function (p) { return L.circleMarker([p.lat, p.lon], { diff --git a/internal/web/static/regionmap.js b/internal/web/static/regionmap.js index 749257f..6345d45 100644 --- a/internal/web/static/regionmap.js +++ b/internal/web/static/regionmap.js @@ -11,13 +11,37 @@ // Distinct translucent fills so overlapping regions read as layered colors. var PALETTE = ["#4dabf7", "#f783ac", "#69db7c", "#ffa94d", "#9775fa", "#ffd43b", "#3bc9db", "#ff8787"]; - function darkTiles(map) { - // CARTO "dark matter" basemap, matching the read-only maps (see meshmap.js). - L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { + // baseLayers adds the dark (default) and a light basemap with a layers control to + // toggle between them, remembering the choice in localStorage (shared with + // meshmap.js's meshBaseLayers, so the preference carries across maps). Left + // expanded — Leaflet's collapsed toggle needs an icon asset we don't bundle. + function baseLayers(map) { + var attribution = "© OpenStreetMap © CARTO"; + var dark = L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { maxZoom: 19, subdomains: "abcd", - attribution: "© OpenStreetMap © CARTO", - }).addTo(map); + attribution: attribution, + }); + var light = L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", { + maxZoom: 19, + subdomains: "abcd", + attribution: attribution, + }); + var pref = null; + try { + pref = localStorage.getItem("mt_map_base"); + } catch (e) { + /* storage unavailable */ + } + (pref === "light" ? light : dark).addTo(map); + L.control.layers({ Dark: dark, Light: light }, null, { position: "topright", collapsed: false }).addTo(map); + map.on("baselayerchange", function (e) { + try { + localStorage.setItem("mt_map_base", e.name === "Light" ? "light" : "dark"); + } catch (e) { + /* ignore */ + } + }); } function styleFor(i, active) { @@ -64,7 +88,7 @@ var listEl = document.getElementById(listId); if (!listEl) return; var map = L.map(mapId); - darkTiles(map); + baseLayers(map); // Geoman toolbar: only the tools that make sense for area geofences. map.pm.addControls({ @@ -242,7 +266,7 @@ window.regionMapView = function (mapId, opts) { opts = opts || {}; var map = L.map(mapId, { scrollWheelZoom: false }); - darkTiles(map); + baseLayers(map); if (opts.pickURL) { map.on("click", function (e) { window.location = opts.pickURL + "lat=" + e.latlng.lat.toFixed(6) + "&lon=" + e.latlng.lng.toFixed(6);