diff --git a/app-profiles/doxa-research-map/research-map.vue b/app-profiles/doxa-research-map/research-map.vue index 63f22078f..56f9edcb4 100644 --- a/app-profiles/doxa-research-map/research-map.vue +++ b/app-profiles/doxa-research-map/research-map.vue @@ -427,6 +427,10 @@ const showTabBar = computed(() => true) const activeTabId = ref(null) const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value) ?? tabs.value[0]) +// Basemap look follows the active tab: prayer → night, everything else → day (monochrome). +// Re-evaluated whenever the tab changes, including when the tab list arrives after boot. +mapTheme.setLook(mapTheme.lookFor(activeTab.value?.colorStrategy)) +watch(() => activeTab.value?.colorStrategy, (strategy) => mapTheme.setLook(mapTheme.lookFor(strategy), map.value)) const activeLegendType = computed(() => activeTab.value?.legend ?? 'default') const activePopupAction = computed(() => activeTab.value?.popup ?? 'pray') @@ -1327,10 +1331,8 @@ function switchTab(tabId) { if (!FEATURES.geocoderPersistAcrossTabs) clearGeocoderProgrammatic() const tab = tabs.value.find(t => t.id === tabId) if (!tab || !map.value) return - // Re-assert the persisted theme's basemap lightPreset/theme on EVERY tab switch — defensive - // so dark mode can't appear "not working" on any tab (bug-dark-mode-multi-tab). Cheap: it's - // a config prop (no setStyle). - mapTheme.applyTheme(map.value) + // The basemap look follows the tab (prayer → night, others → day); config prop only, no reload. + mapTheme.setLook(mapTheme.lookFor(tab.colorStrategy), map.value) // Lazy-load regions GeoJSON the first time the user lands on this tab. if (tab.id === 'doxa-regions') void ensureRegionsLoaded() @@ -1929,7 +1931,7 @@ function applyContainerScale(width) { // cards). SemanticTreeLegend's .stl-panel follows this variable too. // 400px (was 380) since the desktop type scale widened the numeric // columns; keeps "Full Prayer Coverage" on one line (2026-09-16). - el.style.setProperty('--map-legend-width', '400px') + el.style.setProperty('--map-legend-width', '380px') } else if (width >= 500) { el.style.setProperty('--map-btn-size', '36px') el.style.setProperty('--map-toolbar-gap', '6px') diff --git a/app-profiles/doxa-simple-map/doxa-simple-map.vue b/app-profiles/doxa-simple-map/doxa-simple-map.vue index fb5363428..f51320d76 100644 --- a/app-profiles/doxa-simple-map/doxa-simple-map.vue +++ b/app-profiles/doxa-simple-map/doxa-simple-map.vue @@ -419,7 +419,7 @@ function applyContainerScale(width) { el.style.setProperty('--map-toolbar-gap', '8px') el.style.setProperty('--map-font-md', '13px') el.style.setProperty('--map-font-sm', '11px') - el.style.setProperty('--map-legend-width', '400px') + el.style.setProperty('--map-legend-width', '380px') } else if (width >= 500) { el.style.setProperty('--map-btn-size', '36px') el.style.setProperty('--map-toolbar-gap', '6px') @@ -447,6 +447,10 @@ const mapStore = inject('mapStore') const dataStore = inject('dataStore') const uiStore = inject('uiStore') const mapTheme = useMapTheme(uiStore) +// Basemap look follows the active tab: prayer → night, everything else → day (monochrome). +// Re-evaluated whenever the tab changes, including when the tab list arrives after boot. +mapTheme.setLook(mapTheme.lookFor(activeTab.value?.colorStrategy)) +watch(() => activeTab.value?.colorStrategy, (strategy) => mapTheme.setLook(mapTheme.lookFor(strategy), map.value)) // loc-012: feed the active locale into the bulk/list data fetch so the data the // search bar searches comes back in the selected language — the same `lang` param // the people-group detail fetch uses (PeopleGroupDetail.vue). null → API default. @@ -837,9 +841,8 @@ function switchTab(tabId) { activeTabId.value = tabId const tab = tabs.value.find(t => t.id === tabId) if (!tab || !map.value) return - // Re-assert the persisted theme's basemap lightPreset on every tab switch (defensive, - // config-prop only — keeps dark mode consistent across tabs). bug-dark-mode-multi-tab. - mapTheme.applyTheme(map.value) + // The basemap look follows the tab (prayer → night, others → day); config prop only, no reload. + mapTheme.setLook(mapTheme.lookFor(tab.colorStrategy), map.value) const strategyKey = tab.colorStrategy const colorMode = strategyKey === 'engagement' ? COLOR_MODES.ENGAGEMENT : diff --git a/library/components/legends/SemanticTreeLegend.vue b/library/components/legends/SemanticTreeLegend.vue index a38d2c442..bb9a6dd21 100644 --- a/library/components/legends/SemanticTreeLegend.vue +++ b/library/components/legends/SemanticTreeLegend.vue @@ -977,11 +977,11 @@ useShadowStyles(` .stl-panel:not(.legend-mobile-sheet *) .stl-hdr-name { font-size: 13px; } .stl-panel:not(.legend-mobile-sheet *) .stl-hdr-num { font-size: 10px; } .stl-panel:not(.legend-mobile-sheet *) .stl-name { font-size: 15px; } -.stl-panel:not(.legend-mobile-sheet *) .stl-num { font-size: 13.5px; } +.stl-panel:not(.legend-mobile-sheet *) .stl-num { font-size: 13px; } /* The numeric columns must grow with the type: "POPULATION" at 10px is ~65px, ~76px with its sort arrow; "139.0M" at 13.5px is ~49px. A fixed 44px column let the header collide with "UUPGs" and let values spill into the neighbouring column (Driver, 2026-09-16). */ -.stl-panel:not(.legend-mobile-sheet *) .stl-inner { --stl-num-w: 76px; } /* the vars live on .stl-inner */ +.stl-panel:not(.legend-mobile-sheet *) .stl-inner { --stl-num-w: 70px; } /* the vars live on .stl-inner */ .stl-panel:not(.legend-mobile-sheet *) .stl-hdr-num, .stl-panel:not(.legend-mobile-sheet *) .stl-num { white-space: nowrap; } .stl-panel:not(.legend-mobile-sheet *) .stl-info-pop { font-size: 13px; } diff --git a/library/composables/useMapInstance.js b/library/composables/useMapInstance.js index 246ea6140..6e12e9f08 100644 --- a/library/composables/useMapInstance.js +++ b/library/composables/useMapInstance.js @@ -366,6 +366,8 @@ export function useMapInstance({ attributionControl: true // Mapbox's own attribution on every map }) claimLogo(map.value) + // Probe hook: the container element carries its map so gates can read live config (no global). + try { containerRef.value.__doxaMap = map.value } catch (_) {} } else { // Fragment config for THIS boot (a thunk re-reads the theme on every re-init). const initialConfig = typeof config === 'function' ? config() : config @@ -394,6 +396,8 @@ export function useMapInstance({ attributionControl: true // Mapbox's own attribution on every map }) claimLogo(map.value) + // Probe hook: the container element carries its map so gates can read live config (no global). + try { containerRef.value.__doxaMap = map.value } catch (_) {} } // Belt-and-suspenders to dragRotate:false — kills the two-finger twist→rotate diff --git a/library/composables/useMapTheme.js b/library/composables/useMapTheme.js index e4d381830..90c462540 100644 --- a/library/composables/useMapTheme.js +++ b/library/composables/useMapTheme.js @@ -44,6 +44,12 @@ export const THEME_TOGGLE = false // The basemap look that is pinned when THEME_TOGGLE is false. These are the exact // values the ORIGINAL dark mode used, so the map is visually unchanged. export const FIXED_BASEMAP = { lightPreset: 'night', theme: 'default' } +// The light look: the same Standard style lit by day in the monochrome (white) theme, so the +// pins stand out on every map that informs rather than prays. Prayer maps keep the night look (light and darkness). +export const LIGHT_BASEMAP = { lightPreset: 'day', theme: 'monochrome' } +// Which look a colour strategy asks for: prayer → dark, everything else → light. +// Unknown strategy (tabs not loaded yet) → null: the current look is kept. +export function lookForStrategy(strategy) { return !strategy ? null : (/prayer/i.test(String(strategy)) ? 'dark' : 'light') } // 'prayer', 'prayerProgress', … export function useMapTheme(uiStore) { // Single pending 'idle' re-assert guard (per map instance). Prevents stacking one @@ -69,11 +75,20 @@ export function useMapTheme(uiStore) { // both the boot `config` (bootConfig) and every live re-pin (applyTheme), so the first // painted frame and a later re-assert can never disagree. Recomputed fresh on every call // (rapid toggles can't desync — last call wins). + // The pinned look for this map instance: 'dark' (prayer) or 'light' (every other tab). + // Set from the active tab's colour strategy at boot and on each tab switch (setLook). + let look = 'dark' + function lookFor(strategy) { return lookForStrategy(strategy) } + function setLook(next, map) { + if (next !== 'light' && next !== 'dark') return // unknown → keep the current look + look = next + if (map) applyTheme(map) + } function basemapConfig() { - // FLAG OFF → the basemap is PINNED to the dark/colourful look and does NOT follow the - // UI theme. This is what lets the chrome be light while the map keeps its dark-green - // colour strategy. - if (!THEME_TOGGLE) return { ...FIXED_BASEMAP } + // FLAG OFF → the basemap is PINNED per look and does NOT follow the UI theme. This is + // what lets the chrome stay light while a prayer map keeps its dark-green look and an + // informational map its day-lit green look. + if (!THEME_TOGGLE) return { ...(look === 'light' ? LIGHT_BASEMAP : FIXED_BASEMAP) } const dark = isDarkTheme() // lightPreset = day/night lighting. theme = the Standard colour theme: light mode uses // the LIGHTER, cleaner 'monochrome' theme (closer to a flat white map); dark keeps the @@ -189,5 +204,5 @@ export function useMapTheme(uiStore) { applyTheme(map) } - return { STANDARD_STYLE, LIGHT_STYLE, DARK_STYLE, isDarkTheme, lightPreset, basemapConfig, bootStyle, bootConfig, applyTheme, swapTheme } + return { setLook, lookFor, STANDARD_STYLE, LIGHT_STYLE, DARK_STYLE, isDarkTheme, lightPreset, basemapConfig, bootStyle, bootConfig, applyTheme, swapTheme } }