mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
Add theme registry for custom protomaps flavor support
Introduces src/themes/registry.js with:
- getTheme(id) - lookup theme config by ID
- getThemeColors(id) - get flavor object (namedTheme for built-ins, custom colors for others)
- getThemeSprite(id) - get sprite URL with fallback for custom themes
- themeList() - list available themes for UI
Updates MapView.jsx:
- Import registry functions instead of namedTheme directly
- buildStyle() uses getThemeColors() and getThemeSprite()
- Overlay add functions use isCurrentThemeDark() helper that checks
registry dark flag instead of string comparison
Reference files:
- dark-flavor-reference.json - full namedTheme('dark') output (73 flat keys + pois + landcover)
- light-flavor-reference.json - full namedTheme('light') output
- README.md - schema documentation for creating custom themes
This is a refactor only - light/dark themes render identically to before.
Custom themes can now be added to registry.js with full flavor objects.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a39371a7a0
commit
c701463283
5 changed files with 486 additions and 11 deletions
|
|
@ -2,7 +2,8 @@ import { useEffect, useRef, forwardRef, useImperativeHandle, useState } from 're
|
||||||
import maplibregl from 'maplibre-gl'
|
import maplibregl from 'maplibre-gl'
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||||
import { Protocol } from 'pmtiles'
|
import { Protocol } from 'pmtiles'
|
||||||
import { layers, namedTheme } from 'protomaps-themes-base'
|
import { layers } from 'protomaps-themes-base'
|
||||||
|
import { getTheme, getThemeColors, getThemeSprite } from '../themes/registry'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
import { decodePolyline } from '../utils/decode'
|
import { decodePolyline } from '../utils/decode'
|
||||||
import { fetchReverse } from '../api'
|
import { fetchReverse } from '../api'
|
||||||
|
|
@ -12,6 +13,13 @@ import RadialMenu from './RadialMenu'
|
||||||
import useContextMenu from '../hooks/useContextMenu'
|
import useContextMenu from '../hooks/useContextMenu'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
|
||||||
|
/** Check if current theme is dark based on registry */
|
||||||
|
function isCurrentThemeDark() {
|
||||||
|
const themeId = document.documentElement.getAttribute('data-theme') || 'dark'
|
||||||
|
return getTheme(themeId).dark
|
||||||
|
}
|
||||||
|
|
||||||
const ROUTE_SOURCE = 'route-source'
|
const ROUTE_SOURCE = 'route-source'
|
||||||
const BOUNDARY_SOURCE = 'boundary-source'
|
const BOUNDARY_SOURCE = 'boundary-source'
|
||||||
const BOUNDARY_LAYER = 'boundary-layer'
|
const BOUNDARY_LAYER = 'boundary-layer'
|
||||||
|
|
@ -92,7 +100,7 @@ function applyHighlightExpression(map, layerId) {
|
||||||
storeOriginalPaint(map, layerId)
|
storeOriginalPaint(map, layerId)
|
||||||
|
|
||||||
const orig = originalPaintValues[layerId]
|
const orig = originalPaintValues[layerId]
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
const isDark = isCurrentThemeDark()
|
||||||
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#7a9a6b'
|
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#7a9a6b'
|
||||||
|
|
||||||
// Hover: darken text slightly, bump halo to full opacity for focus effect
|
// Hover: darken text slightly, bump halo to full opacity for focus effect
|
||||||
|
|
@ -236,7 +244,7 @@ function clearAllHighlights(map) {
|
||||||
|
|
||||||
/** Apply improved base label styling for readability (Google Maps style) */
|
/** Apply improved base label styling for readability (Google Maps style) */
|
||||||
function applyBaseLabelStyling(map) {
|
function applyBaseLabelStyling(map) {
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
const isDark = isCurrentThemeDark()
|
||||||
|
|
||||||
INTERACTIVE_LABEL_LAYERS.forEach(layerId => {
|
INTERACTIVE_LABEL_LAYERS.forEach(layerId => {
|
||||||
if (!map.getLayer(layerId)) return
|
if (!map.getLayer(layerId)) return
|
||||||
|
|
@ -265,7 +273,7 @@ function buildStyle(themeName) {
|
||||||
return {
|
return {
|
||||||
version: 8,
|
version: 8,
|
||||||
glyphs: 'https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf',
|
glyphs: 'https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf',
|
||||||
sprite: `https://protomaps.github.io/basemaps-assets/sprites/v4/${themeName}`,
|
sprite: getThemeSprite(themeName),
|
||||||
sources: {
|
sources: {
|
||||||
protomaps: {
|
protomaps: {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
|
|
@ -273,7 +281,7 @@ function buildStyle(themeName) {
|
||||||
attribution,
|
attribution,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
layers: layers('protomaps', namedTheme(themeName), { lang: 'en' }),
|
layers: layers('protomaps', getThemeColors(themeName), { lang: 'en' }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,7 +405,7 @@ function addPublicLands(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
const isDark = isCurrentThemeDark()
|
||||||
const opacityMod = isDark ? 0.7 : 1.0
|
const opacityMod = isDark ? 0.7 : 1.0
|
||||||
|
|
||||||
// Fill layer — data-driven color by agency + designation
|
// Fill layer — data-driven color by agency + designation
|
||||||
|
|
@ -541,7 +549,7 @@ function addContours(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
const isDark = isCurrentThemeDark()
|
||||||
const opMod = isDark ? 0.8 : 1.0
|
const opMod = isDark ? 0.8 : 1.0
|
||||||
|
|
||||||
// Minor contours (40ft) — visible z11+
|
// Minor contours (40ft) — visible z11+
|
||||||
|
|
@ -643,7 +651,7 @@ function addContoursTest(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute("data-theme") === "dark"
|
const isDark = isCurrentThemeDark()
|
||||||
const opMod = isDark ? 0.8 : 1.0
|
const opMod = isDark ? 0.8 : 1.0
|
||||||
|
|
||||||
// Minor contours (40ft) — blue scheme
|
// Minor contours (40ft) — blue scheme
|
||||||
|
|
@ -745,7 +753,7 @@ function addContoursTest10ft(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute("data-theme") === "dark"
|
const isDark = isCurrentThemeDark()
|
||||||
const opMod = isDark ? 0.8 : 1.0
|
const opMod = isDark ? 0.8 : 1.0
|
||||||
|
|
||||||
// Minor contours (10ft) — green scheme
|
// Minor contours (10ft) — green scheme
|
||||||
|
|
@ -848,7 +856,7 @@ function addUsfsTrails(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute("data-theme") === "dark"
|
const isDark = isCurrentThemeDark()
|
||||||
|
|
||||||
// Invisible hit-area layers for easier clicking
|
// Invisible hit-area layers for easier clicking
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
|
|
@ -1015,7 +1023,7 @@ function addBlmTrails(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute("data-theme") === "dark"
|
const isDark = isCurrentThemeDark()
|
||||||
|
|
||||||
// Color expression based on route use class - brighter palette
|
// Color expression based on route use class - brighter palette
|
||||||
const colorExpr = [
|
const colorExpr = [
|
||||||
|
|
|
||||||
170
src/themes/README.md
Normal file
170
src/themes/README.md
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
# Navi Theme System
|
||||||
|
|
||||||
|
This directory contains the theme registry and reference files for creating custom map themes.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **registry.js** - Theme registry with getTheme(), getThemeColors(), getThemeSprite(), themeList()
|
||||||
|
- **dark-flavor-reference.json** - Full namedTheme('dark') output for reference
|
||||||
|
- **light-flavor-reference.json** - Full namedTheme('light') output for reference
|
||||||
|
|
||||||
|
## Creating Custom Themes
|
||||||
|
|
||||||
|
Custom themes must provide a complete `colors` object matching the flavor schema from protomaps-themes-base.
|
||||||
|
|
||||||
|
### Required Structure
|
||||||
|
|
||||||
|
The flavor object has **73 flat color keys** plus **2 nested objects**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
// === FLAT COLOR KEYS (73 total) ===
|
||||||
|
|
||||||
|
// Background & earth
|
||||||
|
"background": "#34373d",
|
||||||
|
"earth": "#1f1f1f",
|
||||||
|
|
||||||
|
// Land use areas
|
||||||
|
"park_a": "#1c2421",
|
||||||
|
"park_b": "#192a24",
|
||||||
|
"hospital": "#252424",
|
||||||
|
"industrial": "#222222",
|
||||||
|
"school": "#262323",
|
||||||
|
"wood_a": "#202121",
|
||||||
|
"wood_b": "#202121",
|
||||||
|
"pedestrian": "#1e1e1e",
|
||||||
|
"scrub_a": "#222323",
|
||||||
|
"scrub_b": "#222323",
|
||||||
|
"glacier": "#1c1c1c",
|
||||||
|
"sand": "#212123",
|
||||||
|
"beach": "#28282a",
|
||||||
|
"aerodrome": "#1e1e1e",
|
||||||
|
"runway": "#333333",
|
||||||
|
"water": "#31353f",
|
||||||
|
"zoo": "#222323",
|
||||||
|
"military": "#242323",
|
||||||
|
"pier": "#333333",
|
||||||
|
"buildings": "#111111",
|
||||||
|
|
||||||
|
// Tunnels
|
||||||
|
"tunnel_other_casing": "#141414",
|
||||||
|
"tunnel_minor_casing": "#141414",
|
||||||
|
"tunnel_link_casing": "#141414",
|
||||||
|
"tunnel_major_casing": "#141414",
|
||||||
|
"tunnel_highway_casing": "#141414",
|
||||||
|
"tunnel_other": "#292929",
|
||||||
|
"tunnel_minor": "#292929",
|
||||||
|
"tunnel_link": "#292929",
|
||||||
|
"tunnel_major": "#292929",
|
||||||
|
"tunnel_highway": "#292929",
|
||||||
|
|
||||||
|
// Roads & casings
|
||||||
|
"minor_service_casing": "#1f1f1f",
|
||||||
|
"minor_casing": "#1f1f1f",
|
||||||
|
"link_casing": "#1f1f1f",
|
||||||
|
"major_casing_late": "#1f1f1f",
|
||||||
|
"highway_casing_late": "#1f1f1f",
|
||||||
|
"major_casing_early": "#1f1f1f",
|
||||||
|
"highway_casing_early": "#1f1f1f",
|
||||||
|
"other": "#333333",
|
||||||
|
"minor_service": "#333333",
|
||||||
|
"minor_a": "#3d3d3d",
|
||||||
|
"minor_b": "#333333",
|
||||||
|
"link": "#3d3d3d",
|
||||||
|
"major": "#3d3d3d",
|
||||||
|
"highway": "#474747",
|
||||||
|
"railway": "#000000",
|
||||||
|
"boundaries": "#5b6374",
|
||||||
|
|
||||||
|
// Bridges
|
||||||
|
"bridges_other_casing": "#2b2b2b",
|
||||||
|
"bridges_minor_casing": "#1f1f1f",
|
||||||
|
"bridges_link_casing": "#1f1f1f",
|
||||||
|
"bridges_major_casing": "#1f1f1f",
|
||||||
|
"bridges_highway_casing": "#1f1f1f",
|
||||||
|
"bridges_other": "#333333",
|
||||||
|
"bridges_minor": "#333333",
|
||||||
|
"bridges_link": "#3d3d3d",
|
||||||
|
"bridges_major": "#3d3d3d",
|
||||||
|
"bridges_highway": "#474747",
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
"waterway_label": "#717784",
|
||||||
|
"roads_label_minor": "#525252",
|
||||||
|
"roads_label_minor_halo": "#1f1f1f",
|
||||||
|
"roads_label_major": "#666666",
|
||||||
|
"roads_label_major_halo": "#1f1f1f",
|
||||||
|
"ocean_label": "#717784",
|
||||||
|
"peak_label": "#898080",
|
||||||
|
"subplace_label": "#525252",
|
||||||
|
"subplace_label_halo": "#1f1f1f",
|
||||||
|
"city_label": "#7a7a7a",
|
||||||
|
"city_label_halo": "#212121",
|
||||||
|
"state_label": "#3d3d3d",
|
||||||
|
"state_label_halo": "#1f1f1f",
|
||||||
|
"country_label": "#5c5c5c",
|
||||||
|
"address_label": "#525252",
|
||||||
|
"address_label_halo": "#1f1f1f",
|
||||||
|
|
||||||
|
// === NESTED OBJECTS (REQUIRED) ===
|
||||||
|
|
||||||
|
// POI icon colors - all 8 keys required
|
||||||
|
"pois": {
|
||||||
|
"blue": "#4299BB",
|
||||||
|
"green": "#30C573",
|
||||||
|
"lapis": "#2B5CEA",
|
||||||
|
"pink": "#EF56BA",
|
||||||
|
"red": "#F2567A",
|
||||||
|
"slategray": "#93939F",
|
||||||
|
"tangerine": "#F19B6E",
|
||||||
|
"turquoise": "#00C3D4"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Landcover fill colors - all 7 keys required
|
||||||
|
"landcover": {
|
||||||
|
"grassland": "rgba(30, 41, 31, 1)",
|
||||||
|
"barren": "rgba(38, 38, 36, 1)",
|
||||||
|
"urban_area": "rgba(28, 28, 28, 1)",
|
||||||
|
"farmland": "rgba(31, 36, 32, 1)",
|
||||||
|
"glacier": "rgba(43, 43, 43, 1)",
|
||||||
|
"scrub": "rgba(34, 36, 30, 1)",
|
||||||
|
"forest": "rgba(28, 41, 37, 1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Config
|
||||||
|
|
||||||
|
Add custom themes to `registry.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const themes = {
|
||||||
|
// ... existing themes ...
|
||||||
|
|
||||||
|
'sepia': {
|
||||||
|
id: 'sepia',
|
||||||
|
name: 'Sepia',
|
||||||
|
dark: false, // Affects overlay styling and sprite fallback
|
||||||
|
colors: {
|
||||||
|
// Full flavor object (all 73 flat keys + pois + landcover)
|
||||||
|
},
|
||||||
|
satellite: {
|
||||||
|
// Optional: raster adjustments for satellite layer
|
||||||
|
opacity: 1.0,
|
||||||
|
brightnessMin: 0,
|
||||||
|
brightnessMax: 1,
|
||||||
|
contrast: 0,
|
||||||
|
saturation: 0,
|
||||||
|
hueRotate: 0,
|
||||||
|
},
|
||||||
|
overlay: null, // Reserved for future use
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
1. **All keys are required** - protomaps-themes-base expects every key
|
||||||
|
2. **Nested objects matter** - `pois` and `landcover` are objects, not flat keys
|
||||||
|
3. **Sprite fallback** - Custom themes fall back to dark/light sprite based on `dark` flag
|
||||||
|
4. **CSS vars separate** - Map flavor colors are separate from UI CSS custom properties
|
||||||
95
src/themes/dark-flavor-reference.json
Normal file
95
src/themes/dark-flavor-reference.json
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
{
|
||||||
|
"background": "#34373d",
|
||||||
|
"earth": "#1f1f1f",
|
||||||
|
"park_a": "#1c2421",
|
||||||
|
"park_b": "#192a24",
|
||||||
|
"hospital": "#252424",
|
||||||
|
"industrial": "#222222",
|
||||||
|
"school": "#262323",
|
||||||
|
"wood_a": "#202121",
|
||||||
|
"wood_b": "#202121",
|
||||||
|
"pedestrian": "#1e1e1e",
|
||||||
|
"scrub_a": "#222323",
|
||||||
|
"scrub_b": "#222323",
|
||||||
|
"glacier": "#1c1c1c",
|
||||||
|
"sand": "#212123",
|
||||||
|
"beach": "#28282a",
|
||||||
|
"aerodrome": "#1e1e1e",
|
||||||
|
"runway": "#333333",
|
||||||
|
"water": "#31353f",
|
||||||
|
"zoo": "#222323",
|
||||||
|
"military": "#242323",
|
||||||
|
"tunnel_other_casing": "#141414",
|
||||||
|
"tunnel_minor_casing": "#141414",
|
||||||
|
"tunnel_link_casing": "#141414",
|
||||||
|
"tunnel_major_casing": "#141414",
|
||||||
|
"tunnel_highway_casing": "#141414",
|
||||||
|
"tunnel_other": "#292929",
|
||||||
|
"tunnel_minor": "#292929",
|
||||||
|
"tunnel_link": "#292929",
|
||||||
|
"tunnel_major": "#292929",
|
||||||
|
"tunnel_highway": "#292929",
|
||||||
|
"pier": "#333333",
|
||||||
|
"buildings": "#111111",
|
||||||
|
"minor_service_casing": "#1f1f1f",
|
||||||
|
"minor_casing": "#1f1f1f",
|
||||||
|
"link_casing": "#1f1f1f",
|
||||||
|
"major_casing_late": "#1f1f1f",
|
||||||
|
"highway_casing_late": "#1f1f1f",
|
||||||
|
"other": "#333333",
|
||||||
|
"minor_service": "#333333",
|
||||||
|
"minor_a": "#3d3d3d",
|
||||||
|
"minor_b": "#333333",
|
||||||
|
"link": "#3d3d3d",
|
||||||
|
"major_casing_early": "#1f1f1f",
|
||||||
|
"major": "#3d3d3d",
|
||||||
|
"highway_casing_early": "#1f1f1f",
|
||||||
|
"highway": "#474747",
|
||||||
|
"railway": "#000000",
|
||||||
|
"boundaries": "#5b6374",
|
||||||
|
"waterway_label": "#717784",
|
||||||
|
"bridges_other_casing": "#2b2b2b",
|
||||||
|
"bridges_minor_casing": "#1f1f1f",
|
||||||
|
"bridges_link_casing": "#1f1f1f",
|
||||||
|
"bridges_major_casing": "#1f1f1f",
|
||||||
|
"bridges_highway_casing": "#1f1f1f",
|
||||||
|
"bridges_other": "#333333",
|
||||||
|
"bridges_minor": "#333333",
|
||||||
|
"bridges_link": "#3d3d3d",
|
||||||
|
"bridges_major": "#3d3d3d",
|
||||||
|
"bridges_highway": "#474747",
|
||||||
|
"roads_label_minor": "#525252",
|
||||||
|
"roads_label_minor_halo": "#1f1f1f",
|
||||||
|
"roads_label_major": "#666666",
|
||||||
|
"roads_label_major_halo": "#1f1f1f",
|
||||||
|
"ocean_label": "#717784",
|
||||||
|
"peak_label": "#898080",
|
||||||
|
"subplace_label": "#525252",
|
||||||
|
"subplace_label_halo": "#1f1f1f",
|
||||||
|
"city_label": "#7a7a7a",
|
||||||
|
"city_label_halo": "#212121",
|
||||||
|
"state_label": "#3d3d3d",
|
||||||
|
"state_label_halo": "#1f1f1f",
|
||||||
|
"country_label": "#5c5c5c",
|
||||||
|
"address_label": "#525252",
|
||||||
|
"address_label_halo": "#1f1f1f",
|
||||||
|
"pois": {
|
||||||
|
"blue": "#4299BB",
|
||||||
|
"green": "#30C573",
|
||||||
|
"lapis": "#2B5CEA",
|
||||||
|
"pink": "#EF56BA",
|
||||||
|
"red": "#F2567A",
|
||||||
|
"slategray": "#93939F",
|
||||||
|
"tangerine": "#F19B6E",
|
||||||
|
"turquoise": "#00C3D4"
|
||||||
|
},
|
||||||
|
"landcover": {
|
||||||
|
"grassland": "rgba(30, 41, 31, 1)",
|
||||||
|
"barren": "rgba(38, 38, 36, 1)",
|
||||||
|
"urban_area": "rgba(28, 28, 28, 1)",
|
||||||
|
"farmland": "rgba(31, 36, 32, 1)",
|
||||||
|
"glacier": "rgba(43, 43, 43, 1)",
|
||||||
|
"scrub": "rgba(34, 36, 30, 1)",
|
||||||
|
"forest": "rgba(28, 41, 37, 1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/themes/light-flavor-reference.json
Normal file
95
src/themes/light-flavor-reference.json
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
{
|
||||||
|
"background": "#cccccc",
|
||||||
|
"earth": "#e2dfda",
|
||||||
|
"park_a": "#cfddd5",
|
||||||
|
"park_b": "#9cd3b4",
|
||||||
|
"hospital": "#e4dad9",
|
||||||
|
"industrial": "#d1dde1",
|
||||||
|
"school": "#e4ded7",
|
||||||
|
"wood_a": "#d0ded0",
|
||||||
|
"wood_b": "#a0d9a0",
|
||||||
|
"pedestrian": "#e3e0d4",
|
||||||
|
"scrub_a": "#cedcd7",
|
||||||
|
"scrub_b": "#99d2bb",
|
||||||
|
"glacier": "#e7e7e7",
|
||||||
|
"sand": "#e2e0d7",
|
||||||
|
"beach": "#e8e4d0",
|
||||||
|
"aerodrome": "#dadbdf",
|
||||||
|
"runway": "#e9e9ed",
|
||||||
|
"water": "#80deea",
|
||||||
|
"zoo": "#c6dcdc",
|
||||||
|
"military": "#dcdcdc",
|
||||||
|
"tunnel_other_casing": "#e0e0e0",
|
||||||
|
"tunnel_minor_casing": "#e0e0e0",
|
||||||
|
"tunnel_link_casing": "#e0e0e0",
|
||||||
|
"tunnel_major_casing": "#e0e0e0",
|
||||||
|
"tunnel_highway_casing": "#e0e0e0",
|
||||||
|
"tunnel_other": "#d5d5d5",
|
||||||
|
"tunnel_minor": "#d5d5d5",
|
||||||
|
"tunnel_link": "#d5d5d5",
|
||||||
|
"tunnel_major": "#d5d5d5",
|
||||||
|
"tunnel_highway": "#d5d5d5",
|
||||||
|
"pier": "#e0e0e0",
|
||||||
|
"buildings": "#cccccc",
|
||||||
|
"minor_service_casing": "#e0e0e0",
|
||||||
|
"minor_casing": "#e0e0e0",
|
||||||
|
"link_casing": "#e0e0e0",
|
||||||
|
"major_casing_late": "#e0e0e0",
|
||||||
|
"highway_casing_late": "#e0e0e0",
|
||||||
|
"other": "#ebebeb",
|
||||||
|
"minor_service": "#ebebeb",
|
||||||
|
"minor_a": "#ebebeb",
|
||||||
|
"minor_b": "#ffffff",
|
||||||
|
"link": "#ffffff",
|
||||||
|
"major_casing_early": "#e0e0e0",
|
||||||
|
"major": "#ffffff",
|
||||||
|
"highway_casing_early": "#e0e0e0",
|
||||||
|
"highway": "#ffffff",
|
||||||
|
"railway": "#a7b1b3",
|
||||||
|
"boundaries": "#adadad",
|
||||||
|
"waterway_label": "#ffffff",
|
||||||
|
"bridges_other_casing": "#e0e0e0",
|
||||||
|
"bridges_minor_casing": "#e0e0e0",
|
||||||
|
"bridges_link_casing": "#e0e0e0",
|
||||||
|
"bridges_major_casing": "#e0e0e0",
|
||||||
|
"bridges_highway_casing": "#e0e0e0",
|
||||||
|
"bridges_other": "#ebebeb",
|
||||||
|
"bridges_minor": "#ffffff",
|
||||||
|
"bridges_link": "#ffffff",
|
||||||
|
"bridges_major": "#f5f5f5",
|
||||||
|
"bridges_highway": "#ffffff",
|
||||||
|
"roads_label_minor": "#91888b",
|
||||||
|
"roads_label_minor_halo": "#ffffff",
|
||||||
|
"roads_label_major": "#938a8d",
|
||||||
|
"roads_label_major_halo": "#ffffff",
|
||||||
|
"ocean_label": "#728dd4",
|
||||||
|
"peak_label": "#7e9aa0",
|
||||||
|
"subplace_label": "#8f8f8f",
|
||||||
|
"subplace_label_halo": "#e0e0e0",
|
||||||
|
"city_label": "#5c5c5c",
|
||||||
|
"city_label_halo": "#e0e0e0",
|
||||||
|
"state_label": "#b3b3b3",
|
||||||
|
"state_label_halo": "#e0e0e0",
|
||||||
|
"country_label": "#a3a3a3",
|
||||||
|
"address_label": "#91888b",
|
||||||
|
"address_label_halo": "#ffffff",
|
||||||
|
"pois": {
|
||||||
|
"blue": "#1A8CBD",
|
||||||
|
"green": "#20834D",
|
||||||
|
"lapis": "#315BCF",
|
||||||
|
"pink": "#EF56BA",
|
||||||
|
"red": "#F2567A",
|
||||||
|
"slategray": "#6A5B8F",
|
||||||
|
"tangerine": "#CB6704",
|
||||||
|
"turquoise": "#00C3D4"
|
||||||
|
},
|
||||||
|
"landcover": {
|
||||||
|
"grassland": "rgba(210, 239, 207, 1)",
|
||||||
|
"barren": "rgba(255, 243, 215, 1)",
|
||||||
|
"urban_area": "rgba(230, 230, 230, 1)",
|
||||||
|
"farmland": "rgba(216, 239, 210, 1)",
|
||||||
|
"glacier": "rgba(255, 255, 255, 1)",
|
||||||
|
"scrub": "rgba(234, 239, 210, 1)",
|
||||||
|
"forest": "rgba(196, 231, 210, 1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/themes/registry.js
Normal file
107
src/themes/registry.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* 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|null - reserved for future overlay-specific customizations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { namedTheme } from 'protomaps-themes-base'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: null,
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
id: 'dark',
|
||||||
|
name: 'Dark',
|
||||||
|
dark: true,
|
||||||
|
colors: null, // Use namedTheme('dark')
|
||||||
|
satellite: null,
|
||||||
|
overlay: null,
|
||||||
|
},
|
||||||
|
// 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: null,
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 list of available themes for UI display
|
||||||
|
* @returns {Array<{id: string, name: string, dark: boolean}>}
|
||||||
|
*/
|
||||||
|
export function themeList() {
|
||||||
|
return Object.values(themes).map(({ id, name, dark }) => ({ id, name, dark }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue