From 7ec87f0945a8727f03e47a629b85e5ecc03726c9 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 May 2026 16:27:08 +0000 Subject: [PATCH] =?UTF-8?q?feat(themes):=20add=20Clean=20theme=20=E2=80=94?= =?UTF-8?q?=20Google=20Maps-inspired=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a plain, utilitarian theme focused on readability and wayfinding: - White/light gray land (#f5f5f5), soft pastel green parks (#c3ecb2) - Gentle blue water (#aadaff) with classic road hierarchy - White minor roads, yellow primary (#fbc02d), orange motorway (#f9a825) - Pure white UI panels with Google-standard gray text - All 73 protomaps flavor keys + pois + landcover objects - Full UI CSS custom properties using Google color palette - Overlay config for hillshade, contours, public lands, trails Update theme switcher to cycle through all available themes. Co-Authored-By: Claude Opus 4.5 --- src/components/Panel.jsx | 35 +++- src/themes/clean.js | 351 +++++++++++++++++++++++++++++++++++++++ src/themes/registry.js | 4 +- 3 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 src/themes/clean.js diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx index e490324..cd6dd43 100644 --- a/src/components/Panel.jsx +++ b/src/components/Panel.jsx @@ -1,5 +1,6 @@ import { useRef, useCallback, useEffect, useState } from 'react' -import { Sun, Moon, LogIn, LogOut } from 'lucide-react' +import { Sun, Moon, Sparkles, LogIn, LogOut } from 'lucide-react' +import { themeList } from '../themes/registry' import { useStore, usePanelState } from '../store' import { hasFeature } from '../config' import SearchBar from './SearchBar' @@ -54,10 +55,30 @@ export default function Panel({ onManeuverClick }) { return () => window.removeEventListener('resize', check) }, []) - // Theme toggle + // Theme toggle - cycles through all available themes const toggleTheme = () => { - const next = theme === 'dark' ? 'light' : 'dark' - setThemeOverride(next) + const themes = themeList() + const currentIdx = themes.findIndex(t => t.id === theme) + const nextIdx = (currentIdx + 1) % themes.length + setThemeOverride(themes[nextIdx].id) + } + + // Get theme icon based on current theme + const getThemeIcon = () => { + switch (theme) { + case 'dark': return + case 'light': return + case 'clean': return + default: return + } + } + + // Get next theme name for tooltip + const getNextThemeName = () => { + const themes = themeList() + const currentIdx = themes.findIndex(t => t.id === theme) + const nextIdx = (currentIdx + 1) % themes.length + return themes[nextIdx].name } // Auth handlers @@ -257,10 +278,10 @@ export default function Panel({ onManeuverClick }) { onClick={toggleTheme} className="p-1.5 rounded" style={{ color: 'var(--text-secondary)' }} - aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`} - title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`} + aria-label={`Switch to ${getNextThemeName()} theme`} + title={`Switch to ${getNextThemeName()} theme`} > - {theme === 'dark' ? : } + {getThemeIcon()} diff --git a/src/themes/clean.js b/src/themes/clean.js new file mode 100644 index 0000000..3215ede --- /dev/null +++ b/src/themes/clean.js @@ -0,0 +1,351 @@ +/** + * Clean Theme for Navi + * + * A plain, familiar, Google Maps-inspired style focused on maximum usability. + * Clean, neutral, utilitarian. White/light gray land, soft pastel green parks, + * gentle blue water, classic gray→yellow→orange road hierarchy. No strong + * personality — everything serves readability and wayfinding. + * + * The theme equivalent of a rental car: nothing exciting, nothing wrong. + */ + +// ═══════════════════════════════════════════════════════════════════════════ +// PALETTE +// ═══════════════════════════════════════════════════════════════════════════ +// +// base: #f5f5f5 ← land, app background +// surface: #ffffff ← panels, cards, modals +// surfaceAlt: #f8f9fa ← secondary panels, hover states +// border: #dadce0 ← Google's standard border gray +// text: #202124 ← primary text (Google dark) +// textSecondary: #5f6368 ← secondary text +// textMuted: #9aa0a6 ← placeholders, hints +// accent: #1a73e8 ← Google blue — links, active states +// accentHover: #1557b0 ← darker blue hover +// success: #34a853 ← Google green +// warning: #fbbc04 ← Google yellow +// danger: #ea4335 ← Google red +// water: #aadaff ← soft sky blue (Google's water) +// waterDark: #73b3e8 ← water labels +// vegetation: #c3ecb2 ← pastel green parks +// forest: #a8dda0 ← slightly deeper green +// road: #ffffff ← minor roads — white +// roadPrimary: #fbc02d ← yellow +// roadMotorway: #f9a825 ← deeper yellow-orange +// roadCasing: #e0e0e0 ← light gray casing +// building: #e8e4de ← warm light gray +// contour: #c8b8a0 ← subtle warm brown +// +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Map flavor colors - protomaps-themes-base schema + * All 73 flat keys + pois + landcover nested objects + */ +const cleanColors = { + // Background & earth + background: '#e8e8e8', + earth: '#f5f5f5', + + // Land use areas + park_a: '#d4ecd0', + park_b: '#c3ecb2', + hospital: '#fde8e8', + industrial: '#ebeff1', + school: '#fff3e0', + wood_a: '#d8ecd4', + wood_b: '#a8dda0', + pedestrian: '#f0f0f0', + scrub_a: '#dcecd8', + scrub_b: '#c8e4c0', + glacier: '#f8fcff', + sand: '#f5f0e0', + beach: '#fef8e0', + aerodrome: '#eaecef', + runway: '#d0d0d0', + water: '#aadaff', + zoo: '#d8e8d8', + military: '#e8e8e8', + + // Tunnels + tunnel_other_casing: '#d8d8d8', + tunnel_minor_casing: '#d8d8d8', + tunnel_link_casing: '#d8d8d8', + tunnel_major_casing: '#d8d8d8', + tunnel_highway_casing: '#d8d8d8', + tunnel_other: '#e8e8e8', + tunnel_minor: '#e8e8e8', + tunnel_link: '#f0e0a0', + tunnel_major: '#f0e0a0', + tunnel_highway: '#f0d080', + + // Pier & buildings + pier: '#e0e0e0', + buildings: '#e8e4de', + + // Roads & casings + minor_service_casing: '#e0e0e0', + minor_casing: '#e0e0e0', + link_casing: '#d8c080', + major_casing_late: '#d8c080', + highway_casing_late: '#d8a860', + other: '#f0f0f0', + minor_service: '#ffffff', + minor_a: '#ffffff', + minor_b: '#ffffff', + link: '#fbc02d', + major_casing_early: '#d8c080', + major: '#fbc02d', + highway_casing_early: '#d8a860', + highway: '#f9a825', + railway: '#a0a0a0', + boundaries: '#c0c0c0', + + // Waterway label + waterway_label: '#73b3e8', + + // Bridges + bridges_other_casing: '#d0d0d0', + bridges_minor_casing: '#d0d0d0', + bridges_link_casing: '#d8c080', + bridges_major_casing: '#d8c080', + bridges_highway_casing: '#d8a860', + bridges_other: '#f0f0f0', + bridges_minor: '#ffffff', + bridges_link: '#fbc02d', + bridges_major: '#fbc02d', + bridges_highway: '#f9a825', + + // Labels + roads_label_minor: '#5f6368', + roads_label_minor_halo: '#ffffff', + roads_label_major: '#5f6368', + roads_label_major_halo: '#ffffff', + ocean_label: '#73b3e8', + peak_label: '#5f6368', + subplace_label: '#5f6368', + subplace_label_halo: '#ffffff', + city_label: '#202124', + city_label_halo: '#ffffff', + state_label: '#9aa0a6', + state_label_halo: '#ffffff', + country_label: '#5f6368', + address_label: '#5f6368', + address_label_halo: '#ffffff', + + // POI icon colors + pois: { + blue: '#1a73e8', + green: '#34a853', + lapis: '#4285f4', + pink: '#e91e63', + red: '#ea4335', + slategray: '#5f6368', + tangerine: '#f9a825', + turquoise: '#00bcd4', + }, + + // Landcover fill colors + landcover: { + grassland: 'rgba(200, 232, 192, 1)', + barren: 'rgba(240, 235, 220, 1)', + urban_area: 'rgba(235, 235, 235, 1)', + farmland: 'rgba(216, 240, 210, 1)', + glacier: 'rgba(250, 252, 255, 1)', + scrub: 'rgba(220, 236, 216, 1)', + forest: 'rgba(180, 224, 176, 1)', + }, +} + +/** + * UI CSS custom properties - app chrome styling + * Clean Google-inspired white panels with standard gray text + */ +const cleanUI = { + '--bg-base': '#f5f5f5', + '--bg-raised': '#ffffff', + '--bg-overlay': '#ffffff', + '--bg-input': '#ffffff', + '--text-primary': '#202124', + '--text-secondary': '#5f6368', + '--text-tertiary': '#9aa0a6', + '--text-inverse': '#ffffff', + '--border': '#dadce0', + '--border-subtle': '#e8eaed', + '--accent': '#1a73e8', + '--accent-hover': '#1557b0', + '--accent-muted': '#e8f0fe', + '--tan': '#f9a825', + '--tan-muted': '#fef7e0', + '--pin-origin': '#34a853', + '--pin-destination': '#ea4335', + '--pin-intermediate': '#5f6368', + '--pin-stroke': '#ffffff', + '--status-success': '#34a853', + '--status-warning': '#fbbc04', + '--status-danger': '#ea4335', + '--route-line': '#1a73e8', + '--shadow': '0 1px 3px rgba(60, 64, 67, 0.15), 0 1px 2px rgba(60, 64, 67, 0.1)', + '--shadow-lg': '0 2px 6px rgba(60, 64, 67, 0.2), 0 1px 3px rgba(60, 64, 67, 0.15)', +} + +/** + * Overlay configuration overrides + * Light shadow hillshade, warm brown contours, standard public lands + */ +const cleanOverlay = { + // Hillshade - light and natural + hillshade: { + exaggeration: 0.4, + illuminationDirection: 315, + shadowColor: '#000000', + highlightColor: '#ffffff', + }, + + // Contours - warm brown, subtle + contours: { + opacityMod: 0.9, + minorColor: '#c8b8a0', + minorOpacity: 0.35, + minorWidth: { z11: 0.5, z14: 0.8 }, + intermediateColor: '#c8b8a0', + intermediateOpacity: 0.55, + intermediateWidth: { z8: 0.7, z14: 1.0 }, + indexColor: '#a89878', + indexOpacity: 0.75, + indexWidth: { z4: 1.0, z14: 1.5 }, + labelColor: '#8a7a60', + labelHaloColor: '#ffffff', + labelHaloWidth: 1.5, + labelOpacity: 0.8, + labelSize: 10, + labelFont: ['Noto Sans Regular'], + }, + + // Contours Test - blue variant + contoursTest: { + minorColor: '#5a9ab8', + intermediateColor: '#5a9ab8', + indexColor: '#3a7a98', + labelColor: '#3a6a88', + }, + + // Contours Test 10ft - green variant + contoursTest10ft: { + minorColor: '#4a9a5f', + intermediateColor: '#4a9a5f', + indexColor: '#2a7a4a', + labelColor: '#2a5a40', + }, + + // Public Lands - standard green tints with dark labels + publicLands: { + opacityMod: 0.9, + // Fill colors per category + fillWA: '#8a7a40', + fillNPS: '#4a8030', + fillUSFS: '#6a9040', + fillBLM: '#d4b880', + fillFWS: '#5a9068', + fillSTAT: '#6aa088', + fillLOC: '#9ab8a8', + fillDefault: '#b0b0b0', + // Fill opacities + fillOpacityWA: 0.25, + fillOpacityNPS: 0.25, + fillOpacityUSFS: 0.20, + fillOpacityBLM: 0.18, + fillOpacitySTAT: 0.22, + fillOpacityLOC: 0.18, + fillOpacityDefault: 0.12, + // Outline colors + outlineWA: '#6a5a28', + outlineNPS: '#2a5018', + outlineUSFS: '#4a6828', + outlineBLM: '#9a8050', + outlineFWS: '#3a6848', + outlineSTAT: '#4a7060', + outlineLOC: '#6a8070', + outlineDefault: '#808080', + // Outline opacities + outlineOpacityNPS: 0.65, + outlineOpacityUSFS: 0.55, + outlineOpacityDefault: 0.45, + // Outline width + outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 }, + // Labels - dark for readability + labelColor: '#2a3a28', + labelHaloColor: '#ffffff', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: { z10: 10, z14: 13 }, + labelFont: ['Noto Sans Regular'], + }, + + // USFS Trails - standard trail colors + usfsTrails: { + roadsColor: '#c09050', + roadsOpacity: 0.85, + roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, + trailsMotorized: '#e07030', + trailsBicycle: '#d0a030', + trailsHiker: '#50b040', + trailsDefault: '#b09050', + trailsOpacity: 0.85, + trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + trailsDash: [2, 1.5], + roadsLabelColor: '#5a4a30', + roadsLabelHaloColor: '#ffffff', + roadsLabelHaloWidth: 1.5, + roadsLabelOpacity: 0.85, + roadsLabelSize: 11, + trailsLabelColor: '#4a3a28', + trailsLabelHaloColor: '#ffffff', + trailsLabelHaloWidth: 1.5, + trailsLabelOpacity: 0.85, + trailsLabelSize: 11, + labelFont: ['Noto Sans Regular'], + hitWidth: 14, + }, + + // BLM Trails - standard route colors + blmTrails: { + color4wdHigh: '#e07030', + color4wdLow: '#d0a030', + colorAtv: '#d03030', + colorMotoSingle: '#a060b0', + color2wdLow: '#e0c060', + colorNonMech: '#50b040', + colorDefault: '#b09050', + colorSnow: '#6090c0', + lineOpacity: 0.85, + lineOpacityOther: 0.80, + lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + dashImproved: [4, 2], + dashAggregate: [1, 2], + dashSnow: [4, 2, 1, 2], + dashOther: [4, 2, 1, 2, 1, 2], + labelColor: '#4a3a28', + labelHaloColor: '#ffffff', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: 11, + labelFont: ['Noto Sans Regular'], + hitWidth: 14, + }, +} + +/** + * Clean theme configuration + */ +const cleanTheme = { + id: 'clean', + name: 'Clean', + dark: false, + colors: cleanColors, + satellite: null, // No adjustments — default clear view + overlay: cleanOverlay, + ui: cleanUI, +} + +export default cleanTheme diff --git a/src/themes/registry.js b/src/themes/registry.js index 95246f9..dfc419d 100644 --- a/src/themes/registry.js +++ b/src/themes/registry.js @@ -14,7 +14,8 @@ * ui: object - CSS custom properties for UI elements */ -import { namedTheme } from 'protomaps-themes-base' +import { namedTheme } from 'protomaps-themes-base' +import cleanTheme from './clean.js' // ═══════════════════════════════════════════════════════════════════════════ // UI CSS CUSTOM PROPERTIES @@ -441,6 +442,7 @@ const themes = { overlay: darkOverlay, ui: darkUI, }, + clean: cleanTheme, // Custom themes go here. Example: // 'midnight': { // id: 'midnight',