Completes the clone. Two things were missing: the map both their pages render, and the fact that a COUNTRY is a different page from a region. THE MAP — plugin-designs/regions-dashboard/src/RegionCountryMap.vue. Layers are DX-life's, expression for expression (CountryMap.vue:186-240): the country-boundaries fill at 0.12, the 2px outline, the single-worldview filter that stops the highlight drawing three times, the prayer / in-country colour cases, the circle-sort-key that puts better-covered pins on top, the zoom-interpolated radius, 0.75 opacity on neighbours, their seven colours. The machinery is ours: theirs does `new mapboxgl.Map` in a Nuxt page, which cannot work in a shadow root — container-by-id does not cross the boundary, the async CSS link gives a first-initialising map a 0-height canvas, and the wordmark duplicates per instance. @map/composables/useMapInstance.js already solves all three, so this supplies only the layers. Points come down from the parent instead of a second fetch, so the pins and the figures above them provably describe the same rows. THE COUNTRY PAGE — CountryDetail.vue is head + map note + map + the people-group list, with NO stat cards and no goal cards. It now renders that, not the region page wearing a country's numbers. Its map passes no color-by-prayer, so pins read in-country vs surrounding, which is what its own note tells you to look for. The list uses @map/components/tables/DataTable.vue rather than a third copy of their 662-line UupgsList — this repo already ships that as people-groups-grid, and nesting the real element is a cap-4 composition question. Four defects found by measuring, not by reading: 1. A CANVAS AT THE RIGHT SIZE IS NOT A MAP. With a <template> branch per subject kind, region -> country unmounted one map and mounted another into a zero-size container: 1200x780 canvas, correct centre and zoom, layers [], nothing drawn, a blank white card — and every assertion passed. One instance driven by props now, and the suite asserts queryRenderedFeatures (934 pins / 51 outline for Africa, 61 / 5 for Cameroon) instead of canvas dimensions. 2. isStyleLoaded() IS THE WRONG GATE for adding layers. It also waits for the style's sources, which never arrive at zoom 6 on this box because the local token is URL-restricted and its high-zoom tiles 403. Same code drew a region and drew nothing for a country. Keyed on 'style.load' — the style being PARSED is what makes addSource/addLayer legal — with a parsed-style fallback. 3. THE PIN MODE WAS FROZEN at whatever it was when the layer was created, so a country page painted prayer colours. Caught in a screenshot by one yellow "1+ praying" pin; invisible to every assertion, because the pins were rendered and the legend was correctly absent. applySubject now re-applies circle-color and circle-sort-key. 4. THE PROFILE CONFIG ARRIVES BY INJECT, NOT AS A PROP. The element this bundle registers IS ProfileLoader, which parses the profile-config attribute and provides the parsed object (ProfileLoader.vue:195). Both tabs read it off their own string prop, which is always empty — prayBaseUrl, languageCode, instanceId and assetsBase all silently defaulted. Nothing looked broken until assetsBase mattered: the vendored Tabulator 404'd twice with no error and the country list just never drew. Fixed in both tabs, prop kept as a fallback. Token delivery needs no wiring: the generator detects a map under the bundle and bakes the ladder itself, so all three leaves now carry it and check:nested-token passes. |
||
|---|---|---|
| .. | ||
| all-people-groups | ||
| big-picture | ||
| cities-internet | ||
| country-posters | ||
| dashboard-plus-grid | ||
| demo-map | ||
| doxa-maps-spa | ||
| doxa-research-map | ||
| doxa-simple-map | ||
| doxa-vision-card | ||
| golden-path-check | ||
| gospel-resources | ||
| key-cities | ||
| my-upg-100-list | ||
| people-groups-grid | ||
| plugin-studio | ||
| prayer-walk-planner | ||
| regions-dashboard | ||
| research-dashboard | ||
| research-map-plus-grid | ||
| stack-demo | ||
| template-bundle | ||
| .gitkeep | ||
| README.md | ||
plugin-designs/ — where YOUR maps live
The shared template lives in ../library/ (aliased @map) — you never edit it per-map.
You build your map by making a folder here, plugin-designs/<your-map>/, and composing the
template's building blocks. Your folder is yours (colors, API, i18n); the template is shared
(Apache-2.0, propagated to every map on the next build).
Shared vs. yours — the balance
library/ (@map) ── SHARED, reusable, parameterized ──► legend · map · map-controls
│ (every map reuses these)
│ imported by
▼
plugin-designs/<your-map>/ ── YOURS ──► the map's "main" class (the .vue profile = the Vue
│ instance for that map)
└── src/ ── YOURS ──► this map's OWN non-reusable components, colors, API, utils
One bundle can hold as many maps as you want — add another .vue profile and rebuild.
- Reusable →
library/. The legend, the map, the map-controls are shared, parameterized components every map reuses. Fix once, every map benefits. Never edit it to customize one map. - This map's own →
plugin-designs/<your-map>/. It holds two things: the map's "main" class (each.vueprofile is the Vue instance for that map) and that map's own non-reusable components (insrc/). Nothing here leaks to another map. - Many maps per bundle. The bundler bundles one map or many — add another flat
.vueand rebuild.
Production or experimental — every profile declares itself
Every app profile is one of two things, and experimental profiles are unlimited and encouraged — create as many as you need:
- Experimental, not built: prefix the folder with
_— the build skips it; it just sits in code. Zero cost, zero risk. - Experimental, built: builds normally, but it belongs in the Experimental section of the power app's layout (nav section) — never embedded on partner surfaces.
- Production: a real product — main sections of the power app + individually embeddable.
(The power app = the SPA layout that composes all profiles into one application.)
The pattern in one picture
plugin-designs/<your-map>/ ← one FOLDER = one BUNDLE, named after the folder
├── index.js ← entry: registers the custom element, finds the profiles
├── overview.vue ← one .vue = one PROFILE (flat in the folder — no profiles/ subfolder)
└── detail.vue ← another profile in the SAME bundle
builds to ─────────────► public/doxa-maps/<your-map>/<your-map>.js (one file)
- Folder name = bundle name. Folders starting with
_or.are skipped (handy for WIP). - Each
.vue= one profile (one map screen). One bundle can hold many. - Pick the profile at embed time with the
profilefield — every profile shares one bundle:
<script src="/doxa-maps/<your-map>/<your-map>.js"></script>
<doxa-map profile-config='{"profile":"overview","tk":"pk.eyJ..."}'></doxa-map>
<doxa-map profile-config='{"profile":"detail","tk":"pk.eyJ..."}'></doxa-map>
Add a map — three steps
# 1. create the folder + one profile
mkdir -p plugin-designs/<your-map>
# index.js ← copy doxa-simple-map/index.js, rename the tag
# <name>.vue ← your screen, built from @map/* blocks (flat in the folder)
# 2. build
npm run build # → public/doxa-maps/<your-map>/<your-map>.js
# (just this one: BUNDLE=<your-map> npm run build:bundle)
# 3. embed the script tag + element (dev staging at `npm run dev` shows a paste-ready snippet)
Your three things (everything else is shared)
- Colors — how pins are colored. Pick one the template ships (
getColorStrategy('language-family')), or drop your own inplugin-designs/<your-map>/src/colors/(start fromtemplate-bundle/src/colors/example-mode.js). Never edit@map/colors/to recolor your map. - API — your map reads its data from the
profile-configon the page, so the same bundle points at different data with no rebuild.DataSourceManageracceptscsv(bundled file),api(one JSON URL), orrest-api(a live REST endpoint, the default). Prefer the real API over inlined JSON. - i18n — the template ships 11 base locales in
@map/i18n/locales/. UseuseI18n()for strings andRTL_LOCALESfor right-to-left; add or override strings from your own profile.
The one hosting variable
The only thing that changes between hosting modes is the <script src>: a relative path when
your own site serves the file, or an absolute CDN URL when a partner site serves it. The bundle
is identical either way. See ../documentation/CONTRIBUTING.md for the two CDN modes and partner-site embedding.
Profile patterns (mix freely)
- Individual — one
.vueper bundle (simplest). - Parameterized — one profile, behavior driven by
profile-configprops (e.g.tabs[]). - Nested — one profile that renders others (workbench-style, like
doxa-research-map).
See also
doxa-simple-map/index.js— fully commented reference bundle entrytemplate-bundle/— minimal two-profile skeleton to copy../library/colors/README.md— the color-strategy contract../README.md— repo overview & build pipeline ·../documentation/CONTRIBUTING.md— build vs dev, CDN modes