Compare commits

...

1 commit

68 changed files with 4176 additions and 18111 deletions

View file

@ -5,10 +5,12 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000
JWT_SECRET=your-super-secret-jwt-key-change-this
MAILGUN_API_KEY=your-mailgun-api-key
MAILGUN_DOMAIN=mg.yourdomain.com
# Optional: Use 'api.eu.mailgun.net' for EU region
MAILGUN_HOST=
# SendGrid (transactional email: invites, password resets, verification).
# Sends through the v3 JSON API. The From address in SMTP_FROM must be on a
# domain authenticated in the SendGrid account.
SENDGRID_API_KEY=your-sendgrid-api-key
# Optional API host override; defaults to api.sendgrid.com (use api.eu.sendgrid.com for EU)
SENDGRID_HOST=
SMTP_FROM=noreply@yourdomain.com
SMTP_FROM_NAME=My App

View file

@ -7,6 +7,11 @@
// Destination comes from NUXT_PUBLIC_FEEDBACK_API_BASE / _PROJECT_ID. With no
// project id there is nowhere to post, so the widget and its bundle are skipped.
const { public: pub } = useRuntimeConfig()
// Bare pages (layout:false — e.g. /doxa-maps, the full-page maps SPA) get NO
// site chrome at all: the fixed feedback bubble lives outside NuxtLayout, so it
// must opt out here or it floats over the map.
const route = useRoute()
const isBarePage = computed(() => route.meta.layout === false)
const feedbackEnabled = Boolean(pub.feedbackProjectId)
if (feedbackEnabled) useFeedbackScript()
@ -27,7 +32,7 @@ const siteFeedbackConfig = JSON.stringify({
</NuxtLayout>
<ClientOnly>
<div
v-if="feedbackEnabled"
v-if="feedbackEnabled && !isBarePage"
class="site-feedback-widget"
>
<feedback-web-component :profile-config="siteFeedbackConfig" />

View file

@ -600,7 +600,7 @@ const showMore = computed(() => hasMore())
<div v-if="useSelectCard" class="stack stack--sm | card | highlighted-uupg__card">
<div class="repel align-start">
<img :src="uupg.image_url" :alt="uupg.name">
<p class="color-brand-lighter uppercase text-end overflow-wrap-anywhere" v-html="uupg.wagf_region_label ? uupg.wagf_region_label : uupg.wagf_region.label" />
<p class="color-brand-lighter uppercase text-end overflow-wrap-anywhere" v-html="uupg.wagf_region_label ? uupg.wagf_region_label : uupg.wagf_region?.label" />
</div>
<div>
<p class="line-height-tight" v-html="uupg.name" />
@ -633,9 +633,10 @@ const showMore = computed(() => hasMore())
<h3 class="uupg__name line-height-tight" v-html="uupg.name" />
<p class="uupg__country">
<a
v-if="uupg.country_code"
class="uupg__country-link light-link"
:href="localePath(`/regions/${countrySlug(uupg.country_code.value, uupg.country_code.label)}`)"
><span v-html="uupg.country_label ? uupg.country_label : uupg.country_code.label" /></a> (<span v-html="uupg.rop1_label ? uupg.rop1_label : uupg.rop1.label" />)
><span v-html="uupg.country_label ? uupg.country_label : uupg.country_code.label" /></a> (<span v-html="uupg.rop1_label ? uupg.rop1_label : uupg.rop1?.label" />)
</p>
<template v-if="uupg.matches">
<p v-for="(match, mi) in uupg.matches" :key="mi" class="font-size-sm color-brand-lighter">

66
app/pages/doxa-maps.vue Normal file
View file

@ -0,0 +1,66 @@
<script setup lang="ts">
// doxa.life/doxa-maps — the DOXA maps SPA (every map, one app).
//
// Frames the bundler's single-page app (the app-wrapper shell) from the
// drop-in at public/js/doxa-maps-build/ — every map, table and dashboard the
// bundler ships, composed into one lazy-loaded app, always exactly as current
// as the build in this repo. Pointed to from the research page
// ("See all DOXA maps →").
//
// TOKEN: the site's EXISTING endpoint, nothing new. /api/maps/token (see
// server/api/maps/token.get.ts) passes a pk.* through or mints a 1-hour tk.*
// from a secret key — the single source of truth built for embeds like this.
// We fetch it once and hand it to the shell via its native ?tk= rung.
// FULL PAGE: no site chrome — the default layout's header/footer overlapped
// the map. layout:false renders this page bare; the SPA shell IS the chrome.
definePageMeta({ layout: false })
const frameSrc = ref('')
onMounted(async () => {
let tk = ''
try {
const r = await $fetch<{ token?: string }>('/api/maps/token')
tk = r?.token || ''
} catch { /* endpoint down → shell falls back to its own ladder */ }
// Forward this page's own ?utm_source into the framed SPA so clicks inside
// it attribute to whoever linked here (the question-mark tracking mechanism).
const utm = (() => { try { return new URLSearchParams(window.location.search).get('utm_source') || '' } catch { return '' } })()
const q = new URLSearchParams()
if (tk) q.set('tk', tk)
if (utm) q.set('utm_source', utm)
const qs = q.toString()
frameSrc.value = '/js/doxa-maps-build/index.html' + (qs ? `?${qs}` : '')
})
useHead({
title: 'All DOXA Maps',
meta: [{ name: 'robots', content: 'noindex' }],
})
</script>
<template>
<div class="maps-room">
<iframe
v-if="frameSrc"
:src="frameSrc"
title="The DOXA maps app"
class="maps-room-frame"
loading="eager"
allowfullscreen
/>
</div>
</template>
<style scoped>
.maps-room {
/* Full-bleed room: the staging index brings its own chrome. */
position: fixed;
inset: 0;
background: #0a0e15;
}
.maps-room-frame {
width: 100%;
height: 100%;
border: 0;
display: block;
}
</style>

View file

@ -39,6 +39,7 @@ useTextHighlight()
<div class="stack stack--xs text-center">
<h1 class="highlight" data-highlight-last>{{ t('Find a UUPG') }}</h1>
<p><NuxtLink class="light-link" :to="localePath('/regions')">{{ t('By Region') }} &rarr;</NuxtLink></p>
<p><NuxtLink class="light-link" to="/doxa-maps">{{ t('See all DOXA maps') }} &rarr;</NuxtLink></p>
</div>
<!-- Research map embed — sits directly under the H1, above the UUPG

View file

@ -38,7 +38,6 @@
"mapbox-gl": "^3.22.0",
"markdown-it": "^14.1.1",
"nodemailer": "^7.0.6",
"nodemailer-mailgun-transport": "^2.1.5",
"nuxt": "^4.4.2",
"postgres": "^3.4.7",
"tailwindcss": "^4.2.2",
@ -54,7 +53,6 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/markdown-it": "^14.1.2",
"@types/nodemailer": "^7.0.1",
"@types/nodemailer-mailgun-transport": "^1.4.6",
"@types/turndown": "^5.0.6",
"eslint": "^10.2.0",
"sass": "^1.99.0",
@ -892,8 +890,6 @@
"@types/nodemailer": ["@types/nodemailer@7.0.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g=="],
"@types/nodemailer-mailgun-transport": ["@types/nodemailer-mailgun-transport@1.4.6", "", { "dependencies": { "@types/nodemailer": "*" } }, "sha512-6qhtDo+1ZLtrmmpQN7O9e3NLK5ggnTS2Oca+22SvmhwChNKxDZErecTlF6qTOLnNW/CCcHmDaSmG2MXUeP1w9g=="],
"@types/pbf": ["@types/pbf@3.0.5", "", {}, "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
@ -1004,7 +1000,7 @@
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.34", "", { "dependencies": { "@vue/compiler-dom": "3.5.34", "@vue/shared": "3.5.34" } }, "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/devtools-api": ["@vue/devtools-api@8.1.2", "", { "dependencies": { "@vue/devtools-kit": "^8.1.2" } }, "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg=="],
"@vue/devtools-core": ["@vue/devtools-core@8.1.2", "", { "dependencies": { "@vue/devtools-kit": "^8.1.2", "@vue/devtools-shared": "^8.1.2" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-ZGGyaSBP4/+bN2Nd9ZHNYAVDRIzMw1rv2RyXWtyZlo6mQal+IDmTvKY4V+DjAEBhaXt30mHmsgYp1yXJ/2tIWg=="],
@ -1100,8 +1096,6 @@
"bare-url": ["bare-url@2.4.3", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ=="],
"base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q=="],
@ -1114,8 +1108,6 @@
"birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@ -1196,8 +1188,6 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"consolidate": ["consolidate@0.15.1", "", { "dependencies": { "bluebird": "^3.1.1" } }, "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@ -1762,8 +1752,6 @@
"magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
"mailgun.js": ["mailgun.js@8.2.2", "", { "dependencies": { "axios": "^1.3.3", "base-64": "^1.0.0", "url-join": "^4.0.1" } }, "sha512-po/KtofzrTuKhHLenbmliDsVVOFANwcfDFUGnggwnyZJmZz7JgBlV6nzK9o2Fk+OK2SiBmJTK25RbkAj57Hd+Q=="],
"mapbox-gl": ["mapbox-gl@3.24.0", "", { "dependencies": { "@mapbox/mapbox-gl-supported": "^3.0.0", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.6", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "^3.2.5", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "cheap-ruler": "^4.0.0", "csscolorparser": "~1.0.3", "earcut": "^3.0.1", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.4", "kdbush": "^4.0.2", "martinez-polygon-clipping": "^0.8.1", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.0.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0" } }, "sha512-R+FdFUB3DnoE5FYASV7lGSiRyMkSblcZ2UEy7b2pt7s5ZbCxFIUPXd0E6iAFd8OdvdA2VtbvZZVylzAZNaurjA=="],
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
@ -1848,8 +1836,6 @@
"nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
"nodemailer-mailgun-transport": ["nodemailer-mailgun-transport@2.1.5", "", { "dependencies": { "consolidate": "^0.15.1", "form-data": "^4.0.0", "mailgun.js": "^8.0.1" } }, "sha512-hF7POkaxFgMvYEd5aHLaQJI2511ld+aQlQi7JH6bGjhjlZ33cIbTB9PimlIrLu5XC3z76Kde6e65OIwL9lOdTA=="],
"nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@ -2354,8 +2340,6 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"valibot": ["valibot@1.4.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg=="],
@ -2696,7 +2680,7 @@
"vite-plugin-inspect/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.1.2", "", { "dependencies": { "@vue/devtools-kit": "^8.1.2" } }, "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@ -2766,6 +2750,8 @@
"@intlify/vue-i18n-extensions/vue-i18n/@intlify/core-base": ["@intlify/core-base@10.0.8", "", { "dependencies": { "@intlify/message-compiler": "10.0.8", "@intlify/shared": "10.0.8" } }, "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ=="],
"@intlify/vue-i18n-extensions/vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@mapbox/node-pre-gyp/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],

View file

@ -1,52 +0,0 @@
# micro-frontends/
Full-application-sized custom elements (Vue + Vite) that bundle into single IIFE files and register as custom elements. Not to be confused with `web-components/` (small/single-purpose things).
## What lives here
- `_template/` — shared blueprint: components, composables, config loaders that every micro-frontend imports from. The leading underscore marks this as "not a shippable micro-frontend."
- `<name>/` — one folder per micro-frontend. Each has its own `package.json`, `vite.config.js`, `src/`. Built output goes either:
- `../../public/js/<name>.iife.js` (public — served at `/js/<name>.iife.js`)
- `./app/<name>.iife.js` (private — stays inside the widget, never served)
## Naming
- No `-mfe` suffix on folder names. The parent `micro-frontends/` folder makes the category clear.
- Custom-element tag name matches the folder name in kebab-case (e.g. `<doxa-simple-map>`).
## Build
```bash
cd micro-frontends/doxa-simple-map
npm install
npm run build
```
Build output lands at `doxa-life/public/js/doxa-simple-map.iife.js`. Nuxt serves it as a static asset; any external site can `<script src="https://doxa.life/js/doxa-simple-map.iife.js">`.
## Sharing via `_template/`
Each widget's `vite.config.js` adds an alias to the shared blueprint:
```js
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@template': fileURLToPath(new URL('../_template/src', import.meta.url))
}
}
```
Then import in code:
```js
import Legend from '@template/components/Legend.vue' // shared
import Profile from '@/app-profiles/doxa-simple-map.vue' // local to this micro-frontend
```
Vite traces imports + tree-shakes — each micro-frontend bundles only what it actually imports.
## See also
- `web-components/` — smaller, single-purpose custom elements.
- `doxa-life/public/js/` — where public bundles are served from.

View file

@ -1,34 +0,0 @@
# web-components/
Small, single-purpose custom elements (Vue + Vite) — smaller in scope than anything in `micro-frontends/`.
**Empty for now.** Populate as small reusable custom elements emerge (e.g. a map legend alone, a standalone language picker, a prayer timer).
## Difference vs `micro-frontends/`
| | web-components/ | micro-frontends/ |
|---|---|---|
| Size | Small, single-purpose | Full app — sub-menus, panels, application profiles |
| UI composition | One or two components | Many components, often its own routing / state |
| Example | Chat bubble, form field, language picker | Map with filters, dashboard, full research tool |
| Build output | `public/js/<name>.iife.js` | `public/js/<name>.iife.js` (same destination) |
The distinction is granularity (size) — both build to the same `public/js/` location and embed via `<script>` + custom element. Angular Architects call the larger ones "coarse-grained web components that represent entire domains" — that maps to `micro-frontends/` here.
## When to create one
- Does it fit in ~1 Vue component with maybe 1–2 children? → `web-components/`
- Does it have its own menu structure, data sources, or application profiles? → `micro-frontends/`
When in doubt, start in `web-components/` and promote later if it grows.
## Build pattern (same as micro-frontends)
```bash
cd web-components/my-element
npm install
npm run build
# → doxa-life/public/js/my-element.iife.js
```
Import from shared blueprint (if needed) via the same `@template` alias that micro-frontends use — but prefer keeping `web-components/` small and dependency-free.

View file

@ -154,6 +154,11 @@ export default defineNuxtConfig({
},
routeRules: {
// LEGACY MAP PATHS HEAL THEMSELVES: the drop-in moved to /js/doxa-maps-build/
// (one output folder under the js path). Any old /js/doxa-maps/... link —
// bookmarks, muscle memory, older embeds — 301s to the current location.
'/js/doxa-maps/**': { redirect: { to: '/js/doxa-maps-build/doxa-maps/**', statusCode: 301 } },
// Content-hashed build assets never change under the same URL — cache forever.
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
// Fonts/images use stable filenames, so avoid `immutable`: a 7-day cache
@ -245,9 +250,8 @@ export default defineNuxtConfig({
oauthDcrDefaultScopes: ['pages.view', 'pages.write', 'pages.publish'],
databaseUrl: process.env.DATABASE_URL || '',
jwtSecret: process.env.JWT_SECRET || '',
mailgunApiKey: process.env.MAILGUN_API_KEY || '',
mailgunDomain: process.env.MAILGUN_DOMAIN || '',
mailgunHost: process.env.MAILGUN_HOST || '',
sendgridApiKey: process.env.SENDGRID_API_KEY || '',
sendgridHost: process.env.SENDGRID_HOST || 'api.sendgrid.com',
smtpFrom: process.env.SMTP_FROM || '',
smtpFromName: process.env.SMTP_FROM_NAME || '',
s3Endpoint: process.env.S3_ENDPOINT || '',

View file

@ -45,7 +45,6 @@
"mapbox-gl": "^3.22.0",
"markdown-it": "^14.1.1",
"nodemailer": "^7.0.6",
"nodemailer-mailgun-transport": "^2.1.5",
"nuxt": "^4.4.2",
"postgres": "^3.4.7",
"tailwindcss": "^4.2.2",
@ -61,7 +60,6 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/markdown-it": "^14.1.2",
"@types/nodemailer": "^7.0.1",
"@types/nodemailer-mailgun-transport": "^1.4.6",
"@types/turndown": "^5.0.6",
"eslint": "^10.2.0",
"sass": "^1.99.0",

173
public/embed/map/index.html Normal file
View file

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="robots" content="noindex" />
<title>Doxa map</title>
<link rel="icon" href="/favicon.ico">
<!--
Embeddable map page. Renders one Doxa map with no site chrome so any
website can drop it in an iframe:
<iframe src="https://doxa.life/embed/map?tab=prayer"
width="100%" height="600" style="border:none"
allowfullscreen loading="lazy"></iframe>
Query parameters (all optional):
tab prayer | engagement | adoption. A comma list renders a tab bar
with the first entry open; "all" is every tab. Default: prayer.
lang UI language: ar de en es fr hi it pt ro ru zh. Default: en.
map "research" loads the multi-layer research map instead of the
simple map (tab is then ignored).
utm_source
Appended as utm_source to the people-group sign-up links the
popup opens on the prayer site, so sign-ups can be attributed
to the embedding website.
The Mapbox token is fetched from /api/maps/token at load so it never
lands in this file. People-group data comes from the prayer API, whose
origin is set on window.MAP_APP_API_URL before the bundle loads.
base href="/" so bundle asset fetches resolve against the site root,
not /embed/map/.
-->
<base href="/">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
background: #f3f3f1;
overflow: hidden;
}
doxa-map, doxa-research-map {
display: block;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
doxa-map:fullscreen, doxa-map:-webkit-full-screen,
doxa-research-map:fullscreen, doxa-research-map:-webkit-full-screen {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
}
.embed-error {
padding: 20px;
max-width: 600px;
margin: 40px auto;
background: #fff3f3;
border: 1px solid #e05a5a;
color: #7a1f1f;
font: 13px/1.5 ui-monospace, monospace;
border-radius: 4px;
}
.embed-error code { background: #fde3e3; padding: 1px 5px; border-radius: 2px; }
</style>
</head>
<body>
<script>
(async function init() {
const params = new URLSearchParams(window.location.search);
// ── Language ─────────────────────────────────────────────────────────
// The bundle's i18n reads <html lang> when the element is created, so
// it must be set before the custom element is appended.
const LOCALES = ['ar', 'de', 'en', 'es', 'fr', 'hi', 'it', 'pt', 'ro', 'ru', 'zh'];
const requestedLang = (params.get('lang') || 'en').split('-')[0].toLowerCase();
const lang = LOCALES.includes(requestedLang) ? requestedLang : 'en';
document.documentElement.lang = lang;
document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
// ── Tabs ─────────────────────────────────────────────────────────────
const TAB_IDS = ['prayer', 'engagement', 'adoption'];
const rawTab = (params.get('tab') || 'prayer').toLowerCase();
const wanted = rawTab === 'all' ? TAB_IDS : rawTab.split(',').map(s => s.trim());
const tabIds = [...new Set(wanted.filter(id => TAB_IDS.includes(id)))];
if (tabIds.length === 0) tabIds.push('prayer');
const tabs = tabIds.map(id => ({ id, colorStrategy: id, legend: id, popup: id }));
const isResearch = params.get('map') === 'research';
// ── Prayer API origin ────────────────────────────────────────────────
// Read by the bundle's getApiBaseUrl(); without it the map fetches
// relative /api/people-groups/* paths, which this site does not serve.
window.MAP_APP_API_URL = 'https://pray.doxa.life';
// ── Sign-up link attribution ─────────────────────────────────────────
// The popup opens its Pray links with window.open, so links bound for
// the prayer site are rewritten before the bundle loads: the bundle's
// own source=doxalife tag is dropped and, when this page was given a
// utm_source, that is set instead.
const utmSource = (params.get('utm_source') || '').trim().slice(0, 100);
const prayHost = new URL(window.MAP_APP_API_URL).hostname;
const nativeOpen = window.open.bind(window);
window.open = function (url, ...rest) {
try {
const target = new URL(url, window.location.href);
if (target.hostname === prayHost) {
target.searchParams.delete('source');
if (utmSource) target.searchParams.set('utm_source', utmSource);
url = target.toString();
}
} catch (_) {
// Not a URL: pass through unchanged.
}
return nativeOpen(url, ...rest);
};
function showError(message) {
const box = document.createElement('div');
box.className = 'embed-error';
box.innerHTML = message;
document.body.appendChild(box);
}
// ── Mapbox token ─────────────────────────────────────────────────────
let token;
try {
const res = await fetch('/api/maps/token');
if (!res.ok) throw new Error('Token endpoint returned ' + res.status);
token = (await res.json()).token;
if (!token) throw new Error('Token endpoint returned an empty token');
// The map bundles only accept public keys.
if (!token.startsWith('pk.')) throw new Error('Expected a pk. token, got ' + token.slice(0, 3) + '…');
} catch (err) {
console.error('[embed-map] Failed to fetch Mapbox token:', err);
showError(
'<strong>Could not load the Mapbox token from <code>/api/maps/token</code>.</strong><br><br>' +
'Check that <code>NUXT_PUBLIC_MAPBOX_TOKEN</code> is set to a public (pk.) token ' +
'in the host environment. Error: ' + err.message
);
return;
}
// ── Render the custom element ────────────────────────────────────────
const config = isResearch
? { profile: 'research-map', tk: token, dataSource: 'pray-tools', instanceId: 'embed-research' }
: { profile: 'doxa-simple-map', tk: token, dataSource: 'pray-tools', instanceId: 'embed-map', tabs };
const el = document.createElement(isResearch ? 'doxa-research-map' : 'doxa-map');
el.setAttribute('profile-config', JSON.stringify(config));
document.body.appendChild(el);
// ── Load the bundle (registers the custom element) ───────────────────
const s = document.createElement('script');
// Bundles live in the doxa-maps-build drop-in (one output folder under /js).
s.src = isResearch
? '/js/doxa-maps-build/doxa-maps/doxa-research-map/doxa-research-map.js'
: '/js/doxa-maps-build/doxa-maps/doxa-simple-map/doxa-simple-map.js';
s.onerror = () => showError('<strong>Could not load the map bundle</strong> <code>' + s.src + '</code>.');
document.head.appendChild(s);
})();
</script>
</body>
</html>

View file

@ -1,24 +0,0 @@
# doxa-maps-build — embeddable maps (drop-in)
Pre-built, self-contained map web components. Drop this folder onto any static host and
every map is embeddable via a `<script>` tag or an `<iframe>` — no build step on the host.
## Embed a map (web component)
```html
<script src="/doxa-maps-build/doxa-maps/<bundle>/<bundle>.js"></script>
<bundle-tag profile-config='{"profile":"<profile>","tk":"YOUR_MAPBOX_TOKEN"}'></bundle-tag>
```
## Or embed via iframe (the generated page)
```html
<iframe src="/doxa-maps-build/doxa-maps/<bundle>/<profile>/index.html"
width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>
```
## Contents
- `doxa-maps/<bundle>/<bundle>.js` — one self-contained web-component bundle per map
- `doxa-maps/<bundle>/<profile>/index.html` — a standalone embeddable page per map
- `doxa-maps/manifest.json` — machine-readable index of bundles/profiles (+ SRI hashes)
- `assets/` — shared previews, boundaries, and table vendor assets
Generated by the doxa-maps JAMstack bundler (separate repo). To update, drop a fresh build here.

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"generated": "2026-08-01",
"method": "IEMV — extraction by the fetch/derive scripts, MODELING + VALIDATION here",
"method": "IEMV \u2014 extraction by the fetch/derive scripts, MODELING + VALIDATION here",
"tables": [
{
"name": "people-group-resources",
@ -58,10 +58,6 @@
"name": "platform-reachability",
"rows": 2625
},
{
"name": "field-access-visas",
"rows": 1146
},
{
"name": "gaps-and-opportunities",
"rows": 2106,

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>all-people-groups · all-people-groups</title>
<link rel="icon" href="data:,">
<!-- Mapbox peer loaded ONCE here; the map custom element mounts in this page and
uses window.mapboxgl directly (same realm → no cross-realm container errors). -->
<link rel="preconnect" href="https://api.mapbox.com" crossorigin />
<link href="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css" rel="stylesheet" />
<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js"></script>
<style>
/* BARE full-page map — no chrome. The custom element IS the page, so the
browser URL shares the map itself (design note: "Share shares THE MAP"). */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, all-people-groups { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
padding: 24px; text-align: center; color: #9aa7b8;
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
#status.err { color: #f87171; }
</style>
</head>
<body>
<div id="slot"></div>
<div id="status">loading token…</div>
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
<script src="../all-people-groups.js" integrity="sha384-jD1/0tFzjeM4sSBNhuPeWdbQFIok3ATZ5it0KkbDTDcsozWGOReP4YQLB4S12/0Z" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
// 2. ?tk= URL param — token carried in the shared link
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
// *endpoint* is resolved and fetched:
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
// means "read that global"; anything else is used as a URL — so both a global
// name and a plain URL are safe (no SyntaxError footgun).
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"all-people-groups","dataSource":"pray-tools","instanceId":"all-people-groups-all-people-groups","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";
var BAKED_TOKEN = "";
var DEFAULT_TOKEN_URL = '/api/maps/token';
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
function askToken() {
if (!status) return;
status.className = 'err';
status.innerHTML = 'map needs a Mapbox token — ' +
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
var go = document.getElementById('tkGo');
if (go) go.addEventListener('click', function () {
var v = (document.getElementById('tkIn').value || '').trim();
if (!v) return;
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
mount(v);
});
}
function done() { if (status && status.parentNode) { status.parentNode.removeChild(status); } }
function mount(token) {
if (!token && !TOKEN_FREE) { return askToken(); }
try {
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
var el = document.createElement("all-people-groups");
el.setAttribute('profile-config', JSON.stringify(cfg));
slot.innerHTML = '';
slot.appendChild(el);
done();
} catch (e) { fail(e && e.message ? e.message : String(e)); }
}
function qp(name) {
try { return new URLSearchParams(location.search).get(name) || ''; }
catch (e) { return ''; }
}
function extractToken(data) {
if (typeof data === 'string') return data.trim();
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
return '';
}
// Rungs 1-3b: an actual token → mount directly, no fetch.
// 2b = localStorage: the paste-once store shared with the staging pages
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
var stored = '';
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
var urlTk = qp('tk');
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || BAKED_TOKEN;
if (TOKEN_FREE) { return mount(''); }
if (directToken) { return mount(directToken); }
// Rungs 4-6: resolve a token endpoint and fetch it.
var tokenUrl =
(window.MAP_TOKEN_URL) ||
(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '') ||
DEFAULT_TOKEN_URL;
fetch(tokenUrl)
.then(function (r) {
var ct = r.headers.get('content-type') || '';
return ct.indexOf('json') !== -1 ? r.json() : r.text();
})
.then(function (data) { mount(extractToken(data)); })
.catch(function (e) {
fail('Token fetch failed from ' + tokenUrl + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + (e && e.message ? e.message : e) + ')');
});
})();
</script>
</body>
</html>

View file

@ -0,0 +1,371 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>doxa-maps</title>
<link rel="icon" href="data:,">
<link rel="preconnect" href="https://api.mapbox.com" crossorigin />
<style>/* staging-forge.css — the ONE-PAGE semantic staging theme (root + bundle pages).
design brief 2026-07-11: tree sidebar left on desktop, tree-top + scroll-to-map
on mobile, GEM FRAMES as the hero (each map in its colored jewel frame), quiet
architectural dark, zero folder-hopping. doxa-maps design language. */
:root {
--bg:#0a0e15; --surf:#121a27; --surf2:#0e1623; --line:#223047;
--ink:#eef2f7; --mut:#9aa7b8; --dim:#6b7787;
--cy:#46d4ff; --vi:#b9a6f5; --gold:#ffc24b; --grn:#5ee0a0;
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
background:
radial-gradient(1100px 480px at 82% -12%, rgba(70,212,255,.10), transparent 60%),
radial-gradient(820px 460px at -8% 112%, rgba(185,166,245,.10), transparent 60%),
var(--bg);
color: var(--ink); font-family: var(--sans); -webkit-font-smoothing: antialiased;
}
code { font-family: var(--mono); }
.hidden { display: none !important; }
.empty { color: var(--dim); font: 12px/1.5 var(--mono); padding: 18px; }
/* ── Shell: header strip, then sidebar|stage grid (desktop) ── */
header.top { padding: 14px 18px 10px; border-bottom: 1px solid var(--surf2); }
header.top h1 { font: 800 15px/1.2 var(--sans); letter-spacing: -.01em; }
header.top h1 .ac { color: var(--cy); font-weight: 600; }
header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut); }
.strip { padding: 7px 18px; font: 11px/1.4 var(--mono); color: var(--mut);
border-bottom: 1px solid var(--surf2); background: rgba(255,255,255,.012); }
.strip .pat { color: var(--cy); } .strip .err { color: #f87171; } .strip strong { color: var(--ink); }
/* token bar — visible only while no token resolves */
#tokenBar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
padding: 9px 18px; font: 12px/1.4 var(--sans); color: var(--gold);
border-bottom: 1px solid var(--surf2); background: rgba(255,194,75,.06); }
#tokenBar input { flex: 1 1 220px; max-width: 420px; font: 12px/1 var(--mono);
color: var(--ink); background: var(--surf2); border: 1px solid var(--line);
border-radius: 8px; padding: 8px 10px; }
#tokenBar button { font: 700 11px/1 var(--mono); color: #0a0e15; cursor: pointer;
background: linear-gradient(90deg, var(--cy), var(--vi)); border: 0;
border-radius: 999px; padding: 8px 14px; }
/* ── The one-page layout ── */
#shell { display: grid; grid-template-columns: minmax(280px, 340px) 1fr;
height: calc(100vh - 118px); min-height: 420px; }
/* sidebar: THE semantic tree — navigation and architecture in one */
#treeNav { overflow-y: auto; border-right: 1px solid var(--surf2);
padding: 12px 10px 28px; background: rgba(255,255,255,.008); }
.bgroup { margin-bottom: 8px; border: 1px solid var(--line); border-radius: 10px;
background: var(--surf); overflow: hidden; }
.bgroup summary { list-style: none; cursor: pointer; display: flex; align-items: baseline;
gap: 8px; padding: 10px 12px; font: 700 13px/1.2 var(--sans); }
.bgroup summary::-webkit-details-marker { display: none; }
.bgroup summary .bdot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
align-self: center; background: var(--gem, var(--cy)); box-shadow: 0 0 8px var(--gem, var(--cy)); }
.bgroup summary .bmeta { margin-left: auto; font: 10px/1.3 var(--mono); color: var(--dim); font-weight: 400; }
.bgroup[open] summary { border-bottom: 1px solid var(--surf2); }
.node { display: block; width: 100%; text-align: left; cursor: pointer; color: inherit;
background: transparent; border: 0; border-left: 2px solid transparent;
padding: 8px 12px 8px 22px; transition: background .12s; }
.node:hover { background: rgba(255,255,255,.03); }
.node.active { background: color-mix(in srgb, var(--gem, var(--cy)) 10%, transparent);
border-left-color: var(--gem, var(--cy)); }
.node .nlabel { display: block; font: 600 12.5px/1.3 var(--sans); }
.node .nsub { display: block; font: 10.5px/1.4 var(--mono); color: var(--mut); margin-top: 1px; }
.node .nedit { display: block; font: 9.5px/1.4 var(--mono); color: var(--dim); margin-top: 2px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* kind glyphs */
.node.k-instance .nlabel::before { content: "◇ "; color: var(--grn); }
.node.k-profile .nlabel::before { content: "● "; color: var(--vi); }
.node.k-nested .nlabel::before { content: "◉ "; color: var(--gold); }
/* ── Stage: the ONE live map inside its GEM FRAME (the recurring hero theme) ── */
#stage { display: flex; flex-direction: column; min-width: 0; padding: 12px 16px 16px; }
.stage-head { display: flex; align-items: center; gap: 10px; padding: 0 2px 9px; }
.stage-head .sctag { font: 800 13px/1 var(--mono); color: var(--gem, var(--cy)); }
.stage-head .sckind { font: 8.5px/1 var(--mono); color: var(--dim); border: 1px solid var(--line);
border-radius: 999px; padding: 3px 8px; text-transform: uppercase; letter-spacing: .08em; }
.stage-head .popout { margin-left: auto; font: 600 11px/1 var(--mono); color: var(--mut);
text-decoration: none; border: 1px solid var(--line); border-radius: 999px; padding: 7px 12px; }
.stage-head .popout:hover { color: var(--ink); border-color: #3a4a63; }
/* the gem frame: colored jewel border + inner ring + soft glow in the node's gem color */
.map-slot { flex: 1 1 0; min-height: 0; position: relative; border-radius: 12px;
border: 1px solid var(--gem, var(--cy));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--gem, var(--cy)) 45%, transparent) inset,
0 0 26px rgba(var(--gem-rgb, 70,212,255), .16),
0 8px 30px rgba(0,0,0,.35);
background: #0d1622; overflow: hidden; padding: 8px; }
.map-slot iframe { position: absolute; inset: 8px; width: calc(100% - 16px);
height: calc(100% - 16px); border: 0; border-radius: 8px; }
/* ── Mobile: single column — tree on top, the map takes the FULL SCREEN below ──
(2026-07-11: mobile map = pretty much full screen; scroll must reach it.
100dvh = dynamic viewport height, tracks the mobile URL bar; 100vh = fallback.) */
@media (max-width: 899px) {
html, body { height: auto; } /* let the page actually scroll */
#shell { display: block; height: auto; }
#treeNav { border-right: 0; border-bottom: 1px solid var(--surf2); max-height: 44vh; }
#stage { height: 100vh; height: 100dvh; padding: 8px; } /* full-screen map pane */
.stage-head { padding-bottom: 6px; }
.map-slot { padding: 4px; border-radius: 10px; } /* slimmer gem frame = max map */
.map-slot iframe { inset: 4px; width: calc(100% - 8px); height: calc(100% - 8px); }
.node .nedit { display: none; } /* edit paths are a desktop affordance */
}
</style>
</head>
<body>
<header class="top">
<h1>doxa-maps<span class="ac"> / all-people-groups</span></h1>
<div class="sub">all-people-groups — one bundle, its profiles as paste-anywhere maps</div>
</header>
<div class="strip" id="frameInfo">loading…</div>
<div id="tokenBar" class="hidden"></div>
<div id="shell">
<nav id="treeNav" aria-label="semantic tree"></nav>
<main id="stage"></main>
</div>
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"all-people-groups","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"all-people-groups","scriptUrl":"./all-people-groups/all-people-groups.js","output":"doxa-maps/all-people-groups/all-people-groups.js","integrity":"sha384-jD1/0tFzjeM4sSBNhuPeWdbQFIok3ATZ5it0KkbDTDcsozWGOReP4YQLB4S12/0Z","elements":["all-people-groups"],"profiles":[{"profile":"all-people-groups","file":"all-people-groups.vue","description":"All 12,379 people groups (peoplegroups.org) in four colors — reached,","element":"all-people-groups","iframeUrl":"./all-people-groups/all-people-groups/index.html","scriptUrl":"./all-people-groups/all-people-groups.js","embed":{"iframe":"\u003ciframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-jD1/0tFzjeM4sSBNhuPeWdbQFIok3ATZ5it0KkbDTDcsozWGOReP4YQLB4S12/0Z\" crossorigin=\"anonymous\">\u003c/script>\n\u003call-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/all-people-groups>"},"bundle":"all-people-groups"}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
* design brief (2026-07-11, the merger of all prior staging files):
* - ONE page. No folder-hopping: the semantic tree IS the navigation.
* - Desktop: hierarchical tree SIDEBAR on the left (bundle → profile → instance,
* kind-badged, "where do I edit" hints) · selected map fills the stage on the right.
* - Mobile: tree on top, map below — scroll down to the map. Native responsive,
* no simulator toggle.
* - Token UX: resolve ?tk= → localStorage → none. If none, show a token bar
* (paste once, stored locally in the browser, never in files/links unless you
* use ?tk= on purpose). Maps must Just Work after one paste.
* - Auto-generated: whatever app-profiles exist, the tree detects and shows them.
*
* file:// contract: classic script, manifest inlined at build (window.FORGE.manifest),
* fetch only as dev fallback. No import/export. The DEV-HMR block at the bottom is
* stripped by generate-tree.mjs for built pages.
*/
/* global window, document */
const FORGE = window.FORGE || {}
const SCOPE = FORGE.scope || 'root'
const MODE = FORGE.mode || 'built'
const MANIFEST_URL = FORGE.manifestUrl || './manifest.json'
const HREF_BASE = FORGE.hrefBase || './'
const GEMS = ['#46d4ff', '#b9a6f5', '#ffc24b', '#5ee0a0', '#f97fb5', '#8ef0e4']
const $ = (sel) => document.querySelector(sel)
let tree = null
let flat = [] // flat list of selectable map nodes (profiles + instances)
let activeKey = null // selected node key
// ── Token resolution (maintainer ladder: ?tk= → localStorage → ask once) ─────────
const TK_STORE = 'MAPS_STAGING_TK'
function resolveToken() {
const qp = new URLSearchParams(location.search).get('tk')
if (qp) { try { localStorage.setItem(TK_STORE, qp) } catch (e) {} return qp }
try { return localStorage.getItem(TK_STORE) || '' } catch (e) { return '' }
}
let TK = resolveToken()
function withTk(href) {
if (!TK) return href
return href + (href.includes('?') ? '&' : '?') + 'tk=' + encodeURIComponent(TK)
}
// Carry the page's ?provider= into the embedded map iframe so the engine choice propagates.
function withProvider(href) {
const p = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('provider') : null
return p ? href + (href.includes('?') ? '&' : '?') + 'provider=' + encodeURIComponent(p) : href
}
// ── Load the semantic tree (inline-first; fetch = dev fallback) ──────────────
async function loadTree() {
if (FORGE.manifest) return FORGE.manifest
const res = await fetch(MANIFEST_URL, { cache: 'no-cache' })
if (!res.ok) throw new Error('manifest ' + res.status + ' from ' + MANIFEST_URL)
return res.json()
}
const treeHref = (rel) => HREF_BASE + rel
const hexRgb = (hex) => {
const h = hex.replace('#', '')
return parseInt(h.slice(0,2),16) + ',' + parseInt(h.slice(2,4),16) + ',' + parseInt(h.slice(4,6),16)
}
function profileKind(p) {
if (Array.isArray(p.instances) && p.instances.length) return 'parameterized'
if (p.nested || /nested|tabs|web components/i.test(p.description || '')) return 'nested'
return 'individual'
}
// ── Build the flat selectable list + the sidebar tree ────────────────────────
// Selectable = anything that renders as ONE map page: individual/nested profiles
// and each instance of a parameterized profile.
function buildFlat() {
flat = []
;(tree.bundles || []).forEach((b, bi) => { b.__gem = GEMS[bi % GEMS.length] })
for (const b of tree.bundles || []) {
for (const p of b.profiles || []) {
const kind = profileKind(p)
if (kind === 'parameterized') {
for (const inst of p.instances) {
flat.push({
key: b.bundle + '/' + p.profile + '/' + inst.id,
kind: 'instance', label: inst.title || inst.id,
sub: p.profile + ' · preset "' + inst.id + '"',
edit: b.bundle + '/' + p.profile + '.instances.json',
href: treeHref(b.bundle + '/' + p.profile + '/' + inst.id + '/index.html'),
bundle: b.bundle, element: p.element || (b.elements || [])[0], gem: b.__gem,
})
}
} else {
flat.push({
key: b.bundle + '/' + p.profile,
kind: kind === 'nested' ? 'nested' : 'profile',
label: p.profile,
sub: p.description || (kind === 'nested' ? 'embeds child web components' : 'one web component = one map'),
edit: 'app-profiles/' + b.bundle + '/' + (p.file || p.profile + '.vue'),
href: treeHref(b.bundle + '/' + p.profile + '/index.html'),
bundle: b.bundle, element: p.element || (b.elements || [])[0], gem: b.__gem,
})
}
}
}
}
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
}
// Sidebar: bundles are <details> groups (open by default), each map node a row.
function renderSidebar() {
const el = $('#treeNav')
if (!el) return
const parts = []
const bundles = tree.bundles || []
bundles.forEach((b, bi) => {
const gem = b.__gem || GEMS[bi % GEMS.length]
const rows = flat.filter((n) => n.bundle === b.bundle).map((n) => `
<button class="node k-${n.kind}${n.key === activeKey ? ' active' : ''}" data-key="${esc(n.key)}" style="--gem:${gem}">
<span class="nlabel">${esc(n.label)}</span>
<span class="nsub">${esc(n.sub)}</span>
<code class="nedit">${esc(n.edit)}</code>
</button>`).join('')
parts.push(`
<details class="bgroup" open style="--gem:${gem}">
<summary><span class="bdot"></span>${esc(b.bundle)}
<span class="bmeta">${(b.profiles || []).length} profile${(b.profiles || []).length === 1 ? '' : 's'} · one .js</span>
</summary>
${rows}
</details>`)
})
el.innerHTML = parts.join('') || '<p class="empty">no bundles built yet</p>'
el.querySelectorAll('.node').forEach((btn) =>
btn.addEventListener('click', () => select(btn.dataset.key)))
}
// ── Stage: the ONE live map (iframe of the node's self-contained page) ───────
function renderStage() {
const st = $('#stage')
if (!st) return
const nd = flat.find((n) => n.key === activeKey) || flat[0]
if (!nd) { st.innerHTML = '<p class="empty">no maps in this tree</p>'; return }
activeKey = nd.key
const gem = nd.gem || '#46d4ff'
st.style.setProperty('--gem', gem)
st.style.setProperty('--gem-rgb', hexRgb(gem))
st.innerHTML = `
<div class="stage-head">
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))
}
function select(key) {
activeKey = key
renderStage()
// Mobile: after picking from the tree, bring the map into view (scroll down to it).
if (window.matchMedia('(max-width: 899px)').matches) {
$('#stage')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
// ── Token bar: shown only while no token resolves; paste once, stored locally ─
function renderTokenBar() {
const bar = $('#tokenBar')
if (!bar) return
if (TK) { bar.classList.add('hidden'); return }
bar.classList.remove('hidden')
bar.innerHTML = `
<span>maps need a Mapbox token —</span>
<input id="tkInput" type="password" placeholder="paste pk.… (stays in this browser only)" autocomplete="off" />
<button id="tkSave">use it</button>`
$('#tkSave').addEventListener('click', () => {
const v = $('#tkInput').value.trim()
if (!v) return
TK = v
try { localStorage.setItem(TK_STORE, v) } catch (e) {}
bar.classList.add('hidden')
renderStage()
})
}
// ── Boot ─────────────────────────────────────────────────────────────────────
async function boot() {
const info = $('#frameInfo')
try {
tree = await loadTree()
buildFlat()
if (info) {
info.innerHTML = '<span class="pat">semantic tree</span> — '
+ (tree.bundles || []).length + ' bundle(s) · ' + flat.length + ' map(s) · '
+ '<strong>' + esc(MODE) + '</strong>'
}
renderSidebar()
renderTokenBar()
if (flat.length) { activeKey = flat[0].key; renderStage() }
} catch (e) {
if (info) info.innerHTML = '<span class="err">failed to load semantic tree: ' + esc(e.message) + '</span>'
}
}
boot()
/* dev-hmr stripped for built pages */
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"connectivity-list","dataSource":"pray-tools","instanceId":"cities-internet-connectivity-list","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"connectivity-list","dataSource":"pray-tools","instanceId":"cities-internet-connectivity-list","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"connectivity-map","dataSource":"pray-tools","instanceId":"cities-internet-connectivity-map","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"connectivity-map","dataSource":"pray-tools","instanceId":"cities-internet-connectivity-map","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"data-tables","dataSource":"pray-tools","instanceId":"cities-internet-data-tables","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"data-tables","dataSource":"pray-tools","instanceId":"cities-internet-data-tables","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"cities-internet","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"cities-internet","scriptUrl":"./cities-internet/cities-internet.js","output":"doxa-maps/cities-internet/cities-internet.js","integrity":"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg","elements":["cities-internet-map"],"profiles":[{"profile":"connectivity-list","file":"connectivity-list.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/connectivity-list/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/cities-internet-map>"}},{"profile":"connectivity-map","file":"connectivity-map.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/connectivity-map/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/cities-internet-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/data-tables/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/data-tables/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/cities-internet-map>"}}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"cities-internet","scriptUrl":"./cities-internet/cities-internet.js","output":"doxa-maps/cities-internet/cities-internet.js","integrity":"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD","elements":["cities-internet-map"],"profiles":[{"profile":"connectivity-list","file":"connectivity-list.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/connectivity-list/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/cities-internet-map>"}},{"profile":"connectivity-map","file":"connectivity-map.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/connectivity-map/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/cities-internet-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/data-tables/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/cities-internet-map>"}}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"countries-map","dataSource":"pray-tools","instanceId":"country-posters-countries-map","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"countries-map","dataSource":"pray-tools","instanceId":"country-posters-countries-map","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"country-poster-wall","dataSource":"pray-tools","instanceId":"country-posters-country-poster-wall","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"country-poster-wall","dataSource":"pray-tools","instanceId":"country-posters-country-poster-wall","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

File diff suppressed because one or more lines are too long

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"country-posters","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"country-posters","scriptUrl":"./country-posters/country-posters.js","output":"doxa-maps/country-posters/country-posters.js","integrity":"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX","elements":["country-posters-map"],"profiles":[{"profile":"countries-map","file":"countries-map.vue","description":"","element":"country-posters-map","iframeUrl":"./country-posters/countries-map/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/countries-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/country-posters-map>"}},{"profile":"country-poster-wall","file":"country-poster-wall.vue","description":"","element":"country-posters-map","iframeUrl":"./country-posters/country-poster-wall/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/country-poster-wall/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/country-posters-map>"}}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"country-posters","scriptUrl":"./country-posters/country-posters.js","output":"doxa-maps/country-posters/country-posters.js","integrity":"sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/","elements":["country-posters-map"],"profiles":[{"profile":"countries-map","file":"countries-map.vue","description":"","element":"country-posters-map","iframeUrl":"./country-posters/countries-map/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/country-posters-map>"}},{"profile":"country-poster-wall","file":"country-poster-wall.vue","description":"","element":"country-posters-map","iframeUrl":"./country-posters/country-poster-wall/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/country-posters-map>"}}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../demo-map.js" integrity="sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2" crossorigin="anonymous"></script>
<script src="../demo-map.js" integrity="sha384-3K61vrZb7gtpShtG7dujt+zboQee344tTZT2e55FPLS2tKyjWSbOT/dA70FxQi4Y" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"demo-map","dataSource":"pray-tools","instanceId":"demo-map-demo-map","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"demo-map","dataSource":"pray-tools","instanceId":"demo-map-demo-map","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -27,7 +27,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
<script src="../../demo-map.js" integrity="sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2" crossorigin="anonymous"></script>
<script src="../../demo-map.js" integrity="sha384-3K61vrZb7gtpShtG7dujt+zboQee344tTZT2e55FPLS2tKyjWSbOT/dA70FxQi4Y" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -43,7 +43,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre","qa":true};
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = true;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"demo-map","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"demo-map","scriptUrl":"./demo-map/demo-map.js","output":"doxa-maps/demo-map/demo-map.js","integrity":"sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2","elements":["demo-map"],"profiles":[{"profile":"demo-map","file":"demo-map.vue","description":"Simple two-tab demo map. Tab 1 colors pins by religion (shared","element":"demo-map","iframeUrl":"./demo-map/demo-map/index.html","scriptUrl":"./demo-map/demo-map.js","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./demo-map/demo-map.js\" integrity=\"sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdemo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/demo-map>"},"instances":[{"id":"maplibre","title":"Demo map — MapLibre (token-free)","apiBaseUrl":"","props":{"profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"},"element":"demo-map","iframeUrl":"./demo-map/demo-map/maplibre/index.html","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/maplibre/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"demo-map"}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"demo-map","scriptUrl":"./demo-map/demo-map.js","output":"doxa-maps/demo-map/demo-map.js","integrity":"sha384-3K61vrZb7gtpShtG7dujt+zboQee344tTZT2e55FPLS2tKyjWSbOT/dA70FxQi4Y","elements":["demo-map"],"profiles":[{"profile":"demo-map","file":"demo-map.vue","description":"Simple two-tab demo map. Tab 1 colors pins by religion (shared","element":"demo-map","iframeUrl":"./demo-map/demo-map/index.html","scriptUrl":"./demo-map/demo-map.js","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./demo-map/demo-map.js\" integrity=\"sha384-3K61vrZb7gtpShtG7dujt+zboQee344tTZT2e55FPLS2tKyjWSbOT/dA70FxQi4Y\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdemo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/demo-map>"},"instances":[{"id":"maplibre","title":"Demo map — MapLibre (token-free)","apiBaseUrl":"","props":{"profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"},"element":"demo-map","iframeUrl":"./demo-map/demo-map/maplibre/index.html","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/maplibre/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"demo-map"}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

File diff suppressed because one or more lines are too long

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"doxa-research-map","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"doxa-research-map","scriptUrl":"./doxa-research-map/doxa-research-map.js","output":"doxa-maps/doxa-research-map/doxa-research-map.js","integrity":"sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL","elements":["doxa-research-map"],"profiles":[{"profile":"research-map","file":"research-map.vue","description":"","element":"doxa-research-map","iframeUrl":"./doxa-research-map/research-map/index.html","scriptUrl":"./doxa-research-map/doxa-research-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-research-map/research-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/doxa-research-map>"}}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"doxa-research-map","scriptUrl":"./doxa-research-map/doxa-research-map.js","output":"doxa-maps/doxa-research-map/doxa-research-map.js","integrity":"sha384-9OB3pemF6VMyXIIIr8ttC3fIroDvfpvDoSrxnK48sxq7Y/zkXnqiFBwTg7PEoY7D","elements":["doxa-research-map"],"profiles":[{"profile":"research-map","file":"research-map.vue","description":"","element":"doxa-research-map","iframeUrl":"./doxa-research-map/research-map/index.html","scriptUrl":"./doxa-research-map/doxa-research-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-9OB3pemF6VMyXIIIr8ttC3fIroDvfpvDoSrxnK48sxq7Y/zkXnqiFBwTg7PEoY7D\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/doxa-research-map>"}}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
<script src="../doxa-research-map.js" integrity="sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL" crossorigin="anonymous"></script>
<script src="../doxa-research-map.js" integrity="sha384-9OB3pemF6VMyXIIIr8ttC3fIroDvfpvDoSrxnK48sxq7Y/zkXnqiFBwTg7PEoY7D" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"research-map","dataSource":"pray-tools","instanceId":"doxa-research-map-research-map","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"research-map","dataSource":"pray-tools","instanceId":"doxa-research-map-research-map","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}],"qa":true};
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}],"qa":true};
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../doxa-simple-map.js" integrity="sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a" crossorigin="anonymous"></script>
<script src="../doxa-simple-map.js" integrity="sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"doxa-simple-map","dataSource":"pray-tools","instanceId":"doxa-simple-map-doxa-simple-map","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"doxa-simple-map","dataSource":"pray-tools","instanceId":"doxa-simple-map-doxa-simple-map","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
<script src="../../doxa-simple-map.js" integrity="sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}],"qa":true};
var BASE_CONFIG = {"assetsBase":"../../../../assets","profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"doxa-simple-map","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"doxa-simple-map","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","output":"doxa-maps/doxa-simple-map/doxa-simple-map.js","integrity":"sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a","elements":["doxa-map","doxa-simple-map"],"profiles":[{"profile":"doxa-simple-map","file":"doxa-simple-map.vue","description":"","element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/index.html","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/doxa-map>"},"instances":[{"id":"prayer","title":"Prayer Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/prayer/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/prayer/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"engagement","title":"Engagement Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/engagement/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/engagement/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"adoption","title":"Adoption Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/adoption/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/adoption/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"doxa-simple-map","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","output":"doxa-maps/doxa-simple-map/doxa-simple-map.js","integrity":"sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB","elements":["doxa-map","doxa-simple-map"],"profiles":[{"profile":"doxa-simple-map","file":"doxa-simple-map.vue","description":"","element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/index.html","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/doxa-map>"},"instances":[{"id":"prayer","title":"Prayer Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/prayer/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/prayer/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"engagement","title":"Engagement Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/engagement/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/engagement/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"adoption","title":"Adoption Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/adoption/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/adoption/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

View file

@ -29,7 +29,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-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"charts","dataSource":"pray-tools","instanceId":"gospel-resources-charts","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"charts","dataSource":"pray-tools","instanceId":"gospel-resources-charts","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,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-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"data-tables","dataSource":"pray-tools","instanceId":"gospel-resources-data-tables","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"data-tables","dataSource":"pray-tools","instanceId":"gospel-resources-data-tables","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

File diff suppressed because one or more lines are too long

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"gospel-resources","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"gospel-resources","scriptUrl":"./gospel-resources/gospel-resources.js","output":"doxa-maps/gospel-resources/gospel-resources.js","integrity":"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx","elements":["gospel-resources-map"],"profiles":[{"profile":"charts","file":"charts.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/charts/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/charts/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/data-tables/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/data-tables/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-list","file":"resources-list.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/resources-list/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-map","file":"resources-map.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/resources-map/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"gospel-resources","scriptUrl":"./gospel-resources/gospel-resources.js","output":"doxa-maps/gospel-resources/gospel-resources.js","integrity":"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc","elements":["gospel-resources-map"],"profiles":[{"profile":"charts","file":"charts.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/charts/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/gospel-resources-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/data-tables/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-list","file":"resources-list.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/resources-list/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-map","file":"resources-map.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/resources-map/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/gospel-resources-map>"}}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

View file

@ -29,7 +29,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-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"resources-list","dataSource":"pray-tools","instanceId":"gospel-resources-resources-list","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"resources-list","dataSource":"pray-tools","instanceId":"gospel-resources-resources-list","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,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-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"resources-map","dataSource":"pray-tools","instanceId":"gospel-resources-resources-map","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"resources-map","dataSource":"pray-tools","instanceId":"gospel-resources-resources-map","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,14 +2,38 @@
"schema": "doxa-maps-manifest@1",
"generatedFrom": "app-profiles/*/*.vue",
"embedBase": ".",
"bundleCount": 9,
"profileCount": 17,
"bundleCount": 10,
"profileCount": 18,
"bundles": [
{
"bundle": "all-people-groups",
"scriptUrl": "./all-people-groups/all-people-groups.js",
"output": "doxa-maps/all-people-groups/all-people-groups.js",
"integrity": "sha384-jD1/0tFzjeM4sSBNhuPeWdbQFIok3ATZ5it0KkbDTDcsozWGOReP4YQLB4S12/0Z",
"elements": [
"all-people-groups"
],
"profiles": [
{
"profile": "all-people-groups",
"file": "all-people-groups.vue",
"description": "All 12,379 people groups (peoplegroups.org) in four colors — reached,",
"element": "all-people-groups",
"iframeUrl": "./all-people-groups/all-people-groups/index.html",
"scriptUrl": "./all-people-groups/all-people-groups.js",
"embed": {
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-jD1/0tFzjeM4sSBNhuPeWdbQFIok3ATZ5it0KkbDTDcsozWGOReP4YQLB4S12/0Z\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></all-people-groups>"
},
"bundle": "all-people-groups"
}
]
},
{
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "doxa-maps/cities-internet/cities-internet.js",
"integrity": "sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg",
"integrity": "sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD",
"elements": [
"cities-internet-map"
],
@ -22,8 +46,8 @@
"iframeUrl": "./cities-internet/connectivity-list/index.html",
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></cities-internet-map>"
"iframe": "<iframe src=\"./cities-internet/connectivity-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
}
},
{
@ -34,8 +58,8 @@
"iframeUrl": "./cities-internet/connectivity-map/index.html",
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></cities-internet-map>"
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
}
},
{
@ -46,8 +70,8 @@
"iframeUrl": "./cities-internet/data-tables/index.html",
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></cities-internet-map>"
"iframe": "<iframe src=\"./cities-internet/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-KjP9vcYgXqhfXYxNS9D2sy5IlDpI18BdqjdBqr0nJ/qGWxb/KkP25DjbWbZfcZpD\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
}
}
]
@ -56,7 +80,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "doxa-maps/country-posters/country-posters.js",
"integrity": "sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX",
"integrity": "sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/",
"elements": [
"country-posters-map"
],
@ -69,8 +93,8 @@
"iframeUrl": "./country-posters/countries-map/index.html",
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></country-posters-map>"
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></country-posters-map>"
}
},
{
@ -81,8 +105,8 @@
"iframeUrl": "./country-posters/country-poster-wall/index.html",
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></country-posters-map>"
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-uzVZRaD6wmMfQuDoWGCWc4a4vsTqzUw410fysaPsUTDDYPmg9Opa5/WTEXR1oju/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></country-posters-map>"
}
}
]
@ -91,7 +115,7 @@
"bundle": "demo-map",
"scriptUrl": "./demo-map/demo-map.js",
"output": "doxa-maps/demo-map/demo-map.js",
"integrity": "sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2",
"integrity": "sha384-3K61vrZb7gtpShtG7dujt+zboQee344tTZT2e55FPLS2tKyjWSbOT/dA70FxQi4Y",
"elements": [
"demo-map"
],
@ -104,8 +128,8 @@
"iframeUrl": "./demo-map/demo-map/index.html",
"scriptUrl": "./demo-map/demo-map.js",
"embed": {
"iframe": "<iframe src=\"./demo-map/demo-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></demo-map>"
"iframe": "<iframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-3K61vrZb7gtpShtG7dujt+zboQee344tTZT2e55FPLS2tKyjWSbOT/dA70FxQi4Y\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></demo-map>"
},
"instances": [
{
@ -120,7 +144,7 @@
"element": "demo-map",
"iframeUrl": "./demo-map/demo-map/maplibre/index.html",
"embed": {
"iframe": "<iframe src=\"./demo-map/demo-map/maplibre/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
"iframe": "<iframe src=\"./demo-map/demo-map/maplibre/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
}
],
@ -132,7 +156,7 @@
"bundle": "doxa-research-map",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"output": "doxa-maps/doxa-research-map/doxa-research-map.js",
"integrity": "sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL",
"integrity": "sha384-9OB3pemF6VMyXIIIr8ttC3fIroDvfpvDoSrxnK48sxq7Y/zkXnqiFBwTg7PEoY7D",
"elements": [
"doxa-research-map"
],
@ -145,8 +169,8 @@
"iframeUrl": "./doxa-research-map/research-map/index.html",
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></doxa-research-map>"
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-9OB3pemF6VMyXIIIr8ttC3fIroDvfpvDoSrxnK48sxq7Y/zkXnqiFBwTg7PEoY7D\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></doxa-research-map>"
}
}
]
@ -155,7 +179,7 @@
"bundle": "doxa-simple-map",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"output": "doxa-maps/doxa-simple-map/doxa-simple-map.js",
"integrity": "sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a",
"integrity": "sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -169,8 +193,8 @@
"iframeUrl": "./doxa-simple-map/doxa-simple-map/index.html",
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></doxa-map>"
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-vvuDU7leqbJ6T8jJkoAX9A7tHfZmRwiDdja6fthbXB3zxtA9XEZGQrvu12io7KbB\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></doxa-map>"
},
"instances": [
{
@ -193,7 +217,7 @@
"element": "doxa-map",
"iframeUrl": "./doxa-simple-map/doxa-simple-map/prayer/index.html",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/prayer/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/prayer/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
@ -216,7 +240,7 @@
"element": "doxa-map",
"iframeUrl": "./doxa-simple-map/doxa-simple-map/engagement/index.html",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/engagement/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/engagement/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
},
{
@ -239,7 +263,7 @@
"element": "doxa-map",
"iframeUrl": "./doxa-simple-map/doxa-simple-map/adoption/index.html",
"embed": {
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/adoption/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/adoption/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>"
}
}
]
@ -250,7 +274,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx",
"integrity": "sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc",
"elements": [
"gospel-resources-map"
],
@ -263,8 +287,8 @@
"iframeUrl": "./gospel-resources/charts/index.html",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></gospel-resources-map>"
"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 src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
},
{
@ -275,8 +299,8 @@
"iframeUrl": "./gospel-resources/data-tables/index.html",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></gospel-resources-map>"
"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 src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
},
{
@ -287,8 +311,8 @@
"iframeUrl": "./gospel-resources/resources-list/index.html",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></gospel-resources-map>"
"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 src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
},
{
@ -299,8 +323,8 @@
"iframeUrl": "./gospel-resources/resources-map/index.html",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></gospel-resources-map>"
"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": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KIl3/mrv+4VV6c+jt1JMlwOZ6hNTaVYrahzVEnDNS23NG9lHAbuyoxdX4U8PLIEc\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
}
]
@ -309,7 +333,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi",
"integrity": "sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj",
"elements": [
"my-upg-100-list-map"
],
@ -322,8 +346,8 @@
"iframeUrl": "./my-upg-100-list/poster-wall/index.html",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></my-upg-100-list-map>"
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></my-upg-100-list-map>"
}
},
{
@ -334,8 +358,8 @@
"iframeUrl": "./my-upg-100-list/upg-100-list/index.html",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></my-upg-100-list-map>"
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></my-upg-100-list-map>"
}
}
]
@ -344,7 +368,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "doxa-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq",
"integrity": "sha384-9+QpRNZ+XKx6COLMQmi5v/hYHpQnAEDHqn28eVwnTJperBNgTLoDM6EmOMNgXfSQ",
"elements": [
"research-dashboard-map"
],
@ -357,8 +381,8 @@
"iframeUrl": "./research-dashboard/research-dashboard/index.html",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></research-dashboard-map>"
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-9+QpRNZ+XKx6COLMQmi5v/hYHpQnAEDHqn28eVwnTJperBNgTLoDM6EmOMNgXfSQ\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}
@ -368,7 +392,7 @@
"bundle": "template-bundle",
"scriptUrl": "./template-bundle/template-bundle.js",
"output": "doxa-maps/template-bundle/template-bundle.js",
"integrity": "sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI",
"integrity": "sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ",
"elements": [
"template-bundle"
],
@ -381,8 +405,8 @@
"iframeUrl": "./template-bundle/template-map-a/index.html",
"scriptUrl": "./template-bundle/template-bundle.js",
"embed": {
"iframe": "<iframe src=\"./template-bundle/template-map-a/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></template-bundle>"
"iframe": "<iframe src=\"./template-bundle/template-map-a/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></template-bundle>"
},
"bundle": "template-bundle"
},
@ -394,8 +418,8 @@
"iframeUrl": "./template-bundle/template-map-b/index.html",
"scriptUrl": "./template-bundle/template-bundle.js",
"embed": {
"iframe": "<iframe src=\"./template-bundle/template-map-b/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'></template-bundle>"
"iframe": "<iframe src=\"./template-bundle/template-map-b/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ\" crossorigin=\"anonymous\"></script>\n<template-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></template-bundle>"
},
"bundle": "template-bundle"
}

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"my-upg-100-list","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"my-upg-100-list","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","output":"doxa-maps/my-upg-100-list/my-upg-100-list.js","integrity":"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi","elements":["my-upg-100-list-map"],"profiles":[{"profile":"poster-wall","file":"poster-wall.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/poster-wall/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/poster-wall/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/my-upg-100-list-map>"}},{"profile":"upg-100-list","file":"upg-100-list.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/upg-100-list/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/my-upg-100-list-map>"}}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"my-upg-100-list","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","output":"doxa-maps/my-upg-100-list/my-upg-100-list.js","integrity":"sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj","elements":["my-upg-100-list-map"],"profiles":[{"profile":"poster-wall","file":"poster-wall.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/poster-wall/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/my-upg-100-list-map>"}},{"profile":"upg-100-list","file":"upg-100-list.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/upg-100-list/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/my-upg-100-list-map>"}}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"poster-wall","dataSource":"pray-tools","instanceId":"my-upg-100-list-poster-wall","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"poster-wall","dataSource":"pray-tools","instanceId":"my-upg-100-list-poster-wall","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
<script src="../my-upg-100-list.js" integrity="sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-2glb9WoqMp/D9ngfM8IPoKE8ysdaR0oTiHKtaNoi2bYVtDjJpO4rwKfVv3hYwfmj" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"upg-100-list","dataSource":"pray-tools","instanceId":"my-upg-100-list-upg-100-list","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"upg-100-list","dataSource":"pray-tools","instanceId":"my-upg-100-list-upg-100-list","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"research-dashboard","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-9+QpRNZ+XKx6COLMQmi5v/hYHpQnAEDHqn28eVwnTJperBNgTLoDM6EmOMNgXfSQ","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-9+QpRNZ+XKx6COLMQmi5v/hYHpQnAEDHqn28eVwnTJperBNgTLoDM6EmOMNgXfSQ\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
<script src="../research-dashboard.js" integrity="sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq" crossorigin="anonymous"></script>
<script src="../research-dashboard.js" integrity="sha384-9+QpRNZ+XKx6COLMQmi5v/hYHpQnAEDHqn28eVwnTJperBNgTLoDM6EmOMNgXfSQ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"research-dashboard","dataSource":"pray-tools","instanceId":"research-dashboard-research-dashboard","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"research-dashboard","dataSource":"pray-tools","instanceId":"research-dashboard-research-dashboard","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"template-bundle","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"template-bundle","scriptUrl":"./template-bundle/template-bundle.js","output":"doxa-maps/template-bundle/template-bundle.js","integrity":"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI","elements":["template-bundle"],"profiles":[{"profile":"template-map-a","file":"template-map-a.vue","description":"Minimal example profile A — a colored placeholder, no Mapbox or data.","element":"template-bundle","iframeUrl":"./template-bundle/template-map-a/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-a/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"},{"profile":"template-map-b","file":"template-map-b.vue","description":"Minimal example profile B — a second profile in the same bundle.","element":"template-bundle","iframeUrl":"./template-bundle/template-map-b/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-b/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":18,"bundles":[{"bundle":"template-bundle","scriptUrl":"./template-bundle/template-bundle.js","output":"doxa-maps/template-bundle/template-bundle.js","integrity":"sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ","elements":["template-bundle"],"profiles":[{"profile":"template-map-a","file":"template-map-a.vue","description":"Minimal example profile A — a colored placeholder, no Mapbox or data.","element":"template-bundle","iframeUrl":"./template-bundle/template-map-a/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-a/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"},{"profile":"template-map-b","file":"template-map-b.vue","description":"Minimal example profile B — a second profile in the same bundle.","element":"template-bundle","iframeUrl":"./template-bundle/template-map-b/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-b/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*
@ -285,8 +285,31 @@ function renderStage() {
<span class="sctag">&lt;${esc(nd.element || nd.label)}&gt;</span>
<span class="sckind">${esc(nd.kind)}</span>
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
<button class="popout" id="copyEmbed" type="button" title="Copy an iframe snippet with this host's full URL">copy embed</button>
</div>
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
// COPY EMBED — the partner handoff. The snippet is built AT CLICK TIME from
// location.href, so it always carries the FULL URL of whatever host serves
// this tree (localhost, doxa.life/js/doxa-maps-build/…, any CDN) — embeds
// work no matter where the build folder lives. utm_source placeholder =
// per-partner traffic attribution (see the manifest snippets).
const copyBtn = $('#copyEmbed')
if (copyBtn) copyBtn.addEventListener('click', async () => {
const abs = new URL(nd.href, location.href)
abs.searchParams.set('utm_source', 'YOUR_SOURCE')
const snip = '<iframe src="' + abs.href + '" width="100%" height="600" '
+ 'style="border:none;border-radius:12px;" allowfullscreen></iframe>'
let ok = false
try { await navigator.clipboard.writeText(snip); ok = true } catch (_) {
try {
const ta = document.createElement('textarea')
ta.value = snip; document.body.appendChild(ta); ta.select()
ok = document.execCommand('copy'); ta.remove()
} catch (_) {}
}
copyBtn.textContent = ok ? 'copied ✓' : 'copy failed'
setTimeout(() => { copyBtn.textContent = 'copy embed' }, 1800)
})
// keep the sidebar highlight in sync
document.querySelectorAll('#treeNav .node').forEach((b) =>
b.classList.toggle('active', b.dataset.key === activeKey))

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <template-bundle> custom element. -->
<script src="../template-bundle.js" integrity="sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI" crossorigin="anonymous"></script>
<script src="../template-bundle.js" integrity="sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"template-map-a","dataSource":"pray-tools","instanceId":"template-bundle-template-map-a","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"template-map-a","dataSource":"pray-tools","instanceId":"template-bundle-template-map-a","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <template-bundle> custom element. -->
<script src="../template-bundle.js" integrity="sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI" crossorigin="anonymous"></script>
<script src="../template-bundle.js" integrity="sha384-7oKMpdhxEwHZk1lLpNS5+8FNfK7CEv2BAlgmw65N5F6CkNhduVcSVgIshYLDKYhJ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:
@ -45,7 +45,7 @@
(function () {
var slot = document.getElementById('slot');
var status = document.getElementById('status');
var BASE_CONFIG = {"profile":"template-map-b","dataSource":"pray-tools","instanceId":"template-bundle-template-map-b","assetsBase":"../../../assets","qa":true};
var BASE_CONFIG = {"profile":"template-map-b","dataSource":"pray-tools","instanceId":"template-bundle-template-map-b","assetsBase":"../../../assets"};
// MapLibre pages need NO token: the ladder below is skipped entirely.
var TOKEN_FREE = false;
var TOKEN_SRC = "/api/maps/token.json";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,62 @@
/**
* icons.js — SELF-HOSTED icon set. No CDN, no icon font, no Google/YouTube request.
*
* These are hand-written 24x24 stroke paths in the Lucide idiom (ISC-licensed style:
* 2px stroke, round cap/join, 24x24 box). Shipping them as inline SVG means:
* - zero network requests (works offline, works in Capacitor from local assets)
* - F-Droid safe (no proprietary blobs, no tracking, reproducible from source)
* - no FOUT/icon-font flash
*
* To swap in a real icon package later, replace these path strings — the API
* (`icon(name)` returning an <svg> string) stays the same.
*/
const P = {
menu: '<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>',
map: '<polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/><line x1="9" y1="3" x2="9" y2="18"/><line x1="15" y1="6" x2="15" y2="21"/>',
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a15 15 0 0 1 0 18a15 15 0 0 1 0-18"/>',
heart: '<path d="M19 14c1.5-1.5 3-3.5 3-5.5A4.5 4.5 0 0 0 12 6a4.5 4.5 0 0 0-10 2.5C2 10.5 3.5 12.5 5 14l7 7Z"/>',
// `prayer` = Prayer Mobilization — two hands pressed together (praying): pointed
// fingertips at top, knuckle bulges on the outer edges, wrists curling in at the base.
prayer: '<path d="M12 4.2c-.9 1.4-1.9 2.6-2.9 3.8-1 1.2-1.6 2.6-1.6 4.2v3.3c0 1.3.7 2.4 1.8 3 .5.3 1.1-.1 1.1-.7"/><path d="M12 4.2c.9 1.4 1.9 2.6 2.9 3.8 1 1.2 1.6 2.6 1.6 4.2v3.3c0 1.3-.7 2.4-1.8 3-.5.3-1.1-.1-1.1-.7"/><path d="M12 4.2V17"/>',
list: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1"/><circle cx="4" cy="12" r="1"/><circle cx="4" cy="18" r="1"/>',
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9"/><path d="M16 3.1a4 4 0 0 1 0 7.8"/>',
flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V4s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/>',
target: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/>',
pin: '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"/><circle cx="12" cy="10" r="3"/>',
monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
layers: '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
flask: '<path d="M9 3h6v5l5 11a2 2 0 0 1-2 3H6a2 2 0 0 1-2-3l5-11z"/><line x1="9" y1="3" x2="15" y2="3"/><line x1="6" y1="15" x2="18" y2="15"/>',
share: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/><line x1="15.4" y1="6.5" x2="8.6" y2="10.5"/>',
beaker: '<path d="M6 3h12"/><path d="M8 3v7l-3 8a2 2 0 0 0 2 3h10a2 2 0 0 0 2-3l-3-8V3"/>',
chevron: '<polyline points="9 18 15 12 9 6"/>',
grid: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
tree: '<rect x="9" y="3" width="6" height="5" rx="1"/><rect x="2" y="16" width="6" height="5" rx="1"/><rect x="16" y="16" width="6" height="5" rx="1"/><path d="M12 8v4M5 16v-2h14v2"/>',
dots: '<circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/>',
// `embed` = the <> script/code-embed mark. Signals "this map is the embeddable
// one you paste into a partner site" (2026-07-29, for Prayer Progress).
embed: '<polyline points="9 7 4 12 9 17"/><polyline points="15 7 20 12 15 17"/>',
// `send` = Laborer Mobilization (sending labourers) — a paper-plane/send mark,
// deliberately distinct from `heart` (Prayer Mobilization) and `users` (People Groups).
send: '<path d="M21 3 L3 10.5 L9.5 13.5 L12.5 20 Z"/><line x1="21" y1="3" x2="9.5" y2="13.5"/>',
// `poster` = a printable poster asset (Country Posters / Key City Posters) —
// a framed sheet with a pinned marker, distinct from `map` and `pin`.
poster: '<rect x="4" y="3" width="16" height="18" rx="2"/><circle cx="12" cy="10" r="2.5"/><path d="M8 17h8"/>',
// `search` = Material "search" magnifying glass, hand-drawn to Material's geometry
// (circle r=7.5 centred at 10.5,10.5 with the handle running to ~20,20 at 45°).
// Written out locally — NEVER fetched from a Google/Material CDN. That is a standing
// law, not a style choice: a CDN icon request leaks the end user's IP to Google,
// which in a persecution context is a safety risk. Self-hosted also = works offline
// and inside Capacitor/F-Droid from local files.
search: '<circle cx="10.5" cy="10.5" r="7.5"/><line x1="15.8" y1="15.8" x2="20.5" y2="20.5"/>',
// `book` = Gospel Access (an open book / the Word reaching a place).
book: '<path d="M12 6.5C12 6.5 9.5 4.5 4 4.5v13C9.5 17.5 12 19.5 12 19.5"/><path d="M12 6.5C12 6.5 14.5 4.5 20 4.5v13C14.5 17.5 12 19.5 12 19.5"/>',
// `wifi` = Cities with Internet (connectivity arcs + node). Local, no CDN.
wifi: '<path d="M3.5 9a13 13 0 0 1 17 0"/><path d="M6.8 12.4a8.5 8.5 0 0 1 10.4 0"/><path d="M10 15.7a4 4 0 0 1 4 0"/><circle cx="12" cy="19" r="1"/>',
}
export function icon(name, cls = '') {
const d = P[name] || P.map
return `<svg class="ic ${cls}" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
/**
* languages.js — the language list for the app wrapper's locale dropdown.
*
* PORTED from dx-life's single source of truth: `config/languages.ts`
* (LANGUAGES / ENABLED_LANGUAGES / hideFromSwitcher). Kept DELIBERATELY MINIMAL:
* dx-life's version also carries DeepL codes, Bible translation ids, glossary ids
* and font flags — all of which belong to the CMS/translation pipeline, not to a
* map wrapper. Only the four fields the switcher actually renders are kept, plus
* `dir` (RTL) because the wrapper has to flip layout direction.
*
* WHY NOT A LIBRARY: the switcher is ~30 lines of state over this array. Adding a
* dependency to render a <select> would be more complexity, not less — and every
* candidate assumes a router (dx-life's uses `switchLocalePath()` because it
* routes /es, /fr …). The wrapper is a STATIC app with no locale routing: it just
* needs to emit a code. So: same DATA, simpler MECHANISM.
*
* THE CONTRACT (already built into the bundles — do not reinvent):
* ProfileLoader reads `profile-config.locale` (falls back to `.lang`), lowercases
* it, strips any region suffix ('pt-BR' -> 'pt'), drives the bundle's i18n global
* locale from it, and provides `lang` to descendants. So switching language =
* re-setting profile-config.locale on the mounted element. Nothing else.
*/
export const LANGUAGES = [
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇵🇹' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦', dir: 'rtl' },
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
// Present in dx-life but hidden from its public switcher (hideFromSwitcher: true).
// Kept here, flagged, so the list stays a faithful mirror of the source of truth.
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', hideFromSwitcher: true },
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳', hideFromSwitcher: true },
// enabled:false in dx-life (routable/translatable but not offered): it, zh, ro.
]
/** Languages offered in the dropdown. */
export const SWITCHER_LANGUAGES = LANGUAGES.filter(l => !l.hideFromSwitcher)
export function getLanguage(code) {
return LANGUAGES.find(l => l.code === code)
}
/** Normalise any incoming tag to a supported code ('pt-BR' -> 'pt'; unknown -> 'en'). */
export function normaliseLocale(tag) {
const base = String(tag || '').split('-')[0].toLowerCase()
return LANGUAGES.some(l => l.code === base) ? base : 'en'
}
/** Persisted choice (survives reload), defaulting to the browser language. */
const KEY = 'doxa-maps:locale'
export function loadLocale() {
try {
const saved = localStorage.getItem(KEY)
if (saved) return normaliseLocale(saved)
} catch (_) { /* storage may be unavailable */ }
return normaliseLocale(navigator?.language)
}
export function saveLocale(code) {
try { localStorage.setItem(KEY, code) } catch (_) { /* non-fatal */ }
}

View file

@ -0,0 +1,128 @@
/**
* nav.js — the sidebar data model for the app-wrapper-material layout.
*
* Extends a conventional `SidebarNavItem` shape ({ to, label, icon, children })
* with the two things doxa-maps needs:
* - `kind` : 'item' | 'section' | 'folder'
* section = always-open, labelled group (YouTube "Subscriptions" style)
* folder = collapsible group (click to open/close)
* - `target` : 'both' | 'desktop' | 'mobile' <-- per-target packaging flag.
* 'desktop' items are hidden on mobile AND excluded from the
* Capacitor build's webDir (the mobile build copies fewer bundles).
*
* Order matters: this array renders top-to-bottom, exactly as authored.
* Icons are LOCAL names resolved from ./icons/<name>.svg — no CDN, no icon font.
* (F-Droid requires self-hosted, non-proprietary assets.)
*/
export const NAV = [
// ── PRAYER MOBILIZATION FIRST: the prayer map is the landing view, so this
// section sits at the TOP and its first child is the DEFAULT profile.
// (2026-07-29: "so that the first map they see is the prayer map".)
{
kind: 'section', id: 'prayer-mobilization', label: 'Prayer Mobilization', icon: 'heart', target: 'both',
children: [
// Prayer Progress = the research map opened on its PRAYER tab (colorStrategy
// 'prayerProgress'). That tab is tabs[0] in the bundle, so it is already the
// default view — no separate bundle needed. The `embed` icon (<>) marks it as
// the embeddable/share-first map.
{ kind: 'item', id: 'prayer-progress', label: 'Prayer Progress', icon: 'embed', target: 'both' },
{ kind: 'item', id: 'upg-100-list', label: 'My UPG 100 List', icon: 'list', target: 'both' },
// REMOVED 'Prayer List' (2026-07-29): not a real app.
],
},
// ── LABORER MOBILIZATION — mobilising the labourers/disciple-makers themselves
// (as distinct from mobilising prayer). UPG Neighbor MOVED here from Prayer
// Mobilization: it is a labourer/sending tool, not a prayer tool.
// (2026-07-29).
{
kind: 'section', id: 'laborer-mobilization', label: 'Laborer Mobilization', icon: 'send', target: 'both',
children: [
{ kind: 'item', id: 'upg-neighbor', label: 'UPG Neighbor', icon: 'users', target: 'both' },
],
},
// ── RESEARCH — a TOP-LEVEL section, PEER of Prayer/Laborer Mobilization
// (2026-07-29: "a section to the same degree as Laborer", i.e. a
// sibling, NOT nested under it). Holds the research/general maps.
{
kind: 'section', id: 'research', label: 'Research', icon: 'search', target: 'both',
children: [
{ kind: 'item', id: 'research-map', label: 'Research Map', icon: 'map', target: 'both' },
// 'All People Groups' = the FULL global people-group set (~15,000), as distinct
// from the ~2,100-UPG DX cluster the prayer map plots. Different datasets —
// keep them straight. (The "15k" suffix was dropped from the label at the
// maintainer's request 2026-07-29; the distinction is recorded here instead.)
// ⚠ NO BUNDLE YET — renders the placeholder card.
{ kind: 'item', id: 'all-people-groups', label: 'All People Groups', icon: 'globe', target: 'both' },
// REMOVED 'Reached People Groups' (2026-07-29) — added and withdrawn the
// same round; do not re-add without an explicit ask.
// REMOVED 'Simple Map' (2026-07-29). REASON WORTH KEEPING: the
// doxa-simple-map BUNDLE is not one map — it carries THREE (prayer, adoption,
// engagement). doxa-maps only wants to preview PRAYER, and that is already
// surfaced as 'Prayer Progress' in Prayer Mobilization. Exposing "Simple Map"
// would advertise adoption+engagement views we are not previewing here.
// => If a simple prayer-only view is ever wanted as its own entry, add a
// profile-config that pins doxa-simple-map to its prayer profile only.
],
},
// ── GOSPEL ACCESS — where the gospel can actually REACH: the infrastructure
// (internet-connected cities) and the assets (gospel resources) that make
// access possible. Populated by the maintainer 2026-07-29.
// Both items are wired to real bundles (cities-internet + gospel-resources).
{
kind: 'section', id: 'gospel-access', label: 'Gospel Access', icon: 'book', target: 'both',
children: [
{ kind: 'item', id: 'cities-with-internet', label: 'Cities with Internet', icon: 'wifi', target: 'both' },
{ kind: 'item', id: 'gospel-resources', label: 'Gospel Resources', icon: 'book', target: 'both' },
],
},
// ── 'People Groups' section REMOVED (2026-07-29): it is now empty —
// Countries -> Prayer Mobilization as Country Posters; Impact Map -> Experimental
// maps; People Pins -> deleted outright.
// ── DASHBOARDS — a SECTION (always visible, no folder to open), DESKTOP ONLY
// (excluded from the mobile/Capacitor build). "Big Picture" carries a Desktop Only badge. ──
{
kind: 'section', id: 'dashboards', label: 'Dashboards', icon: 'monitor', target: 'desktop',
children: [
{ kind: 'item', id: 'research-dashboard', label: 'Big Picture', icon: 'chart', target: 'desktop' },
],
},
// ── a FOLDER at the BOTTOM (design note: "experimental maps") ──
{
kind: 'folder', id: 'experimental', label: 'Experimental maps', icon: 'flask', target: 'both',
open: false,
children: [
{ kind: 'item', id: 'nationmapper', label: 'Nationmapper', badge: 'soon', icon: 'share', target: 'both' },
// Country Posters MOVED here from Prayer Mobilization to Experimental.
{ kind: 'item', id: 'countries-map', label: 'Country Posters', icon: 'poster', target: 'both' },
// Outreach Planner = geo-steward, renamed + moved out of the Dashboard folder.
{ kind: 'item', id: 'geo-steward', label: 'Outreach Planner', badge: 'soon', icon: 'layers', target: 'both' },
// Key City Posters MOVED here from Laborer Mobilization.
{ kind: 'item', id: 'key-city-posters', label: 'Key City Posters', icon: 'poster', target: 'both' },
// Full UPG-Neighbor hub (all lenses: cockpit / flower / lang-cluster / send).
// The production 'UPG Neighbor' in Laborer Mobilization is the focused lang-cluster map.
{ kind: 'item', id: 'upg-neighbor-lab', label: 'UPG Neighbor Lab', icon: 'flask', target: 'both' },
],
},
]
/**
* Filter the tree for a build/runtime target. Used by BOTH the UI and the packager.
* RECURSIVE — the tree is no longer only two levels deep (a section may hold a
* 'subgroup', e.g. Laborer Mobilization > Research > Research Map), so a flat
* one-level filter would silently drop or mis-filter nested entries.
*/
export function navForTarget(nav, target /* 'desktop' | 'mobile' */) {
const keep = (n) => n.target === 'both' || n.target === target
const walk = (nodes) => nodes
.filter(keep)
.map((n) => (n.children ? { ...n, children: walk(n.children) } : n))
// prune groups whose children were all filtered out (an empty group is noise)
.filter((n) => !n.children || n.children.length > 0)
return walk(nav)
}

View file

@ -0,0 +1,176 @@
/**
* pane-wrapper — the MFE pane GRID, a layout nested inside app-wrapper-material.
*
* WHAT IT IS
* `app-wrapper-material` owns the chrome (header, side menu, the View fieldset).
* This layout owns the STAGE INSIDE it: a parameterized grid of panes, each pane
* holding a different app-profile MFE that is another VIEW of the same subject.
* `my-upg-100-list` is a family — a map view and a poster-wall view — not one app
* with tabs. Together the two layouts form the app harness that displays the
* custom elements shipped in the vite bundles.
*
* THE MOTION CONTRACT (the design decision everything else follows from)
* • VERTICAL neighbours are ONE SCROLLABLE PAGE. Both panes are live; you scroll
* between them. The edge strips (useEdgeScrollAffordance) are what let you
* escape a scroll-capturing child like a Mapbox canvas.
* • HORIZONTAL movement is a FLY-TO. The target column is NOT loaded until you
* go there, and the origin column is released on arrival.
* So AT MOST TWO PANES ARE EVER ALIVE. That is not an arbitrary cap — it is what
* preserves the ~16-WebGL-context budget the wrapper's teardown has always
* protected. A naive "keep all four panes mounted" grid would break on a phone.
*
* WHY A COLUMN IS ONE SCROLLING PAGE RATHER THAN TWO SWAPPED VIEWS
* A pane you scroll to keeps its sibling on screen during the transition, so the
* relationship between the views stays legible ("the list is UNDER the map"). A
* swap teaches nothing.
*
* 2×2 IS A DEFAULT, NOT A CEILING. Panes carry [row, col] coordinates rather than
* a fixed enum, so the grid expands (2×3, 3×3 …) and contracts (a single column,
* or one pane) from the same code. A profile with no `panes` key behaves exactly
* as before this layout existed.
*
* AR is the reason the grid exists at all: today you see one pane (or one
* scrolling column), but the coordinates mean a future AR client can zoom out and
* see every pane laid out like posters on a wall.
*/
export const PANE_GRID_STYLES = `
/* The column that scrolls. Vertical neighbours stack here and the user scrolls
between them; scroll-snap makes each pane settle cleanly instead of stopping
half-way between two views. */
.pane-col{
position:absolute; inset:0; overflow-y:auto; overflow-x:hidden;
scroll-snap-type:y mandatory; scroll-behavior:smooth;
-webkit-overflow-scrolling:touch;
/* DOUBLE SCROLLBAR FIX. A pane's own content (e.g. the poster wall) scrolls
inside the pane; this column only scrolls BETWEEN panes. With both visible
the desktop showed two bars side by side. The column's bar is hidden — pane
navigation is driven by the View switcher and scroll-snap, not by dragging a
bar — while the inner content keeps its own, which is the one a user wants.
Scrolling still works here; only the indicator is suppressed. */
scrollbar-width:none; /* Firefox */
-ms-overflow-style:none; /* legacy Edge */
}
.pane-col::-webkit-scrollbar{ width:0; height:0; } /* WebKit/Blink */
/* Each pane fills the stage exactly, so "scroll down" moves you one full view. */
.pane{
position:relative; width:100%; height:100%;
scroll-snap-align:start; scroll-snap-stop:always;
}
/* The live custom element fills its pane. */
.pane > .pane-mount{ position:absolute; inset:0; }
/* Placeholder copy for a pane whose profile is not built yet — same card styling
as the wrapper's own placeholder so an unbuilt pane never looks broken. */
.pane-placeholder{
position:absolute; inset:0; display:grid; place-items:center; padding:24px;
text-align:center;
}
.pane-placeholder .pane-card{
max-width:520px; background:var(--card,#fff); border-radius:14px; padding:22px 26px;
box-shadow:0 1px 3px rgba(0,0,0,.12),0 6px 20px rgba(0,0,0,.08);
}
.pane-placeholder h2{ margin:0 0 8px; font-size:18px; font-weight:600 }
.pane-placeholder p{ margin:0; font-size:13px; line-height:1.55; opacity:.78 }
/* A small persistent hint that there IS something below — the affordance the
edge strips reinforce once you start interacting. Hidden on the last pane. */
.pane-more{
position:absolute; left:50%; bottom:10px; transform:translateX(-50%);
display:flex; align-items:center; gap:6px; padding:5px 12px; border-radius:999px;
background:rgba(0,0,0,.55); color:#fff; font-size:11px; letter-spacing:.02em;
pointer-events:none; z-index:6; opacity:.9;
}
`
/**
* Build the pane column for a profile.
*
* @param {Object} spec the PROFILES registry entry
* @param {Object} opts
* @param {HTMLElement} opts.host container to render into (the wrapper's #slot)
* @param {(pane:Object, mountEl:HTMLElement) => Promise<void>} opts.mountPane
* called for each pane that should be LIVE; the caller owns element creation
* so this layout never needs to know about bundles, tags or tokens.
* @returns {{ panes:Object[], scrollTo:(paneId:string)=>void, destroy:()=>void }}
*/
export function buildPaneColumn(spec, { host, mountPane }) {
const panes = Array.isArray(spec?.panes) ? spec.panes : []
// No pane declaration → not a pane profile. The caller falls back to its
// original single-mount path, so nothing changes for existing profiles.
if (panes.length < 2) return null
// v1 renders ONE COLUMN (col 0). Other columns are reachable by fly-to, which is
// a later slice — but the coordinates are already carried on every pane, so that
// work does not require reshaping this data.
const column = panes
.filter(p => (p.at?.[1] ?? 0) === 0)
.sort((a, b) => (a.at?.[0] ?? 0) - (b.at?.[0] ?? 0))
if (column.length < 2) return null
const col = document.createElement('div')
col.className = 'pane-col'
const mounts = new Map()
column.forEach((pane, i) => {
const el = document.createElement('div')
el.className = 'pane'
el.dataset.paneId = pane.id
const mount = document.createElement('div')
mount.className = 'pane-mount'
el.appendChild(mount)
mounts.set(pane.id, mount)
// A pane with no bundle yet still renders — as a labelled placeholder, so the
// column is scrollable and testable before every view profile exists.
if (pane.placeholder) {
const ph = document.createElement('div')
ph.className = 'pane-placeholder'
ph.innerHTML = `<div class="pane-card"><h2>${pane.label}</h2><p>${pane.placeholder}</p></div>`
el.appendChild(ph)
}
// "scroll for more" hint on every pane except the last.
// OPT-OUT per profile via `paneHint: false` in the PROFILES registry entry.
// The header already carries a Map | Posters view switcher, so on a profile
// where that is visible this pill is a second, redundant control that just
// sits on top of the map. Left ON by default so existing profiles are
// unchanged. (design note 2026-08-01: "component that says posters below should
// not exist.")
if (i < column.length - 1 && spec?.paneHint !== false) {
const more = document.createElement('div')
more.className = 'pane-more'
more.innerHTML = `<span>${column[i + 1].label} below</span><span aria-hidden="true">↓</span>`
el.appendChild(more)
}
col.appendChild(el)
})
host.appendChild(col)
return {
panes: column,
/** Animate to a pane by id — this is what the View fieldset tabs call. */
scrollTo(paneId) {
const target = col.querySelector(`.pane[data-pane-id="${paneId}"]`)
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' })
},
/** Which pane is currently filling the viewport (for URL sync). */
current() {
const tops = column.map(p => {
const el = col.querySelector(`.pane[data-pane-id="${p.id}"]`)
return { id: p.id, d: Math.abs((el?.offsetTop ?? 0) - col.scrollTop) }
})
tops.sort((a, b) => a.d - b.d)
return tops[0]?.id || column[0].id
},
onScroll(cb) {
let t = null
const h = () => { if (t) clearTimeout(t); t = setTimeout(cb, 120) }
col.addEventListener('scroll', h, { passive: true })
return () => { if (t) clearTimeout(t); col.removeEventListener('scroll', h) }
},
mountEl: (paneId) => mounts.get(paneId) || null,
destroy() { col.remove(); mounts.clear() },
}
}

View file

@ -1,28 +1,23 @@
import nodemailer from 'nodemailer'
import type { Transporter } from 'nodemailer'
import mailgunTransport from 'nodemailer-mailgun-transport'
import { sendViaSendgrid } from './sendgrid'
import { renderEmailTemplate, type EmailTemplateData } from './email-templates'
const isDevelopment = (process.env.NODE_ENV || 'development') === 'development'
// MailHog transporter, development only. Production sends go through SendGrid.
let transporter: Transporter | null = null
function getEmailConfig() {
try {
const config = useRuntimeConfig()
return {
mailgunApiKey: config.mailgunApiKey || process.env.MAILGUN_API_KEY,
mailgunDomain: config.mailgunDomain || process.env.MAILGUN_DOMAIN,
mailgunHost: config.mailgunHost || process.env.MAILGUN_HOST,
smtpFrom: config.smtpFrom || process.env.SMTP_FROM,
smtpFromName: config.smtpFromName || process.env.SMTP_FROM_NAME,
appName: config.appName || process.env.APP_NAME
}
} catch {
return {
mailgunApiKey: process.env.MAILGUN_API_KEY,
mailgunDomain: process.env.MAILGUN_DOMAIN,
mailgunHost: process.env.MAILGUN_HOST,
smtpFrom: process.env.SMTP_FROM,
smtpFromName: process.env.SMTP_FROM_NAME,
appName: process.env.APP_NAME
@ -30,39 +25,16 @@ function getEmailConfig() {
}
}
function getTransporter(): Transporter {
function getDevTransporter(): Transporter {
if (transporter) return transporter
const config = getEmailConfig()
if (isDevelopment) {
console.log('[Email] Using MailHog (development mode)')
transporter = nodemailer.createTransport({
host: 'localhost',
port: 1025,
secure: false,
tls: { rejectUnauthorized: false }
})
return transporter
}
if (!config.mailgunApiKey || !config.mailgunDomain) {
throw new Error('Mailgun configuration incomplete. Set MAILGUN_API_KEY and MAILGUN_DOMAIN.')
}
console.log('[Email] Using Mailgun HTTP API')
const mailgunOptions: any = {
auth: {
api_key: config.mailgunApiKey,
domain: config.mailgunDomain
}
}
if (config.mailgunHost) {
mailgunOptions.host = config.mailgunHost
}
transporter = nodemailer.createTransport(mailgunTransport(mailgunOptions))
console.log('[Email] Using MailHog (development mode)')
transporter = nodemailer.createTransport({
host: 'localhost',
port: 1025,
secure: false,
tls: { rejectUnauthorized: false }
})
return transporter
}
@ -95,16 +67,31 @@ export async function sendEmail(options: EmailOptions): Promise<boolean> {
fromEmail = fromName ? `${fromName} <${fromAddress}>` : fromAddress
}
const mailOptions = {
from: fromEmail,
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
subject: options.subject,
html: options.html,
text: options.text || options.html.replace(/<[^>]*>/g, '')
}
const text = options.text || options.html.replace(/<[^>]*>/g, '')
const info = await getTransporter().sendMail(mailOptions)
console.log('[Email] Sent successfully:', info.messageId)
let messageId: string | undefined
if (isDevelopment) {
const info = await getDevTransporter().sendMail({
from: fromEmail,
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
subject: options.subject,
html: options.html,
text
})
messageId = info.messageId
} else {
// Recipient arrays go through as-is: SendGrid takes one address object per
// recipient, and a comma-joined string would be read as a single address.
const info = await sendViaSendgrid({
from: fromEmail,
to: options.to,
subject: options.subject,
html: options.html,
text
})
messageId = info.messageId
}
console.log('[Email] Sent successfully:', messageId)
return true
} catch (error) {
if (!process.env.VITEST) {

108
server/utils/sendgrid.ts Normal file
View file

@ -0,0 +1,108 @@
/**
* Twilio SendGrid transport for transactional email. Sends via a direct fetch
* to the v3 mail/send JSON API (no SDK); the response's X-Message-Id header is
* the id SendGrid shows in its Activity Feed and echoes in event webhooks.
*/
export function getSendgridConfig() {
let apiKey = ''
let host = ''
try {
const config = useRuntimeConfig()
apiKey = config.sendgridApiKey
host = config.sendgridHost
} catch {
// Outside the Nitro context (scripts) fall through to process.env
}
return {
apiKey: apiKey || process.env.SENDGRID_API_KEY || '',
host: host || process.env.SENDGRID_HOST || 'api.sendgrid.com'
}
}
export function isSendgridConfigured(): boolean {
return Boolean(getSendgridConfig().apiKey)
}
export interface SendgridSendOptions {
from: string // full address, e.g. '"Doxa" <noreply@mail.doxa.life>'
to: string | string[]
subject: string
html: string
text?: string
replyTo?: string
headers?: Record<string, string>
// Per-request cap; default 30s.
timeoutMs?: number
}
/** Bare lowercase address from a `"Name" <a@b>` or plain `a@b` value. */
function extractEmailAddress(value: string): string | null {
const angle = value.match(/<([^>]+)>/)
const bare = (angle ? angle[1]! : value).trim().toLowerCase()
const match = bare.match(/[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+/)
return match ? match[0] : null
}
/** Display name before the angle brackets of a `"Name" <a@b>` value, if any. */
function extractDisplayName(value: string): string | null {
const angle = value.match(/^\s*"?([^"<]*?)"?\s*</)
const name = angle?.[1]?.trim()
return name || null
}
/** Split a `"Name" <a@b>` style address into SendGrid's {email, name} shape. */
function toAddressObject(value: string): { email: string, name?: string } {
const email = extractEmailAddress(value) || value
const name = extractDisplayName(value)
return name ? { email, name } : { email }
}
/**
* Send one message via the SendGrid v3 API. Throws on a non-2xx response;
* returns the X-Message-Id SendGrid assigned.
*/
export async function sendViaSendgrid(options: SendgridSendOptions): Promise<{ messageId?: string }> {
const { apiKey, host } = getSendgridConfig()
if (!apiKey) {
throw new Error('SendGrid configuration incomplete. Set SENDGRID_API_KEY.')
}
const recipients = (Array.isArray(options.to) ? options.to : [options.to]).map(toAddressObject)
const payload: Record<string, unknown> = {
personalizations: [{ to: recipients }],
from: toAddressObject(options.from),
subject: options.subject,
content: [
{ type: 'text/plain', value: options.text || options.html.replace(/<[^>]*>/g, '') },
{ type: 'text/html', value: options.html }
]
}
if (options.replyTo) payload.reply_to = toAddressObject(options.replyTo)
if (options.headers && Object.keys(options.headers).length) payload.headers = options.headers
// Bound the request with a manually-cleared AbortController rather than
// AbortSignal.timeout(): under Bun the latter's per-call timer is never
// reclaimed, so each send leaks memory.
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), options.timeoutMs || 30000)
try {
const res = await fetch(`https://${host}/v3/mail/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: controller.signal
})
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(`SendGrid responded ${res.status}: ${body}`)
}
return { messageId: res.headers.get('x-message-id') || undefined }
} finally {
clearTimeout(timeout)
}
}