../../plugin-designs/country-posters/data/countries.json
+// -> ../../data/country-posters.json (library/data — two consumers)
//
// SHEET LADDER (design note 2026-08-01) — the count selects the SHEET SIZE, it does not
// exclude a country:
@@ -23,7 +23,10 @@ import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
-const OUT_DIR = resolve(__dirname, '..', '..', '..', 'plugin-designs', 'country-posters', 'data');
+// Writes to library/data now, not into the country-posters bundle: the regions-dashboard
+// poster dropdown is a SECOND consumer of this dataset, and a generator that still wrote to
+// one plugin's folder would quietly recreate the duplicate this move removed.
+const OUT_DIR = resolve(__dirname, '..', '..', 'data');
const RECORDS = join(__dirname, 'out', '_records.json');
const ADM0 = resolve(__dirname, '..', '..', '..', 'plugin-designs', 'my-upg-100-list', 'data', 'adm0', 'world-adm0.geojson');
@@ -124,7 +127,7 @@ const stats = {
};
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
-writeFileSync(join(OUT_DIR, 'countries.json'), JSON.stringify({ stats, countries }, null, 1));
+writeFileSync(join(OUT_DIR, 'country-posters.json'), JSON.stringify({ stats, countries }, null, 1));
console.log(`[countries] ${stats.countries} countries, ${stats.people_groups} people groups`);
console.log(`[countries] 3x3=${stats.sheet_3x3} 3x4=${stats.sheet_3x4} unsupported=${stats.unsupported}`);
diff --git a/plugin-designs/country-posters/country-poster-wall.vue b/plugin-designs/country-posters/country-poster-wall.vue
index fef3f2c2b..cc6ade0e3 100644
--- a/plugin-designs/country-posters/country-poster-wall.vue
+++ b/plugin-designs/country-posters/country-poster-wall.vue
@@ -37,7 +37,10 @@
*/
import { computed, inject, ref } from 'vue'
import PosterWall from '@map/components/poster/PosterWall.vue'
-import data from './data/countries.json'
+// MOVED to the shared library 2026-09-24: the regions-dashboard poster dropdown is a second
+// consumer of this same dataset, and RULE 2 puts a dataset with a named second consumer in
+// 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'
const profileConfig = inject('profileConfig', ref(null))
diff --git a/plugin-designs/regions-dashboard/regions-list.vue b/plugin-designs/regions-dashboard/regions-list.vue
index c05b54179..bc0b9143a 100644
--- a/plugin-designs/regions-dashboard/regions-list.vue
+++ b/plugin-designs/regions-dashboard/regions-list.vue
@@ -40,6 +40,20 @@ import { RTL_LOCALES } from '@map/i18n/index.js'
import { useI18n } from 'vue-i18n'
import { useDocumentFont, BEBAS_NEUE_FACES, POPPINS_FACES } from '@map/composables/useDocumentFont.js'
import { summarizeRegions, slugify, emptyStats, toNumber, PEOPLE_GROUP_LIST_FIELDS } from '@map/utils/countries-meta.js'
+// THE COUNTRY-POSTER DROPDOWN (Driver 2026-09-24) — the THIRD sanctioned addition, after the
+// SELECT VIEW tab strip and the no-selection chooser. Their wording: "a toggle or carrot added
+// to the bottom of each region section that says view country posters".
+//
+// NOTHING HERE IS REBUILT. PosterWall is already the shared, fully parameterised grid that
+// my-upg-100-list/poster-wall.vue and country-posters/country-poster-wall.vue both render, and
+// it already owns the card layout, the "Show on map" header button, the Print Options footer
+// and the client-side raster. This file supplies only data and a toggle — the same division of
+// labour country-poster-wall.vue states in its own header ("this profile supplies only its
+// data and taxonomy").
+import PosterWall from '@map/components/poster/PosterWall.vue'
+import posterData from '@map/data/country-posters.json'
+import { resolveAssetsBase } from '@map/utils/embedBase.js'
+import { onBeforeUnmount } from 'vue'
const props = defineProps({
profileConfig: { type: String, default: '' },
@@ -180,6 +194,74 @@ async function loadRegions() {
}
}
+// ─── COUNTRY POSTERS, per region ─────────────────────────────────────────────────────────
+// One wall per region, opened by the footer toggle. instanceId is suffixed with the region
+// slug because PosterWall scopes its CustomEvent by instanceId so several embeds on one page
+// never cross-talk (PosterWall.vue:40) — and here there genuinely are several.
+const POSTER_EVENT = 'doxa-regions:show-country-on-map'
+// Read off the injected config the same way every other value in this file is (see the note
+// above cfg) — a bare default here would make two embeds on one page answer each other's
+// poster events.
+const instanceId = computed(() => cfg.value.instanceId || 'regions-list')
+const posterAssetsBase = computed(() => resolveAssetsBase(cfg.value))
+const openPosters = ref(new Set())
+
+function togglePosters(region) {
+ const next = new Set(openPosters.value)
+ next.has(region.slug) ? next.delete(region.slug) : next.add(region.slug)
+ openPosters.value = next // a new Set, so the computed template actually re-renders
+}
+const postersOpen = (region) => openPosters.value.has(region.slug)
+
+// The poster dataset is keyed by ISO-3 (`iso3`); the regions rollup keys by `code`. Same
+// codes, so the join is direct.
+const POSTER_BY_ISO3 = new Map((posterData.countries || []).map(c => [String(c.iso3 || '').toUpperCase(), c]))
+
+// Every country in the region gets a card, in PosterWall's own shape. A country with no
+// generated poster is NOT dropped — it is passed with a reason, which is what the wall's
+// `unsupported` slot is for, so the grid never silently loses a country.
+function postersFor(region) {
+ return (region.countries || []).map((c) => {
+ const rec = POSTER_BY_ISO3.get(String(c.code || '').toUpperCase())
+ if (rec) return rec
+ return {
+ id: `country-${String(c.code || '').toLowerCase()}`,
+ iso3: c.code,
+ label: c.name,
+ count: c.count,
+ unsupportedReason: t('No poster has been generated for this country yet.'),
+ }
+ })
+}
+
+// Their own not-yet reason travels WITH the record, so the number the wall prints can never
+// drift from the data — country-poster-wall.vue:63 does exactly this. India is the only
+// country in the dataset that carries one (855 people groups, no sheet); China is fine at 3x4.
+const posterUnsupported = (c) => c.unsupportedReason || null
+
+// One region's wall is already inside a region section, so it needs no second level of
+// collapsing and no grouping: everything in it is that one region.
+const posterGroupBy = () => 'countries'
+
+// PosterWall's "Show on map" dispatches a window CustomEvent carrying listId (the poster id,
+// e.g. "country-chn") or null when the user toggles the same card off. Mapping that id back to
+// the country record and reusing selectCountry() is what makes the button open the dashboard
+// tab for that country — the Driver's "click view on map button, which will open the country
+// dashboard page for that country".
+function onPosterShowOnMap(ev) {
+ const d = ev?.detail || {}
+ if (!String(d.instanceId || '').startsWith(instanceId.value)) return
+ if (!d.listId) return // a deselect is not a navigation
+ const iso3 = String(d.listId).replace(/^country-/, '').toUpperCase()
+ for (const region of regions.value) {
+ const hit = (region.countries || []).find(c => String(c.code || '').toUpperCase() === iso3)
+ if (hit) { selectCountry(hit, region); return }
+ }
+}
+
+onMounted(() => { if (typeof window !== 'undefined') window.addEventListener(POSTER_EVENT, onPosterShowOnMap) })
+onBeforeUnmount(() => { if (typeof window !== 'undefined') window.removeEventListener(POSTER_EVENT, onPosterShowOnMap) })
+
onMounted(loadRegions)
// Design values are a DIRECT PORT of DX-life's live /regions page (root-dx-life/doxa-life —
@@ -312,6 +394,11 @@ useShadowStyles(
// NO margin-bottom. This element is a ; its user-agent default margin-block-end (~19.5312px at
// this font-size) was leaking through because only margin-top was ever set -- pin margin:0
// first, then apply margin-top, so the bottom UA default can't survive the cascade.
+ // The toggle row and the wall it opens. The caret rotates rather than swapping glyphs,
+ // which is what makes open/closed legible at a glance.
+ '.region-card__posters-toggle{ margin-top:0; }' +
+ '.region-card__icon--open{ transform:rotate(180deg); }' +
+ '.region-card__posters{ margin-top:13.3px; }' +
'.region-card__footer{ display:flex; justify-content:flex-end; margin:0; margin-top:23.5264px;' +
' padding:0 clamp(17.689px, 4cqw, 41.6158px) 23.5264px; }' +
'.region-card__footer-link{ appearance:none; background:none; border:0; padding:0; margin:0;' +
@@ -384,6 +471,56 @@ useShadowStyles(
+
+
+
+
+
+