From 6c9b844b8ee5560dc8d6cbd2a72bcacdfa2e1481 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Wed, 30 Sep 2026 12:17:39 +0000 Subject: [PATCH] Country Posters: the wall shows a host's countries, or one region, flat, on request profile-config gains two optional keys for the country-poster-wall profile: `countries` (ISO-3 codes, or {code, name, count} records, in the host's order) and `region` (one WAGF region as the data names it). With either, the wall is flat: one section, nothing collapsed, two columns unless `columns` says otherwise, no heading unless `heading` is given, because it sits under the host's own region heading. A requested country with no poster keeps its card and says so in the regions catalogue's words. With neither key, every binding is what it was, so the plain wall is unchanged. PosterWall itself is untouched; only the country-posters bundle rebuilds. The usage lines in index.js and the profile's meta note name the keys. --- .../country-poster-wall.meta.json | 2 +- .../country-posters/country-poster-wall.vue | 62 ++++++++++++++++--- plugin-designs/country-posters/index.js | 3 + 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/plugin-designs/country-posters/country-poster-wall.meta.json b/plugin-designs/country-posters/country-poster-wall.meta.json index ae9b999f4..94a3b2e0a 100644 --- a/plugin-designs/country-posters/country-poster-wall.meta.json +++ b/plugin-designs/country-posters/country-poster-wall.meta.json @@ -1,5 +1,5 @@ { "type": "single", "label": "Country Posters — poster wall", - "note": "The printable poster wall pane of the Country Posters element (country-posters-mfe composes it); also embeddable alone." + "note": "The printable poster wall pane of the Country Posters element (country-posters-mfe composes it); also embeddable alone. profile-config `countries` (ISO-3 list or {code,name,count} records) or `region` (a WAGF region label) shows only those, flat, under a host's own region heading (DX Life's /regions, 2026-09-30); without them the wall is unchanged." } diff --git a/plugin-designs/country-posters/country-poster-wall.vue b/plugin-designs/country-posters/country-poster-wall.vue index 4ce2ec9b3..ca1a6190f 100644 --- a/plugin-designs/country-posters/country-poster-wall.vue +++ b/plugin-designs/country-posters/country-poster-wall.vue @@ -48,6 +48,9 @@ import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js' // library/. Generated by library/scripts/posters/emit-countries.mjs, which now writes there. import data from '@map/data/country-posters.json' import { resolveAssetsBase } from '@map/utils/embedBase.js' +// The regions catalogue holds the not-yet sentence a host's country gets when no poster exists (all eleven locales); +// its keys are English sentences with dots, so it is read by literal key, the way regions-list.vue reads it. +import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js' const profileConfig = inject('profileConfig', ref(null)) @@ -62,10 +65,49 @@ const assetsBase = computed(() => resolveAssetsBase(profileConfig.value)) // The i18n locale REF is read so the list is recomputed on a runtime language switch. const { locale: wallLocale } = useOptionalI18n('') const byId = new Map((data.countries || []).map((c) => [c.id, c])) + +// ★ A HOST MAY ASK FOR SOME COUNTRIES, OR ONE REGION (2026-09-30). DX Life keeps its own /regions +// page and wants "View country posters" under each region card there, and a host page can only speak +// profile-config. So the profile takes, from profile-config, the filter regions-list.vue builds in code: +// countries the countries to show, in the host's order: ISO-3 codes ("IND,PAK" or ["IND","PAK"]) or the +// host's own records ({ code, name, count }). A code with no poster is NOT dropped: it gets the +// same not-yet reason regions-list.vue gives one (its postersFor), so no country silently +// disappears from a region. +// region one WAGF region as the data names it ("Asia"): every poster in that region. +// Either makes the wall FLAT — one section, nothing collapsed, two columns unless the host says `columns`, no +// heading, tag or intro unless the host passes `heading` — because it already sits under the host's own region +// heading. With neither key every binding below is what it was, so the wall is unchanged. +const requested = computed(() => { + const v = profileConfig.value?.countries + const list = Array.isArray(v) ? v : String(v || '').split(/[\s,]+/) + return list + .map((x) => (x && typeof x === 'object' ? { ...x, code: String(x.code || x.iso3 || '').toUpperCase() } : { code: String(x || '').toUpperCase() })) + .filter((x) => x.code) +}) +const requestedRegion = computed(() => String(profileConfig.value?.region || '').trim()) +const filtered = computed(() => requested.value.length > 0 || requestedRegion.value !== '') +const byIso3 = new Map((data.countries || []).map((c) => [String(c.iso3 || '').toUpperCase(), c])) +const selected = computed(() => { + const all = data.countries || [] + if (requested.value.length) { + return requested.value.map((r) => byIso3.get(r.code) || { + id: `country-${r.code.toLowerCase()}`, iso3: r.code, label: r.name || r.label || r.code, count: Number(r.count) || 0, noPoster: true, + }) + } + if (requestedRegion.value) return all.filter((c) => (c.region || 'Other') === requestedRegion.value) + return all +}) +const flatGroup = () => 'countries' +const neverCollapsed = () => false +const hostHeading = computed(() => String(profileConfig.value?.heading || '')) +const hostColumns = computed(() => Number(profileConfig.value?.columns) || 2) +let tRegions = (key) => key +try { tRegions = useRegionsI18n().t } catch (_) { /* a host with no i18n reads the English sentence, like every string here */ } + const items = computed(() => { const l = wallLocale && (wallLocale.value ?? wallLocale) const locale = (typeof l === 'string' && l) || getRuntimeLocale() || detectLocale() || 'en' - const countries = data.countries || [] + const countries = selected.value if (/^en\b/i.test(locale)) return countries return countries.map((c) => { const name = countryName(c.iso3, locale) @@ -137,7 +179,8 @@ const collapsedBy = (sec) => sec.total < 60 // The reason is shown on the card and in the print HUD, so it speaks the reader's language: the data's // unsupportedReason is an English note (and a flag), not display text. The card already states the // count above it, so the sentence needs no number and no plural agreement. -const unsupported = (c) => (c.unsupportedReason ? t('messages.countryPosterTooMany') : null) +// A requested country with no poster at all says so in regions-list.vue's words (the regions catalogue). +const unsupported = (c) => (c.noPoster ? tRegions('No poster has been generated for this country yet.') : c.unsupportedReason ? t('messages.countryPosterTooMany') : null) // ★ THROUGH THE CATALOGUE, like the UPG-100 wall: the heading, the tag, the intro and the counts read // in the page's language. The intro keeps its one emphasis as three plain keys (A, the emphasis, B) @@ -152,14 +195,14 @@ const soonMessage = computed(() => t('messages.countryWallSoon'))