usePosterLayout has always computed a real layout: the map narrowed by the legend column and inset by the margins, the title strip and the footer strip. Nothing in the exporter ever read it. The map was rastered at full page size -- edge to edge, across the margins and both bands -- and the title, legend and footer were then composited on top of that image. The legend lay over the map it describes, and there were no bands at all. What kept it hidden is that PosterPreview.vue DOES honour layout.map, so the on-screen preview has always shown the intended design and only the exported file was wrong. A preview is not evidence for an export. The obvious fix breaks a different product. country-posters embeds this renderer's output as a full-sheet <img> inside its own packet page, beneath the packet's own title, so chrome that is on by default prints a second header and footer within their page. UPG-100's packet does not call this renderer at all and was never at risk -- checked rather than assumed. So the composed sheet is requested with spec.layout === 'poster', and every existing caller, which sets none, keeps what it had. That guarantee is stronger than "equivalent": with no layout the renderer passes the SAME px OBJECT it always did to the rasteriser and composites no sheet, so the untouched path is unchanged by identity, not merely by value. The check asserts exactly that, by executing the decision with four real caller shapes -- including country-posters' A4 at 150 DPI -- rather than reading the source for a promise. Its second negative test is the ruling's own failure mode: make defaultPosterSpec set the layout and country-posters is named as silently opted in. A sheet with no room left for a map after its margins, bands and legend column falls back to full-bleed and says so, because silence there is the bug above. Simple Research's export asks for the layout explicitly. |
||
|---|---|---|
| .. | ||
| api | ||
| charts | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| data | ||
| elements | ||
| i18n | ||
| layouts | ||
| lists | ||
| maps | ||
| packet | ||
| packets | ||
| scripts | ||
| stores | ||
| styles | ||
| utils | ||
| vendor/cldr | ||
| index.js | ||
| ProfileLoader.vue | ||
| README.md | ||
library/ (@map) — the shared map template
The other half of the architecture (see ../plugin-designs/README.md):
every map in plugin-designs/<your-map>/ composes the reusable, parameterized pieces that live here.
Fix something here and every map benefits on the next build; break something here and every map
breaks — read components/WARNING.md before touching shared pieces.
| Folder | What lives there |
|---|---|
ProfileLoader.vue |
The root of every bundle: parses profile-config, loads the profile, provides the stores + config |
components/ |
Shared UI — map, legends, controls, popup (PeopleGroupDetail), tables, charts, hints (README) |
composables/ |
Shared logic — map engine (useMapInstance), provider switch, shadow-DOM styles, theming |
stores/ |
Per-instance Pinia stores (ui / map / data) — provided by ProfileLoader, injected by children |
api/ |
Data-source registry (sources.json) + fetch layer — the JAMstack runtime wiring |
colors/ |
Color sets maps opt into via profile-config.colorSet |
i18n/ |
The shared string catalogs (all maps speak the same languages) |
data/ |
Small shared datasets baked at build time (big served data lives in ../data/) |
constants/ |
Shared constants |
scripts/ |
Maintainer pipelines (posters, research) — not part of any bundle |
The rule of thumb (same as WARNING.md): custom code in your app-profile, reusable code here — and things only move from the first to the second once they've earned it.