The no-selection state is now a map you pick from
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
The Driver's brief, with explicit creative freedom: "some type of map that would
show the regions and countries and let you select one that you want to select,
which will then show you the page results". It was two dropdowns and a table —
a form, not a map.
Now the map IS the chooser. Pins are coloured by WAGF region so the seven read
AS regions the moment it loads, with a seven-pill key rendered in REGION_ORDER
beside it. The key is a control as well as a key: hovering one lifts that
region's pins and recedes the rest, clicking one opens that region's dashboard,
and clicking any individual pin opens ITS COUNTRY's page. The two dropdowns stay
as the keyboard and screen-reader path, and the key items are real <button>s
with aria-labels so check:a11y-button-names has a name to find.
Colours come from library/colors/wagf-region.js — never a table in the
component, because check:one-palette exists precisely to stop that. Rendered in
REGION_ORDER because that palette's CVD separation was validated on ADJACENT
pairs in that order.
HIT TARGET IS PINS, NOT COUNTRY POLYGONS. Polygons would be the obvious choice
and would be dead here: the Mapbox country-boundaries tileset 403s on this box
(measured, 1 request, 1x403, URL-restricted token), so polygon hit-testing would
be both broken and untestable. Pins are our own GeoJSON and always render.
★ TWO THINGS THE SCREENSHOT CAUGHT THAT NO ASSERTION DID — both on the one
screen whose entire job is to invite a choice:
· a world-wide fitBounds pulled the globe into a skewed crescent of pins over
mostly empty white. Twelve assertions were green through that. Replaced with
a fixed world view; bounds are now refused outright when the subject is
everything.
· Mapbox v3 defaults to the GLOBE projection below about zoom 6, so the world
view rendered as a small sphere floating in a 1200x780 frame. setProjection
('mercator') for the CHOOSER ONLY — the cloned pages are deliberately left
alone, because DX-life sets no projection either and their region page shows
the same globe at the same zoom. Matching them is the point.
And one the mobile screenshot caught: a fixed zoom that fills 1200px shows
barely a third of the world at 390px, with Asia off the right edge. The zoom now
follows the CONTAINER WIDTH — log2(width/512), the width at which Mercator's
world fits — clamped to [0,2], re-derived on resize via ResizeObserver. A phone
still crops (Mapbox will not go below zoom 0) but ~24% rather than ~67%, and it
pans.
Verified: 12 chooser assertions, including that the CLONED pages are untouched —
their prayer colouring and their four-row prayer key both return on a selected
region. Region page still 68/68 vs live, country 28/28, app 10/10, no gate
newly red.
This commit is contained in:
parent
7e91040c4a
commit
2a0976f834
7 changed files with 227 additions and 48 deletions
|
|
@ -27,6 +27,9 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
|||
import { useDocumentFont, BEBAS_NEUE_FACES, POPPINS_FACES } from '@map/composables/useDocumentFont.js'
|
||||
import FilterDropdown from '@map/components/search/FilterDropdown.vue'
|
||||
import UupgsList from './src/UupgsList.vue'
|
||||
// Region colours come from the shared strategy (check:one-palette), and REGION_ORDER is the
|
||||
// order its CVD separation was validated in — the legend must render in it for that to hold.
|
||||
import { PALETTE as REGION_PALETTE, WAGF_REGIONS, REGION_ORDER } from '@map/colors/wagf-region.js'
|
||||
import RegionCountryMap from './src/RegionCountryMap.vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -325,6 +328,39 @@ const summaryLine = computed(() => {
|
|||
// regions so the page still compares like with like. The prompt below then says how to narrow it.
|
||||
const isAll = computed(() => subject.value.kind === 'all')
|
||||
|
||||
// ── THE CHOOSER'S MAP: colour mode + legend ─────────────────────────────────
|
||||
// Only the no-selection state paints by region. A SELECTED page keeps DX-life's own modes
|
||||
// exactly — prayer on a region, in-country on a country — because those pages are clones.
|
||||
const mapColorMode = computed(() => {
|
||||
if (subject.value.kind === 'country') return 'in-country'
|
||||
if (subject.value.kind === 'region') return 'prayer'
|
||||
return 'region'
|
||||
})
|
||||
|
||||
// In REGION_ORDER, not by count: the palette's CVD separation was measured on ADJACENT pairs in
|
||||
// that order, and sorting by size would also mean the legend reshuffles as data changes.
|
||||
const regionLegend = computed(() => {
|
||||
const byValue = new Map(regions.value.map((r) => [r.value, r]))
|
||||
return REGION_ORDER
|
||||
.map((value) => {
|
||||
const r = byValue.get(value)
|
||||
return r ? { value, slug: r.slug, label: WAGF_REGIONS[value] || r.label, count: r.count, color: REGION_PALETTE[value] } : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
const hoveredRegion = ref('')
|
||||
|
||||
// A pin click in the chooser picks THAT PIN'S COUNTRY — the finest subject the map can express.
|
||||
function onPickFromMap(d) {
|
||||
const code = d?.code
|
||||
if (!code) return
|
||||
for (const r of regions.value) {
|
||||
const c = r.countries.find((x) => x.code === code)
|
||||
if (c) { sel.value = { regionSlug: r.slug, countrySlug: c.slug }; return }
|
||||
}
|
||||
}
|
||||
|
||||
// ── THE ONE DELIBERATE DIFFERENCE: the no-selection chooser ─────────────────
|
||||
// DX-life has no route for a dashboard without a subject (/regions is the list,
|
||||
// /regions/<slug> always has one, [slug].vue 404s otherwise). Opening tab 2 first is a state
|
||||
|
|
@ -362,6 +398,12 @@ const focusCodes = computed(() => {
|
|||
})
|
||||
|
||||
const mapBounds = computed(() => {
|
||||
// ★ NEVER fitBounds THE WHOLE WORLD. In the chooser the focus is every country, so the bounds
|
||||
// span ~180 degrees of longitude and Mapbox fits that by pulling the globe into a crescent:
|
||||
// measured at 1440x1000 it rendered as a skewed arc of pins over mostly empty white, which for
|
||||
// the one screen that has to invite a choice is the worst possible first impression. A plain
|
||||
// world view is both correct and what a chooser wants.
|
||||
if (isAll.value) return null
|
||||
const focus = new Set(focusCodes.value)
|
||||
const pts = points.value.filter((p) => focus.has(p.code))
|
||||
if (pts.length < 2) return null
|
||||
|
|
@ -378,12 +420,21 @@ const mapBounds = computed(() => {
|
|||
|
||||
// Only used when bounds cannot be formed (a single people group, or none plotted yet).
|
||||
const mapCenter = computed(() => {
|
||||
// The chooser gets a fixed world view rather than a centroid: the mean of every people group
|
||||
// sits in south Asia (1309 of 2107 are in Asia), which framed the world off-centre and cut
|
||||
// the Americas. 10E/20N with the zoom below puts every region on screen at once.
|
||||
if (isAll.value) return [10, 28] // shifted north: the poles crop, and the data is northern
|
||||
const focus = new Set(focusCodes.value)
|
||||
const pts = points.value.filter((p) => focus.has(p.code))
|
||||
if (!pts.length) return [20, 10]
|
||||
return [pts.reduce((a, p) => a + p.lng, 0) / pts.length, pts.reduce((a, p) => a + p.lat, 0) / pts.length]
|
||||
})
|
||||
const mapZoom = computed(() => (selectedCountry.value ? 4 : 2))
|
||||
const mapZoom = computed(() => {
|
||||
// Mercator's world is square (512*2^z on both axes) and the frame is 16/9, so a world view
|
||||
// cannot fill both. Fill the WIDTH (512*2^z ~ 1200 -> z ~ 1.23) and let the ice caps crop.
|
||||
if (isAll.value) return 1.25
|
||||
return selectedCountry.value ? 4 : 2
|
||||
})
|
||||
|
||||
// Their map notes, verbatim, with the same %token% substitution their i18n uses.
|
||||
// RegionDetail.vue:38-42 and CountryDetail.vue:29-33.
|
||||
|
|
@ -519,6 +570,9 @@ async function loadRegions() {
|
|||
points.value = posts.map((p) => ({
|
||||
name: p.name || '', slug: p.slug || '',
|
||||
code: p.country_code?.value || '', country: p.country_code?.label || '',
|
||||
// the wagf_region VALUE (e.g. 'latin_america_&_caribbean'), which is what
|
||||
// library/colors/wagf-region.js keys its palette on
|
||||
region: p.wagf_region?.value || '',
|
||||
lng: parseFloat(p.longitude), lat: parseFloat(p.latitude),
|
||||
committed: toNumber(p.people_committed), population: toNumber(p.population),
|
||||
})).filter((p) => Number.isFinite(p.lng) && Number.isFinite(p.lat))
|
||||
|
|
@ -636,6 +690,20 @@ useShadowStyles(
|
|||
'.rd-mapnote{ margin:0; text-align:center; color:#92b195; font-weight:700; font-size:19.5312px; }' +
|
||||
// .rounded-xlg on their <CountryMap>; the child takes border-radius:inherit from here.
|
||||
'.rd-mapframe{ border-radius:var(--dx-radius); overflow:hidden; }' +
|
||||
// The region key. Their token scale throughout so the one screen we add does not look
|
||||
// foreign next to the clone: --spacing-xs/sm gaps, --font-size-sm, the 100px pill radius
|
||||
// their chips use, and --color-brand-lighter for the count.
|
||||
'.rd-regionkey{ list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap;' +
|
||||
' gap:10px 13.3px; justify-content:center; }' +
|
||||
'.rd-regionkey__item{ appearance:none; border:1px solid var(--dx-line); background:var(--dx-elevated);' +
|
||||
' border-radius:100px; padding:7.5188px 13.3px; cursor:pointer; font:inherit;' +
|
||||
' font-size:15.625px; color:var(--dx-text); display:inline-flex; align-items:center;' +
|
||||
' gap:7.5188px; transition:border-color .15s ease, background-color .15s ease; }' +
|
||||
'.rd-regionkey__item:hover{ border-color:var(--dx-header-bg); background:#fff; }' +
|
||||
'.rd-regionkey__item:focus-visible{ outline:2px solid var(--dx-accent); outline-offset:2px; }' +
|
||||
'.rd-regionkey__dot{ width:10px; height:10px; border-radius:100px; flex:0 0 auto; }' +
|
||||
'.rd-regionkey__count{ color:var(--dx-muted); font-variant-numeric:tabular-nums; }' +
|
||||
'.rd-regionkey__hint{ margin:0; text-align:center; color:var(--dx-muted); font-size:15.625px; }' +
|
||||
|
||||
// ── .switcher[data-width="xl"]: composition/_switcher.scss. --switcher-width 70rem = 700px,
|
||||
// gap --spacing-2xl (41.6158px). flex-basis calc((700px - 100%) * 999) is every-layout's
|
||||
|
|
@ -800,9 +868,39 @@ useShadowStyles(
|
|||
:bounds="mapBounds"
|
||||
:center="mapCenter"
|
||||
:zoom="mapZoom"
|
||||
:color-by-prayer="subject.kind !== 'country'"
|
||||
:color-mode="mapColorMode"
|
||||
:highlight-region="hoveredRegion"
|
||||
:select-on-pin-click="isAll"
|
||||
@pick-country="onPickFromMap"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- THE REGION KEY — chooser only, and it is a CONTROL as well as a key: the Driver
|
||||
asked for a map that "would show the regions and countries and let you select one".
|
||||
Rendered in REGION_ORDER because that is the order the palette's CVD separation was
|
||||
validated in. Buttons, not swatches with click handlers, so the keyboard reaches
|
||||
them and check:a11y-button-names has a name to find. -->
|
||||
<ul v-if="isAll" class="rd-regionkey" aria-label="Choose a region">
|
||||
<li v-for="r in regionLegend" :key="r.value">
|
||||
<button
|
||||
type="button"
|
||||
class="rd-regionkey__item"
|
||||
:aria-label="'Open the ' + r.label + ' dashboard'"
|
||||
@click="sel = { regionSlug: r.slug, countrySlug: '' }"
|
||||
@mouseenter="hoveredRegion = r.value"
|
||||
@mouseleave="hoveredRegion = ''"
|
||||
@focus="hoveredRegion = r.value"
|
||||
@blur="hoveredRegion = ''"
|
||||
>
|
||||
<i class="rd-regionkey__dot" :style="{ background: r.color }" aria-hidden="true" />
|
||||
<span class="rd-regionkey__label">{{ r.label }}</span>
|
||||
<span class="rd-regionkey__count">{{ formatNumber(r.count) }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="isAll" class="rd-regionkey__hint">
|
||||
Pick a region above, or click any dot to open that people group's country.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- RegionDetail.vue:88-141 — the three DOXA goal cards in their .switcher -->
|
||||
|
|
|
|||
|
|
@ -28,14 +28,27 @@
|
|||
import { ref, computed, watch, onMounted, onBeforeUnmount, inject } from 'vue'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
// The region palette comes from the shared strategy, never from a table in here:
|
||||
// check:one-palette exists because a private palette in a consumer is what once made two
|
||||
// surfaces disagree about what a colour meant.
|
||||
import { applyColor as regionColorExpression } from '@map/colors/wagf-region.js'
|
||||
|
||||
const emit = defineEmits(['pick-country'])
|
||||
const props = defineProps({
|
||||
// ISO-3 codes to highlight, e.g. ['IND'] for a country or every code in a region.
|
||||
countryCodes: { type: Array, default: () => [] },
|
||||
// Every people group we know about: { name, slug, code, lng, lat, committed, country, population }.
|
||||
points: { type: Array, default: () => [] },
|
||||
// Colour the focus pins by prayer coverage and show the key (their `color-by-prayer`).
|
||||
colorByPrayer: { type: Boolean, default: false },
|
||||
// HOW to colour the pins. Three modes, and only one of them is ours:
|
||||
// 'prayer' — DX-life's color-by-prayer, what RegionDetail passes
|
||||
// 'in-country' — DX-life's default, what CountryDetail passes (in-country vs surrounding)
|
||||
// 'region' — OURS, and only for the no-selection chooser: paints every pin by its WAGF
|
||||
// region so the seven read AS regions on load. Never used on a cloned page.
|
||||
colorMode: { type: String, default: 'in-country' },
|
||||
// Chooser only: lift one region's pins and recede the rest, on legend hover.
|
||||
highlightRegion: { type: String, default: '' },
|
||||
// Chooser only: a pin click selects, instead of opening their popup.
|
||||
selectOnPinClick: { type: Boolean, default: false },
|
||||
center: { type: Array, default: () => [20, 10] },
|
||||
zoom: { type: Number, default: 2 },
|
||||
// [[west, south], [east, north]] — takes precedence over center/zoom, as theirs does.
|
||||
|
|
@ -78,6 +91,7 @@ const geojson = computed(() => {
|
|||
properties: {
|
||||
name: p.name || '', slug: p.slug || '', country: p.country || '',
|
||||
inCountry: focus.has(p.code), committed: Number(p.committed) || 0,
|
||||
region: p.region || '', code: p.code || '',
|
||||
population: Number(p.population) || 0,
|
||||
},
|
||||
})),
|
||||
|
|
@ -123,20 +137,38 @@ function pinExpressions() {
|
|||
const outsideFocus = ['!', ['get', 'inCountry']]
|
||||
const full = ['>=', ['get', 'committed'], FULL_PRAYER_COVERAGE_COUNT]
|
||||
const some = ['>', ['get', 'committed'], 0]
|
||||
const lit = props.highlightRegion
|
||||
const isLit = ['==', ['get', 'region'], lit]
|
||||
|
||||
if (props.colorMode === 'region') {
|
||||
// OURS, chooser only. Colour from the shared strategy; a hovered region lifts above the rest
|
||||
// and the others recede, which is the cheapest way to say "these pins are one group".
|
||||
return {
|
||||
color: regionColorExpression(),
|
||||
sortKey: lit ? ['case', isLit, 2, 1] : 1,
|
||||
opacity: lit ? ['case', isLit, 1, 0.25] : 0.9,
|
||||
radius: lit
|
||||
? ['case', isLit, ['interpolate', ['linear'], ['zoom'], 2, 4.5, 6, 8], ['interpolate', ['linear'], ['zoom'], 2, 2.5, 6, 5]]
|
||||
: ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 6.5],
|
||||
}
|
||||
}
|
||||
// DX-life's two modes, unchanged (CountryMap.vue:218-223).
|
||||
return {
|
||||
color: props.colorByPrayer
|
||||
color: props.colorMode === 'prayer'
|
||||
? ['case', outsideFocus, C.outside, full, C.prayerFull, some, C.prayerPartial, C.prayerNone]
|
||||
: ['case', ['get', 'inCountry'], C.inCountry, C.muted],
|
||||
sortKey: props.colorByPrayer
|
||||
sortKey: props.colorMode === 'prayer'
|
||||
? ['case', outsideFocus, 0, full, 3, some, 2, 1]
|
||||
: ['case', ['get', 'inCountry'], 1, 0],
|
||||
opacity: ['case', ['get', 'inCountry'], 1, 0.75],
|
||||
radius: ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 6.5],
|
||||
}
|
||||
}
|
||||
|
||||
function addLayers() {
|
||||
const m = map.value
|
||||
if (!m) return
|
||||
const { color: pinColor, sortKey: pinSortKey } = pinExpressions()
|
||||
const { color: pinColor, sortKey: pinSortKey, opacity: pinOpacity, radius: pinRadius } = pinExpressions()
|
||||
|
||||
if (!m.getSource(SRC_COUNTRIES)) {
|
||||
m.addSource(SRC_COUNTRIES, { type: 'vector', url: 'mapbox://mapbox.country-boundaries-v1' })
|
||||
|
|
@ -157,9 +189,9 @@ function addLayers() {
|
|||
id: 'rcm-pins', type: 'circle', source: SRC_PINS,
|
||||
layout: { 'circle-sort-key': pinSortKey },
|
||||
paint: {
|
||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 6.5],
|
||||
'circle-radius': pinRadius,
|
||||
'circle-color': pinColor,
|
||||
'circle-opacity': ['case', ['get', 'inCountry'], 1, 0.75],
|
||||
'circle-opacity': pinOpacity,
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#ffffff',
|
||||
},
|
||||
|
|
@ -174,8 +206,16 @@ function addLayers() {
|
|||
let popup = null
|
||||
function onPinClick(e) {
|
||||
const f = e.features && e.features[0]
|
||||
if (!f) return
|
||||
// CHOOSER: a pin click is a CHOICE, not a popup — the whole point of the state is to pick a
|
||||
// subject. On a cloned page it stays their popup, because that is what their page does.
|
||||
if (props.selectOnPinClick) {
|
||||
const p = f.properties || {}
|
||||
emit('pick-country', { code: p.code || '', region: p.region || '', name: p.name || '' })
|
||||
return
|
||||
}
|
||||
const g = window.mapboxgl
|
||||
if (!f || !g) return
|
||||
if (!g) return
|
||||
const p = f.properties || {}
|
||||
const pop = Number(p.population) || 0
|
||||
const html = '<div style="font:14px/1.45 Poppins,sans-serif;color:#2a332a">' +
|
||||
|
|
@ -194,6 +234,24 @@ function esc(s) {
|
|||
|
||||
// Re-frame and re-source on a new selection instead of tearing the map down: a rebuilt map
|
||||
// re-downloads tiles and flashes, and the subject changes on every tab-1 click.
|
||||
/**
|
||||
* The chooser's zoom has to follow the CONTAINER WIDTH, not be a constant.
|
||||
*
|
||||
* MEASURED: a fixed zoom of 1.25 fills a 1200px desktop frame and shows barely a third of the
|
||||
* world at 390px — Asia fell off the right edge, on the one screen whose job is "pick a region".
|
||||
* Mercator's world is 512*2^z wide, so the zoom that fits it is log2(width/512): ~1.23 at 1200px,
|
||||
* ~-0.39 at 390px. Mapbox will not go below 0, so a phone still crops, but ~24% instead of ~67%,
|
||||
* and the map pans. Clamped to [0, 2] so a very wide embed does not zoom past the useful range.
|
||||
* Only the chooser uses this; a selected page is framed by their own bounds/centre/zoom.
|
||||
*/
|
||||
function chooserZoom(m) {
|
||||
if (props.colorMode !== 'region') return props.zoom
|
||||
let w = 0
|
||||
try { w = m.getContainer()?.clientWidth || 0 } catch (_) { w = 0 }
|
||||
if (!w) return props.zoom
|
||||
return Math.max(0, Math.min(2, Math.log2(w / 512)))
|
||||
}
|
||||
|
||||
function applySubject() {
|
||||
const m = map.value
|
||||
// Deliberately NOT gated on isMapReady: that flag tracks useMapInstance's own view of the
|
||||
|
|
@ -204,15 +262,26 @@ function applySubject() {
|
|||
if (src) src.setData(geojson.value)
|
||||
// Re-apply the MODE, not just the data — see pinExpressions().
|
||||
if (m.getLayer('rcm-pins')) {
|
||||
const { color, sortKey } = pinExpressions()
|
||||
const { color, sortKey, opacity, radius } = pinExpressions()
|
||||
m.setPaintProperty('rcm-pins', 'circle-color', color)
|
||||
m.setPaintProperty('rcm-pins', 'circle-opacity', opacity)
|
||||
m.setPaintProperty('rcm-pins', 'circle-radius', radius)
|
||||
m.setLayoutProperty('rcm-pins', 'circle-sort-key', sortKey)
|
||||
}
|
||||
for (const id of ['rcm-focus-fill', 'rcm-focus-outline']) {
|
||||
if (m.getLayer(id)) m.setFilter(id, countryFilter())
|
||||
}
|
||||
// PROJECTION follows the mode, and only the chooser changes it.
|
||||
// Mapbox v3 defaults to the GLOBE below about zoom 6, so a world-view chooser rendered as a
|
||||
// small sphere floating in a 1200x780 frame — most of the surface empty, the regions tiny.
|
||||
// Mercator fills the frame and reads as a world map, which is what a chooser wants.
|
||||
// The CLONED pages are deliberately left alone: DX-life sets no projection either, so their
|
||||
// region page shows the same globe at the same zoom, and matching them is the whole point.
|
||||
// setProjection is the house pattern for this (doxa-simple-map.vue:1241, all-people-groups,
|
||||
// demo-map, poster-wall all do it).
|
||||
try { if (props.colorMode === 'region') m.setProjection('mercator') } catch (_) {}
|
||||
if (props.bounds) m.fitBounds(props.bounds, { padding: 40, maxZoom: 6, duration: 600 })
|
||||
else m.flyTo({ center: props.center, zoom: props.zoom, duration: 600 })
|
||||
else m.flyTo({ center: props.center, zoom: chooserZoom(m), duration: 600 })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -254,7 +323,7 @@ onMounted(async () => {
|
|||
try {
|
||||
await initializeMap()
|
||||
if (!map.value) { mapFailed.value = true; return }
|
||||
whenStyleReady(map.value, () => { addLayers(); applySubject() })
|
||||
whenStyleReady(map.value, () => { addLayers(); applySubject(); watchResize() })
|
||||
} catch (e) {
|
||||
console.error('[RegionCountryMap] init failed:', e)
|
||||
mapFailed.value = true
|
||||
|
|
@ -265,9 +334,21 @@ onMounted(async () => {
|
|||
// country map rendered fully blank while 'a real GL canvas exists at 1200x780' passed.
|
||||
// Same seam verify:interop reads for the camera (window.__rdashMap).
|
||||
watch(isMapReady, (r) => { if (r && typeof window !== 'undefined') window.__rcmMap = map.value })
|
||||
onBeforeUnmount(() => { if (popup) popup.remove(); try { delete window.__rcmMap } catch (_) {} destroy() })
|
||||
// A rotated phone or a resized pane changes the width the zoom was derived from.
|
||||
let ro = null
|
||||
function watchResize() {
|
||||
if (typeof ResizeObserver === 'undefined' || !mapContainer.value) return
|
||||
ro = new ResizeObserver(() => {
|
||||
const m = map.value
|
||||
if (!m || props.colorMode !== 'region' || props.bounds) return
|
||||
try { m.setZoom(chooserZoom(m)) } catch (_) {}
|
||||
})
|
||||
ro.observe(mapContainer.value)
|
||||
}
|
||||
|
||||
watch(() => [props.countryCodes, props.bounds, props.center, props.zoom, props.points, props.colorByPrayer], applySubject, { deep: true })
|
||||
onBeforeUnmount(() => { if (ro) { try { ro.disconnect() } catch (_) {} } if (popup) popup.remove(); try { delete window.__rcmMap } catch (_) {} destroy() })
|
||||
|
||||
watch(() => [props.countryCodes, props.bounds, props.center, props.zoom, props.points, props.colorMode, props.highlightRegion], applySubject, { deep: true })
|
||||
|
||||
// Their .country-map block, CountryMap.vue's scoped style: 16/9 with a 780px floor, flipping to
|
||||
// 1/2 and no floor under 768px. Ported as an aspect-ratio pair; radius:inherit so the card that
|
||||
|
|
@ -296,7 +377,7 @@ useShadowStyles(
|
|||
The map could not start. This is a map-token or connection problem, not a filter — the
|
||||
figures above are unaffected.
|
||||
</p>
|
||||
<div v-if="colorByPrayer && !mapFailed" class="rcm__legend">
|
||||
<div v-if="colorMode === 'prayer' && !mapFailed" class="rcm__legend">
|
||||
<span><i :style="{ background: C.prayerFull }" />100+ People Praying</span>
|
||||
<span><i :style="{ background: C.prayerPartial }" />1+ People Praying</span>
|
||||
<span><i :style="{ background: C.prayerNone }" />No One Praying</span>
|
||||
|
|
|
|||
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -838,7 +838,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8",
|
||||
"integrity": "sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -858,7 +858,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -872,7 +872,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -886,7 +886,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-TVO7+r7hZgEnCZo1wxlSkoGjh8U0UrQ1St3W9KPAP4jyOxfpO6njV/CQMCf9ptG8" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-gzqCEAoNJjv96WjkGUzVtzvyDAK+vLL7/hX/aCJf5ikDznGXSkbsKYORSBw5GBwG" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue