Update from source 932bf553
This commit is contained in:
parent
1d27e792b6
commit
28e74f9787
18 changed files with 478 additions and 328 deletions
|
|
@ -14,7 +14,7 @@ capabilities, three outputs, one shared library, one build.
|
|||
|
||||
| | |
|
||||
|---|---|
|
||||
| 🗺️ **Two map engines** | Every map draws with **Mapbox** or **MapLibre** (free, no key) — switch with `"provider"` in its settings or `?provider=maplibre` in the address. Mapbox is the default. |
|
||||
| 🗺️ **Two map engines** | Every map draws with **Mapbox** or **MapLibre** (free, no key) — switch with `"provider"` in its settings or `?provider=maplibre` in the address. By default it uses Mapbox when a token is available, MapLibre otherwise. |
|
||||
| 🔑 **A key or a key service** | Give Mapbox a public token, or the address of a token service that hands one out — set once in the Plugin Studio header. |
|
||||
| 🌍 **11 languages** | `?lang=` or `"locale"`, with Arabic right to left — [Languages](#languages). |
|
||||
| 🧩 **Web components** | Each map is its own HTML tag, like `<all-people-groups>`, that works on any page — no framework needed. |
|
||||
|
|
|
|||
|
|
@ -40,8 +40,39 @@ if (!wanted.length) {
|
|||
process.exit(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠ A DYNAMIC FETCH RESOLVES TO A DIRECTORY, AND A DIRECTORY ALWAYS EXISTS.
|
||||
* The province layer fetches `${assetsBase}/boundaries/${PROVINCE_FILE[iso3]}`, so the parse
|
||||
* above captures the literal prefix "boundaries/" and stops at the interpolation. That path is
|
||||
* a real directory of 4,096 bytes, so it passed — proving nothing about the two files the map
|
||||
* actually asks for. A check that cannot fail is the thing this repo keeps rediscovering.
|
||||
* So a directory is never accepted as an asset, and any literal asset FILENAME mentioned in the
|
||||
* leaf is checked on its own. That covers the interpolated case without trying to evaluate it.
|
||||
*/
|
||||
const dirLike = wanted.filter((r) => r.endsWith('/'))
|
||||
const named = [...new Set(
|
||||
[...src.matchAll(/['"`]([A-Za-z0-9_\-]+\.(?:geojson|json))['"`]/g)].map((m) => m[1]),
|
||||
)]
|
||||
/* ⚠ AND THE EXPANSION MUST NOT CONSULT THE FILESYSTEM TO DECIDE WHAT TO EXPECT.
|
||||
* The first version only added a name if the file already existed, so deleting one simply
|
||||
* removed it from the list and the gate went green on nine assets instead of ten. Proven by
|
||||
* unstaging gb-CHN-ADM1.geojson: PASS, exit 0. The expectation has to come from the SOURCE —
|
||||
* the filenames the leaf names — so that a file the leaf asks for and the tree lacks is a
|
||||
* failure rather than a shorter list. */
|
||||
for (const prefix of dirLike) {
|
||||
const ext = /boundaries\//.test(prefix) ? '.geojson' : '.json'
|
||||
for (const file of named.filter((f) => f.endsWith(ext))) wanted.push(prefix + file)
|
||||
}
|
||||
|
||||
const missing = []
|
||||
for (const rel of wanted) {
|
||||
if (rel.endsWith('/')) {
|
||||
/* Counted, not silently skipped: if the expansion above found nothing for this prefix the
|
||||
reader must know the interpolated fetch went unverified. */
|
||||
const found = wanted.filter((w) => w !== rel && w.startsWith(rel))
|
||||
if (!found.length) missing.push(`${rel} — a dynamic fetch this check could not resolve to a file`)
|
||||
continue
|
||||
}
|
||||
const p = resolve(POOL, rel)
|
||||
if (!existsSync(p)) { missing.push(`${rel} — not staged`); continue }
|
||||
const bytes = statSync(p).size
|
||||
|
|
|
|||
|
|
@ -964,12 +964,16 @@ function openCountry(iso) {
|
|||
if (!iso || !byIso.value[iso]) return
|
||||
selected.value = byIso.value[iso]
|
||||
selectedGroup.value = null
|
||||
selectedProvince.value = null
|
||||
shown.value = PAGE
|
||||
ensurePeopleGroups()
|
||||
/* Opening India or China IS the intent signal. 1.6 MB of Indian geometry is not something to
|
||||
spend on the 173 countries nobody is looking at. */
|
||||
ensureProvinces(iso)
|
||||
}
|
||||
|
||||
function openGroup(g) { selectedGroup.value = g }
|
||||
function closeSheet() { selected.value = null; selectedGroup.value = null }
|
||||
function closeSheet() { selected.value = null; selectedGroup.value = null; selectedProvince.value = null }
|
||||
|
||||
async function load() {
|
||||
const [data, adm0, pinData, doxaData] = await Promise.all([
|
||||
|
|
@ -1087,6 +1091,108 @@ function paint(map, adm0) {
|
|||
map.on('mouseleave', 'gr-fill', () => { map.getCanvas().style.cursor = '' })
|
||||
}
|
||||
|
||||
/**
|
||||
* ═══ PROVINCES, FETCHED ONLY WHEN SOMEONE LOOKS ══════════════════════════════════════════
|
||||
*
|
||||
* India and China hold 47.9% of the roster (855 + 154 of 2,107) and a national fill cannot
|
||||
* answer "where would a team go" for half the map. But India's ADM1 geometry is 1,628 KB —
|
||||
* nearly the size of the whole world ADM0 file — on a leaf that already loads about 4 MB and
|
||||
* is meant to be embedded on thousands of partner sites. Loading it for the 173 countries
|
||||
* nobody is looking at would be a tax on every reader for the benefit of two.
|
||||
*
|
||||
* So it arrives on intent: opening India or China fetches that country's provinces once, and
|
||||
* nothing else changes for anyone else. China is 54 KB, India 1,628 KB, and both are cached
|
||||
* after the first open.
|
||||
*
|
||||
* COLOURED BY THE SAME LADDER as the country fill, because a reader who has just learned what
|
||||
* the clay ramp means must not have to learn a second scale one zoom later.
|
||||
*/
|
||||
const provinceState = ref({}) // iso3 -> 'loading' | 'ready' | 'failed'
|
||||
const provinceGeo = ref({}) // iso3 -> the loaded FeatureCollection, kept for recolouring
|
||||
/* A province is coloured by the SAME rule as a country, mode included — a reader who has just
|
||||
learned the clay ramp must not meet a second scale one zoom later. featureColor cannot be
|
||||
reused directly because it looks a country up by iso3; this is the same two lines against a
|
||||
province's own counts. */
|
||||
const colourProvinces = (geo) => {
|
||||
for (const f of geo.features) {
|
||||
const n = mode.value === 'gap' ? f.properties.zero : f.properties.groups - f.properties.zero
|
||||
const b = f.properties.groups > 0 ? bandFor(mode.value === 'gap' ? BANDS : COVER_BANDS, n) : null
|
||||
f.properties.pcolor = b ? b.color : NO_DOXA_FILL
|
||||
}
|
||||
}
|
||||
/* Mode changes after a province layer exists would otherwise leave it painted in the old
|
||||
scale — the country beneath it would say one thing and the province another. */
|
||||
const repaintProvinces = () => {
|
||||
const map = mapRef.value
|
||||
if (!map) return
|
||||
for (const [iso3, geo] of Object.entries(provinceGeo.value)) {
|
||||
colourProvinces(geo)
|
||||
const src = map.getSource(`gr-prov-${iso3}`)
|
||||
if (src) src.setData(geo)
|
||||
}
|
||||
}
|
||||
const provinceRows = ref(null) // the measured table: province -> counts
|
||||
const selectedProvince = ref(null)
|
||||
|
||||
async function ensureProvinceTable() {
|
||||
if (provinceRows.value) return provinceRows.value
|
||||
const r = await fetch(`${assetsBase.value}/tables/gospel-by-province.json`)
|
||||
provinceRows.value = await r.json()
|
||||
return provinceRows.value
|
||||
}
|
||||
|
||||
const PROVINCE_FILE = { IND: 'gb-IND-ADM1.geojson', CHN: 'gb-CHN-ADM1.geojson' }
|
||||
|
||||
async function ensureProvinces(iso3) {
|
||||
const map = mapRef.value
|
||||
if (!map || !PROVINCE_FILE[iso3] || provinceState.value[iso3]) return
|
||||
provinceState.value = { ...provinceState.value, [iso3]: 'loading' }
|
||||
try {
|
||||
const [geo, table] = await Promise.all([
|
||||
fetch(`${assetsBase.value}/boundaries/${PROVINCE_FILE[iso3]}`).then((r) => r.json()),
|
||||
ensureProvinceTable(),
|
||||
])
|
||||
/* The counts come from the measured table, never recomputed here — the point-in-polygon
|
||||
that produced them belongs in the build, and a second implementation in the leaf is how
|
||||
two surfaces come to disagree. */
|
||||
const by = {}
|
||||
for (const row of (table.rows || [])) if (row.iso3 === iso3) by[row.province] = row
|
||||
for (const f of geo.features) {
|
||||
const row = by[f.properties.shapeName]
|
||||
f.properties.zero = row ? row.zero_access_groups : 0
|
||||
f.properties.groups = row ? row.groups : 0
|
||||
}
|
||||
provinceGeo.value = { ...provinceGeo.value, [iso3]: geo }
|
||||
colourProvinces(geo)
|
||||
const src = `gr-prov-${iso3}`
|
||||
if (!map.getSource(src)) {
|
||||
map.addSource(src, { type: 'geojson', data: geo })
|
||||
map.addLayer({
|
||||
id: `${src}-fill`, type: 'fill', source: src,
|
||||
paint: { 'fill-color': ['get', 'pcolor'], 'fill-opacity': ['case', ['>', ['get', 'groups'], 0], 0.82, 0] },
|
||||
}, 'gr-line')
|
||||
map.addLayer({
|
||||
id: `${src}-line`, type: 'line', source: src,
|
||||
paint: { 'line-color': 'rgba(255,255,255,0.75)', 'line-width': 0.7 },
|
||||
}, 'gr-pins-halo')
|
||||
map.on('click', `${src}-fill`, (e) => openProvince(iso3, e.features?.[0]?.properties?.shapeName))
|
||||
map.on('mouseenter', `${src}-fill`, () => { map.getCanvas().style.cursor = 'pointer' })
|
||||
map.on('mouseleave', `${src}-fill`, () => { map.getCanvas().style.cursor = '' })
|
||||
}
|
||||
provinceState.value = { ...provinceState.value, [iso3]: 'ready' }
|
||||
} catch (_) {
|
||||
/* A province view is an enrichment; losing it must never take the country view with it. */
|
||||
provinceState.value = { ...provinceState.value, [iso3]: 'failed' }
|
||||
}
|
||||
}
|
||||
|
||||
function openProvince(iso3, name) {
|
||||
const t = provinceRows.value
|
||||
if (!t || !name) return
|
||||
selectedProvince.value = (t.rows || []).find((r) => r.iso3 === iso3 && r.province === name) || null
|
||||
}
|
||||
function closeProvince() { selectedProvince.value = null }
|
||||
|
||||
function pinFC() {
|
||||
const feats = new Array(pinRows.value.length)
|
||||
const hnrFor = (code) => {
|
||||
|
|
@ -1367,6 +1473,7 @@ function setMode(m) {
|
|||
if (mode.value === m) return
|
||||
mode.value = m
|
||||
if (mapRef.value && adm0Ref.value) paint(mapRef.value, adm0Ref.value)
|
||||
repaintProvinces()
|
||||
}
|
||||
|
||||
/* ═══ NUMBER FORMAT IS HELD FOR THE DRIVER ════════════════════════════════════════════════
|
||||
|
|
@ -1883,7 +1990,34 @@ useShadowStyles(
|
|||
<div v-if="err" class="gm-err">{{ err }}</div>
|
||||
|
||||
<!-- Tap a country: bottom sheet on mobile, corner card on desktop. -->
|
||||
<div v-if="selected || showGapList" class="gm-sheet">
|
||||
<!-- ═══ A PROVINCE, WHEN ONE IS TAPPED ═══════════════════════════════════════════════
|
||||
Sits above the country sheet because it is the narrower answer to the same question,
|
||||
and a reader who tapped a province asked about the province. Closing it returns to the
|
||||
country rather than to the world, so the drill-down is reversible one step at a time.
|
||||
English prose in the caveat line, per the standing decision. -->
|
||||
<div v-if="selectedProvince" class="gm-sheet">
|
||||
<div class="gm-sh-head">
|
||||
<button type="button" class="gm-back" @click="closeProvince">‹ {{ selectedProvince.country }}</button>
|
||||
<button type="button" class="gm-back" @click="closeSheet">{{ t('gospel.sheet.close') }}</button>
|
||||
</div>
|
||||
<h3 class="gm-sh-title">{{ selectedProvince.province }}</h3>
|
||||
<div class="gm-stat"><span>{{ t('gospel.stat.peopleGroups') }}</span><b>{{ fmt(selectedProvince.groups) }}</b></div>
|
||||
<div class="gm-stat"><span>{{ t('gospel.stat.population') }}</span><b>{{ fmt(selectedProvince.people) }}</b></div>
|
||||
<div class="gm-stat"><span>{{ t('gospel.legend.tier0') }}</span><b>{{ fmt(selectedProvince.zero_access_groups) }}</b></div>
|
||||
<div class="gm-stat" v-if="selectedProvince.zero_access_people">
|
||||
<span>{{ t('gospel.stat.peopleAffected') }}</span><b>{{ fmt(selectedProvince.zero_access_people) }}</b>
|
||||
</div>
|
||||
<p v-if="selectedProvince.zero_access_examples" class="gm-note" dir="ltr" style="margin:6px 0 0">
|
||||
Largest with nothing: <b>{{ selectedProvince.zero_access_examples }}</b>
|
||||
<template v-if="selectedProvince.ways_in"><br>Way in: {{ selectedProvince.ways_in }} — chosen for the country, not this province.</template>
|
||||
</p>
|
||||
<p class="gm-note" dir="ltr" style="margin:6px 0 0">
|
||||
A group is placed in a province from its own coordinate, which marks where a people is
|
||||
found rather than a settlement. Near a border that point can fall on the wrong side.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selected || showGapList" class="gm-sheet">
|
||||
|
||||
<!-- ITEM 0 — the ranked gap: the largest groups with nothing found anywhere. -->
|
||||
<template v-if="showGapList && !selected">
|
||||
|
|
|
|||
|
|
@ -358,7 +358,17 @@ useShadowStyles(
|
|||
// commercial and BLOCKED ON THE DRIVER, so this button keeps our stand-in and the family remains
|
||||
// the one measured difference here. Everything geometric now matches.
|
||||
'.filters__toggle{ background:none; cursor:pointer; font:inherit; font-size:var(--font-size-sm);' +
|
||||
' padding:var(--spacing-xs) var(--spacing-2xl); border:1px solid transparent; border-radius:100px;' +
|
||||
// ★ LITERAL 10px, NOT var(--spacing-xs) -- I used the token and it DOES NOT EXIST.
|
||||
// dxlife-tokens.js:43-45 says so explicitly and gives the reason: "--spacing-xs (10px) and
|
||||
// --spacing-sm (13.3px) are deliberately NOT tokenised: '10px' is far too common a literal in this
|
||||
// CSS to substitute safely, and a wrong substitution here is worse than a magic number because it
|
||||
// reads as deliberate." I substituted anyway.
|
||||
// WHAT AN UNDEFINED var() COSTS, measured by rega: it invalidates the WHOLE declaration at compute
|
||||
// time, so padding fell to 0 on BOTH axes and the button went 25.44 -> 17.63 -- FURTHER from their
|
||||
// 37.63 than before I touched it. The border, radius and line-height all landed; only this line
|
||||
// failed, and it failed silently. Their computed value is "10px 41.6158px", so with the literal:
|
||||
// 15.625 + 2x10 + 2x1 = 37.63 tall and ~210.7 wide (theirs 210.73).
|
||||
' padding:10px var(--spacing-2xl); border:1px solid transparent; border-radius:100px;' +
|
||||
' line-height:var(--line-height-none);' +
|
||||
' display:inline-flex; align-items:center; gap:var(--spacing-2xs); margin-inline:auto; color:var(--dx-text); }' +
|
||||
'.filters__toggle svg{ transition:transform 200ms ease; }' +
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS",
|
||||
"integrity": "sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -598,7 +598,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -612,7 +612,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -626,7 +626,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -640,7 +640,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -654,7 +654,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -887,7 +887,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ",
|
||||
"integrity": "sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -907,7 +907,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -921,7 +921,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -935,7 +935,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "dx-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS",
|
||||
"integrity": "sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -598,7 +598,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -612,7 +612,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -626,7 +626,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -640,7 +640,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -654,7 +654,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-cZU1Cw8MbYFPD8keWQ8q+LgMCkXDihd9UvXZoEtrravoxAfPQenQuErMIYUGIYfS\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-oUChFeTRtQcf2dYiiovA3j3+zrvKjvsmPkMddc6pLnOAk0eUUzU8ItIZvMC/2OZI\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -887,7 +887,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ",
|
||||
"integrity": "sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -907,7 +907,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -921,7 +921,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -935,7 +935,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-p3OwJ38WP4LzT1Bu38EwBYic+bbo+3N8EMIG6DQcRHDe7E2JERlDreBRUMzOW7AZ" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-MJahBXMtSLaS+4a+KMIuI17Ca6fF5nkuri80WNZVofIGZAH8HC+rPPlcZVTCovbd" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
---
|
||||
name: make-a-doxa-map
|
||||
description: >-
|
||||
Use this skill when a user (typically a non-coder "map maker" / prayer
|
||||
mobilizer) asks you to build a new map/plugin with the doxa-maps bundler, add
|
||||
another screen to a map, embed one on a site, or ship one as a mobile app.
|
||||
Teaches the 5 plugin capabilities, the 5 deployment methods, the
|
||||
plugin-designs/ root-Vue pattern, the graduation path, and the exact
|
||||
copy-the-starter -> `bun run build` flow. Grounded in the REAL package.json
|
||||
scripts (dev/build/preview/phone) and the REAL structure (plugin-designs/,
|
||||
library/, internal/).
|
||||
---
|
||||
|
||||
# Make a doxa-map (use the plugin bundler)
|
||||
|
||||
You are the AI agent for a **map maker** — usually someone who does NOT write
|
||||
code. Your job is to build maps for them by driving this bundler. This skill is
|
||||
your operating manual: what the bundler is, how to make a new plugin by copying
|
||||
the starter, and how to ship it.
|
||||
|
||||
> **The one thing to remember:** a new plugin is **one folder** under
|
||||
> `plugin-designs/`, and **`bun run build`** turns every folder into a
|
||||
> paste-anywhere `.js` custom element. Everything else is detail.
|
||||
|
||||
Run every command from the bundler repo root
|
||||
(`.../doxa-maps-plugin-studio/`, the folder that has `package.json` and
|
||||
`plugin-designs/`).
|
||||
|
||||
---
|
||||
|
||||
## 0. Mental model (read this first)
|
||||
|
||||
The bundler is a **JAMstack plugin bundler**: one build step turns each map into
|
||||
a self-contained web component ("plugin") that plugs in anywhere. Four words:
|
||||
|
||||
| Word | Plain meaning |
|
||||
|---|---|
|
||||
| **bundle / plugin** | One folder under `plugin-designs/` = one plugin. It compiles into a **single `.js` file** you can paste on any page. "Bundle" and "plugin" are the same thing. |
|
||||
| **profile** | One `.vue` file inside the folder = one **screen** (one map). A bundle can hold one profile or many. The `.vue` filename (minus `.vue`) IS the profile's name. |
|
||||
| **root Vue / root.vue** | Each profile is a normal Vue component — the "root" of that one screen. This is the **root-Vue pattern**: a folder with a root Vue file = a plugin. |
|
||||
| **`src/`** | The plugin's **private sandbox** (its own colors, components, helpers). Nothing here can affect another plugin. |
|
||||
|
||||
Three zones — always know which one a file is in:
|
||||
|
||||
| Zone | What | Edit it? |
|
||||
|---|---|---|
|
||||
| `library/` (imported as `@map/…`) | SHARED, reusable, parameterized building blocks — the map, legend, controls, search bar, composables, color/API registries | **No — reuse only.** A change here can break every map. |
|
||||
| `plugin-designs/<your-map>/` | YOUR plugin: root `.vue` profiles (flat) + a `src/` sandbox | **Yes — build anything.** Nothing here leaks. |
|
||||
| `internal/` | the scanner/build machinery (clean, tree, gallery, guards) | **Never touch.** |
|
||||
|
||||
Rule of thumb: **local by default.** Code only moves into `library/` after a
|
||||
*second* plugin genuinely needs it (see the Graduation Path below).
|
||||
|
||||
---
|
||||
|
||||
## 1. The 5 PLUGIN CAPABILITIES (how much the author nests)
|
||||
|
||||
Capability = how much composition the plugin does. It is NOT "complexity" and it
|
||||
is NOT a quality ranking — it is a ladder of nesting. The studio gallery sorts
|
||||
**5 → 1** (most-composed first). Pick the LOWEST tier that does the job.
|
||||
|
||||
| # | Capability | What it is | Live example in this repo |
|
||||
|---|---|---|---|
|
||||
| 1 | **Single-element plugin** | one folder → one JS bundle → one custom element. Simplest, lightest. | `plugin-designs/doxa-vision-card/` (a pure single element, no map, renders instantly) |
|
||||
| 2 | **Prop-table plugin** | one root Vue defines a **prop table** + **element table** → ONE plugin renders many parameterized variants | `plugin-designs/doxa-simple-map/` (Prayer / Engagement / Adoption from one plugin, via `simple-map.instances.json`) |
|
||||
| 3 | **Monolithic plugin** | one root Vue nests many maps/layouts *internally* → one element. Downside: you can't embed just one inner tab. | `plugin-designs/doxa-research-map/` (research map: many maps + layout in one element) |
|
||||
| 4 | **Nested-element SPA** | each inner map is its OWN plugin-design; a **layout** nests them → one element, but each inner map is *also* embeddable on its own AND the whole SPA embeds | `plugin-designs/my-upg-100-list/`, `plugin-designs/research-dashboard/`, `plugin-designs/cities-internet/` |
|
||||
| 5 | **Nested-SPA / multi-SPA** | nest multiple SPAs into one SPA → a whole modular platform | the doxa-maps studio / mobile app: `internal/generate-gallery.mjs` emits `public/index.html` as a Vue nested-SPA (vertical tabs, each tab a plugin of a different tier) |
|
||||
|
||||
**How to choose:** one screen, no data variants → cap 1. One screen, several
|
||||
preset variants → cap 2 (add an `*.instances.json`). Several screens fused into
|
||||
one you never split → cap 3. Several screens that must *also* be individually
|
||||
embeddable → cap 4. A whole platform of SPAs → cap 5 (the studio already IS this;
|
||||
you rarely author a new cap-5 — you add cap 1–4 plugins and the studio composes
|
||||
them for you).
|
||||
|
||||
---
|
||||
|
||||
## 2. The 5 DEPLOYMENT METHODS (where the SAME plugin runs)
|
||||
|
||||
Author once; the SAME built `.js` (or its standalone page) runs all of these.
|
||||
Methods 1–3 are **proven**; 4–5 are **deferred POCs** (no build script on this
|
||||
branch yet — do not promise a one-command WordPress/desktop build).
|
||||
|
||||
| # | Method | How | Status | Command |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **iframe** on any/partner site | paste the `embed.iframe` snippet from `manifest.json` | ✅ proven | `bun run build` → host `public/doxa-maps/` |
|
||||
| 2 | **Mobile app** (Capacitor, Android) | build wraps the studio SPA into an APK; no new code | ✅ proven | `bun run phone` |
|
||||
| 3 | **Direct-JS** (`<script src>` + custom-element tag, SRI-hashed, props/events, Shadow DOM) | paste the `embed.script` snippet from `manifest.json` | ✅ proven | `bun run build`, then paste snippet |
|
||||
| 4 | **WordPress plugin** | same iframe or `<script>` snippet in a WP **Custom HTML** block | ⚠️ deferred POC (no WP build script) | `bun run build`, paste snippet |
|
||||
| 5 | **Rust desktop (Tauri)** | front-end → desktop shell | ⚠️ deferred POC (not scaffolded) | none exists yet |
|
||||
|
||||
Full command/install detail per method:
|
||||
[`documentation/README-SECTION-3-cli-commands.md`](../../documentation/README-SECTION-3-cli-commands.md).
|
||||
|
||||
Every `bun run build` writes `public/doxa-maps/manifest.json` — the machine-
|
||||
readable index of all bundles + profiles, each carrying ready-to-paste
|
||||
`embed.iframe`, `embed.script` (with an SRI `integrity` hash), and raw
|
||||
`iframeUrl` / `scriptUrl`. URLs are root-relative, so the whole `doxa-maps/`
|
||||
folder is position-independent (drop it on Cloudflare/Vercel/`file://`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Make a NEW plugin — the copy-the-starter flow (the main task)
|
||||
|
||||
The starter is **`plugin-designs/template-bundle/`** (it has its own README). It
|
||||
is a tiny working bundle: two flat profiles that render a colored box (no Mapbox,
|
||||
no data) plus a laid-out `src/` sandbox. **Copy it; never start from blank.**
|
||||
|
||||
### 3a. The 2-move happy path
|
||||
|
||||
```bash
|
||||
# 1. copy the starter to your new plugin name (run from the repo root)
|
||||
cp -r plugin-designs/template-bundle plugin-designs/my-plugin
|
||||
|
||||
# 2. build the one command
|
||||
bun run build # → public/doxa-maps/my-plugin/my-plugin.js
|
||||
```
|
||||
|
||||
The build auto-detects the new folder (it loops `plugin-designs/*/`), compiles it
|
||||
to one IIFE `.js` custom element, runs the guards, and lists it in the studio.
|
||||
|
||||
### 3b. Rename exactly FOUR things so the copy has its own name
|
||||
|
||||
After `cp -r`, edit these four (nothing else) inside `plugin-designs/my-plugin/`:
|
||||
|
||||
1. **The folder name** — already done by `cp -r` (= bundle name = output
|
||||
`my-plugin.js`).
|
||||
2. **The tag in `index.js`** — `customElements.define('template-bundle', …)` →
|
||||
`customElements.define('my-plugin', …)`. (There are two spots: the `.define`
|
||||
call and the `!customElements.get('template-bundle')` guard.)
|
||||
3. **The tag + `profile` strings in `index.html`** — the staging page mounts the
|
||||
element, so replace `<template-bundle profile-config='{"profile":"…"}'>` tags
|
||||
with `<my-plugin profile-config='{"profile":"my-screen"}'>`, and the
|
||||
`template-bundle { … }` CSS selector.
|
||||
4. **The `.vue` filenames** — rename `template-map-a.vue` to your screen's name
|
||||
(e.g. `my-screen.vue`); the filename becomes the profile name. Delete
|
||||
`template-map-b.vue` if you only want one screen.
|
||||
|
||||
Then `bun run build` again.
|
||||
|
||||
> **The flat rule:** profile `.vue` files sit **directly** in the plugin folder —
|
||||
> there is NO `profiles/` subfolder. The entry discovers them with a flat glob
|
||||
> (`import.meta.glob('./*.vue')`); a `.vue` in a subfolder would NOT be found.
|
||||
> The small pieces a profile is built from live in `src/`.
|
||||
|
||||
### 3c. Fill in the map
|
||||
|
||||
1. **List what the map needs** — data, colors, controls (legend, search, share,
|
||||
fullscreen…).
|
||||
2. **Reuse from the library** by reference — `import { useMapInstance } from '@map/composables/useMapInstance.js'`. The menu of what `@map/…` offers is [`documentation/reference.md`](../../documentation/reference.md). **Never copy library code into your folder.**
|
||||
3. **Put your own code in `src/`** — screens, components, hooks, helpers (imported by relative path, e.g. `import colors from './src/colors/colors.json'`). There is no alias for your sandbox — that boundary keeps it private.
|
||||
4. **Set colors** in `src/colors/colors.json` (plain hex). For color that varies by data, copy `src/colors/example-mode.js` (a strategy `.js`).
|
||||
|
||||
### 3d. Add extra preset variants (capability 2)
|
||||
|
||||
To ship one plugin as several named embeddable variants (like Prayer / Engagement
|
||||
/ Adoption from `doxa-simple-map`), add a `<profile>.instances.json` next to the
|
||||
`.vue`. The build emits a standalone page per instance at
|
||||
`my-plugin/<instance>/index.html`. This is the cap-2 prop-table pattern.
|
||||
|
||||
### 3e. Embed the result
|
||||
|
||||
From `public/doxa-maps/manifest.json`, or hand-write from the starter's shape:
|
||||
|
||||
```html
|
||||
<script src="/doxa-maps/my-plugin/my-plugin.js"></script>
|
||||
<my-plugin profile-config='{"profile":"my-screen"}'></my-plugin>
|
||||
```
|
||||
|
||||
`profile` must match a `.vue` filename (without `.vue`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Iterate live (the dev server)
|
||||
|
||||
```bash
|
||||
bun run dev # Vite dev server at http://localhost:5173
|
||||
```
|
||||
|
||||
- The plugin's staging page is
|
||||
`http://localhost:5173/plugin-designs/my-plugin/index.html`.
|
||||
- Edits hot-reload — this is how you show the map maker changes in near-real-time
|
||||
and take feedback.
|
||||
- `bun run preview` serves the last production build; `bun run serve` serves
|
||||
`public/` (the built tree + studio) at `http://localhost:4173`.
|
||||
|
||||
---
|
||||
|
||||
## 5. The graduation path (why the root-Vue pattern matters)
|
||||
|
||||
A plugin starts **non-reusable** — its own components live in its `src/`.
|
||||
**Graduation** = deliberately extracting a component into the shared `library/`
|
||||
**only after** it is proven reusable AND parameterized.
|
||||
|
||||
- **Do NOT graduate prematurely.** Pushing non-parameterized code into `library/`
|
||||
poisons every other plugin with bad examples / breakage. Local by default;
|
||||
promote later — an expert move, not a first move.
|
||||
- The bundler is a **refactor-cruncher**: take an open-source map app → make it a
|
||||
plugin (a folder + root Vue) → make its components reusable/parameterized → now
|
||||
it runs in all 5 deploy targets and every other plugin can share the pieces.
|
||||
|
||||
Practical rule for you as the agent: keep new work in the plugin's `src/`. Only
|
||||
suggest moving something to `library/` when a *second* plugin needs the exact same
|
||||
thing and it takes props (no hard-coded specifics).
|
||||
|
||||
---
|
||||
|
||||
## 6. The plugin-studio (the auto-generated gallery)
|
||||
|
||||
On build, `bun run gallery` (the last build step) writes `public/index.html` — a
|
||||
**Category-5 Vue nested-SPA** that auto-detects every plugin and lists them sorted
|
||||
**capability 5 → 1**. It's called a "studio" (not "gallery") because it's the
|
||||
development location where all tiers sit in one place. You don't hand-edit it; it
|
||||
regenerates from `manifest.json` every build. Your new plugin appears in it
|
||||
automatically.
|
||||
|
||||
The same `public/index.html` is the Capacitor mobile-app launch entry
|
||||
(`capacitor.config.json` `webDir: public`), so restoring/building the studio SPA
|
||||
is also the deploy-method-2 mobile app.
|
||||
|
||||
---
|
||||
|
||||
## 7. Verify your work (what "done" looks like)
|
||||
|
||||
1. `bun run build` prints **ALL GUARDS PASS** (network-policy, iso2-map,
|
||||
resource-join, tables-complete, bundles-shipped, map-paint) and ends with the
|
||||
gallery writing `public/index.html`. If a guard fails, read its message — it
|
||||
names the exact file/rule.
|
||||
2. `public/doxa-maps/my-plugin/my-plugin.js` exists (the IIFE bundle) and
|
||||
`public/doxa-maps/manifest.json` lists `my-plugin` with your profile(s) + embed
|
||||
snippets.
|
||||
3. `bun run dev` → the plugin renders at its staging URL and in the studio.
|
||||
|
||||
> **Concurrency note:** if a build fails mid-run with files vanishing (e.g. a
|
||||
> `clean` wiped `public/doxa-maps/`), another process ran `bun run clean` on the
|
||||
> same folder. Re-run `bun run build` cleanly (serialize it) — that is not a code
|
||||
> bug.
|
||||
|
||||
---
|
||||
|
||||
## 8. Command cheat-sheet (all real, from `package.json`)
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `bun install` | install deps into `node_modules/` (once; `npm install` also works) |
|
||||
| `bun run dev` | Vite dev server at `:5173` (live plugin staging) |
|
||||
| `bun run build` | clean → write token → build every `plugin-designs/*/` → tree → guards → studio. **The one command.** |
|
||||
| `bun run preview` | serve the last production build |
|
||||
| `bun run serve` | serve `public/` (built tree + studio) at `:4173` |
|
||||
| `bun run phone` | build → Capacitor sync → assemble + install the Android APK (deploy method 2) |
|
||||
| `BUNDLE=my-plugin bun run build:bundle` | build just one plugin (faster while iterating) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Guardrails
|
||||
|
||||
- **Additive by default.** A new map = a NEW folder under `plugin-designs/`. Don't
|
||||
edit other people's plugins or `internal/`.
|
||||
- **Never edit `library/` to customize one map** — that's what `src/` is for.
|
||||
- **Keep profile `.vue` files flat** in the plugin folder (no `profiles/` subdir).
|
||||
- **Don't promise deploy 4/5 as one-command** — WordPress/Tauri are deferred POCs;
|
||||
they reuse the method-1/3 artifacts.
|
||||
- **Explain it in plain English** to the map maker — if you can't, they can't
|
||||
follow. Simplicity is the goal.
|
||||
|
||||
## References (in this repo)
|
||||
|
||||
- `plugin-designs/template-bundle/README.md` — the starter's own README (copy-me detail)
|
||||
- `plugin-designs/README.md` — the plugin-designs authoring guide (shared vs. yours, production vs. experimental)
|
||||
- `documentation/README-SECTION-3-cli-commands.md` — CLI commands + installs per deploy method
|
||||
- `documentation/reference.md` — the `@map/…` menu of reusable library pieces
|
||||
- `documentation/tutorial.md` — 3 zones + 3 rules in one page
|
||||
- `plugin-designs/doxa-simple-map/index.js` — a fuller, real-map reference entry
|
||||
249
skills/plugin-designer/SKILL.md
Normal file
249
skills/plugin-designer/SKILL.md
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
---
|
||||
name: plugin-designer
|
||||
description: >-
|
||||
Use this skill to design and build a new map plugin in this repo's
|
||||
plugin-designs/ folder, or to add a screen to an existing one. Teaches the
|
||||
bundle = folder = one JS file pattern, reusing library/ before writing code,
|
||||
the universal props every plugin must honour (map engine, language, Mapbox
|
||||
token or token service), the rules for working under both MapLibre and
|
||||
Mapbox, localization and right-to-left, the five capability levels, the one
|
||||
build step, and the three outputs (web folder, mobile app, desktop app).
|
||||
Works directly with the Plugin Studio (`bun run dev`).
|
||||
---
|
||||
|
||||
# Plugin designer
|
||||
|
||||
You build map plugins for this repo. A **plugin** is a folder under `plugin-designs/`; one build
|
||||
turns it into one JavaScript file that any page can embed as an HTML tag. Your plugin must work
|
||||
the same under both map engines, in every language, and with or without a Mapbox token.
|
||||
|
||||
Run every command from the repo root (the folder with `package.json`).
|
||||
|
||||
---
|
||||
|
||||
## 1. The pattern: bundle = folder = one JS file
|
||||
|
||||
```
|
||||
plugin-designs/<your-plugin>/ one FOLDER = one BUNDLE (the folder name is the bundle name)
|
||||
├── index.js entry: registers the custom element, finds the profiles
|
||||
├── index.html a staging page that mounts the element
|
||||
├── <screen>.vue one .vue = one PROFILE (one screen), FLAT in the folder
|
||||
├── <screen>.meta.json optional: {"type": "<capability>", "label": "…"}
|
||||
├── <screen>.instances.json optional: named presets of one screen (capability 2)
|
||||
├── data/ optional: the bundle's own data files (imported, so they ship inside the bundle)
|
||||
└── src/ the bundle's PRIVATE code: components, colors, helpers
|
||||
builds to ─────────────► public/dx-maps/<your-plugin>/<your-plugin>.js
|
||||
```
|
||||
|
||||
- **Profiles are flat.** `index.js` finds them with `import.meta.glob('./*.vue')`; a `.vue` in a
|
||||
subfolder is not found. The filename (without `.vue`) is the profile's name.
|
||||
- **Elements live inside.** One bundle registers one tag named after the folder, and can hold more
|
||||
elements for its own screens to nest (see `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json`).
|
||||
- **Folders starting with `_` are skipped** by the build: a free place for work in progress.
|
||||
- **New plugins appear in the Plugin Studio at once, and in the Research Gallery's Experimental
|
||||
section automatically.** Moving one into a production section is a deliberate edit to the
|
||||
gallery's curated sections (`plugin-designs/doxa-maps-spa/doxa-maps-spa.vue`), not your call alone.
|
||||
|
||||
## 2. Start by copying the starter
|
||||
|
||||
```bash
|
||||
cp -r plugin-designs/template-bundle plugin-designs/<your-plugin>
|
||||
```
|
||||
|
||||
Then rename exactly these, and nothing else:
|
||||
|
||||
1. the tag in `index.js` — both the `customElements.define('template-bundle', …)` call and the
|
||||
`customElements.get('template-bundle')` guard;
|
||||
2. the tag, the `profile` strings and the CSS selector in `index.html`;
|
||||
3. the `.vue` filenames (`template-map-a.vue` → `<screen>.vue`); delete `template-map-b.vue` if
|
||||
you need one screen.
|
||||
|
||||
`template-map-a.vue` is a real minimal map: it draws `data/places.geojson` on a basemap with one
|
||||
translated title, and already follows the page's engine, language (with right to left) and token,
|
||||
falling back to keyless MapLibre. **Replace `data/places.geojson` with your own points** and you have
|
||||
a working map to build on. Its title key (`template.title`) lives in the library catalogues. Your own
|
||||
translated text needs its own catalogue file in `library/i18n/locales/`, which is a library change
|
||||
(section 3). Until that lands, plain English in your `.vue` is fine.
|
||||
|
||||
Keep the entry's `configureApp` exactly as the starter has it: it installs Pinia, installs i18n
|
||||
**and** provides it as `appI18n`, and provides `profileModules`. The element's root
|
||||
(`library/ProfileLoader.vue`) cannot call `useI18n()`; it drives the language through the
|
||||
provided instance. Drop the provide and a host can no longer set your plugin's language.
|
||||
|
||||
## 3. Reuse `library/` first
|
||||
|
||||
`library/` (imported as `@map/…`) is shared by every plugin. Its menu is
|
||||
`documentation/reference.md`.
|
||||
|
||||
- **Reuse by import, never by copy.** `import { useMapInstance } from '@map/composables/useMapInstance.js'`.
|
||||
- **Your own code goes in your `src/`**, imported by relative path. Nothing there reaches another plugin.
|
||||
- **Don't edit `library/` to customise one plugin.** A library change reaches every plugin that
|
||||
imports it, so it needs every consumer rebuilt. On a team with a build owner, hand library
|
||||
changes to that owner rather than landing them yourself.
|
||||
- **Graduate late.** Move code into `library/` only when a second plugin needs the same thing
|
||||
and it takes props instead of hard-coded specifics.
|
||||
|
||||
## 4. The universal props — every plugin honours these
|
||||
|
||||
A plugin never decides these for itself; it reads them from whoever embeds it. The Plugin Studio
|
||||
header sets all of them, which is how you test yours.
|
||||
|
||||
| Prop | Values | Channels, first found wins | Default |
|
||||
|---|---|---|---|
|
||||
| **Map engine** (`provider`) | `mapbox` · `maplibre` · `auto` (any case) | `profile-config` `"provider"` → `?provider=` in the page address → `window.MAP_PROVIDER`; an explicit choice always wins | `auto`: Mapbox when a token is available (any row below), MapLibre otherwise. Confirm in `library/composables/useMapProvider.js`, never assume it |
|
||||
| **Language** (`locale`) | one of the 11 in `library/i18n/locales/` | `profile-config` `"locale"` (or `"lang"`) → `?lang=` / `?locale=` → `<html lang>` | English |
|
||||
| **Mapbox token** (`tk`) | a public `pk.…` token | `profile-config` `"tk"` → `?tk=` on a generated page → `window.MAP_TOKEN`; generated pages also keep a token pasted once in the tree (browser storage) | none. The embed snippet's `YOUR_MAPBOX_TOKEN` placeholder counts as no token |
|
||||
| **Token service** (`tokenUrl`) | a URL that returns a `pk.` token | `profile-config` `"tokenUrl"` (the element asks it when it has no token; the studio header's field feeds this) → on generated pages also `window.MAP_TOKEN_URL` → the build config's `tokenUrl` → same-origin `/api/maps/token` | none |
|
||||
|
||||
```html
|
||||
<your-plugin profile-config='{"profile":"<screen>","provider":"maplibre","locale":"ar"}'></your-plugin>
|
||||
```
|
||||
|
||||
Read them through the library (`resolveMapProvider`, the injected `mapboxToken`, the i18n
|
||||
instance), never by parsing the URL yourself, and never fetch a token service yourself: the
|
||||
library asks it, accepts only a `pk.` answer, and a Mapbox map waits for it before it starts.
|
||||
**Your plugin must draw under MapLibre with no token at all.** `library/composables/useMapProvider.js` and
|
||||
`library/ProfileLoader.vue` are the authority when this table and the code disagree.
|
||||
|
||||
## 5. Two engines: rules that keep MapLibre and Mapbox equal
|
||||
|
||||
Write every map feature once, so it works under both engines. These rules each come from a
|
||||
real bug:
|
||||
|
||||
1. **Ask the map which engine it is.** Use `engineFor(map)` / `isMapboxMap(map)` from
|
||||
`@map/composables/useMapProvider.js`, never a global that may still be defined after a switch.
|
||||
2. **Some APIs differ in shape.** `getProjection()` is `{name}` on Mapbox but `{type}` or
|
||||
`undefined` on MapLibre; `setProjection` takes `'mercator'` on Mapbox but `{type:'mercator'}` on
|
||||
MapLibre; `setConfigProperty` and light presets exist only on Mapbox. Wrap these calls in `try`,
|
||||
and never assume the result's shape. An uncaught throw silently skips everything after it.
|
||||
3. **Mapbox-only paint properties drop the whole layer on MapLibre.** Never write
|
||||
`'circle-emissive-strength'` (or any `*-emissive-strength`) literally. Spread
|
||||
`...emissivePaint(map, kind)` from `useMapProvider.js`.
|
||||
4. **Fonts must exist on the engine's glyph server.** Under MapLibre, a Mapbox font name
|
||||
(`DIN Offc Pro`, `Arial Unicode MS`) gets a 404, and the layer draws **no text** with no error.
|
||||
Spread `...labelFont(map, {…})` from `@map/maps/label-font.js` into every symbol layer.
|
||||
5. **`mapbox://` sources don't load on MapLibre.** The country polygons come from the Mapbox tileset
|
||||
`mapbox.country-boundaries-v1` under Mapbox. Under MapLibre, use the local file
|
||||
`boundaries/world-adm0.geojson` under the assets base. It names the country code `iso3`, where
|
||||
the tileset says `iso_3166_1_alpha_3`, so normalise once. `library/composables/useMapLayers.js`
|
||||
(the "region polygons, per engine" block) is the worked example.
|
||||
6. **Load assets from the assets base, never a page-relative path.** Use
|
||||
`resolveAssetsBase(profileConfig)` from `@map/utils/embedBase.js`. A hard-coded `./assets`
|
||||
works in the built folder and breaks under `bun run dev`.
|
||||
7. **Prove your expressions parse under MapLibre without a browser.** MapLibre's own parser,
|
||||
`@maplibre/maplibre-gl-style-spec`, is installed as a dependency of `maplibre-gl`. It is NOT
|
||||
declared in `package.json`, so a lockfile change can remove it: if the import fails, add it as a
|
||||
dev dependency rather than dropping the check.
|
||||
|
||||
```js
|
||||
import { expression, featureFilter } from '@maplibre/maplibre-gl-style-spec'
|
||||
const e = expression.createExpression(['match', ['get', 'status'], 'reached', '#2e7d32', '#c62828'])
|
||||
if (e.result !== 'success') throw new Error(e.value.map(x => x.message).join('; '))
|
||||
const admits = featureFilter(['==', ['get', 'iso3'], 'TUN']).filter({ zoom: 2 }, { properties: { iso3: 'TUN' } })
|
||||
```
|
||||
|
||||
Run each paint and filter expression your layers use against a few real features from the data.
|
||||
|
||||
## 6. Localization and right to left
|
||||
|
||||
- **Every word on screen comes from a catalogue** in `library/i18n/locales/<code>/*.json`, the same
|
||||
key in all 11 languages. Components use `t('file.key')`; code that runs outside the Vue app uses
|
||||
`tStatic(path, locale)` from `@map/i18n/index.js`.
|
||||
- **Add a key to all 11 catalogues at once.** `bun run check:i18n-parity` fails when one is missing;
|
||||
`bun run check:i18n-never-translated` flags a key that was copied, not translated.
|
||||
- **Names from the data stay as the data has them** (people groups, places); translate only the
|
||||
interface.
|
||||
- **Right to left belongs to your element, not the page.** Arabic mirrors the plugin's own box; the
|
||||
library sets `dir` on it. Never write `document.documentElement.dir` from a plugin: an embedded
|
||||
plugin that does flips the whole partner page.
|
||||
|
||||
## 7. Choose the capability level
|
||||
|
||||
Pick the lowest level that does the job. Declare levels 3–5 in `<screen>.meta.json` `"type"`;
|
||||
levels 1 and 2 are detected on their own.
|
||||
|
||||
| Level | Name (`type`) | What it is | Example |
|
||||
|---|---|---|---|
|
||||
| 1 | single element (`single`) | one screen, one element | `plugin-designs/prayer-walk-planner/` |
|
||||
| 2 | preset element (`parameterized`) | one screen, several named presets from `<screen>.instances.json` | `plugin-designs/doxa-simple-map/` |
|
||||
| 3 | multi-view element (`monolithic`) | many views fused inside one element | `plugin-designs/doxa-research-map/` |
|
||||
| 4 | composite element (`nested-elements-mfe`) | nests other elements, each still embeddable alone, wired by props and events | `plugin-designs/dashboard-plus-grid/` (the Big Picture tab) |
|
||||
| 5 | app shell (`nested-mfe-spa`) | nests whole apps | `plugin-designs/doxa-maps-spa/` (the Research Gallery) |
|
||||
|
||||
## 8. Learn from the reference plugins
|
||||
|
||||
The seven production plugins are the reference: each shows what a good plugin does. Before you
|
||||
write a rule's code, read the plugin that shows it best. Find code by the name given here, not by
|
||||
line number, because lines move.
|
||||
|
||||
| Rule | Read this | What to look at |
|
||||
|---|---|---|
|
||||
| Entry: i18n installed **and** provided; one bundle, two tags | Prayer Progress: `plugin-designs/doxa-simple-map/index.js` | `configureApp` and its `app.provide('appI18n', …)` |
|
||||
| Presets from one screen (capability 2) | Prayer Progress: `plugin-designs/doxa-simple-map/simple-map.instances.json` | Prayer / Engagement / Adoption as three embeddable pages |
|
||||
| Engine-asymmetric APIs, guarded | Regions: `plugin-designs/regions-dashboard/src/RegionCountryMap.vue` | `projectionOf` and `setProjectionOn` — both shapes, inside `try` |
|
||||
| Polygons both engines can draw; data from the assets base | Cities with Internet: `plugin-designs/cities-internet/connectivity-map.vue` | `resolveAssetsBase(…)`, the `boundaries/world-adm0.geojson` fetch (polygon data with no `mapbox://` source), and `resolveNamedStyle(…, provider)` for a basemap that matches the engine |
|
||||
| Interface text from the catalogues | Gospel Resources: `plugin-designs/gospel-resources/resources-map.vue` | its interface text goes through `t('…')` (about 80 calls) |
|
||||
| Basemap labels in the reader's language; right to left | Prayer Progress: `plugin-designs/doxa-simple-map/doxa-simple-map.vue` | the `name_<locale>` label switch and `isRTL` |
|
||||
| Interop: one element reacts to another's events | Simple Research: `plugin-designs/research-map-plus-grid/research-map-plus-grid.vue` | the map listening for the grid's `groups-filtered` event |
|
||||
| Composite element (capability 4) | Big Picture: `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json` | `"composes"`: two elements, each still embeddable alone |
|
||||
| One bundle, several embeddable screens | UPG-100: `plugin-designs/my-upg-100-list/` | `upg-100-mfe`, `upg-100-list` and `poster-wall` from one bundle |
|
||||
| App shell (capability 5) | Research Gallery: `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue` | every production plugin as a tab, and the Experimental folder filled from the manifest |
|
||||
|
||||
`emissivePaint` and `labelFont` (section 5) have no production-plugin exemplar, because the
|
||||
production plugins get them through the library's own layers. The starter uses both directly:
|
||||
read the pin and label layers in `plugin-designs/template-bundle/template-map-a.vue`.
|
||||
|
||||
## 9. Develop in the Plugin Studio
|
||||
|
||||
```bash
|
||||
bun install # once
|
||||
bun run dev # the Plugin Studio at http://localhost:5173/
|
||||
```
|
||||
|
||||
- Your plugin appears in the studio as soon as its folder exists, and edits reload live.
|
||||
- One screen on its own: `http://localhost:5173/__leaf/<your-plugin>/<screen>`.
|
||||
- Test the universal props from the studio header: switch the **engine**, the **language**
|
||||
(include Arabic), and the **token** (none, a token, a token service). Your plugin must work in
|
||||
every combination.
|
||||
- `bun run check:dev-assets` checks that every map's data files load under the dev server.
|
||||
|
||||
## 10. Build: one step
|
||||
|
||||
```bash
|
||||
BUNDLE=<your-plugin> bun x vite build # just your bundle → public/dx-maps/<your-plugin>/
|
||||
BUILD_ALLOW=1 bun run build # every bundle, the tree, the manifest, the studio
|
||||
```
|
||||
|
||||
The full build rewrites every plugin's output. It refuses to run without `BUILD_ALLOW=1`, and
|
||||
on a team one agent owns it. Build only your own bundle unless you are that agent.
|
||||
`public/dx-maps/manifest.json` then lists your plugin with ready-to-paste embed snippets.
|
||||
|
||||
## 11. The three outputs
|
||||
|
||||
| Output | What it is | How |
|
||||
|---|---|---|
|
||||
| **1. The web folder** | `public/dx-maps/`: every plugin, embeddable by **iframe** or **script tag**, from any host | copy the folder; snippets are in `manifest.json` |
|
||||
| **2. The mobile app** | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir: public/dx-maps`) | `bun run phone` (Android) |
|
||||
| **3. The desktop app** | the same folder in Tauri (`deployment/src-tauri/`), a proof of concept | `bun run tauri:build` |
|
||||
|
||||
WordPress (`deployment/wordpress/`) embeds the web folder's snippets; it's experimental.
|
||||
|
||||
## 12. Done means
|
||||
|
||||
- `BUNDLE=<your-plugin> bun x vite build` succeeds, and your plugin is in `manifest.json`.
|
||||
- It works in the Plugin Studio under **both engines**, **with and without a token**, in
|
||||
**English and Arabic**.
|
||||
- Every new string is in all 11 catalogues (`bun run check:i18n-parity`).
|
||||
- Your layer expressions parse under MapLibre (section 5, rule 7).
|
||||
- You changed nothing in `library/` or in another plugin's folder, or you handed that change to
|
||||
the build owner.
|
||||
|
||||
## References (all in this repo)
|
||||
|
||||
- `plugin-designs/README.md`: authoring guide, production vs experimental
|
||||
- `plugin-designs/template-bundle/`: the starter, and its README
|
||||
- `documentation/reference.md`: the `@map/…` library menu and the profile types
|
||||
- `documentation/how-to.md`: recipes, including swapping the map engine
|
||||
- `library/composables/useMapProvider.js`: engine resolution and engine helpers
|
||||
- `library/maps/label-font.js`: fonts per engine
|
||||
- `library/i18n/index.js`: languages, `tStatic`, right to left
|
||||
Loading…
Reference in a new issue