From 40855e126174b62e9ec7362f2b9960b7efb31b59 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 May 2026 18:34:36 +0000 Subject: [PATCH] Revert "feat(themes): add theme-aware boundary highlight config" This reverts commit fef10664c8230183eb7789e171637e9b54aba269. --- src/components/MapView.jsx | 38 +- src/themes/clean.js | 746 ++++++++++---------- src/themes/cyberpunk.js | 816 +++++++++++----------- src/themes/registry.js | 1316 ++++++++++++++++++------------------ 4 files changed, 1431 insertions(+), 1485 deletions(-) diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx index 5b8eb3c..a49143e 100644 --- a/src/components/MapView.jsx +++ b/src/components/MapView.jsx @@ -1247,10 +1247,10 @@ function removeBlmTrails(map) { } -/** Add boundary polygon layers using theme-aware highlight config */ +/** Add boundary polygon layers with computed accent color (MapLibre rejects CSS vars in paint) */ const BOUNDARY_FILL_LAYER = 'boundary-fill-layer' -function addBoundaryLayer(map, themeId) { +function addBoundaryLayer(map) { if (!map || map.getLayer(BOUNDARY_LAYER)) return if (!map.getSource(BOUNDARY_SOURCE)) { map.addSource(BOUNDARY_SOURCE, { @@ -1258,14 +1258,7 @@ function addBoundaryLayer(map, themeId) { data: { type: "FeatureCollection", features: [] }, }) } - // Get highlight config from theme overlay - const highlight = getOverlayConfig(themeId, "highlight") || {} - const lineColor = highlight.lineColor || "#7a9a6b" - const lineWidth = highlight.lineWidth || 2 - const lineDash = highlight.lineDash || [4, 4] - const lineOpacity = highlight.lineOpacity || 0.8 - const fillColor = highlight.fillColor || lineColor - const fillOpacity = highlight.fillOpacity || 0.08 + const accentColor = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#7a9a6b" // Find first symbol layer to insert boundary layers below labels const layers = map.getStyle().layers @@ -1283,8 +1276,8 @@ function addBoundaryLayer(map, themeId) { type: "fill", source: BOUNDARY_SOURCE, paint: { - "fill-color": fillColor, - "fill-opacity": fillOpacity, + "fill-color": accentColor, + "fill-opacity": 0.05, }, }, firstSymbolId) @@ -1294,10 +1287,10 @@ function addBoundaryLayer(map, themeId) { type: "line", source: BOUNDARY_SOURCE, paint: { - "line-color": lineColor, - "line-width": lineWidth, - "line-opacity": lineOpacity, - "line-dasharray": lineDash, + "line-color": accentColor, + "line-width": 2, + "line-opacity": 0.7, + "line-dasharray": [3, 2], }, }, firstSymbolId) } @@ -1505,14 +1498,7 @@ const MapView = forwardRef(function MapView(_, ref) { type: "geojson", data: { type: "FeatureCollection", features: [] }, }) - // Get highlight config from theme overlay - const highlight = getOverlayConfig(themeId, "highlight") || {} - const lineColor = highlight.lineColor || "#7a9a6b" - const lineWidth = highlight.lineWidth || 2 - const lineDash = highlight.lineDash || [4, 4] - const lineOpacity = highlight.lineOpacity || 0.8 - const fillColor = highlight.fillColor || lineColor - const fillOpacity = highlight.fillOpacity || 0.08 + const accentColor = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#7a9a6b" map.addLayer({ id: MEASURE_LINE_LAYER, type: "line", @@ -2138,7 +2124,7 @@ const MapView = forwardRef(function MapView(_, ref) { // Boundary polygon layer for selected places if (!map.getLayer(BOUNDARY_LAYER)) { - addBoundaryLayer(map, currentThemeRef.current) + addBoundaryLayer(map) } // Apply improved base label styling for readability @@ -2347,7 +2333,7 @@ const MapView = forwardRef(function MapView(_, ref) { // Boundary polygon layer if (!map.getLayer(BOUNDARY_LAYER)) { - addBoundaryLayer(map, currentThemeRef.current) + addBoundaryLayer(map) } // Apply improved base label styling for readability diff --git a/src/themes/clean.js b/src/themes/clean.js index 2ed3b87..b13e52d 100644 --- a/src/themes/clean.js +++ b/src/themes/clean.js @@ -1,379 +1,369 @@ -/** - * 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 = { - // Fonts - '--font-sans': "'Inter', system-ui, -apple-system, sans-serif", - '--font-mono': "'JetBrains Mono', ui-monospace, monospace", +/** + * 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 = { + // Fonts + '--font-sans': "'Inter', system-ui, -apple-system, sans-serif", + '--font-mono': "'JetBrains Mono', ui-monospace, monospace", '--font-heading': "'Inter', system-ui, -apple-system, sans-serif", - // Backgrounds - '--bg-base': '#f5f5f5', - '--bg-raised': '#ffffff', - '--bg-overlay': '#ffffff', - '--bg-input': '#ffffff', - '--bg-inset': '#f0f0f0', - '--bg-muted': '#f8f9fa', - // Text - '--text-primary': '#202124', - '--text-secondary': '#5f6368', - '--text-tertiary': '#9aa0a6', - '--text-inverse': '#ffffff', - // Borders - '--border': '#dadce0', - '--border-subtle': '#e8eaed', - // Accent - '--accent': '#1a73e8', - '--accent-hover': '#1557b0', - '--accent-muted': '#e8f0fe', - // Tan - '--tan': '#f9a825', - '--tan-muted': '#fef7e0', - // Pins - '--pin-origin': '#34a853', - '--pin-destination': '#ea4335', - '--pin-intermediate': '#5f6368', - '--pin-stroke': '#ffffff', - // Status - '--status-success': '#34a853', - '--status-warning': '#fbbc04', - '--status-danger': '#ea4335', - '--success': '#34a853', - '--warning': '#fbbc04', - '--warning-muted': '#fef7e0', - // Route - '--route-line': '#1a73e8', - // Shadows - '--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, - }, - - // ── Highlight (boundary/selection) ──────────────────────────────────────── - highlight: { - lineColor: "#1a73e8", // Google blue for selection - lineWidth: 2, - lineDash: [4, 4], - lineOpacity: 0.7, - fillColor: "#1a73e8", - fillOpacity: 0.06, - }, -} - -/** - * 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 + // Backgrounds + '--bg-base': '#f5f5f5', + '--bg-raised': '#ffffff', + '--bg-overlay': '#ffffff', + '--bg-input': '#ffffff', + '--bg-inset': '#f0f0f0', + '--bg-muted': '#f8f9fa', + // Text + '--text-primary': '#202124', + '--text-secondary': '#5f6368', + '--text-tertiary': '#9aa0a6', + '--text-inverse': '#ffffff', + // Borders + '--border': '#dadce0', + '--border-subtle': '#e8eaed', + // Accent + '--accent': '#1a73e8', + '--accent-hover': '#1557b0', + '--accent-muted': '#e8f0fe', + // Tan + '--tan': '#f9a825', + '--tan-muted': '#fef7e0', + // Pins + '--pin-origin': '#34a853', + '--pin-destination': '#ea4335', + '--pin-intermediate': '#5f6368', + '--pin-stroke': '#ffffff', + // Status + '--status-success': '#34a853', + '--status-warning': '#fbbc04', + '--status-danger': '#ea4335', + '--success': '#34a853', + '--warning': '#fbbc04', + '--warning-muted': '#fef7e0', + // Route + '--route-line': '#1a73e8', + // Shadows + '--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/cyberpunk.js b/src/themes/cyberpunk.js index d5e09c4..86a8e92 100644 --- a/src/themes/cyberpunk.js +++ b/src/themes/cyberpunk.js @@ -1,413 +1,403 @@ -/** - * Cyberpunk Theme for Navi - * - * Inspired by Mapbox's "Terminal" cyberpunk style, Blade Runner, and Ghost in - * the Shell. A tactical display in a neon-lit command center. Near-black base - * with deep blue-purple undertones. Roads glow in hot magenta and electric cyan. - * Water is inky dark. Vegetation is barely there — dark teal hints. Labels are - * cool white with colored halos. - * - * The whole thing should feel like you're navigating Night City. - * - * CUSTOM FONTS: - * - Heading: "Orbitron" — geometric, futuristic display font - * - Body: "Share Tech Mono" — monospaced terminal feel for entire UI - */ - -// ═══════════════════════════════════════════════════════════════════════════ -// PALETTE -// ═══════════════════════════════════════════════════════════════════════════ -// -// base: #0a0a14 ← near-black with blue-purple undertone -// surface: #10101e ← panels, cards -// surfaceAlt: #161628 ← secondary surfaces, hover states -// border: #1e1e3a ← subtle purple edges -// text: #d0d0e8 ← cool white text -// textSecondary: #8888aa ← lavender-gray -// textMuted: #5a5a7a ← dark purple-gray -// textInverse: #0a0a14 ← text on neon backgrounds -// accent: #ff2d6b ← hot pink/magenta — primary actions -// accentHover: #ff4d8b ← lighter magenta -// accentAlt: #00f0ff ← electric cyan — secondary accent -// success: #00ff88 ← neon green -// warning: #ffaa00 ← amber -// danger: #ff3333 ← neon red -// water: #06061a ← deep dark blue-black -// waterLabel: #3a6a8a ← muted blue for water labels -// vegetation: #0a1a12 ← barely-there dark teal-green -// forest: #0e1e14 ← slightly deeper -// road: #1a1a3a ← ghost purple minor roads -// roadSecondary: #2a2a5a -// roadPrimary: #8833aa ← purple for primary -// roadMotorway: #ff2d6b ← hot magenta for motorways -// roadCasing: #0a0a14 ← dark casing -// building: #141428 ← dark purple-gray buildings -// contour: #1e1e3e ← dark lines, just visible -// contourLabel: #5a5a7a -// -// ═══════════════════════════════════════════════════════════════════════════ - -/** - * Map flavor colors - protomaps-themes-base schema - * All 73 flat keys + pois + landcover nested objects - */ -const cyberpunkColors = { - // Background & earth - background: '#08080f', - earth: '#0a0a14', - - // Land use areas - dark with slight purple undertones - park_a: '#0a1a14', - park_b: '#0e1e18', - hospital: '#1a1020', - industrial: '#0e0e1a', - school: '#14101e', - wood_a: '#0a1a12', - wood_b: '#0e1e14', - pedestrian: '#0c0c18', - scrub_a: '#0a1410', - scrub_b: '#0c1812', - glacier: '#101020', - sand: '#12101a', - beach: '#14121c', - aerodrome: '#0a0a16', - runway: '#1a1a30', - water: '#06061a', - zoo: '#0c1614', - military: '#100a14', - - // Tunnels - dark purple casings - tunnel_other_casing: '#0a0a14', - tunnel_minor_casing: '#0a0a14', - tunnel_link_casing: '#0a0a14', - tunnel_major_casing: '#0a0a14', - tunnel_highway_casing: '#0a0a14', - tunnel_other: '#161628', - tunnel_minor: '#161628', - tunnel_link: '#2a2050', - tunnel_major: '#4a2870', - tunnel_highway: '#801848', - - // Pier & buildings - pier: '#1a1a30', - buildings: '#141428', - - // Roads & casings - glowing neon progression - minor_service_casing: '#0a0a14', - minor_casing: '#0a0a14', - link_casing: '#0a0a14', - major_casing_late: '#0a0a14', - highway_casing_late: '#0a0a14', - other: '#1a1a3a', - minor_service: '#1a1a3a', - minor_a: '#2a2a5a', - minor_b: '#1a1a3a', - link: '#5a3888', - major_casing_early: '#0a0a14', - major: '#8833aa', - highway_casing_early: '#0a0a14', - highway: '#ff2d6b', - railway: '#2a2050', - boundaries: '#4a4a6a', - - // Waterway label - waterway_label: '#3a6a8a', - - // Bridges - same neon colors - bridges_other_casing: '#0c0c18', - bridges_minor_casing: '#0a0a14', - bridges_link_casing: '#0a0a14', - bridges_major_casing: '#0a0a14', - bridges_highway_casing: '#0a0a14', - bridges_other: '#1a1a3a', - bridges_minor: '#2a2a5a', - bridges_link: '#5a3888', - bridges_major: '#8833aa', - bridges_highway: '#ff2d6b', - - // Labels - cool white with DARK halos - roads_label_minor: '#8888aa', - roads_label_minor_halo: '#0a0a14', - roads_label_major: '#a0a0c0', - roads_label_major_halo: '#0a0a14', - ocean_label: '#3a6a8a', - peak_label: '#8888aa', - subplace_label: '#8888aa', - subplace_label_halo: '#0a0a14', - city_label: '#d0d0e8', - city_label_halo: '#0a0a14', - state_label: '#5a5a7a', - state_label_halo: '#0a0a14', - country_label: '#7a7a9a', - address_label: '#8888aa', - address_label_halo: '#0a0a14', - - // POI icon colors - neon palette - pois: { - blue: '#00a0ff', - green: '#00ff88', - lapis: '#6060ff', - pink: '#ff2d6b', - red: '#ff3333', - slategray: '#8888aa', - tangerine: '#ffaa00', - turquoise: '#00f0ff', - }, - - // Landcover fill colors - very dark, barely visible - landcover: { - grassland: 'rgba(10, 26, 18, 1)', - barren: 'rgba(18, 16, 26, 1)', - urban_area: 'rgba(14, 14, 26, 1)', - farmland: 'rgba(12, 24, 16, 1)', - glacier: 'rgba(16, 16, 32, 1)', - scrub: 'rgba(12, 20, 16, 1)', - forest: 'rgba(14, 30, 20, 1)', - }, -} - -/** - * UI CSS custom properties - neon command center aesthetic - * Dark translucent panels with magenta/cyan accents - */ -const cyberpunkUI = { - // Fonts - monospace terminal feel - '--font-sans': "'Share Tech Mono', monospace", - '--font-mono': "'Share Tech Mono', monospace", - '--font-heading': "'Orbitron', sans-serif", - // Backgrounds - dark with blue-purple undertone - '--bg-base': '#0a0a14', - '--bg-raised': '#10101e', - '--bg-overlay': '#161628', - '--bg-input': '#0c0c18', - '--bg-inset': '#08080f', - '--bg-muted': '#12121e', - // Text - cool white spectrum - '--text-primary': '#d0d0e8', - '--text-secondary': '#8888aa', - '--text-tertiary': '#5a5a7a', - '--text-inverse': '#0a0a14', - // Borders - subtle purple edges - '--border': '#1e1e3a', - '--border-subtle': '#141428', - // Accent - hot magenta - '--accent': '#ff2d6b', - '--accent-hover': '#ff4d8b', - '--accent-muted': '#3a1828', - // Tan becomes cyan in this theme - '--tan': '#00f0ff', - '--tan-muted': '#0a2830', - // Pins - neon colors - '--pin-origin': '#ff2d6b', - '--pin-destination': '#00f0ff', - '--pin-intermediate': '#8833aa', - '--pin-stroke': '#0a0a14', - // Status - neon signals - '--status-success': '#00ff88', - '--status-warning': '#ffaa00', - '--status-danger': '#ff3333', - '--success': '#00ff88', - '--warning': '#ffaa00', - '--warning-muted': '#2a2010', - // Route - cyan for contrast with magenta UI - '--route-line': '#00f0ff', - // Shadows - subtle magenta glow - '--shadow': '0 2px 8px rgba(255, 45, 107, 0.25)', - '--shadow-lg': '0 4px 16px rgba(255, 45, 107, 0.35)', -} - -/** - * Overlay configuration - subtle, muted for dark theme - */ -const cyberpunkOverlay = { - // Hillshade - dramatic shadows - hillshade: { - exaggeration: 0.6, - illuminationDirection: 315, - shadowColor: '#000000', - highlightColor: '#2a2a4a', - }, - - // Contours - very subtle dark purple-gray - contours: { - opacityMod: 0.5, - minorColor: '#1e1e3e', - minorOpacity: 0.3, - minorWidth: { z11: 0.4, z14: 0.8 }, - intermediateColor: '#2a2a4a', - intermediateOpacity: 0.4, - intermediateWidth: { z8: 0.6, z14: 1.0 }, - indexColor: '#3a3a5a', - indexOpacity: 0.5, - indexWidth: { z4: 0.8, z14: 1.2 }, - labelColor: '#5a5a7a', - labelHaloColor: '#0a0a14', - labelHaloWidth: 1.5, - labelOpacity: 0.6, - labelSize: 10, - labelFont: ['Noto Sans Regular'], - }, - - // Contours Test - cyan variant - contoursTest: { - minorColor: '#1a3a4a', - intermediateColor: '#2a4a5a', - indexColor: '#3a5a6a', - labelColor: '#5a8a9a', - }, - - // Contours Test 10ft - purple variant - contoursTest10ft: { - minorColor: '#2a1a4a', - intermediateColor: '#3a2a5a', - indexColor: '#4a3a6a', - labelColor: '#7a6a9a', - }, - - // Public Lands - very muted fills - publicLands: { - opacityMod: 0.5, - // Fill colors - dark teal/purple tints - fillWA: '#1a2a20', - fillNPS: '#0a2a1a', - fillUSFS: '#102820', - fillBLM: '#1a2828', - fillFWS: '#0a2a2a', - fillSTAT: '#102028', - fillLOC: '#182028', - fillDefault: '#1a1a2a', - // Fill opacities - very low - fillOpacityWA: 0.25, - fillOpacityNPS: 0.25, - fillOpacityUSFS: 0.20, - fillOpacityBLM: 0.15, - fillOpacitySTAT: 0.20, - fillOpacityLOC: 0.15, - fillOpacityDefault: 0.10, - // Outline colors - subtle - outlineWA: '#2a3a30', - outlineNPS: '#1a3a2a', - outlineUSFS: '#203830', - outlineBLM: '#2a3838', - outlineFWS: '#1a3a3a', - outlineSTAT: '#203038', - outlineLOC: '#283038', - outlineDefault: '#2a2a3a', - // Outline opacities - outlineOpacityNPS: 0.5, - outlineOpacityUSFS: 0.4, - outlineOpacityDefault: 0.3, - // Outline width - outlineWidth: { z4: 0.3, z8: 0.6, z12: 1.0 }, - // Labels - muted teal - labelColor: '#5a8a8a', - labelHaloColor: '#0a0a14', - labelHaloWidth: 1.5, - labelOpacity: 0.7, - labelSize: { z10: 10, z14: 12 }, - labelFont: ['Noto Sans Regular'], - }, - - // USFS Trails - purple/magenta/cyan family instead of earthy browns - usfsTrails: { - // Roads - purple - roadsColor: '#8833aa', - roadsOpacity: 0.85, - roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, - // Trails - neon colors by use type - trailsMotorized: '#ff2d6b', - trailsBicycle: '#ffaa00', - trailsHiker: '#00ff88', - trailsDefault: '#8833aa', - trailsOpacity: 0.85, - trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, - trailsDash: [2, 1.5], - // Road labels - roadsLabelColor: '#a080c0', - roadsLabelHaloColor: '#0a0a14', - roadsLabelHaloWidth: 1.5, - roadsLabelOpacity: 0.85, - roadsLabelSize: 11, - // Trail labels - trailsLabelColor: '#a080c0', - trailsLabelHaloColor: '#0a0a14', - trailsLabelHaloWidth: 1.5, - trailsLabelOpacity: 0.85, - trailsLabelSize: 11, - labelFont: ['Noto Sans Regular'], - // Hit layer - hitWidth: 14, - }, - - // BLM Trails - purple/cyan/magenta family - blmTrails: { - // Route colors - neon family - color4wdHigh: '#ff2d6b', - color4wdLow: '#cc2288', - colorAtv: '#ff3333', - colorMotoSingle: '#aa44cc', - color2wdLow: '#8833aa', - colorNonMech: '#00ff88', - colorDefault: '#6644aa', - colorSnow: '#00f0ff', - lineOpacity: 0.85, - lineOpacityOther: 0.75, - lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, - // Dash patterns - dashImproved: [4, 2], - dashAggregate: [1, 2], - dashSnow: [4, 2, 1, 2], - dashOther: [4, 2, 1, 2, 1, 2], - // Labels - labelColor: '#a080c0', - labelHaloColor: '#0a0a14', - labelHaloWidth: 1.5, - labelOpacity: 0.85, - labelSize: 11, - labelFont: ['Noto Sans Regular'], - // Hit layer - hitWidth: 14, - }, - - // ── Highlight (boundary/selection) ──────────────────────────────────────── - highlight: { - lineColor: "#00f0ff", // Electric cyan for selection - lineWidth: 2, - lineDash: [4, 4], - lineOpacity: 0.9, - fillColor: "#00f0ff", - fillOpacity: 0.1, - }, -} - -/** - * Satellite adjustments - dark, desaturated, purple-shifted - */ -const cyberpunkSatellite = { - opacity: 0.8, - brightnessMin: 0.0, - brightnessMax: 0.30, - contrast: 0.15, - saturation: -0.6, - hueRotate: 280, -} - -/** - * Cyberpunk theme configuration - */ -const cyberpunkTheme = { - id: 'cyberpunk', - name: 'Cyberpunk', - dark: true, - swatch: ['#0a0a14', '#ff2d6b', '#00f0ff'], - fontImports: [ - 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap', - 'https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap', - ], - colors: cyberpunkColors, - satellite: cyberpunkSatellite, - overlay: cyberpunkOverlay, - ui: cyberpunkUI, -} - -export default cyberpunkTheme +/** + * Cyberpunk Theme for Navi + * + * Inspired by Mapbox's "Terminal" cyberpunk style, Blade Runner, and Ghost in + * the Shell. A tactical display in a neon-lit command center. Near-black base + * with deep blue-purple undertones. Roads glow in hot magenta and electric cyan. + * Water is inky dark. Vegetation is barely there — dark teal hints. Labels are + * cool white with colored halos. + * + * The whole thing should feel like you're navigating Night City. + * + * CUSTOM FONTS: + * - Heading: "Orbitron" — geometric, futuristic display font + * - Body: "Share Tech Mono" — monospaced terminal feel for entire UI + */ + +// ═══════════════════════════════════════════════════════════════════════════ +// PALETTE +// ═══════════════════════════════════════════════════════════════════════════ +// +// base: #0a0a14 ← near-black with blue-purple undertone +// surface: #10101e ← panels, cards +// surfaceAlt: #161628 ← secondary surfaces, hover states +// border: #1e1e3a ← subtle purple edges +// text: #d0d0e8 ← cool white text +// textSecondary: #8888aa ← lavender-gray +// textMuted: #5a5a7a ← dark purple-gray +// textInverse: #0a0a14 ← text on neon backgrounds +// accent: #ff2d6b ← hot pink/magenta — primary actions +// accentHover: #ff4d8b ← lighter magenta +// accentAlt: #00f0ff ← electric cyan — secondary accent +// success: #00ff88 ← neon green +// warning: #ffaa00 ← amber +// danger: #ff3333 ← neon red +// water: #06061a ← deep dark blue-black +// waterLabel: #3a6a8a ← muted blue for water labels +// vegetation: #0a1a12 ← barely-there dark teal-green +// forest: #0e1e14 ← slightly deeper +// road: #1a1a3a ← ghost purple minor roads +// roadSecondary: #2a2a5a +// roadPrimary: #8833aa ← purple for primary +// roadMotorway: #ff2d6b ← hot magenta for motorways +// roadCasing: #0a0a14 ← dark casing +// building: #141428 ← dark purple-gray buildings +// contour: #1e1e3e ← dark lines, just visible +// contourLabel: #5a5a7a +// +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Map flavor colors - protomaps-themes-base schema + * All 73 flat keys + pois + landcover nested objects + */ +const cyberpunkColors = { + // Background & earth + background: '#08080f', + earth: '#0a0a14', + + // Land use areas - dark with slight purple undertones + park_a: '#0a1a14', + park_b: '#0e1e18', + hospital: '#1a1020', + industrial: '#0e0e1a', + school: '#14101e', + wood_a: '#0a1a12', + wood_b: '#0e1e14', + pedestrian: '#0c0c18', + scrub_a: '#0a1410', + scrub_b: '#0c1812', + glacier: '#101020', + sand: '#12101a', + beach: '#14121c', + aerodrome: '#0a0a16', + runway: '#1a1a30', + water: '#06061a', + zoo: '#0c1614', + military: '#100a14', + + // Tunnels - dark purple casings + tunnel_other_casing: '#0a0a14', + tunnel_minor_casing: '#0a0a14', + tunnel_link_casing: '#0a0a14', + tunnel_major_casing: '#0a0a14', + tunnel_highway_casing: '#0a0a14', + tunnel_other: '#161628', + tunnel_minor: '#161628', + tunnel_link: '#2a2050', + tunnel_major: '#4a2870', + tunnel_highway: '#801848', + + // Pier & buildings + pier: '#1a1a30', + buildings: '#141428', + + // Roads & casings - glowing neon progression + minor_service_casing: '#0a0a14', + minor_casing: '#0a0a14', + link_casing: '#0a0a14', + major_casing_late: '#0a0a14', + highway_casing_late: '#0a0a14', + other: '#1a1a3a', + minor_service: '#1a1a3a', + minor_a: '#2a2a5a', + minor_b: '#1a1a3a', + link: '#5a3888', + major_casing_early: '#0a0a14', + major: '#8833aa', + highway_casing_early: '#0a0a14', + highway: '#ff2d6b', + railway: '#2a2050', + boundaries: '#4a4a6a', + + // Waterway label + waterway_label: '#3a6a8a', + + // Bridges - same neon colors + bridges_other_casing: '#0c0c18', + bridges_minor_casing: '#0a0a14', + bridges_link_casing: '#0a0a14', + bridges_major_casing: '#0a0a14', + bridges_highway_casing: '#0a0a14', + bridges_other: '#1a1a3a', + bridges_minor: '#2a2a5a', + bridges_link: '#5a3888', + bridges_major: '#8833aa', + bridges_highway: '#ff2d6b', + + // Labels - cool white with DARK halos + roads_label_minor: '#8888aa', + roads_label_minor_halo: '#0a0a14', + roads_label_major: '#a0a0c0', + roads_label_major_halo: '#0a0a14', + ocean_label: '#3a6a8a', + peak_label: '#8888aa', + subplace_label: '#8888aa', + subplace_label_halo: '#0a0a14', + city_label: '#d0d0e8', + city_label_halo: '#0a0a14', + state_label: '#5a5a7a', + state_label_halo: '#0a0a14', + country_label: '#7a7a9a', + address_label: '#8888aa', + address_label_halo: '#0a0a14', + + // POI icon colors - neon palette + pois: { + blue: '#00a0ff', + green: '#00ff88', + lapis: '#6060ff', + pink: '#ff2d6b', + red: '#ff3333', + slategray: '#8888aa', + tangerine: '#ffaa00', + turquoise: '#00f0ff', + }, + + // Landcover fill colors - very dark, barely visible + landcover: { + grassland: 'rgba(10, 26, 18, 1)', + barren: 'rgba(18, 16, 26, 1)', + urban_area: 'rgba(14, 14, 26, 1)', + farmland: 'rgba(12, 24, 16, 1)', + glacier: 'rgba(16, 16, 32, 1)', + scrub: 'rgba(12, 20, 16, 1)', + forest: 'rgba(14, 30, 20, 1)', + }, +} + +/** + * UI CSS custom properties - neon command center aesthetic + * Dark translucent panels with magenta/cyan accents + */ +const cyberpunkUI = { + // Fonts - monospace terminal feel + '--font-sans': "'Share Tech Mono', monospace", + '--font-mono': "'Share Tech Mono', monospace", + '--font-heading': "'Orbitron', sans-serif", + // Backgrounds - dark with blue-purple undertone + '--bg-base': '#0a0a14', + '--bg-raised': '#10101e', + '--bg-overlay': '#161628', + '--bg-input': '#0c0c18', + '--bg-inset': '#08080f', + '--bg-muted': '#12121e', + // Text - cool white spectrum + '--text-primary': '#d0d0e8', + '--text-secondary': '#8888aa', + '--text-tertiary': '#5a5a7a', + '--text-inverse': '#0a0a14', + // Borders - subtle purple edges + '--border': '#1e1e3a', + '--border-subtle': '#141428', + // Accent - hot magenta + '--accent': '#ff2d6b', + '--accent-hover': '#ff4d8b', + '--accent-muted': '#3a1828', + // Tan becomes cyan in this theme + '--tan': '#00f0ff', + '--tan-muted': '#0a2830', + // Pins - neon colors + '--pin-origin': '#ff2d6b', + '--pin-destination': '#00f0ff', + '--pin-intermediate': '#8833aa', + '--pin-stroke': '#0a0a14', + // Status - neon signals + '--status-success': '#00ff88', + '--status-warning': '#ffaa00', + '--status-danger': '#ff3333', + '--success': '#00ff88', + '--warning': '#ffaa00', + '--warning-muted': '#2a2010', + // Route - cyan for contrast with magenta UI + '--route-line': '#00f0ff', + // Shadows - subtle magenta glow + '--shadow': '0 2px 8px rgba(255, 45, 107, 0.25)', + '--shadow-lg': '0 4px 16px rgba(255, 45, 107, 0.35)', +} + +/** + * Overlay configuration - subtle, muted for dark theme + */ +const cyberpunkOverlay = { + // Hillshade - dramatic shadows + hillshade: { + exaggeration: 0.6, + illuminationDirection: 315, + shadowColor: '#000000', + highlightColor: '#2a2a4a', + }, + + // Contours - very subtle dark purple-gray + contours: { + opacityMod: 0.5, + minorColor: '#1e1e3e', + minorOpacity: 0.3, + minorWidth: { z11: 0.4, z14: 0.8 }, + intermediateColor: '#2a2a4a', + intermediateOpacity: 0.4, + intermediateWidth: { z8: 0.6, z14: 1.0 }, + indexColor: '#3a3a5a', + indexOpacity: 0.5, + indexWidth: { z4: 0.8, z14: 1.2 }, + labelColor: '#5a5a7a', + labelHaloColor: '#0a0a14', + labelHaloWidth: 1.5, + labelOpacity: 0.6, + labelSize: 10, + labelFont: ['Noto Sans Regular'], + }, + + // Contours Test - cyan variant + contoursTest: { + minorColor: '#1a3a4a', + intermediateColor: '#2a4a5a', + indexColor: '#3a5a6a', + labelColor: '#5a8a9a', + }, + + // Contours Test 10ft - purple variant + contoursTest10ft: { + minorColor: '#2a1a4a', + intermediateColor: '#3a2a5a', + indexColor: '#4a3a6a', + labelColor: '#7a6a9a', + }, + + // Public Lands - very muted fills + publicLands: { + opacityMod: 0.5, + // Fill colors - dark teal/purple tints + fillWA: '#1a2a20', + fillNPS: '#0a2a1a', + fillUSFS: '#102820', + fillBLM: '#1a2828', + fillFWS: '#0a2a2a', + fillSTAT: '#102028', + fillLOC: '#182028', + fillDefault: '#1a1a2a', + // Fill opacities - very low + fillOpacityWA: 0.25, + fillOpacityNPS: 0.25, + fillOpacityUSFS: 0.20, + fillOpacityBLM: 0.15, + fillOpacitySTAT: 0.20, + fillOpacityLOC: 0.15, + fillOpacityDefault: 0.10, + // Outline colors - subtle + outlineWA: '#2a3a30', + outlineNPS: '#1a3a2a', + outlineUSFS: '#203830', + outlineBLM: '#2a3838', + outlineFWS: '#1a3a3a', + outlineSTAT: '#203038', + outlineLOC: '#283038', + outlineDefault: '#2a2a3a', + // Outline opacities + outlineOpacityNPS: 0.5, + outlineOpacityUSFS: 0.4, + outlineOpacityDefault: 0.3, + // Outline width + outlineWidth: { z4: 0.3, z8: 0.6, z12: 1.0 }, + // Labels - muted teal + labelColor: '#5a8a8a', + labelHaloColor: '#0a0a14', + labelHaloWidth: 1.5, + labelOpacity: 0.7, + labelSize: { z10: 10, z14: 12 }, + labelFont: ['Noto Sans Regular'], + }, + + // USFS Trails - purple/magenta/cyan family instead of earthy browns + usfsTrails: { + // Roads - purple + roadsColor: '#8833aa', + roadsOpacity: 0.85, + roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, + // Trails - neon colors by use type + trailsMotorized: '#ff2d6b', + trailsBicycle: '#ffaa00', + trailsHiker: '#00ff88', + trailsDefault: '#8833aa', + trailsOpacity: 0.85, + trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + trailsDash: [2, 1.5], + // Road labels + roadsLabelColor: '#a080c0', + roadsLabelHaloColor: '#0a0a14', + roadsLabelHaloWidth: 1.5, + roadsLabelOpacity: 0.85, + roadsLabelSize: 11, + // Trail labels + trailsLabelColor: '#a080c0', + trailsLabelHaloColor: '#0a0a14', + trailsLabelHaloWidth: 1.5, + trailsLabelOpacity: 0.85, + trailsLabelSize: 11, + labelFont: ['Noto Sans Regular'], + // Hit layer + hitWidth: 14, + }, + + // BLM Trails - purple/cyan/magenta family + blmTrails: { + // Route colors - neon family + color4wdHigh: '#ff2d6b', + color4wdLow: '#cc2288', + colorAtv: '#ff3333', + colorMotoSingle: '#aa44cc', + color2wdLow: '#8833aa', + colorNonMech: '#00ff88', + colorDefault: '#6644aa', + colorSnow: '#00f0ff', + lineOpacity: 0.85, + lineOpacityOther: 0.75, + lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + // Dash patterns + dashImproved: [4, 2], + dashAggregate: [1, 2], + dashSnow: [4, 2, 1, 2], + dashOther: [4, 2, 1, 2, 1, 2], + // Labels + labelColor: '#a080c0', + labelHaloColor: '#0a0a14', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: 11, + labelFont: ['Noto Sans Regular'], + // Hit layer + hitWidth: 14, + }, +} + +/** + * Satellite adjustments - dark, desaturated, purple-shifted + */ +const cyberpunkSatellite = { + opacity: 0.8, + brightnessMin: 0.0, + brightnessMax: 0.30, + contrast: 0.15, + saturation: -0.6, + hueRotate: 280, +} + +/** + * Cyberpunk theme configuration + */ +const cyberpunkTheme = { + id: 'cyberpunk', + name: 'Cyberpunk', + dark: true, + swatch: ['#0a0a14', '#ff2d6b', '#00f0ff'], + fontImports: [ + 'https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap', + 'https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap', + ], + colors: cyberpunkColors, + satellite: cyberpunkSatellite, + overlay: cyberpunkOverlay, + ui: cyberpunkUI, +} + +export default cyberpunkTheme diff --git a/src/themes/registry.js b/src/themes/registry.js index 7f6b74c..cc48c8f 100644 --- a/src/themes/registry.js +++ b/src/themes/registry.js @@ -1,672 +1,652 @@ -/** - * Theme Registry for Navi - * - * Provides a centralized registry for map themes, supporting both built-in - * protomaps themes (light/dark) and custom themes with full flavor objects. - * - * Theme config structure: - * id: string - unique identifier (used in store, data-theme attr) - * name: string - display name for UI - * dark: boolean - true if dark theme (affects overlay styling, sprite fallback) - * colors: object|null - null for built-in themes, full flavor object for custom - * satellite: object|null - raster adjustments when satellite layer is present - * overlay: object - overlay layer styling configuration - * ui: object - CSS custom properties for UI elements - * swatch: string[3] - 3 hex colors for theme picker preview - * fontImports: string[] - URLs for font CSS imports (empty for system fonts) - */ - -import { namedTheme } from 'protomaps-themes-base' -import cleanTheme from './clean.js' +/** + * Theme Registry for Navi + * + * Provides a centralized registry for map themes, supporting both built-in + * protomaps themes (light/dark) and custom themes with full flavor objects. + * + * Theme config structure: + * id: string - unique identifier (used in store, data-theme attr) + * name: string - display name for UI + * dark: boolean - true if dark theme (affects overlay styling, sprite fallback) + * colors: object|null - null for built-in themes, full flavor object for custom + * satellite: object|null - raster adjustments when satellite layer is present + * overlay: object - overlay layer styling configuration + * ui: object - CSS custom properties for UI elements + * swatch: string[3] - 3 hex colors for theme picker preview + * fontImports: string[] - URLs for font CSS imports (empty for system fonts) + */ + +import { namedTheme } from 'protomaps-themes-base' +import cleanTheme from './clean.js' import cyberpunkTheme from './cyberpunk.js' - -// ═══════════════════════════════════════════════════════════════════════════ -// UI CSS CUSTOM PROPERTIES -// ═══════════════════════════════════════════════════════════════════════════ - -/** - * Dark theme UI configuration - * All CSS custom properties for dark theme UI - */ -const darkUI = { - // Fonts - '--font-sans': "'Inter', system-ui, -apple-system, sans-serif", - '--font-mono': "'JetBrains Mono', ui-monospace, monospace", + +// ═══════════════════════════════════════════════════════════════════════════ +// UI CSS CUSTOM PROPERTIES +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Dark theme UI configuration + * All CSS custom properties for dark theme UI + */ +const darkUI = { + // Fonts + '--font-sans': "'Inter', system-ui, -apple-system, sans-serif", + '--font-mono': "'JetBrains Mono', ui-monospace, monospace", '--font-heading': "'Inter', system-ui, -apple-system, sans-serif", - // Backgrounds - '--bg-base': '#1c1917', - '--bg-raised': '#252220', - '--bg-overlay': '#2e2a27', - '--bg-input': '#201d1a', - '--bg-inset': '#181614', - '--bg-muted': '#2a2725', - // Text - '--text-primary': '#dde3dc', - '--text-secondary': '#8f9a8e', - '--text-tertiary': '#5e6b5d', - '--text-inverse': '#1c1917', - // Borders - '--border': '#3a3530', - '--border-subtle': '#2a2624', - // Accent - '--accent': '#7a9a6b', - '--accent-hover': '#8fad7f', - '--accent-muted': '#3d4d36', - // Tan - '--tan': '#b8a88a', - '--tan-muted': '#4a4235', - // Pins - '--pin-origin': '#6b8f5e', - '--pin-destination': '#a67c52', - '--pin-intermediate': '#6b7268', - '--pin-stroke': '#1c1917', - // Status - '--status-success': '#6b8f5e', - '--status-warning': '#b89a4a', - '--status-danger': '#a65c52', - '--success': '#6b8f5e', - '--warning': '#b89a4a', - '--warning-muted': '#4a4235', - // Route - '--route-line': '#7a9a6b', - // Shadows - '--shadow': '0 2px 8px rgba(0, 0, 0, 0.4)', - '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.5)', -} - -/** - * Light theme UI configuration - * All CSS custom properties for light theme UI - */ -const lightUI = { - // Fonts - '--font-sans': "'Inter', system-ui, -apple-system, sans-serif", - '--font-mono': "'JetBrains Mono', ui-monospace, monospace", + // Backgrounds + '--bg-base': '#1c1917', + '--bg-raised': '#252220', + '--bg-overlay': '#2e2a27', + '--bg-input': '#201d1a', + '--bg-inset': '#181614', + '--bg-muted': '#2a2725', + // Text + '--text-primary': '#dde3dc', + '--text-secondary': '#8f9a8e', + '--text-tertiary': '#5e6b5d', + '--text-inverse': '#1c1917', + // Borders + '--border': '#3a3530', + '--border-subtle': '#2a2624', + // Accent + '--accent': '#7a9a6b', + '--accent-hover': '#8fad7f', + '--accent-muted': '#3d4d36', + // Tan + '--tan': '#b8a88a', + '--tan-muted': '#4a4235', + // Pins + '--pin-origin': '#6b8f5e', + '--pin-destination': '#a67c52', + '--pin-intermediate': '#6b7268', + '--pin-stroke': '#1c1917', + // Status + '--status-success': '#6b8f5e', + '--status-warning': '#b89a4a', + '--status-danger': '#a65c52', + '--success': '#6b8f5e', + '--warning': '#b89a4a', + '--warning-muted': '#4a4235', + // Route + '--route-line': '#7a9a6b', + // Shadows + '--shadow': '0 2px 8px rgba(0, 0, 0, 0.4)', + '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.5)', +} + +/** + * Light theme UI configuration + * All CSS custom properties for light theme UI + */ +const lightUI = { + // Fonts + '--font-sans': "'Inter', system-ui, -apple-system, sans-serif", + '--font-mono': "'JetBrains Mono', ui-monospace, monospace", '--font-heading': "'Inter', system-ui, -apple-system, sans-serif", - // Backgrounds - '--bg-base': '#ddd2b9', - '--bg-raised': '#e8dec8', - '--bg-overlay': '#e3d9c1', - '--bg-input': '#e8dec8', - '--bg-inset': '#d5cab2', - '--bg-muted': '#e0d6c0', - // Text - '--text-primary': '#1a1d1a', - '--text-secondary': '#4f5a49', - '--text-tertiary': '#7a8674', - '--text-inverse': '#f5f2ed', - // Borders - '--border': '#c4b89e', - '--border-subtle': '#d5cab2', - // Accent - '--accent': '#4a7040', - '--accent-hover': '#3d5e35', - '--accent-muted': '#dce8d6', - // Tan - '--tan': '#8a7556', - '--tan-muted': '#f0e8d8', - // Pins - '--pin-origin': '#4a7040', - '--pin-destination': '#8a5c35', - '--pin-intermediate': '#6b6960', - '--pin-stroke': '#1a1d1a', - // Status - '--status-success': '#4a7040', - '--status-warning': '#8a7040', - '--status-danger': '#8a4040', - '--success': '#4a7040', - '--warning': '#8a7040', - '--warning-muted': '#f0e8d8', - // Route - '--route-line': '#4a7040', - // Shadows - '--shadow': '0 2px 8px rgba(0, 0, 0, 0.08)', - '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.12)', -} - -// ═══════════════════════════════════════════════════════════════════════════ -// OVERLAY CONFIGURATIONS -// ═══════════════════════════════════════════════════════════════════════════ - -/** - * Dark theme overlay configuration - * All hardcoded values from overlay add functions extracted here - */ -const darkOverlay = { - // ── Hillshade ───────────────────────────────────────────────────────────── - hillshade: { - exaggeration: 0.5, - illuminationDirection: 315, - shadowColor: '#000000', - highlightColor: '#ffffff', - }, - - // ── Traffic ─────────────────────────────────────────────────────────────── - traffic: { - opacity: 0.6, - }, - - // ── Contours (main, brown/tan scheme) ───────────────────────────────────── - contours: { - opacityMod: 0.8, - minorColor: '#8b6f47', - minorOpacity: 0.4, - minorWidth: { z11: 0.5, z14: 1.0 }, - intermediateColor: '#8b6f47', - intermediateOpacity: 0.7, - intermediateWidth: { z8: 0.8, z14: 1.2 }, - indexColor: '#6b4f2a', - indexOpacity: 0.9, - indexWidth: { z4: 1.2, z14: 1.8 }, - labelColor: '#c0b898', - labelHaloColor: '#1a1a1a', - labelHaloWidth: 1.5, - labelOpacity: 0.85, - labelSize: 10, - labelFont: ['Noto Sans Regular'], - }, - - // ── Contours Test (blue scheme) ─────────────────────────────────────────── - // Missing keys cascade from contours - contoursTest: { - minorColor: '#4a7c9b', - intermediateColor: '#4a7c9b', - indexColor: '#2a5a7c', - labelColor: '#98b8d0', - }, - - // ── Contours Test 10ft (green scheme) ───────────────────────────────────── - // Missing keys cascade from contours - contoursTest10ft: { - minorColor: '#3a7c4f', - intermediateColor: '#3a7c4f', - indexColor: '#2a5c3a', - labelColor: '#98c0a8', - }, - - // ── Public Lands (PAD-US) ───────────────────────────────────────────────── - publicLands: { - opacityMod: 0.7, - // Fill colors per category - fillWA: '#7c6b2f', - fillNPS: '#3d6b1f', - fillUSFS: '#5a7c2f', - fillBLM: '#c4a672', - fillFWS: '#4a7a5a', - fillSTAT: '#5a8c7c', - fillLOC: '#8ca694', - fillDefault: '#a0a0a0', - // Fill base opacities (multiplied by opacityMod) - fillOpacityWA: 0.30, - fillOpacityNPS: 0.30, - fillOpacityUSFS: 0.25, - fillOpacityBLM: 0.20, - fillOpacitySTAT: 0.25, - fillOpacityLOC: 0.20, - fillOpacityDefault: 0.15, - // Outline colors per category - outlineWA: '#5a4d20', - outlineNPS: '#2a4a15', - outlineUSFS: '#3d5520', - outlineBLM: '#8a7343', - outlineFWS: '#2d5a3a', - outlineSTAT: '#3d6055', - outlineLOC: '#5c6e66', - outlineDefault: '#707070', - // Outline opacities - outlineOpacityNPS: 0.7, - outlineOpacityUSFS: 0.6, - outlineOpacityDefault: 0.5, - // Outline width - outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 }, - // Labels - labelColor: '#c0c8b8', - labelHaloColor: '#1a1a1a', - labelHaloWidth: 1.5, - labelOpacity: 0.85, - labelSize: { z10: 10, z14: 13 }, - labelFont: ['Noto Sans Regular'], - }, - - // ── USFS Trails ─────────────────────────────────────────────────────────── - usfsTrails: { - // Roads - roadsColor: '#d0a060', - roadsOpacity: 0.9, - roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, - // Trails colors by use type - trailsMotorized: '#f08040', - trailsBicycle: '#e0b040', - trailsHiker: '#60c050', - trailsDefault: '#c0a060', - trailsOpacity: 0.9, - trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, - trailsDash: [2, 1.5], - // Road labels - roadsLabelColor: '#d0c0a0', - roadsLabelHaloColor: '#1a1a1a', - roadsLabelHaloWidth: 1.5, - roadsLabelOpacity: 0.9, - roadsLabelSize: 11, - // Trail labels - trailsLabelColor: '#d0b090', - trailsLabelHaloColor: '#1a1a1a', - trailsLabelHaloWidth: 1.5, - trailsLabelOpacity: 0.9, - trailsLabelSize: 11, - labelFont: ['Noto Sans Regular'], - // Hit layer - hitWidth: 14, - }, - - // ── BLM Trails / Roads ──────────────────────────────────────────────────── - blmTrails: { - // Route colors by use class - color4wdHigh: '#f08040', - color4wdLow: '#e0b040', - colorAtv: '#e04040', - colorMotoSingle: '#b070c0', - color2wdLow: '#f0d070', - colorNonMech: '#60c050', - colorDefault: '#c0a060', - colorSnow: '#80b0e0', - lineOpacity: 0.9, - lineOpacityOther: 0.85, - lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, - // Dash patterns by surface type - dashImproved: [4, 2], - dashAggregate: [1, 2], - dashSnow: [4, 2, 1, 2], - dashOther: [4, 2, 1, 2, 1, 2], - // Labels - labelColor: '#d0c0a0', - labelHaloColor: '#1a1a1a', - labelHaloWidth: 1.5, - labelOpacity: 0.9, - labelSize: 11, - labelFont: ['Noto Sans Regular'], - // Hit layer - hitWidth: 14, - }, - - // ── Highlight (boundary/selection) ──────────────────────────────────────── - highlight: { - lineColor: "#7a9a6b", // Muted olive-green for dark backgrounds - lineWidth: 2, - lineDash: [4, 4], - lineOpacity: 0.8, - fillColor: "#7a9a6b", - fillOpacity: 0.08, - }, -} - -/** - * Light theme overlay configuration - * All hardcoded values from overlay add functions extracted here - */ -const lightOverlay = { - // ── Hillshade ───────────────────────────────────────────────────────────── - hillshade: { - exaggeration: 0.5, - illuminationDirection: 315, - shadowColor: '#000000', - highlightColor: '#ffffff', - }, - - // ── Traffic ─────────────────────────────────────────────────────────────── - traffic: { - opacity: 0.6, - }, - - // ── Contours (main, brown/tan scheme) ───────────────────────────────────── - contours: { - opacityMod: 1.0, - minorColor: '#8b6f47', - minorOpacity: 0.4, - minorWidth: { z11: 0.5, z14: 1.0 }, - intermediateColor: '#8b6f47', - intermediateOpacity: 0.7, - intermediateWidth: { z8: 0.8, z14: 1.2 }, - indexColor: '#6b4f2a', - indexOpacity: 0.9, - indexWidth: { z4: 1.2, z14: 1.8 }, - labelColor: '#5a4020', - labelHaloColor: '#ffffff', - labelHaloWidth: 1.5, - labelOpacity: 0.85, - labelSize: 10, - labelFont: ['Noto Sans Regular'], - }, - - // ── Contours Test (blue scheme) ─────────────────────────────────────────── - // Missing keys cascade from contours - contoursTest: { - minorColor: '#4a7c9b', - intermediateColor: '#4a7c9b', - indexColor: '#2a5a7c', - labelColor: '#205080', - }, - - // ── Contours Test 10ft (green scheme) ───────────────────────────────────── - // Missing keys cascade from contours - contoursTest10ft: { - minorColor: '#3a7c4f', - intermediateColor: '#3a7c4f', - indexColor: '#2a5c3a', - labelColor: '#2a4030', - }, - - // ── Public Lands (PAD-US) ───────────────────────────────────────────────── - publicLands: { - opacityMod: 1.0, - // Fill colors per category - fillWA: '#7c6b2f', - fillNPS: '#3d6b1f', - fillUSFS: '#5a7c2f', - fillBLM: '#c4a672', - fillFWS: '#4a7a5a', - fillSTAT: '#5a8c7c', - fillLOC: '#8ca694', - fillDefault: '#a0a0a0', - // Fill base opacities (multiplied by opacityMod) - fillOpacityWA: 0.30, - fillOpacityNPS: 0.30, - fillOpacityUSFS: 0.25, - fillOpacityBLM: 0.20, - fillOpacitySTAT: 0.25, - fillOpacityLOC: 0.20, - fillOpacityDefault: 0.15, - // Outline colors per category - outlineWA: '#5a4d20', - outlineNPS: '#2a4a15', - outlineUSFS: '#3d5520', - outlineBLM: '#8a7343', - outlineFWS: '#2d5a3a', - outlineSTAT: '#3d6055', - outlineLOC: '#5c6e66', - outlineDefault: '#707070', - // Outline opacities - outlineOpacityNPS: 0.7, - outlineOpacityUSFS: 0.6, - outlineOpacityDefault: 0.5, - // Outline width - outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 }, - // Labels - labelColor: '#3a4a30', - labelHaloColor: '#ffffff', - labelHaloWidth: 1.5, - labelOpacity: 0.85, - labelSize: { z10: 10, z14: 13 }, - labelFont: ['Noto Sans Regular'], - }, - - // ── USFS Trails ─────────────────────────────────────────────────────────── - usfsTrails: { - // Roads - roadsColor: '#c09050', - roadsOpacity: 0.9, - roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, - // Trails colors by use type - trailsMotorized: '#e07030', - trailsBicycle: '#d0a030', - trailsHiker: '#50b040', - trailsDefault: '#b09050', - trailsOpacity: 0.9, - trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, - trailsDash: [2, 1.5], - // Road labels - roadsLabelColor: '#6a5a40', - roadsLabelHaloColor: '#ffffff', - roadsLabelHaloWidth: 1.5, - roadsLabelOpacity: 0.9, - roadsLabelSize: 11, - // Trail labels - trailsLabelColor: '#5a4a30', - trailsLabelHaloColor: '#ffffff', - trailsLabelHaloWidth: 1.5, - trailsLabelOpacity: 0.9, - trailsLabelSize: 11, - labelFont: ['Noto Sans Regular'], - // Hit layer - hitWidth: 14, - }, - - // ── BLM Trails / Roads ──────────────────────────────────────────────────── - blmTrails: { - // Route colors by use class - color4wdHigh: '#e07030', - color4wdLow: '#d0a030', - colorAtv: '#d03030', - colorMotoSingle: '#a060b0', - color2wdLow: '#e0c060', - colorNonMech: '#50b040', - colorDefault: '#b09050', - colorSnow: '#6090c0', - lineOpacity: 0.9, - lineOpacityOther: 0.85, - lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, - // Dash patterns by surface type - dashImproved: [4, 2], - dashAggregate: [1, 2], - dashSnow: [4, 2, 1, 2], - dashOther: [4, 2, 1, 2, 1, 2], - // Labels - labelColor: '#5a4a30', - labelHaloColor: '#ffffff', - labelHaloWidth: 1.5, - labelOpacity: 0.9, - labelSize: 11, - labelFont: ['Noto Sans Regular'], - // Hit layer - hitWidth: 14, - }, - - // ── Highlight (boundary/selection) ──────────────────────────────────────── - highlight: { - lineColor: "#4a7040", // Forest green for light backgrounds - lineWidth: 2, - lineDash: [4, 4], - lineOpacity: 0.7, - fillColor: "#4a7040", - fillOpacity: 0.06, - }, -} - -// ═══════════════════════════════════════════════════════════════════════════ -// THEME REGISTRY -// ═══════════════════════════════════════════════════════════════════════════ - -/** - * Theme registry - maps theme IDs to theme configurations - * - * Built-in themes (light/dark) use colors: null to signal that namedTheme() - * should be called at render time. Custom themes provide a full flavor object. - */ -const themes = { - light: { - id: 'light', - name: 'Light', - dark: false, - colors: null, // Use namedTheme('light') - satellite: null, - overlay: lightOverlay, - ui: lightUI, - swatch: ['#ddd2b9', '#4a7040', '#8a7556'], - fontImports: [], - }, - dark: { - id: 'dark', - name: 'Dark', - dark: true, - colors: null, // Use namedTheme('dark') - satellite: null, - overlay: darkOverlay, - ui: darkUI, - swatch: ['#1c1917', '#7a9a6b', '#b8a88a'], - fontImports: [], - }, - clean: { - ...cleanTheme, - swatch: ['#f5f5f5', '#1a73e8', '#34a853'], - fontImports: [], - }, + // Backgrounds + '--bg-base': '#ddd2b9', + '--bg-raised': '#e8dec8', + '--bg-overlay': '#e3d9c1', + '--bg-input': '#e8dec8', + '--bg-inset': '#d5cab2', + '--bg-muted': '#e0d6c0', + // Text + '--text-primary': '#1a1d1a', + '--text-secondary': '#4f5a49', + '--text-tertiary': '#7a8674', + '--text-inverse': '#f5f2ed', + // Borders + '--border': '#c4b89e', + '--border-subtle': '#d5cab2', + // Accent + '--accent': '#4a7040', + '--accent-hover': '#3d5e35', + '--accent-muted': '#dce8d6', + // Tan + '--tan': '#8a7556', + '--tan-muted': '#f0e8d8', + // Pins + '--pin-origin': '#4a7040', + '--pin-destination': '#8a5c35', + '--pin-intermediate': '#6b6960', + '--pin-stroke': '#1a1d1a', + // Status + '--status-success': '#4a7040', + '--status-warning': '#8a7040', + '--status-danger': '#8a4040', + '--success': '#4a7040', + '--warning': '#8a7040', + '--warning-muted': '#f0e8d8', + // Route + '--route-line': '#4a7040', + // Shadows + '--shadow': '0 2px 8px rgba(0, 0, 0, 0.08)', + '--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.12)', +} + +// ═══════════════════════════════════════════════════════════════════════════ +// OVERLAY CONFIGURATIONS +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Dark theme overlay configuration + * All hardcoded values from overlay add functions extracted here + */ +const darkOverlay = { + // ── Hillshade ───────────────────────────────────────────────────────────── + hillshade: { + exaggeration: 0.5, + illuminationDirection: 315, + shadowColor: '#000000', + highlightColor: '#ffffff', + }, + + // ── Traffic ─────────────────────────────────────────────────────────────── + traffic: { + opacity: 0.6, + }, + + // ── Contours (main, brown/tan scheme) ───────────────────────────────────── + contours: { + opacityMod: 0.8, + minorColor: '#8b6f47', + minorOpacity: 0.4, + minorWidth: { z11: 0.5, z14: 1.0 }, + intermediateColor: '#8b6f47', + intermediateOpacity: 0.7, + intermediateWidth: { z8: 0.8, z14: 1.2 }, + indexColor: '#6b4f2a', + indexOpacity: 0.9, + indexWidth: { z4: 1.2, z14: 1.8 }, + labelColor: '#c0b898', + labelHaloColor: '#1a1a1a', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: 10, + labelFont: ['Noto Sans Regular'], + }, + + // ── Contours Test (blue scheme) ─────────────────────────────────────────── + // Missing keys cascade from contours + contoursTest: { + minorColor: '#4a7c9b', + intermediateColor: '#4a7c9b', + indexColor: '#2a5a7c', + labelColor: '#98b8d0', + }, + + // ── Contours Test 10ft (green scheme) ───────────────────────────────────── + // Missing keys cascade from contours + contoursTest10ft: { + minorColor: '#3a7c4f', + intermediateColor: '#3a7c4f', + indexColor: '#2a5c3a', + labelColor: '#98c0a8', + }, + + // ── Public Lands (PAD-US) ───────────────────────────────────────────────── + publicLands: { + opacityMod: 0.7, + // Fill colors per category + fillWA: '#7c6b2f', + fillNPS: '#3d6b1f', + fillUSFS: '#5a7c2f', + fillBLM: '#c4a672', + fillFWS: '#4a7a5a', + fillSTAT: '#5a8c7c', + fillLOC: '#8ca694', + fillDefault: '#a0a0a0', + // Fill base opacities (multiplied by opacityMod) + fillOpacityWA: 0.30, + fillOpacityNPS: 0.30, + fillOpacityUSFS: 0.25, + fillOpacityBLM: 0.20, + fillOpacitySTAT: 0.25, + fillOpacityLOC: 0.20, + fillOpacityDefault: 0.15, + // Outline colors per category + outlineWA: '#5a4d20', + outlineNPS: '#2a4a15', + outlineUSFS: '#3d5520', + outlineBLM: '#8a7343', + outlineFWS: '#2d5a3a', + outlineSTAT: '#3d6055', + outlineLOC: '#5c6e66', + outlineDefault: '#707070', + // Outline opacities + outlineOpacityNPS: 0.7, + outlineOpacityUSFS: 0.6, + outlineOpacityDefault: 0.5, + // Outline width + outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 }, + // Labels + labelColor: '#c0c8b8', + labelHaloColor: '#1a1a1a', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: { z10: 10, z14: 13 }, + labelFont: ['Noto Sans Regular'], + }, + + // ── USFS Trails ─────────────────────────────────────────────────────────── + usfsTrails: { + // Roads + roadsColor: '#d0a060', + roadsOpacity: 0.9, + roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, + // Trails colors by use type + trailsMotorized: '#f08040', + trailsBicycle: '#e0b040', + trailsHiker: '#60c050', + trailsDefault: '#c0a060', + trailsOpacity: 0.9, + trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + trailsDash: [2, 1.5], + // Road labels + roadsLabelColor: '#d0c0a0', + roadsLabelHaloColor: '#1a1a1a', + roadsLabelHaloWidth: 1.5, + roadsLabelOpacity: 0.9, + roadsLabelSize: 11, + // Trail labels + trailsLabelColor: '#d0b090', + trailsLabelHaloColor: '#1a1a1a', + trailsLabelHaloWidth: 1.5, + trailsLabelOpacity: 0.9, + trailsLabelSize: 11, + labelFont: ['Noto Sans Regular'], + // Hit layer + hitWidth: 14, + }, + + // ── BLM Trails / Roads ──────────────────────────────────────────────────── + blmTrails: { + // Route colors by use class + color4wdHigh: '#f08040', + color4wdLow: '#e0b040', + colorAtv: '#e04040', + colorMotoSingle: '#b070c0', + color2wdLow: '#f0d070', + colorNonMech: '#60c050', + colorDefault: '#c0a060', + colorSnow: '#80b0e0', + lineOpacity: 0.9, + lineOpacityOther: 0.85, + lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + // Dash patterns by surface type + dashImproved: [4, 2], + dashAggregate: [1, 2], + dashSnow: [4, 2, 1, 2], + dashOther: [4, 2, 1, 2, 1, 2], + // Labels + labelColor: '#d0c0a0', + labelHaloColor: '#1a1a1a', + labelHaloWidth: 1.5, + labelOpacity: 0.9, + labelSize: 11, + labelFont: ['Noto Sans Regular'], + // Hit layer + hitWidth: 14, + }, +} + +/** + * Light theme overlay configuration + * All hardcoded values from overlay add functions extracted here + */ +const lightOverlay = { + // ── Hillshade ───────────────────────────────────────────────────────────── + hillshade: { + exaggeration: 0.5, + illuminationDirection: 315, + shadowColor: '#000000', + highlightColor: '#ffffff', + }, + + // ── Traffic ─────────────────────────────────────────────────────────────── + traffic: { + opacity: 0.6, + }, + + // ── Contours (main, brown/tan scheme) ───────────────────────────────────── + contours: { + opacityMod: 1.0, + minorColor: '#8b6f47', + minorOpacity: 0.4, + minorWidth: { z11: 0.5, z14: 1.0 }, + intermediateColor: '#8b6f47', + intermediateOpacity: 0.7, + intermediateWidth: { z8: 0.8, z14: 1.2 }, + indexColor: '#6b4f2a', + indexOpacity: 0.9, + indexWidth: { z4: 1.2, z14: 1.8 }, + labelColor: '#5a4020', + labelHaloColor: '#ffffff', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: 10, + labelFont: ['Noto Sans Regular'], + }, + + // ── Contours Test (blue scheme) ─────────────────────────────────────────── + // Missing keys cascade from contours + contoursTest: { + minorColor: '#4a7c9b', + intermediateColor: '#4a7c9b', + indexColor: '#2a5a7c', + labelColor: '#205080', + }, + + // ── Contours Test 10ft (green scheme) ───────────────────────────────────── + // Missing keys cascade from contours + contoursTest10ft: { + minorColor: '#3a7c4f', + intermediateColor: '#3a7c4f', + indexColor: '#2a5c3a', + labelColor: '#2a4030', + }, + + // ── Public Lands (PAD-US) ───────────────────────────────────────────────── + publicLands: { + opacityMod: 1.0, + // Fill colors per category + fillWA: '#7c6b2f', + fillNPS: '#3d6b1f', + fillUSFS: '#5a7c2f', + fillBLM: '#c4a672', + fillFWS: '#4a7a5a', + fillSTAT: '#5a8c7c', + fillLOC: '#8ca694', + fillDefault: '#a0a0a0', + // Fill base opacities (multiplied by opacityMod) + fillOpacityWA: 0.30, + fillOpacityNPS: 0.30, + fillOpacityUSFS: 0.25, + fillOpacityBLM: 0.20, + fillOpacitySTAT: 0.25, + fillOpacityLOC: 0.20, + fillOpacityDefault: 0.15, + // Outline colors per category + outlineWA: '#5a4d20', + outlineNPS: '#2a4a15', + outlineUSFS: '#3d5520', + outlineBLM: '#8a7343', + outlineFWS: '#2d5a3a', + outlineSTAT: '#3d6055', + outlineLOC: '#5c6e66', + outlineDefault: '#707070', + // Outline opacities + outlineOpacityNPS: 0.7, + outlineOpacityUSFS: 0.6, + outlineOpacityDefault: 0.5, + // Outline width + outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 }, + // Labels + labelColor: '#3a4a30', + labelHaloColor: '#ffffff', + labelHaloWidth: 1.5, + labelOpacity: 0.85, + labelSize: { z10: 10, z14: 13 }, + labelFont: ['Noto Sans Regular'], + }, + + // ── USFS Trails ─────────────────────────────────────────────────────────── + usfsTrails: { + // Roads + roadsColor: '#c09050', + roadsOpacity: 0.9, + roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 }, + // Trails colors by use type + trailsMotorized: '#e07030', + trailsBicycle: '#d0a030', + trailsHiker: '#50b040', + trailsDefault: '#b09050', + trailsOpacity: 0.9, + trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + trailsDash: [2, 1.5], + // Road labels + roadsLabelColor: '#6a5a40', + roadsLabelHaloColor: '#ffffff', + roadsLabelHaloWidth: 1.5, + roadsLabelOpacity: 0.9, + roadsLabelSize: 11, + // Trail labels + trailsLabelColor: '#5a4a30', + trailsLabelHaloColor: '#ffffff', + trailsLabelHaloWidth: 1.5, + trailsLabelOpacity: 0.9, + trailsLabelSize: 11, + labelFont: ['Noto Sans Regular'], + // Hit layer + hitWidth: 14, + }, + + // ── BLM Trails / Roads ──────────────────────────────────────────────────── + blmTrails: { + // Route colors by use class + color4wdHigh: '#e07030', + color4wdLow: '#d0a030', + colorAtv: '#d03030', + colorMotoSingle: '#a060b0', + color2wdLow: '#e0c060', + colorNonMech: '#50b040', + colorDefault: '#b09050', + colorSnow: '#6090c0', + lineOpacity: 0.9, + lineOpacityOther: 0.85, + lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 }, + // Dash patterns by surface type + dashImproved: [4, 2], + dashAggregate: [1, 2], + dashSnow: [4, 2, 1, 2], + dashOther: [4, 2, 1, 2, 1, 2], + // Labels + labelColor: '#5a4a30', + labelHaloColor: '#ffffff', + labelHaloWidth: 1.5, + labelOpacity: 0.9, + labelSize: 11, + labelFont: ['Noto Sans Regular'], + // Hit layer + hitWidth: 14, + }, +} + +// ═══════════════════════════════════════════════════════════════════════════ +// THEME REGISTRY +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Theme registry - maps theme IDs to theme configurations + * + * Built-in themes (light/dark) use colors: null to signal that namedTheme() + * should be called at render time. Custom themes provide a full flavor object. + */ +const themes = { + light: { + id: 'light', + name: 'Light', + dark: false, + colors: null, // Use namedTheme('light') + satellite: null, + overlay: lightOverlay, + ui: lightUI, + swatch: ['#ddd2b9', '#4a7040', '#8a7556'], + fontImports: [], + }, + dark: { + id: 'dark', + name: 'Dark', + dark: true, + colors: null, // Use namedTheme('dark') + satellite: null, + overlay: darkOverlay, + ui: darkUI, + swatch: ['#1c1917', '#7a9a6b', '#b8a88a'], + fontImports: [], + }, + clean: { + ...cleanTheme, + swatch: ['#f5f5f5', '#1a73e8', '#34a853'], + fontImports: [], + }, cyberpunk: cyberpunkTheme, - // Custom themes go here. Example: - // 'midnight': { - // id: 'midnight', - // name: 'Midnight', - // dark: true, - // colors: { /* full flavor object matching dark-flavor-reference.json schema */ }, - // satellite: { opacity: 0.8, brightnessMin: 0.1 }, - // overlay: { /* partial overrides - missing keys fall back to dark overlay */ }, - // ui: { /* partial overrides - missing keys fall back to dark ui */ }, - // swatch: ['#0a0a12', '#6060ff', '#4040a0'], - // fontImports: ['https://fonts.googleapis.com/css2?family=Orbitron&display=swap'], - // }, -} - -// ═══════════════════════════════════════════════════════════════════════════ -// EXPORTED FUNCTIONS -// ═══════════════════════════════════════════════════════════════════════════ - -/** - * Get a theme configuration by ID - * @param {string} id - Theme ID - * @returns {object} Theme config, falls back to 'dark' if not found - */ -export function getTheme(id) { - return themes[id] || themes.dark -} - -/** - * Get the color flavor for a theme - * For built-in themes, calls namedTheme(). For custom themes, returns colors directly. - * @param {string} id - Theme ID - * @returns {object} Flavor object for use with protomaps layers() - */ -export function getThemeColors(id) { - const theme = getTheme(id) - if (theme.colors === null) { - // Built-in theme - use namedTheme from protomaps-themes-base - return namedTheme(id) - } - return theme.colors -} - -/** - * Get the sprite URL for a theme - * Built-in themes use their own sprites. Custom themes fall back to - * dark or light sprite based on the theme's dark flag. - * @param {string} id - Theme ID - * @returns {string} Full sprite URL - */ -export function getThemeSprite(id) { - const theme = getTheme(id) - // Custom themes don't have matching sprites on CDN - fall back based on dark flag - const spriteTheme = theme.colors === null ? id : (theme.dark ? 'dark' : 'light') - return `https://protomaps.github.io/basemaps-assets/sprites/v4/${spriteTheme}` -} - -/** - * Get overlay configuration for a specific layer - * - * For contour variants (contoursTest, contoursTest10ft), missing keys cascade - * from the same theme's contours config. - * - * For custom themes, missing keys fall back to the appropriate built-in theme - * (dark or light based on theme.dark flag). - * - * @param {string} themeId - Theme ID - * @param {string} layerKey - Overlay layer key (hillshade, contours, publicLands, etc.) - * @returns {object} Merged overlay config for the layer - */ -export function getOverlayConfig(themeId, layerKey) { - const theme = getTheme(themeId) - const builtinTheme = theme.dark ? themes.dark : themes.light - const builtinOverlay = builtinTheme.overlay[layerKey] || {} - - // For contour variants, cascade from same theme's contours config - let baseConfig = builtinOverlay - if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') { - const contoursBase = builtinTheme.overlay.contours || {} - baseConfig = { ...contoursBase, ...builtinOverlay } - } - - // If this is a custom theme with overlay overrides, merge them - if (theme.overlay && theme.overlay[layerKey]) { - // For contour variants in custom themes, also cascade from custom contours - if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') { - const customContours = theme.overlay.contours || {} - return { ...baseConfig, ...customContours, ...theme.overlay[layerKey] } - } - return { ...baseConfig, ...theme.overlay[layerKey] } - } - - return baseConfig -} - -/** - * Apply theme UI CSS custom properties to the document - * - * Sets the data-theme attribute AND applies all CSS variables from the - * theme's ui object directly to document.documentElement.style. - * - * Also manages font imports: removes previously injected font tags - * and injects new ones for the current theme's fontImports array. - * - * For custom themes, missing ui keys fall back to the appropriate built-in - * theme (dark or light based on theme.dark flag). - * - * @param {object} theme - Theme config object (from getTheme()) - */ -export function applyThemeUI(theme) { - const root = document.documentElement - - // Set data-theme attribute for any CSS selectors that still reference it - root.setAttribute('data-theme', theme.id) - - // Get base UI config from appropriate built-in theme - const builtinTheme = theme.dark ? themes.dark : themes.light - const baseUI = builtinTheme.ui - - // Merge with any custom theme overrides - const ui = theme.ui ? { ...baseUI, ...theme.ui } : baseUI - - // Apply all UI variables directly to root element style - for (const [prop, value] of Object.entries(ui)) { - root.style.setProperty(prop, value) - } - - // Manage font imports - // Remove any previously injected theme font links - document.querySelectorAll('link[data-theme-font]').forEach(link => link.remove()) - - // Inject new font links for this theme - const fontImports = theme.fontImports || [] - for (const url of fontImports) { - const link = document.createElement('link') - link.rel = 'stylesheet' - link.href = url - link.setAttribute('data-theme-font', theme.id) - document.head.appendChild(link) - } -} - -/** - * Get list of available themes for UI display - * @returns {Array<{id: string, name: string, dark: boolean, swatch: string[]}>} - */ -export function themeList() { - return Object.values(themes).map(({ id, name, dark, swatch }) => ({ id, name, dark, swatch })) -} - -/** - * Check if a theme ID is valid/registered - * @param {string} id - Theme ID to check - * @returns {boolean} - */ -export function isValidTheme(id) { - return id in themes -} - -export default themes + // Custom themes go here. Example: + // 'midnight': { + // id: 'midnight', + // name: 'Midnight', + // dark: true, + // colors: { /* full flavor object matching dark-flavor-reference.json schema */ }, + // satellite: { opacity: 0.8, brightnessMin: 0.1 }, + // overlay: { /* partial overrides - missing keys fall back to dark overlay */ }, + // ui: { /* partial overrides - missing keys fall back to dark ui */ }, + // swatch: ['#0a0a12', '#6060ff', '#4040a0'], + // fontImports: ['https://fonts.googleapis.com/css2?family=Orbitron&display=swap'], + // }, +} + +// ═══════════════════════════════════════════════════════════════════════════ +// EXPORTED FUNCTIONS +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Get a theme configuration by ID + * @param {string} id - Theme ID + * @returns {object} Theme config, falls back to 'dark' if not found + */ +export function getTheme(id) { + return themes[id] || themes.dark +} + +/** + * Get the color flavor for a theme + * For built-in themes, calls namedTheme(). For custom themes, returns colors directly. + * @param {string} id - Theme ID + * @returns {object} Flavor object for use with protomaps layers() + */ +export function getThemeColors(id) { + const theme = getTheme(id) + if (theme.colors === null) { + // Built-in theme - use namedTheme from protomaps-themes-base + return namedTheme(id) + } + return theme.colors +} + +/** + * Get the sprite URL for a theme + * Built-in themes use their own sprites. Custom themes fall back to + * dark or light sprite based on the theme's dark flag. + * @param {string} id - Theme ID + * @returns {string} Full sprite URL + */ +export function getThemeSprite(id) { + const theme = getTheme(id) + // Custom themes don't have matching sprites on CDN - fall back based on dark flag + const spriteTheme = theme.colors === null ? id : (theme.dark ? 'dark' : 'light') + return `https://protomaps.github.io/basemaps-assets/sprites/v4/${spriteTheme}` +} + +/** + * Get overlay configuration for a specific layer + * + * For contour variants (contoursTest, contoursTest10ft), missing keys cascade + * from the same theme's contours config. + * + * For custom themes, missing keys fall back to the appropriate built-in theme + * (dark or light based on theme.dark flag). + * + * @param {string} themeId - Theme ID + * @param {string} layerKey - Overlay layer key (hillshade, contours, publicLands, etc.) + * @returns {object} Merged overlay config for the layer + */ +export function getOverlayConfig(themeId, layerKey) { + const theme = getTheme(themeId) + const builtinTheme = theme.dark ? themes.dark : themes.light + const builtinOverlay = builtinTheme.overlay[layerKey] || {} + + // For contour variants, cascade from same theme's contours config + let baseConfig = builtinOverlay + if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') { + const contoursBase = builtinTheme.overlay.contours || {} + baseConfig = { ...contoursBase, ...builtinOverlay } + } + + // If this is a custom theme with overlay overrides, merge them + if (theme.overlay && theme.overlay[layerKey]) { + // For contour variants in custom themes, also cascade from custom contours + if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') { + const customContours = theme.overlay.contours || {} + return { ...baseConfig, ...customContours, ...theme.overlay[layerKey] } + } + return { ...baseConfig, ...theme.overlay[layerKey] } + } + + return baseConfig +} + +/** + * Apply theme UI CSS custom properties to the document + * + * Sets the data-theme attribute AND applies all CSS variables from the + * theme's ui object directly to document.documentElement.style. + * + * Also manages font imports: removes previously injected font tags + * and injects new ones for the current theme's fontImports array. + * + * For custom themes, missing ui keys fall back to the appropriate built-in + * theme (dark or light based on theme.dark flag). + * + * @param {object} theme - Theme config object (from getTheme()) + */ +export function applyThemeUI(theme) { + const root = document.documentElement + + // Set data-theme attribute for any CSS selectors that still reference it + root.setAttribute('data-theme', theme.id) + + // Get base UI config from appropriate built-in theme + const builtinTheme = theme.dark ? themes.dark : themes.light + const baseUI = builtinTheme.ui + + // Merge with any custom theme overrides + const ui = theme.ui ? { ...baseUI, ...theme.ui } : baseUI + + // Apply all UI variables directly to root element style + for (const [prop, value] of Object.entries(ui)) { + root.style.setProperty(prop, value) + } + + // Manage font imports + // Remove any previously injected theme font links + document.querySelectorAll('link[data-theme-font]').forEach(link => link.remove()) + + // Inject new font links for this theme + const fontImports = theme.fontImports || [] + for (const url of fontImports) { + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = url + link.setAttribute('data-theme-font', theme.id) + document.head.appendChild(link) + } +} + +/** + * Get list of available themes for UI display + * @returns {Array<{id: string, name: string, dark: boolean, swatch: string[]}>} + */ +export function themeList() { + return Object.values(themes).map(({ id, name, dark, swatch }) => ({ id, name, dark, swatch })) +} + +/** + * Check if a theme ID is valid/registered + * @param {string} id - Theme ID to check + * @returns {boolean} + */ +export function isValidTheme(id) { + return id in themes +} + +export default themes