From 3ad1a8fc379deebdd4f7f6f3ac2ee64c5773017c Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 09:26:21 +0000 Subject: [PATCH] UPG Neighbor on a phone: the attribution sits above the sheet, a pick raises the sheet, and the hover tip stays on screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rega at 390 (b07fc59e0, en + ar, both views): the sheet lowers and raises as it should, but - the map's attribution control sat under the sheet, lowered or raised, in all four cases. On a phone the map's bottom control corners now sit above the sheet: 55% + 6 px when it is up, 62 px when it is down; - a pick with the list lowered moved the map but showed no detail, because the detail is in the sheet. A pick (a group, a neighbour dot, a bridge language, a group opened from Campaigns) now raises the sheet FIRST, so the frame is computed for the sheet that shows the detail; - the hover tip was cut off at the right edge in ar ("Ko…"): it opened to the right of the point wherever the point was, and after a tap it stayed where the tap had been while the map flew to the pick. It now opens toward the side with more room, is at most 45% of the map wide, and clears when the map moves. --- plugin-designs/upg-neighbor/src/campaigns.vue | 30 +++++++++++++++---- plugin-designs/upg-neighbor/src/mobilize.vue | 28 +++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/plugin-designs/upg-neighbor/src/campaigns.vue b/plugin-designs/upg-neighbor/src/campaigns.vue index 6bd3aa62d..27eeddd7c 100644 --- a/plugin-designs/upg-neighbor/src/campaigns.vue +++ b/plugin-designs/upg-neighbor/src/campaigns.vue @@ -47,9 +47,10 @@ const list = computed(() => { }) const active = computed(() => data.value?.bridges.find((b) => b.lang === nbState.lang) || null) -function pick(b) { nbState.lang = b.lang } +// A pick raises the phone sheet first, so its detail is in view and the frame is computed for it. +function pick(b) { nbState.sheetDown = false; nbState.lang = b.lang } function back() { nbState.lang = null } -function openGroup(u) { nbState.pgid = u.pgid; nbState.view = 'mobilize' } +function openGroup(u) { nbState.sheetDown = false; nbState.pgid = u.pgid; nbState.view = 'mobilize' } // Where to run the campaign: believers per country, largest first. const audience = computed(() => { @@ -93,6 +94,15 @@ const fc = (features) => ({ type: 'FeatureCollection', features }) // Hover label: after a language is picked, its believers (green) and the unreached they reach (red) // were anonymous until clicked, and a green dot could not be clicked at all. Same label as Mobilize. const hover = ref(null) // { x, y, text } +// The tip opens toward the side with more room, so near an edge it is never cut off (rega saw "Ko…" +// at the right edge in ar); max-width keeps it inside half the map on a phone. A tip is stale as soon as +// the map moves (a tap, then the fly to the pick), so moving clears it. +const hoverStyle = computed(() => { + const h = hover.value + if (!h) return {} + const w = mapContainer.value?.clientWidth || 0 + return h.x > w / 2 ? { right: (w - h.x + 12) + 'px', top: h.y + 12 + 'px' } : { left: h.x + 12 + 'px', top: h.y + 12 + 'px' } +}) const pt = (g) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [g.lng, g.lat] }, properties: { id: g.pgid, pop: g.pop, b: g.believers } }) function setSrc(m, id, d) { const s = m.getSource(id); if (s) s.setData(d); else m.addSource(id, { type: 'geojson', data: d }) } let repaintPending = false @@ -161,6 +171,7 @@ onMounted(async () => { const b = data.value?.bridges.find((x) => x.lang === ev.features?.[0]?.properties?.lang) if (b) pick(b) }) + m.on('movestart', () => { hover.value = null }) for (const id of ['cp-u', 'cp-r', 'cp-b']) { m.on('mouseenter', id, () => { m.getCanvas().style.cursor = 'pointer' }) m.on('mouseleave', id, () => { m.getCanvas().style.cursor = '' }) @@ -217,22 +228,29 @@ useShadowStyles(` .cp-line { display:flex; justify-content:space-between; gap:8px; font-size:12.5px; padding:5px 0; border-bottom:1px solid #f1f3f6; } .cp-line button { border:0; background:none; padding:0; font:inherit; color:${NAVY}; cursor:pointer; text-align:start; } .cp-note { padding:14px; color:#6b7280; font-size:13px; line-height:1.45; } - .cp-hover { position:absolute; z-index:25; pointer-events:none; background:rgba(0,27,68,.92); color:#fff; font-size:12px; line-height:1.3; padding:5px 8px; border-radius:6px; max-width:260px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + .cp-hover { position:absolute; z-index:25; pointer-events:none; background:rgba(0,27,68,.92); color:#fff; font-size:12px; line-height:1.3; padding:5px 8px; border-radius:6px; max-width:min(260px, 45%); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .cp-maperr { position:absolute; inset-inline-end:12px; top:12px; z-index:15; background:#fff; border:1px solid #d9dee6; border-radius:8px; padding:8px 12px; font-size:13px; color:#6b7280; } .cp-handle { display:none; align-items:center; justify-content:center; gap:8px; width:100%; min-height:40px; border:0; border-bottom:1px solid #eef0f4; background:#fff; color:${NAVY}; font:600 12px/1 inherit; cursor:pointer; flex:0 0 auto; } .cp-handle::before { content:''; width:36px; height:4px; border-radius:2px; background:#c9d1dd; } .cp-handle:focus-visible { outline:2px solid ${NAVY}; outline-offset:-2px; } @media (max-width:760px) { .cp-panel { top:auto; inset-inline:6px; bottom:6px; width:auto; height:55%; } - .cp-panel.down { height:56px; } .cp-panel.down > :not(.cp-handle) { display:none; } .cp-handle { display:flex; } } + .cp-panel.down { height:56px; } .cp-panel.down > :not(.cp-handle) { display:none; } .cp-handle { display:flex; } + /* The map's own bottom controls (the attribution) sit ABOVE the sheet, never under it: + 55% of the height when the sheet is up, a 56 px bar when it is down, both 6 px off the bottom. */ + .cp-root .maplibregl-ctrl-bottom-left, .cp-root .maplibregl-ctrl-bottom-right, + .cp-root .mapboxgl-ctrl-bottom-left, .cp-root .mapboxgl-ctrl-bottom-right { bottom:calc(55% + 6px); } + .cp-root.sheet-down .maplibregl-ctrl-bottom-left, .cp-root.sheet-down .maplibregl-ctrl-bottom-right, + .cp-root.sheet-down .mapboxgl-ctrl-bottom-left, .cp-root.sheet-down .mapboxgl-ctrl-bottom-right { bottom:62px; } + } `, 'upgn-campaigns')