From 10c33affbf2a34ab471bd5e3a14ff26023c56989 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 08:17:59 +0000 Subject: [PATCH] Regions: on a phone the three view tabs scroll between the corner buttons, and a poster card's Show on map opens that country's dashboard --- .../regions-dashboard/regions-mfe.vue | 34 ++- public/dx-maps/doxa-maps-spa/manifest.json | 14 +- public/dx-maps/manifest.json | 14 +- .../country-poster-sections/index.html | 2 +- .../region-dashboards/index.html | 2 +- .../regions-dashboard/regions-dashboard.js | 242 +++++++++--------- .../regions-dashboard/regions-list/index.html | 2 +- .../regions-dashboard/regions-mfe/index.html | 2 +- 8 files changed, 171 insertions(+), 141 deletions(-) diff --git a/plugin-designs/regions-dashboard/regions-mfe.vue b/plugin-designs/regions-dashboard/regions-mfe.vue index 8e56440be..09c5ee279 100644 --- a/plugin-designs/regions-dashboard/regions-mfe.vue +++ b/plugin-designs/regions-dashboard/regions-mfe.vue @@ -24,7 +24,8 @@ * listens for, and scrolls to tab 2. That is the same seam upg-100-mfe uses for the poster * wall's 'show-on-map', and it keeps each pane under its own ProfileLoader/Pinia. */ -import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue' +import { ref, computed, inject, onMounted, onBeforeUnmount, watch, nextTick } from 'vue' +import { countrySlug } from '@map/utils/countries-meta.js' import { usePaneConfig } from '@map/composables/usePaneConfig.js' import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pane-wrapper.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js' @@ -151,6 +152,13 @@ function go(id) { column?.scrollTo(id) } +// On a phone the three tabs scroll sideways (see the 600px rule below): keep the current one in view. +const hdr = ref(null) +watch(view, () => nextTick(() => { + const b = hdr.value && hdr.value.querySelector('.seg button[aria-pressed="true"]') + if (b && b.scrollIntoView) b.scrollIntoView({ inline: 'center', block: 'nearest' }) +})) + // ── tab 1 -> tab 2: forward the selection, then swap the visible pane ─────── const SELECT_EVENT = 'doxa-regions-dashboard:select' @@ -174,6 +182,18 @@ function onCountrySelected(e) { forwardSelection({ regionSlug: d.region?.slug || '', countrySlug: d.slug || '' }) } +// A card's "Show on map" in the third view (Country Posters) opens that country's dashboard, the same move the +// Regions list makes for its own posters (regions-list.vue onPosterShowOnMap). The library's poster sections name +// themselves 'poster-section-country-' (PosterSectionView.vue), and this view is the only place in the +// Regions element that places them; a deselect (listId null) is not a navigation. +const SECTION_EVENT = 'doxa-country-posters:show-on-map' +function onSectionShowOnMap(e) { + const d = e?.detail || {} + if (!String(d.instanceId || '').startsWith('poster-section-country-') || !d.listId) return + const iso3 = String(d.listId).replace(/^country-/, '').toUpperCase() + if (iso3) forwardSelection({ regionSlug: '', countrySlug: countrySlug(iso3) }) +} + onMounted(() => { if (!stage.value) return column = buildPaneColumn(spec.value, { host: stage.value }) @@ -194,11 +214,13 @@ onMounted(() => { // rather than on window keeps two Regions embeds on one page from hearing each other. stage.value.addEventListener('region-selected', onRegionSelected) stage.value.addEventListener('country-selected', onCountrySelected) + window.addEventListener(SECTION_EVENT, onSectionShowOnMap) }) onBeforeUnmount(() => { stage.value?.removeEventListener('region-selected', onRegionSelected) stage.value?.removeEventListener('country-selected', onCountrySelected) + window.removeEventListener(SECTION_EVENT, onSectionShowOnMap) offScroll?.() column?.destroy() column = null @@ -239,6 +261,14 @@ useShadowStyles(SHADOW_BOX_SIZING_RESET + PANE_GRID_STYLES + '\n' + // ON A PHONE the centred view switcher (about 290 px at 390) leaves no room for two buttons at one end, so the // ⓘ takes the header's START edge, where the floating ⓘ sat before; Share keeps the end. '@media (max-width: 600px){ .rmfe .hdr .hdr-btns > :first-child{ position:absolute; inset-inline-start:12px; top:50%; transform:translateY(-50%); } }' + + // THREE VIEWS DO NOT FIT A PHONE'S HEADER (measured at 390: the tabs need about 390 px and ran under the corner + // buttons). The switcher keeps the room between the two 36 px buttons (12 + 36 + 8 on each side) and its tab row + // scrolls sideways, every label whole; the current tab is scrolled into view on each change. A fieldset is + // min-content wide by default, hence min-width:0. Desktop is unchanged. + '@media (max-width: 600px){ .rmfe .hdr .viewsel{ min-width:0; max-width:calc(100% - 112px); }' + + ' .rmfe .hdr .viewsel .seg{ overflow-x:auto; scrollbar-width:none; scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }' + + ' .rmfe .hdr .viewsel .seg::-webkit-scrollbar{ display:none; }' + + ' .rmfe .hdr .viewsel .seg button{ flex:0 0 auto; scroll-snap-align:center; } }' + // Stage: the pane-wrapper column mounts here (its .pane-col is absolute). '.rmfe .stage{ position:relative; flex:1 1 auto; min-height:0; }' + '.pane > .pane-mount > *{ position:absolute; inset:0; width:100%; height:100%; display:block; }', @@ -248,7 +278,7 @@ useShadowStyles(SHADOW_BOX_SIZING_RESET + PANE_GRID_STYLES + '\n' +