mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
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:
parent
9530fbbf76
commit
f0acea33a0
4 changed files with 191 additions and 93 deletions
|
|
@ -1,12 +1,13 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
|
import { getTheme, applyThemeUI } from '../themes/registry'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and manages the theme system.
|
* Initializes and manages the theme system.
|
||||||
* Call once in App — it handles:
|
* Call once in App — it handles:
|
||||||
* - Reading localStorage override on mount
|
* - Reading localStorage override on mount
|
||||||
* - Listening to system prefers-color-scheme
|
* - Listening to system prefers-color-scheme
|
||||||
* - Applying data-theme to <html>
|
* - Applying theme UI via registry (CSS custom properties)
|
||||||
* - Updating store.theme (resolved value)
|
* - Updating store.theme (resolved value)
|
||||||
*/
|
*/
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
|
|
@ -16,8 +17,11 @@ export function useTheme() {
|
||||||
// Initialize override from localStorage on first mount
|
// Initialize override from localStorage on first mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = localStorage.getItem('navi-theme-override')
|
const stored = localStorage.getItem('navi-theme-override')
|
||||||
if (stored === 'dark' || stored === 'light') {
|
if (stored) {
|
||||||
useStore.getState().setThemeOverride(stored)
|
const theme = getTheme(stored)
|
||||||
|
if (theme) {
|
||||||
|
useStore.getState().setThemeOverride(stored)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
@ -30,7 +34,8 @@ export function useTheme() {
|
||||||
|
|
||||||
function apply() {
|
function apply() {
|
||||||
const resolved = resolve()
|
const resolved = resolve()
|
||||||
document.documentElement.setAttribute('data-theme', resolved)
|
const theme = getTheme(resolved)
|
||||||
|
applyThemeUI(theme)
|
||||||
setTheme(resolved)
|
setTheme(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@
|
||||||
NAVI DESIGN TOKENS
|
NAVI DESIGN TOKENS
|
||||||
Warm grays, sage greens, khaki tans, deep blacks.
|
Warm grays, sage greens, khaki tans, deep blacks.
|
||||||
No blue in UI chrome.
|
No blue in UI chrome.
|
||||||
|
|
||||||
|
NOTE: Color tokens (--bg-*, --text-*, --border, etc.)
|
||||||
|
are now applied via applyThemeUI() in src/themes/registry.js.
|
||||||
|
Each theme defines its own ui object with CSS custom properties.
|
||||||
═══════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
@ -19,80 +23,6 @@
|
||||||
--text-lg: 1.125rem; /* 18px */
|
--text-lg: 1.125rem; /* 18px */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══ DARK MODE (default) ═══ */
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--bg-base: #1c1917; /* warm off-black (was #0f1210) */
|
|
||||||
--bg-raised: #252220; /* raised surface (was #181d1a) */
|
|
||||||
--bg-overlay: #2e2a27; /* overlay/dropdown (was #1e2522) */
|
|
||||||
--bg-input: #201d1a; /* input fields (was #141a16) */
|
|
||||||
|
|
||||||
--text-primary: #dde3dc;
|
|
||||||
--text-secondary: #8f9a8e;
|
|
||||||
--text-tertiary: #5e6b5d;
|
|
||||||
--text-inverse: #1c1917;
|
|
||||||
|
|
||||||
--border: #3a3530; /* warm brown-gray (was #2a3329) */
|
|
||||||
--border-subtle: #2a2624; /* (was #1f261e) */
|
|
||||||
|
|
||||||
--accent: #7a9a6b; /* sage green — interactive states */
|
|
||||||
--accent-hover: #8fad7f;
|
|
||||||
--accent-muted: #3d4d36;
|
|
||||||
|
|
||||||
--tan: #b8a88a; /* khaki — secondary highlights */
|
|
||||||
--tan-muted: #4a4235;
|
|
||||||
|
|
||||||
--pin-origin: #6b8f5e; /* sage */
|
|
||||||
--pin-destination: #a67c52; /* rust/tan */
|
|
||||||
--pin-intermediate: #6b7268; /* warm gray */
|
|
||||||
--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 MODE ═══ */
|
|
||||||
[data-theme="light"] {
|
|
||||||
--bg-base: #ddd2b9; /* warm khaki-tan (was #ece8e1) */
|
|
||||||
--bg-raised: #e8dec8; /* raised surface (was #f5f2ec) */
|
|
||||||
--bg-overlay: #e3d9c1; /* overlay/dropdown (was #f0ece5) */
|
|
||||||
--bg-input: #e8dec8; /* input fields (was #f5f2ec) */
|
|
||||||
|
|
||||||
--text-primary: #1a1d1a;
|
|
||||||
--text-secondary: #4f5a49; /* darkened for WCAG AA on new base (was #5c6558) */
|
|
||||||
--text-tertiary: #7a8674; /* darkened proportionally (was #8a9486) */
|
|
||||||
--text-inverse: #f5f2ed;
|
|
||||||
|
|
||||||
--border: #c4b89e; /* warmer border (was #d4cfc5) */
|
|
||||||
--border-subtle: #d5cab2; /* warmer subtle border (was #e8e3db) */
|
|
||||||
|
|
||||||
--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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ═══ BASE STYLES ═══ */
|
/* ═══ BASE STYLES ═══ */
|
||||||
html, body, #root {
|
html, body, #root {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ This directory contains the theme registry and reference files for creating cust
|
||||||
|
|
||||||
## Files
|
## 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
|
- **dark-flavor-reference.json** - Full namedTheme('dark') output for reference
|
||||||
- **light-flavor-reference.json** - Full namedTheme('light') 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
|
```javascript
|
||||||
{
|
{
|
||||||
// === FLAT COLOR KEYS (73 total) ===
|
// === FLAT COLOR KEYS (73 total) ===
|
||||||
|
|
||||||
// Background & earth
|
// Background & earth
|
||||||
"background": "#34373d",
|
"background": "#34373d",
|
||||||
"earth": "#1f1f1f",
|
"earth": "#1f1f1f",
|
||||||
|
|
||||||
// Land use areas
|
// Land use areas
|
||||||
"park_a": "#1c2421",
|
"park_a": "#1c2421",
|
||||||
"park_b": "#192a24",
|
"park_b": "#192a24",
|
||||||
|
|
@ -45,7 +45,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
|
||||||
"military": "#242323",
|
"military": "#242323",
|
||||||
"pier": "#333333",
|
"pier": "#333333",
|
||||||
"buildings": "#111111",
|
"buildings": "#111111",
|
||||||
|
|
||||||
// Tunnels
|
// Tunnels
|
||||||
"tunnel_other_casing": "#141414",
|
"tunnel_other_casing": "#141414",
|
||||||
"tunnel_minor_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_link": "#292929",
|
||||||
"tunnel_major": "#292929",
|
"tunnel_major": "#292929",
|
||||||
"tunnel_highway": "#292929",
|
"tunnel_highway": "#292929",
|
||||||
|
|
||||||
// Roads & casings
|
// Roads & casings
|
||||||
"minor_service_casing": "#1f1f1f",
|
"minor_service_casing": "#1f1f1f",
|
||||||
"minor_casing": "#1f1f1f",
|
"minor_casing": "#1f1f1f",
|
||||||
|
|
@ -75,7 +75,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
|
||||||
"highway": "#474747",
|
"highway": "#474747",
|
||||||
"railway": "#000000",
|
"railway": "#000000",
|
||||||
"boundaries": "#5b6374",
|
"boundaries": "#5b6374",
|
||||||
|
|
||||||
// Bridges
|
// Bridges
|
||||||
"bridges_other_casing": "#2b2b2b",
|
"bridges_other_casing": "#2b2b2b",
|
||||||
"bridges_minor_casing": "#1f1f1f",
|
"bridges_minor_casing": "#1f1f1f",
|
||||||
|
|
@ -87,7 +87,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
|
||||||
"bridges_link": "#3d3d3d",
|
"bridges_link": "#3d3d3d",
|
||||||
"bridges_major": "#3d3d3d",
|
"bridges_major": "#3d3d3d",
|
||||||
"bridges_highway": "#474747",
|
"bridges_highway": "#474747",
|
||||||
|
|
||||||
// Labels
|
// Labels
|
||||||
"waterway_label": "#717784",
|
"waterway_label": "#717784",
|
||||||
"roads_label_minor": "#525252",
|
"roads_label_minor": "#525252",
|
||||||
|
|
@ -105,9 +105,9 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
|
||||||
"country_label": "#5c5c5c",
|
"country_label": "#5c5c5c",
|
||||||
"address_label": "#525252",
|
"address_label": "#525252",
|
||||||
"address_label_halo": "#1f1f1f",
|
"address_label_halo": "#1f1f1f",
|
||||||
|
|
||||||
// === NESTED OBJECTS (REQUIRED) ===
|
// === NESTED OBJECTS (REQUIRED) ===
|
||||||
|
|
||||||
// POI icon colors - all 8 keys required
|
// POI icon colors - all 8 keys required
|
||||||
"pois": {
|
"pois": {
|
||||||
"blue": "#4299BB",
|
"blue": "#4299BB",
|
||||||
|
|
@ -119,7 +119,7 @@ The flavor object has **73 flat color keys** plus **2 nested objects**:
|
||||||
"tangerine": "#F19B6E",
|
"tangerine": "#F19B6E",
|
||||||
"turquoise": "#00C3D4"
|
"turquoise": "#00C3D4"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Landcover fill colors - all 7 keys required
|
// Landcover fill colors - all 7 keys required
|
||||||
"landcover": {
|
"landcover": {
|
||||||
"grassland": "rgba(30, 41, 31, 1)",
|
"grassland": "rgba(30, 41, 31, 1)",
|
||||||
|
|
@ -140,11 +140,11 @@ Add custom themes to `registry.js`:
|
||||||
```javascript
|
```javascript
|
||||||
const themes = {
|
const themes = {
|
||||||
// ... existing themes ...
|
// ... existing themes ...
|
||||||
|
|
||||||
'sepia': {
|
'sepia': {
|
||||||
id: 'sepia',
|
id: 'sepia',
|
||||||
name: 'Sepia',
|
name: 'Sepia',
|
||||||
dark: false, // Affects overlay styling and sprite fallback
|
dark: false, // Affects overlay styling, sprite fallback, and UI cascade
|
||||||
colors: {
|
colors: {
|
||||||
// Full flavor object (all 73 flat keys + pois + landcover)
|
// Full flavor object (all 73 flat keys + pois + landcover)
|
||||||
},
|
},
|
||||||
|
|
@ -157,14 +157,75 @@ const themes = {
|
||||||
saturation: 0,
|
saturation: 0,
|
||||||
hueRotate: 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
|
### 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
|
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
|
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
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,79 @@
|
||||||
* colors: object|null - null for built-in themes, full flavor object for custom
|
* colors: object|null - null for built-in themes, full flavor object for custom
|
||||||
* satellite: object|null - raster adjustments when satellite layer is present
|
* satellite: object|null - raster adjustments when satellite layer is present
|
||||||
* overlay: object - overlay layer styling configuration
|
* overlay: object - overlay layer styling configuration
|
||||||
|
* ui: object - CSS custom properties for UI elements
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { namedTheme } from 'protomaps-themes-base'
|
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
|
// OVERLAY CONFIGURATIONS
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -361,6 +430,7 @@ const themes = {
|
||||||
colors: null, // Use namedTheme('light')
|
colors: null, // Use namedTheme('light')
|
||||||
satellite: null,
|
satellite: null,
|
||||||
overlay: lightOverlay,
|
overlay: lightOverlay,
|
||||||
|
ui: lightUI,
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
id: 'dark',
|
id: 'dark',
|
||||||
|
|
@ -369,6 +439,7 @@ const themes = {
|
||||||
colors: null, // Use namedTheme('dark')
|
colors: null, // Use namedTheme('dark')
|
||||||
satellite: null,
|
satellite: null,
|
||||||
overlay: darkOverlay,
|
overlay: darkOverlay,
|
||||||
|
ui: darkUI,
|
||||||
},
|
},
|
||||||
// Custom themes go here. Example:
|
// Custom themes go here. Example:
|
||||||
// 'midnight': {
|
// 'midnight': {
|
||||||
|
|
@ -378,6 +449,7 @@ const themes = {
|
||||||
// colors: { /* full flavor object matching dark-flavor-reference.json schema */ },
|
// colors: { /* full flavor object matching dark-flavor-reference.json schema */ },
|
||||||
// satellite: { opacity: 0.8, brightnessMin: 0.1 },
|
// satellite: { opacity: 0.8, brightnessMin: 0.1 },
|
||||||
// overlay: { /* partial overrides - missing keys fall back to dark overlay */ },
|
// 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
|
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
|
* Get list of available themes for UI display
|
||||||
* @returns {Array<{id: string, name: string, dark: boolean}>}
|
* @returns {Array<{id: string, name: string, dark: boolean}>}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue