feat(themes): consolidate UI CSS properties into theme registry

- Add darkUI and lightUI objects with all 25 CSS custom properties
- Add applyThemeUI() function to apply CSS vars via JavaScript
- Update useTheme.js to call applyThemeUI() instead of setAttribute
- Remove [data-theme="dark"] and [data-theme="light"] from index.css
- Custom themes can now override individual UI properties with cascade
- Update README.md to document the ui key and cascade behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-05-01 16:17:26 +00:00
commit f0acea33a0
4 changed files with 191 additions and 93 deletions

View file

@ -4,7 +4,7 @@ This directory contains the theme registry and reference files for creating cust
## Files
- **registry.js** - Theme registry with getTheme(), getThemeColors(), getThemeSprite(), themeList()
- **registry.js** - Theme registry with getTheme(), getThemeColors(), getThemeSprite(), getOverlayConfig(), applyThemeUI(), themeList()
- **dark-flavor-reference.json** - Full namedTheme('dark') output for reference
- **light-flavor-reference.json** - Full namedTheme('light') output for reference
@ -19,11 +19,11 @@ 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",
@ -45,7 +45,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
"military": "#242323",
"pier": "#333333",
"buildings": "#111111",
// Tunnels
"tunnel_other_casing": "#141414",
"tunnel_minor_casing": "#141414",
@ -57,7 +57,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
"tunnel_link": "#292929",
"tunnel_major": "#292929",
"tunnel_highway": "#292929",
// Roads & casings
"minor_service_casing": "#1f1f1f",
"minor_casing": "#1f1f1f",
@ -75,7 +75,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
"highway": "#474747",
"railway": "#000000",
"boundaries": "#5b6374",
// Bridges
"bridges_other_casing": "#2b2b2b",
"bridges_minor_casing": "#1f1f1f",
@ -87,7 +87,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
"bridges_link": "#3d3d3d",
"bridges_major": "#3d3d3d",
"bridges_highway": "#474747",
// Labels
"waterway_label": "#717784",
"roads_label_minor": "#525252",
@ -105,9 +105,9 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
"country_label": "#5c5c5c",
"address_label": "#525252",
"address_label_halo": "#1f1f1f",
// === NESTED OBJECTS (REQUIRED) ===
// POI icon colors - all 8 keys required
"pois": {
"blue": "#4299BB",
@ -119,7 +119,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
"tangerine": "#F19B6E",
"turquoise": "#00C3D4"
},
// Landcover fill colors - all 7 keys required
"landcover": {
"grassland": "rgba(30, 41, 31, 1)",
@ -140,11 +140,11 @@ Add custom themes to `registry.js`:
```javascript
const themes = {
// ... existing themes ...
'sepia': {
id: 'sepia',
name: 'Sepia',
dark: false, // Affects overlay styling and sprite fallback
dark: false, // Affects overlay styling, sprite fallback, and UI cascade
colors: {
// Full flavor object (all 73 flat keys + pois + landcover)
},
@ -157,14 +157,75 @@ const themes = {
saturation: 0,
hueRotate: 0,
},
overlay: null, // Reserved for future use
overlay: null, // Optional: custom overlay config, cascades from dark/light
ui: null, // Optional: custom UI CSS vars, cascades from dark/light
},
}
```
### UI Customization
Each theme can define a `ui` object containing CSS custom properties for the application chrome.
Custom themes cascade from the base dark/light UI based on the `dark` flag.
```javascript
// Full list of UI properties (25 total)
ui: {
'--bg-base': '#1c1917',
'--bg-raised': '#252220',
'--bg-overlay': '#2e2a27',
'--bg-input': '#201d1a',
'--text-primary': '#dde3dc',
'--text-secondary': '#8f9a8e',
'--text-tertiary': '#5e6b5d',
'--text-inverse': '#1c1917',
'--border': '#3a3530',
'--border-subtle': '#2a2624',
'--accent': '#7a9a6b',
'--accent-hover': '#8fad7f',
'--accent-muted': '#3d4d36',
'--tan': '#b8a88a',
'--tan-muted': '#4a4235',
'--pin-origin': '#6b8f5e',
'--pin-destination': '#a67c52',
'--pin-intermediate': '#6b7268',
'--pin-stroke': '#1c1917',
'--status-success': '#6b8f5e',
'--status-warning': '#b89a4a',
'--status-danger': '#a65c52',
'--route-line': '#7a9a6b',
'--shadow': '0 2px 8px rgba(0, 0, 0, 0.4)',
'--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.5)',
}
```
Custom themes only need to specify the properties they want to override:
```javascript
'sepia': {
id: 'sepia',
name: 'Sepia',
dark: false,
colors: { /* ... */ },
ui: {
// Only override what's different from the light theme
'--accent': '#8a7040',
'--accent-hover': '#6b5530',
'--tan': '#8a7556',
},
}
```
### Overlay Customization
Overlay styling (hillshade, traffic, contours, public lands, USFS trails, BLM trails) is also
configurable per-theme. See `darkOverlay` and `lightOverlay` in registry.js for the full
structure. Custom themes cascade from dark/light based on the `dark` flag.
### Important Notes
1. **All keys are required** - protomaps-themes-base expects every key
1. **All color 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
4. **Cascading configs** - overlay and ui configs cascade from dark/light if not specified
5. **CSS vars via JS** - UI CSS properties are applied via `applyThemeUI()`, not CSS selectors

View file

@ -11,10 +11,79 @@
* 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
*/
import { namedTheme } from 'protomaps-themes-base'
// ═══════════════════════════════════════════════════════════════════════════
// UI CSS CUSTOM PROPERTIES
// ═══════════════════════════════════════════════════════════════════════════
/**
* Dark theme UI configuration
* All CSS custom properties from [data-theme="dark"] in index.css
*/
const darkUI = {
'--bg-base': '#1c1917',
'--bg-raised': '#252220',
'--bg-overlay': '#2e2a27',
'--bg-input': '#201d1a',
'--text-primary': '#dde3dc',
'--text-secondary': '#8f9a8e',
'--text-tertiary': '#5e6b5d',
'--text-inverse': '#1c1917',
'--border': '#3a3530',
'--border-subtle': '#2a2624',
'--accent': '#7a9a6b',
'--accent-hover': '#8fad7f',
'--accent-muted': '#3d4d36',
'--tan': '#b8a88a',
'--tan-muted': '#4a4235',
'--pin-origin': '#6b8f5e',
'--pin-destination': '#a67c52',
'--pin-intermediate': '#6b7268',
'--pin-stroke': '#1c1917',
'--status-success': '#6b8f5e',
'--status-warning': '#b89a4a',
'--status-danger': '#a65c52',
'--route-line': '#7a9a6b',
'--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 from [data-theme="light"] in index.css
*/
const lightUI = {
'--bg-base': '#ddd2b9',
'--bg-raised': '#e8dec8',
'--bg-overlay': '#e3d9c1',
'--bg-input': '#e8dec8',
'--text-primary': '#1a1d1a',
'--text-secondary': '#4f5a49',
'--text-tertiary': '#7a8674',
'--text-inverse': '#f5f2ed',
'--border': '#c4b89e',
'--border-subtle': '#d5cab2',
'--accent': '#4a7040',
'--accent-hover': '#3d5e35',
'--accent-muted': '#dce8d6',
'--tan': '#8a7556',
'--tan-muted': '#f0e8d8',
'--pin-origin': '#4a7040',
'--pin-destination': '#8a5c35',
'--pin-intermediate': '#6b6960',
'--pin-stroke': '#1a1d1a',
'--status-success': '#4a7040',
'--status-warning': '#8a7040',
'--status-danger': '#8a4040',
'--route-line': '#4a7040',
'--shadow': '0 2px 8px rgba(0, 0, 0, 0.08)',
'--shadow-lg': '0 4px 16px rgba(0, 0, 0, 0.12)',
}
// ═══════════════════════════════════════════════════════════════════════════
// OVERLAY CONFIGURATIONS
// ═══════════════════════════════════════════════════════════════════════════
@ -361,6 +430,7 @@ const themes = {
colors: null, // Use namedTheme('light')
satellite: null,
overlay: lightOverlay,
ui: lightUI,
},
dark: {
id: 'dark',
@ -369,6 +439,7 @@ const themes = {
colors: null, // Use namedTheme('dark')
satellite: null,
overlay: darkOverlay,
ui: darkUI,
},
// Custom themes go here. Example:
// 'midnight': {
@ -378,6 +449,7 @@ const themes = {
// 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 */ },
// },
}
@ -461,6 +533,36 @@ export function getOverlayConfig(themeId, 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.
*
* 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)
}
}
/**
* Get list of available themes for UI display
* @returns {Array<{id: string, name: string, dark: boolean}>}