UPG Neighbor: a neighbor's dot and its card point at each other

A neighbor dot showed a pointer and did nothing on click. Clicking a dot now picks
and scrolls to its card; clicking a card (or Enter on it) flies to the dot and rings
it, so the map and the brief agree on which neighbor you are looking at.
This commit is contained in:
Doxa Maps 2026-09-27 08:52:50 +00:00
parent 33e8ed6c4b
commit 3a1aa55a89

View file

@ -50,6 +50,23 @@ const byLevel = computed(() => [1, 2, 3].map((e) => ({ e, list: neighbors.value.
const believersNear = computed(() => neighbors.value.reduce((a, n) => a + n.g.believers, 0))
function select(g) { if (g && !g.reached) { nbState.pgid = g.pgid; query.value = '' } }
// The neighbor in focus: a dot click picks its card, a card click flies to its dot. Both ways, so the
// map and the brief never disagree about which neighbor you are looking at.
const activeId = ref(null)
watch(() => nbState.pgid, () => { activeId.value = null })
function focusNeighbor(n, fly = true) {
activeId.value = n.g.pgid
applyActive()
const m = map.value
if (fly && m) { try { m.stop(); m.easeTo({ center: [n.g.lng, n.g.lat], zoom: Math.max(m.getZoom(), 5), duration: 700 }) } catch (_) {} }
const card = mapContainer.value?.getRootNode?.()?.querySelector?.(`[data-nb="${n.g.pgid}"]`)
card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' })
}
function applyActive() {
const m = map.value
if (m?.getLayer?.('nb-a')) m.setFilter('nb-a', ['==', ['get', 'id'], activeId.value || ''])
}
function clearSelection() { nbState.pgid = null }
// ─── Search / default list ───────────────────────────────────────────────────
@ -115,9 +132,12 @@ function ensureLayers(m) {
if (!m.getLayer('nb-n')) m.addLayer({ id: 'nb-n', type: 'circle', source: SRC_N, paint: {
'circle-color': ['get', 'color'], 'circle-radius': ['match', ['get', 'e'], 1, 7, 2, 6, 5],
'circle-stroke-width': 1.5, 'circle-stroke-color': '#fff' } })
if (!m.getLayer('nb-a')) m.addLayer({ id: 'nb-a', type: 'circle', source: SRC_N, filter: ['==', ['get', 'id'], ''], paint: {
'circle-color': 'rgba(0,0,0,0)', 'circle-radius': 11, 'circle-stroke-width': 3, 'circle-stroke-color': SELECTED } })
if (!m.getLayer('nb-s')) m.addLayer({ id: 'nb-s', type: 'circle', source: SRC_S, paint: {
'circle-color': UNREACHED, 'circle-radius': 10, 'circle-stroke-width': 3, 'circle-stroke-color': SELECTED } })
applyDim(m)
applyActive()
}
function applyDim(m) {
if (m.getLayer('nb-u')) m.setPaintProperty('nb-u', 'circle-opacity', selected.value ? 0.18 : 0.55)
@ -162,6 +182,12 @@ onMounted(async () => {
const g = data.value?.unreached.find((u) => u.pgid === id)
if (g) select(g)
})
// A neighbor dot had a pointer cursor and no click handler; it now picks its card.
m.on('click', 'nb-n', (ev) => {
const id = ev.features?.[0]?.properties?.id
const n = neighbors.value.find((x) => x.g.pgid === id)
if (n) focusNeighbor(n, false)
})
for (const id of ['nb-u', 'nb-n']) {
m.on('mouseenter', id, () => { m.getCanvas().style.cursor = 'pointer' })
m.on('mouseleave', id, () => { m.getCanvas().style.cursor = '' })
@ -207,6 +233,9 @@ useShadowStyles(`
.mb-level p { margin:0 0 6px; color:#6b7280; font-size:12px; line-height:1.35; }
.mb-dot { width:11px; height:11px; border-radius:50%; flex:0 0 auto; }
.mb-card { border:1px solid #e5e7eb; border-radius:8px; padding:8px 10px; margin-bottom:6px; font-size:12px; line-height:1.35; }
.mb-card { cursor:pointer; }
.mb-card:hover, .mb-card:focus-visible { border-color:#9fb3d9; }
.mb-card.on { border-color:${SELECTED}; box-shadow:0 0 0 2px ${SELECTED}55; }
.mb-card b { font-size:13px; }
.mb-card .muted { color:#6b7280; }
.mb-same { font:700 10px/1 inherit; color:#7a3e00; background:#fdeedd; border-radius:999px; padding:3px 6px; margin-left:4px; }
@ -285,7 +314,7 @@ useShadowStyles(`
<section v-for="lv in byLevel" :key="lv.e" class="mb-level">
<h3><span class="mb-dot" :style="{ background: E_COLORS[lv.e] }"></span>{{ $t(E_TITLE[lv.e]) }}</h3>
<p>{{ $t(E_LONG[lv.e]) }}</p>
<div v-for="n in lv.list" :key="n.g.pgid" class="mb-card">
<div v-for="n in lv.list" :key="n.g.pgid" class="mb-card" :class="{ on: activeId === n.g.pgid }" :data-nb="n.g.pgid" role="button" tabindex="0" @click="focusNeighbor(n)" @keydown.enter="focusNeighbor(n)">
<b>{{ n.g.name }}</b><span v-if="n.same" class="mb-same">{{ $t('neighbor.mob.samePeople') }}</span><br>
{{ n.g.country }} · {{ n.g.langName }}<br>
<span class="muted">{{ $t('neighbor.mob.cardStats', { b: fmtNum(n.g.believers), km: n.km.toLocaleString() }) }}</span>