Start the NPS Unigrid visual redesign

Black topbar banner with live map-center coordinates, a black header
(with NPS pictogram) on waypoint popups, offline basemap wood/park
colors unified with the online Liberty green, and the sidebar grouped
into the planned Explorer / Offline hub / Navigation menu sections.
This commit is contained in:
randy committed 2026-09-09 09:02:29 +02:00
1 parent d1f2e7376e
commit bc9ea58614
4 files changed
+112 -37

No files matched your search

+52 -6
View File
@@ -14,16 +14,24 @@ html, body {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
height: 48px; height: 44px;
padding: 0 16px; padding: 0 16px;
background: #fff; background: #1a1a1a;
border-bottom: 1px solid #ddd;
} }
.topbar__brand { .topbar__brand {
font-size: 15px; font-size: 14px;
font-weight: 700; font-weight: 700;
color: #333; letter-spacing: 0.12em;
text-transform: uppercase;
color: #fff;
}
.topbar__coords {
margin: 0 auto;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: #bbb;
} }
.topbar__auth { .topbar__auth {
@@ -32,7 +40,11 @@ html, body {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 13px; font-size: 13px;
color: #555; color: #ccc;
}
.topbar__auth a {
color: #fff;
} }
.app-body { .app-body {
@@ -61,6 +73,17 @@ html, body {
position: relative; position: relative;
} }
.sidebar-group-label {
width: 28px;
height: 1px;
margin: 4px 0;
background: #ddd;
}
.sidebar-group-label:first-child {
display: none;
}
.sidebar-icon { .sidebar-icon {
width: 36px; width: 36px;
height: 36px; height: 36px;
@@ -649,6 +672,29 @@ html, body {
user-select: none; user-select: none;
} }
.gpx-popup__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: -15px -10px 8px;
padding: 10px 12px;
background: #1a1a1a;
color: #fff;
}
.gpx-popup__header-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
color: #fff;
}
.gpx-popup__header-icon svg {
width: 100%;
height: 100%;
}
.gpx-popup__time { .gpx-popup__time {
color: #888; color: #888;
font-size: 12px; font-size: 12px;
+5 -2
View File
@@ -916,9 +916,12 @@ function gpxFileUrl(path) {
// properties come straight from an uploaded GPX file — untrusted input — // properties come straight from an uploaded GPX file — untrusted input —
// so every value is escaped before landing in the popup's HTML. // so every value is escaped before landing in the popup's HTML.
function popupHtml({ name, description, time, category, photos }) { function popupHtml({ name, description, time, category, photos, icon }) {
const parts = []; const parts = [];
if (name) parts.push(`<strong>${escapeHtml(name)}</strong>`); if (name) {
const glyph = icon && POI_ICONS[icon] ? `<span class="gpx-popup__header-icon">${POI_ICONS[icon]}</span>` : "";
parts.push(`<div class="gpx-popup__header"><strong>${escapeHtml(name)}</strong>${glyph}</div>`);
}
if (category) parts.push(`<p class="gpx-popup__category">${escapeHtml(category)}</p>`); if (category) parts.push(`<p class="gpx-popup__category">${escapeHtml(category)}</p>`);
if (photos?.length > 0) parts.push(formatPhotos(photos)); if (photos?.length > 0) parts.push(formatPhotos(photos));
if (description) parts.push(formatDescription(description)); if (description) parts.push(formatDescription(description));
+54 -29
View File
@@ -22,22 +22,23 @@ const ZOOM = 13;
const ONLINE_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty"; const ONLINE_STYLE_URL = "https://tiles.openfreemap.org/styles/liberty";
// Recolors OpenFreeMap Liberty to the same NPS-inspired palette as // Recolors OpenFreeMap Liberty to the same NPS-inspired palette as
// OFFLINE_THEME below — warm cream ground, brown/khaki land cover, green // OFFLINE_THEME below — warm cream ground, muted roads/boundaries — mapped
// reserved for parks, muted roads/boundaries — mapped onto Liberty's // onto Liberty's OpenMapTiles layer ids instead of protomaps' theme roles
// OpenMapTiles layer ids instead of protomaps' theme roles (the two // (the two schemas don't share layer names, so this is a separate
// schemas don't share layer names, so this is a separate hand-built // hand-built table, not a shared object). Only the layers that dominate the
// table, not a shared object). Only the layers that dominate the map's // map's overall color read are touched; labels, halos, rail, and minor
// overall color read are touched; labels, halos, rail, and minor
// overlays (cemetery/hospital/school/pitch/track shading, building // overlays (cemetery/hospital/school/pitch/track shading, building
// outlines) are left at Liberty's own defaults — same "not a full // outlines) are left at Liberty's own defaults — same "not a full
// recolor" scope as OFFLINE_THEME. Road casings have no protomaps-role // recolor" scope as OFFLINE_THEME. Road casings have no protomaps-role
// equivalent to reuse (offline roads don't get a separate outline), so // equivalent to reuse (offline roads don't get a separate outline), so
// #c9a66b there is reasoned to fit the family rather than sourced from // #c9a66b there is reasoned to fit the family rather than sourced from
// anywhere real. // anywhere real.
//
// wood/grass are deliberately left unpatched — Liberty's own green reads
// better than this app's earlier brown/khaki recolor, and OFFLINE_THEME's
// wood_a/scrub_a below were changed to match Liberty's green instead.
const ONLINE_STYLE_PATCH = { const ONLINE_STYLE_PATCH = {
background: { "background-color": "#f9f6ef" }, background: { "background-color": "#f9f6ef" },
landcover_wood: { "fill-color": "#c9bd9c" },
landcover_grass: { "fill-color": "#d9cfb0" },
landcover_sand: { "fill-color": "#f0e4c4" }, landcover_sand: { "fill-color": "#f0e4c4" },
water: { "fill-color": "#a3cedf" }, water: { "fill-color": "#a3cedf" },
park: { "fill-color": "#b3e378" }, park: { "fill-color": "#b3e378" },
@@ -155,19 +156,25 @@ const OFFLINE_THEME = {
background: "#f9f6ef", // real: Park Tiles' own background, hsl(40, 37%, 97%) background: "#f9f6ef", // real: Park Tiles' own background, hsl(40, 37%, 97%)
earth: "#f9f6ef", earth: "#f9f6ef",
water: "#a3cedf", water: "#a3cedf",
// Brown/khaki, not green: in Park Tiles' own Imagery style, green is // Matches OpenFreeMap Liberty's own wood/grass green (its landcover_wood
// specifically the protected-area/park color (below), not a general land // hsla(98,61%,72%,0.7) and landcover_grass rgba(176,213,154,1) at 0.3
// cover fill — actual forest there is just the aerial photo underneath, // opacity, blended over the cream background) — the online map's actual
// which this offline style has no equivalent of. Using that same vivid // look whenever ONLINE_STYLE_PATCH's brown/khaki recolor fails to apply
// green for ordinary wood/scrub as well (an earlier version of this // (see loadOnlineStyle's fetch fallback) turned out to be the look this
// theme did) made everything read as "green," not "brown with green // app's users actually want, so offline was changed to match it instead
// parks" — reversed here to match the real distinction. // of chasing the patch's own reliability.
wood_a: "#c9bd9c", wood_a: "#c3e8ab",
wood_b: "#bcae8a", wood_b: "#a8d888",
scrub_a: "#d9cfb0", scrub_a: "#dcecc9",
scrub_b: "#cfc29d", scrub_b: "#cfe3b8",
park_a: "#c8e2a0", // Close to wood_b, not the more saturated Park Tiles green: protomaps'
park_b: "#b3e378", // real: Park Tiles' protected-area/park-point green // theme routes "forest"/"nature_reserve"/"protected_area" (e.g. the Malé
// Karpaty CHKO boundary) through this same park_a/park_b pair as actual
// named parks — Liberty's online style doesn't make that reserve-boundary
// distinction at all, so a visibly different green here reads as an
// offline-only blotch rather than a deliberate distinction.
park_a: "#c3e8ab",
park_b: "#a8d888",
sand: "#f0e4c4", sand: "#f0e4c4",
beach: "#f0e4c4", beach: "#f0e4c4",
buildings: "#ded7cc", buildings: "#ded7cc",
@@ -388,25 +395,43 @@ map.once("style.load", () => {
map.addControl(new OverviewMapControl({}), "top-right"); map.addControl(new OverviewMapControl({}), "top-right");
}); });
const coordsLabel = document.querySelector(".topbar__coords");
const updateCoords = () => {
const { lng, lat } = map.getCenter();
coordsLabel.textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
};
updateCoords();
map.on("move", updateCoords);
const sidebar = document.getElementById("sidebar"); const sidebar = document.getElementById("sidebar");
// Grouped into the TODO's planned 4-module menu (Explorer / Offline hub /
// Navigation / Camper & HW) ahead of that split actually happening —
// Camper & HW has no sidebar item yet (leveling/solar/telemetry are all
// still unbuilt), so it's left out rather than shown empty.
const layerToggle = new LayerToggleControl(applyStyle); const layerToggle = new LayerToggleControl(applyStyle);
const profilePanel = new TrackProfilePanel(map);
profilePanel.mount();
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Explorer"></div>`);
new GpxPanelControl(map, profilePanel).mount(sidebar);
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Offline hub"></div>`);
layerToggle.mount(sidebar); layerToggle.mount(sidebar);
layerToggle.setActive("online"); layerToggle.setActive("online");
new RadarControl(map).mount(sidebar);
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Navigation"></div>`);
gnssControl.mount(sidebar);
new LiveTrackPanelControl(map).mount(sidebar);
// The sidebar already has a "Base map" icon (opens the same panel) — // The sidebar already has a "Base map" icon (opens the same panel) —
// this is a second entry point for it, grouped with Home/zoom on the map // this is a second entry point for it, grouped with Home/zoom on the map
// itself rather than off in the page's own side rail, matching NPS's // itself rather than off in the page's own side rail, matching NPS's
// layout. Added after mount() since it reuses that panel/toggle state. // layout. layerToggle only needs to exist (not be mounted yet) for this.
const mapLayersButton = new MapLayersButton(layerToggle); const mapLayersButton = new MapLayersButton(layerToggle);
map.addControl(mapLayersButton, "top-left"); map.addControl(mapLayersButton, "top-left");
const profilePanel = new TrackProfilePanel(map);
profilePanel.mount();
new GpxPanelControl(map, profilePanel).mount(sidebar);
new RadarControl(map).mount(sidebar);
gnssControl.mount(sidebar);
new LiveTrackPanelControl(map).mount(sidebar);
function applyStyle(name) { function applyStyle(name) {
layerToggle.setActive(name); layerToggle.setActive(name);
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE); map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
+1
View File
@@ -1,6 +1,7 @@
<div id="app"> <div id="app">
<header class="topbar"> <header class="topbar">
<span class="topbar__brand" title="rev <%= revision %>">MapTool</span> <span class="topbar__brand" title="rev <%= revision %>">MapTool</span>
<span class="topbar__coords"></span>
<div class="topbar__auth"> <div class="topbar__auth">
<% if logged_in %> <% if logged_in %>
<span><%= user %></span> <span><%= user %></span>