Public build folder is three entries: index.html, assets/, doxa-maps/
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

- index.html is generated by internal/generate-spa.mjs from doxa-maps/manifest.json: one
  self-contained page that lists every bundle, profile and instance, mounts the chosen one,
  and offers Open page / Copy embed; build ends with `bun run spa` (the material shell is no
  longer installed; nav.js, icons.js, languages.js, pane-wrapper/, external/ and _headers
  leave public/)
- The website drop-in follows the same contract: dropin.include in the build config,
  sync-dropin --from-config --prune, public-hygiene --strict fails on any other root entry
- Production bundles no longer carry CSS comments: a build-only Vite plugin strips them from
  every shadow-style block (about 140 per map bundle, 5% smaller); remaining comments reworded
- Share pages and the pin renderer drop their note-style comments
This commit is contained in:
swift-river542 2026-09-16 18:23:14 -05:00
parent 7f6c2286f9
commit 12d1fee133
61 changed files with 1167 additions and 4654 deletions

View file

@ -10,7 +10,7 @@ served web-root by the build (`internal/generate-tree.mjs`), exactly like the pe
| `vendor/` | `/assets/vendor/` | Third-party runtime assets (e.g. table libraries) |
| `posters/` | `/assets/posters/` | Rendered poster webp set (heavy pipeline output; poster walls fetch these) |
| `tables/` | `/assets/tables/` | The MERGED served table set (its index.json is the merge; per-profile `app-profiles/*/data/tables` are build-time sources) |
| `external-bundles/` | `/external/` | Prebuilt map IIFEs vendored from sibling projects (not built here) |
| `external-bundles/` | not served | Prebuilt map IIFEs vendored from sibling projects (not built here); only the optional `layouts/` shell mounted them, so the build no longer copies them into `public/` |
Don't edit anything under `public/` by hand — change it here (or in `app-profiles/*/data/`,
`layouts/*/assets/`) and rebuild.

View file

@ -153,9 +153,9 @@ each bundle registers a **distinct** tag.
| `bun run dev` | Vite dev server + HMR (default `http://localhost:5173`, or the next free port — read the terminal). `/` is the **auto-generated SPA** (playground) of every element, built from `internal/playground/{discover,render,dev-plugin}.mjs`; every bundle is also served at its own html path. Dev-only routes: `/__playground/registry.json` (the registry), `/assets/*` (served from `public/assets/`, else `app-profiles/*/data/`). **Writes nothing to `public/`** — no HMR across that boundary. |
| `bun run smoke:playground` | Opens every element of the auto-generated SPA by deep link in Firefox and proves it boots: element defined, shadow DOM populated, no error card, a WebGL canvas for map profiles (checked through nested shadow roots), no HTML-for-JSON data responses. `--json <file>` writes the evidence; exit 1 on any failure. The pre-handoff gate. |
| `bun run smoke:share` | The share round-trip gate (Firefox): opens a host page (`--playground` = every canvas element of the dev SPA; `--url <page> --tag <element>` = the website or any CDN page), clicks Share, reads the URL, fetches it, reopens it in a fresh page and asserts the SAME custom element mounts with a WebGL canvas. `--json <file>`; exit 1 on any failure — proves REQ-1 ("links work wherever the tree is hosted"). |
| `bun run check:public-hygiene [<dir>] [--strict]` | Proves a served folder is fit to publish: scans for local paths, internal folder names, dev-server URLs, JWT-shaped tokens, `.env`/token files, source maps, documentation and staging/preview pages, and prints an inventory (bundles, leaf pages, assets by folder). Exit 1 on a finding (`--strict` also fails on warnings). Run it on the website's drop-in before any public push. |
| `bun run sync:dropin <dest>` | Mirrors the built web-root `public/` into a host checkout (e.g. the website's `public/js/doxa-maps-build/`): adds, updates and deletes to match; **never copies credentials** (`api/maps/token.json`, `token.local.js`) **nor documentation** (`*.md`, `*.txt` — the README and docs stay private with the bundler; a stray doc in the destination is removed); `--protect a,b` keeps host-owned files; `--dry-run` first. The only sanctioned way a map changes on the site: change the bundler → `bun run build` → sync → commit in the host. |
| `bun run build` | `clean` → one `vite build` per `app-profiles/*/` folder → `tree` → `check:drop` → `check:parse`. |
| `bun run check:public-hygiene [<dir>] [--strict]` | Proves a served folder is fit to publish: scans for local paths, internal folder names, dev-server URLs, JWT-shaped tokens, `.env`/token files, source maps, documentation and staging/preview pages, and prints an inventory (bundles, leaf pages, assets by folder). Under `--strict` any root entry other than `index.html`, `assets/` and `doxa-maps/` is a finding ("outside the drop-in contract"). Exit 1 on a finding. Run it on the website's drop-in before any public push. |
| `bun run sync:dropin <dest> [--from-config \| --only a,b] [--prune]` / `bun run deploy:dev` | Syncs the built `public/` into a host checkout (e.g. the website's `public/js/doxa-maps-build/`). **The drop-in contract** lives in ONE place — `vite.doxa-maps-build-config.json` → `dropin.include`, exactly three root entries: `index.html` (the root page generated by `internal/generate-spa.mjs`, listing every bundle/profile/instance and mounting the chosen one), `assets/` (data) and `doxa-maps/` (the bundle folders + `manifest.json`). `--from-config` copies only those, `--prune` removes everything else in the destination, so a host carries exactly the served web-root and nothing else at its root. Without an allowlist it is a plain full mirror. Never copies credentials, docs or staging pages; `--protect a,b` keeps host-owned files; `--dry-run` first; prints a copied/unchanged/deleted/pruned summary and the resulting top-level tree. `deploy:dev` = `build` → sync `--from-config --prune` into the website (`DOXA_DROPIN_DEST` overrides the path) — the only sanctioned way a map changes on the site; then commit in both repos. |
| `bun run build` | `clean` → one `vite build` per `app-profiles/*/` folder → `tree` → `check:drop` → `check:parse` → `verify` → `spa` (writes `public/index.html` from the manifest). |
| `bun run build:bundle` | Builds ONE bundle (needs `BUNDLE=<name>`). |
| `bun run build:watch` | Rebuilds into `public/` on every change (integration loop). |
| `bun run clean` | Wipes + recreates the `public/<name>/` output tree. |
@ -175,6 +175,11 @@ CSS is injected by JS (`cssCodeSplit: false`), so there is no separate `.css`. T
name comes from `vite.doxa-maps-build-config.json` `name` (default `doxa-maps`).
`emptyOutDir: false` keeps sibling bundles intact when you rebuild one.
**Shadow-style comments are stripped from production bundles.** Component CSS is injected
from `useShadowStyles(\`…\`)` template literals that ship verbatim, so `internal/vite-plugin-strip-shadow-css-comments.mjs`
(build only, `enforce: 'pre'`) removes the `/* … */` blocks inside those literals and collapses the blank lines
they leave; a literal with `${…}` is left untouched with a warning, and dev mode keeps every comment.
**What a built bundle folder contains.** Beyond the IIFE, `bun run tree` generates an
`index.html` **per bundle** (staging/showcase), **per profile**, and **per instance** —
each profile/instance page is a standalone, directly linkable/iframe-able map — plus

327
internal/generate-spa.mjs Normal file
View file

@ -0,0 +1,327 @@
#!/usr/bin/env node
/**
* generate-spa.mjs — write public/index.html, the root page of the served web-root.
*
* public/ holds exactly three things: assets/ (data), <tree>/ (the bundle folders + manifest.json)
* and this one page, which lists every bundle → profile → instance the manifest declares and
* mounts the chosen one. It is GENERATED — never hand-edited — as a single static file: inline
* CSS, inline vanilla JS, every path relative, so the same file works from any static host,
* under a subpath (the website serves it at /js/doxa-maps-build/index.html) and from file://.
*
* At runtime the page fetches <tree>/manifest.json so it stays correct when bundles change; a
* compact copy of the manifest is baked in as the fallback (file://, or the fetch fails). Embed
* snippets are re-derived from the manifest fields with the template generate-tree uses; this
* script checks the derivation against every manifest snippet and bakes any that differ, so the
* fallback is exact without carrying the snippets twice.
*
* The chosen map mounts exactly like its leaf page: same peer engine (Mapbox, or MapLibre for a
* provider:'maplibre' instance), same profile-config (assetsBase, dataSource, instanceId or the
* instance props), window.MAP_APP_API_URL, and the same token ladder — profile-config tk →
* ?tk= on this page's URL → the paste-once localStorage token → baked token → window.MAP_TOKEN_URL
* → the configured tokenUrl → same-origin /api/maps/token → ask once.
*
* Deep link: #/<bundle>/<profile>[/<instance>]. Actions: Open page (the leaf page), Copy embed
* (the manifest's embed.iframe / embed.script).
*
* Usage: node internal/generate-spa.mjs (last step of `bun run build`)
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// Same fields the tree generator bakes into every leaf page (vite.doxa-maps-build-config.json).
function readBuildConfig() {
let cfg = {}
try { cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')) } catch { /* defaults */ }
const str = (k) => (typeof cfg[k] === 'string' ? cfg[k].trim() : '')
let tokenUrl = str('tokenUrl')
if (!tokenUrl) {
try {
const src = JSON.parse(readFileSync(join(ROOT, 'library/api/sources.json'), 'utf8'))
const v = src?._meta?.mapboxTokenUrl || src?.mapboxTokenUrl
if (typeof v === 'string' && v.trim()) tokenUrl = v.trim()
} catch { /* fall through */ }
}
return {
name: str('name') || 'doxa-maps',
title: str('displayTitle') || 'doxa-maps',
tokenUrl: tokenUrl || 'window.MAP_TOKEN_URL',
token: str('token'),
apiBaseUrl: str('apiBaseUrl').replace(/\/+$/, ''),
assetsBaseUrl: str('assetsBaseUrl').replace(/\/+$/, ''),
qaWidget: cfg.qaWidget !== false,
}
}
// ── Shared with the page (injected by source, so Node and the browser run the same code) ──
// The embed snippets exactly as generate-tree writes them into the manifest: profile → iframe +
// script, instance → iframe only. b/p/x are the compact records below.
function snip(b, p, x) {
var u = (x || p).u, tag = (x && x.e) || p.e || b.e[0] || b.b
var r = { iframe: '<iframe src="' + u + '?utm_source=YOUR_SOURCE" width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>' }
if (!x) r.script = '<script src="' + b.s + '"' + (b.i ? ' integrity="' + b.i + '" crossorigin="anonymous"' : '') + '><\/script>\n<' + tag + " profile-config='" + JSON.stringify({ profile: p.p, tk: 'YOUR_MAPBOX_TOKEN', utmSource: 'YOUR_SOURCE' }) + "'></" + tag + '>'
return r
}
// Manifest → compact registry: [{b,s,i,e,p:[{p,e,u,d,em?,in?:[{id,t,a,pr,e,u,em?}]}]}].
// lean = drop every embed snippet the page can re-derive with snip() (the baked fallback);
// the live manifest keeps its snippets verbatim.
function compact(m, lean) {
return (m.bundles || []).map(function (src) {
var b = { b: src.bundle, s: src.scriptUrl || '', i: src.integrity || '', e: src.elements || [], p: [] }
b.p = (src.profiles || []).map(function (sp) {
var p = { p: sp.profile, e: sp.element || '', u: sp.iframeUrl || '', d: sp.description || '' }
var em = sp.embed || {}, d = snip(b, p, null)
if (!lean || em.iframe !== d.iframe || em.script !== d.script) p.em = em
var ins = (sp.instances || []).map(function (sx) {
var x = { id: sx.id, t: sx.title || sx.id, a: sx.apiBaseUrl || '', pr: sx.props || {}, e: sx.element || '', u: sx.iframeUrl || '' }
var xe = sx.embed || {}, xd = snip(b, p, x)
if (!lean || xe.iframe !== xd.iframe) x.em = xe
return x
})
if (ins.length) p.in = ins
return p
})
return b
})
}
const CFG = readBuildConfig()
const TREE = CFG.name
const manifestPath = join(ROOT, 'public', TREE, 'manifest.json')
if (!existsSync(manifestPath)) { console.error(`[generate-spa] missing ${manifestPath} — run \`bun run tree\` first`); process.exit(1) }
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const baked = compact(manifest, true)
const nProfiles = baked.reduce((n, b) => n + b.p.length, 0)
const nInstances = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.in ? p.in.length : 0), 0), 0)
const nOverrides = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.em ? 1 : 0) + (p.in || []).filter(x => x.em).length, 0), 0)
const json = (v) => JSON.stringify(v).replace(/</g, '\\u003c')
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
const html = String.raw`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(CFG.title)}</title>
<link rel="icon" href="data:,">
<style>
:root{--bg:#fff;--bg2:#f6f7f9;--bg3:#eceef2;--bd:#e2e5ea;--fg:#15181d;--mut:#667085;--err:#d13c3c}
@media(prefers-color-scheme:dark){:root{--bg:#111318;--bg2:#181b21;--bg3:#232730;--bd:#2b3038;--fg:#eef0f3;--mut:#9aa3b2;--err:#ff7b7b}}
*{box-sizing:border-box}html,body{margin:0;height:100%}
body{background:var(--bg);color:var(--fg);font:14px/1.4 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;display:flex;flex-direction:column;height:100vh}
header{flex:0 0 auto;display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid var(--bd)}
header b{font-size:15px}.n{color:var(--mut);font-size:12px}
#menu{display:none;margin-left:auto}
button,.btn{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2);color:var(--fg);cursor:pointer;text-decoration:none;white-space:nowrap}
button:hover,.btn:hover{background:var(--bg3)}
.body{flex:1 1 auto;display:flex;min-height:0}
nav{flex:0 0 270px;overflow:auto;border-right:1px solid var(--bd);padding:6px 0 20px}
.gh{padding:12px 14px 4px;font-weight:600;font-size:13px;display:flex;justify-content:space-between;gap:8px}.gh span{color:var(--mut);font-weight:400}
.row{display:block;width:100%;text-align:left;border:0;border-radius:0;background:none;padding:6px 14px;font-size:13px;line-height:1.3;white-space:normal}
.row.in{padding-left:28px}.row code{display:block;font-size:11px;color:var(--mut)}
.row:hover{background:var(--bg2)}.row[aria-current=true]{background:var(--bg3);font-weight:600}
main{flex:1 1 auto;display:flex;flex-direction:column;min-width:0;padding:10px 12px 12px;gap:8px}
.bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.bar .t{flex:1 1 auto;min-width:0}.bar b{font-size:15px}.s{display:block;font-size:12px;color:var(--mut)}
code{font-family:ui-monospace,Menlo,monospace}
#embed{display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2)}
#embed label{font-size:12px;color:var(--mut);display:flex;justify-content:space-between;align-items:center}
#embed textarea{width:100%;height:58px;font:11px/1.35 ui-monospace,Menlo,monospace;border:1px solid var(--bd);border-radius:6px;background:var(--bg);color:var(--fg);padding:6px;resize:vertical}
#slot{flex:1 1 auto;position:relative;min-height:240px;border:1px solid var(--bd);border-radius:10px;overflow:hidden;background:var(--bg2)}
#slot>*{position:absolute;inset:0;width:100%;height:100%;display:block;border:0}
.hint{display:grid;place-items:center;padding:20px;text-align:center;color:var(--mut);font-size:13px}
.hint.err{color:var(--err)}
.hint input{font:12px ui-monospace,Menlo,monospace;padding:6px 8px;border-radius:6px;border:1px solid var(--bd);background:var(--bg);color:var(--fg);width:min(320px,70vw)}
[hidden]{display:none!important}
@media(max-width:700px){.body{flex-direction:column}nav{flex:0 0 auto;max-height:45vh;border-right:0;border-bottom:1px solid var(--bd)}#menu{display:inline-block}}
</style>
</head>
<body>
<header><b>${esc(CFG.title)}</b><span class="n" id="count"></span><button id="menu" type="button" aria-controls="nav" aria-expanded="true">Maps</button></header>
<div class="body">
<nav id="nav" aria-label="Maps"></nav>
<main>
<div class="bar"><div class="t"><b id="ttl">Choose a map</b><span class="s" id="sub"></span></div><a class="btn" id="open" target="_blank" rel="noopener" hidden>Open page</a><button id="emb" type="button" hidden aria-expanded="false">Copy embed</button></div>
<div id="embed" hidden><label>iframe embed <button type="button" data-copy="ifr">Copy</button></label><textarea id="ifr" readonly></textarea><div id="scrw"><label>script embed <button type="button" data-copy="scr">Copy</button></label><textarea id="scr" readonly></textarea></div></div>
<div id="slot"><div class="hint">Choose a map from the list.</div></div>
</main>
</div>
<script>
(function () {
var TREE = ${json(TREE)}, TOKEN_SRC = ${json(CFG.tokenUrl)}, BAKED_TOKEN = ${json(CFG.token)}, API_BASE = ${json(CFG.apiBaseUrl)}, ASSETS_BASE = ${json(CFG.assetsBaseUrl)}, QA = ${json(CFG.qaWidget)};
var DEFAULT_TOKEN_URL = '/api/maps/token';
var BAKED = ${json(baked)};
var snip = ${snip.toString()};
var compact = ${compact.toString()};
// Peer engines, the same tags every leaf page carries; loaded once, before the bundle.
var PEERS = {
mapbox: { css: ['https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css', 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.css'],
js: ['https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js', 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js'] },
maplibre: { css: ['https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.css'], js: ['https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.js'] }
};
var $ = function (id) { return document.getElementById(id) };
var nav = $('nav'), slot = $('slot'), ttl = $('ttl'), sub = $('sub'), open = $('open'), embBtn = $('emb'), embed = $('embed'), ifr = $('ifr'), scr = $('scr'), scrw = $('scrw'), menu = $('menu'), count = $('count');
var ASSETS = ASSETS_BASE || new URL('assets', location.href).href.replace(/\/+$/, '');
var REG = BAKED, active = null, seq = 0, loaded = {}, fetchedToken = '';
var narrow = function () { return window.matchMedia && window.matchMedia('(max-width:700px)').matches };
// Manifest refs are relative to the tree root; this page sits one level above it.
function rel(u) { return /^([a-z]+:|\/)/i.test(u) ? u : TREE + '/' + u.replace(/^\.\//, '') }
function qp(name) { try { return new URLSearchParams(location.search).get(name) || '' } catch (e) { return '' } }
function hint(msg, err) { var d = document.createElement('div'); d.className = 'hint' + (err ? ' err' : ''); d.textContent = msg; slot.innerHTML = ''; slot.appendChild(d); return d }
function assign(t) { for (var i = 1; i < arguments.length; i++) { var s = arguments[i]; for (var k in s) if (Object.prototype.hasOwnProperty.call(s, k)) t[k] = s[k] } return t }
function once(url, kind, attrs) {
if (loaded[url]) return loaded[url];
loaded[url] = new Promise(function (ok, fail) {
var el = document.createElement(kind === 'css' ? 'link' : 'script');
if (kind === 'css') { el.rel = 'stylesheet'; el.href = url } else { el.src = url; assign(el, attrs || {}) }
el.onload = function () { ok() }; el.onerror = function () { delete loaded[url]; fail(new Error('failed to load ' + url)) };
document.head.appendChild(el);
});
return loaded[url];
}
function peer(provider) {
var p = PEERS[provider] || PEERS.mapbox, chain = Promise.resolve();
p.css.forEach(function (u) { once(u, 'css') });
p.js.forEach(function (u) { chain = chain.then(function () { return once(u, 'js') }) });
return chain;
}
function bundle(b) { return once(rel(b.s), 'js', b.i ? { integrity: b.i, crossOrigin: 'anonymous' } : {}) }
function row(key, kind, label, tag, extra) {
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'row' + (extra || ''); btn.setAttribute('data-k', key); btn.setAttribute('data-kind', kind);
var l = document.createElement('span'); l.textContent = label; btn.appendChild(l);
var c = document.createElement('code'); c.textContent = '<' + tag + '>'; btn.appendChild(c);
btn.onclick = function () { location.hash = '#/' + key.split('/').map(encodeURIComponent).join('/') };
return btn;
}
function draw() {
nav.innerHTML = ''; var np = 0, ni = 0;
REG.forEach(function (b) {
var g = document.createElement('div'); g.className = 'grp';
var h = document.createElement('div'); h.className = 'gh'; var hn = document.createElement('span'); hn.textContent = b.b; h.appendChild(hn);
var hc = document.createElement('span'); hc.textContent = b.p.length; h.appendChild(hc); g.appendChild(h);
b.p.forEach(function (p) {
np++; g.appendChild(row(b.b + '/' + p.p, 'profile', p.p, p.e || b.e[0] || b.b));
(p.in || []).forEach(function (x) { ni++; g.appendChild(row(b.b + '/' + p.p + '/' + x.id, 'instance', x.t, x.e || p.e || b.e[0] || b.b, ' in')) });
});
nav.appendChild(g);
});
count.textContent = REG.length + ' bundles · ' + np + ' profiles · ' + ni + ' instances';
}
function find(parts) {
for (var i = 0; i < REG.length; i++) if (REG[i].b === parts[0]) {
var b = REG[i];
for (var j = 0; j < b.p.length; j++) if (b.p[j].p === parts[1]) {
var p = b.p[j];
if (parts.length < 3) return { b: b, p: p, x: null };
for (var k = 0; k < (p.in || []).length; k++) if (p.in[k].id === parts[2]) return { b: b, p: p, x: p.in[k] };
}
}
return null;
}
function select(key) {
var parts = key.split('/').map(function (s) { try { return decodeURIComponent(s) } catch (e) { return s } });
var hit = find(parts);
var rows = nav.querySelectorAll('.row');
for (var i = 0; i < rows.length; i++) rows[i].setAttribute('aria-current', rows[i].getAttribute('data-k') === parts.join('/') ? 'true' : 'false');
if (!hit) { ttl.textContent = 'No such map'; sub.textContent = key; open.hidden = embBtn.hidden = embed.hidden = true; hint('No such map: ' + key, true); return }
var b = hit.b, p = hit.p, x = hit.x, tag = (x && x.e) || p.e || b.e[0] || b.b;
ttl.textContent = x ? x.t : p.p; sub.textContent = b.b + ' · <' + tag + '>' + (p.d ? ' · ' + p.d : '');
open.href = rel((x || p).u); open.hidden = false; embBtn.hidden = false; embed.hidden = true; embBtn.setAttribute('aria-expanded', 'false');
var em = x ? (x.em || snip(b, p, x)) : (p.em || snip(b, p, null));
ifr.value = em.iframe || ''; scr.value = em.script || ''; scrw.hidden = !em.script;
if (narrow()) { nav.hidden = true; menu.setAttribute('aria-expanded', 'false') }
mount(b, p, x, tag);
}
// Same profile-config the leaf page bakes (profile page: profile + dataSource + instanceId;
// instance page: its props), plus the host-supplied share URL of that leaf page.
function mount(b, p, x, tag) {
var cfg = x ? assign({ assetsBase: ASSETS }, x.pr) : { profile: p.p, dataSource: 'pray-tools', instanceId: b.b + '-' + p.p, assetsBase: ASSETS };
if (QA) cfg.qa = true;
cfg.shareUrl = new URL(rel((x || p).u), location.href).href;
window.MAP_APP_API_URL = (x && x.a) || API_BASE;
var provider = cfg.provider === 'maplibre' ? 'maplibre' : 'mapbox', my = ++seq;
hint(provider === 'maplibre' ? 'loading map…' : 'loading token…');
peer(provider).then(function () { return bundle(b) }).then(function () {
if (my !== seq) return;
resolveToken(cfg, provider === 'maplibre', function (token) {
if (my !== seq) return;
try {
var c = token ? assign({}, cfg, { tk: token }) : cfg;
var el = document.createElement(tag); el.setAttribute('profile-config', JSON.stringify(c));
slot.innerHTML = ''; slot.appendChild(el);
} catch (e) { hint(e && e.message ? e.message : String(e), true) }
});
}).catch(function (e) { if (my === seq) hint('Failed to load ' + b.b + ': ' + (e && e.message ? e.message : e), true) });
}
// Mapbox token resolution — the leaf pages' ladder, in precedence order:
// 1. prop (config.tk) 2. ?tk= on this page's URL (stored once for every page of the tree)
// 3. baked token — then, none found, a token ENDPOINT is fetched, first answer wins:
// 4. window.MAP_TOKEN_URL 5. configured TOKEN_SRC 6. same-origin /api/maps/token — else ask once.
function resolveToken(cfg, tokenFree, done) {
if (tokenFree) return done('');
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 = cfg.tk || urlTk || stored || BAKED_TOKEN || fetchedToken;
if (directToken) return done(directToken);
function askToken(prefix) {
var d = hint('', true);
d.innerHTML = (prefix ? '<div></div>' : '') + '<div>map needs a Mapbox token — <input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)"> <button id="tkGo" type="button">use it</button></div>';
if (prefix) d.firstChild.textContent = prefix;
$('tkGo').onclick = function () { var v = ($('tkIn').value || '').trim(); if (!v) return; try { localStorage.setItem('MAPS_STAGING_TK', v) } catch (e) {} done(v) };
}
var candidates = [];
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u) }
addCandidate(window.MAP_TOKEN_URL);
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
addCandidate(DEFAULT_TOKEN_URL);
function extractToken(data) { if (typeof data === 'string') return data.trim(); if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || ''; return '' }
function fetchToken(i, errors) {
if (i >= candidates.length) return askToken('Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')');
var url = candidates[i];
return fetch(url).then(function (r) {
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
return (r.headers.get('content-type') || '').indexOf('json') !== -1 ? r.json() : r.text();
}).then(function (data) {
var tk = extractToken(data); if (!tk) throw new Error(url + ' → no token in response');
fetchedToken = tk; done(tk);
}).catch(function (e) { errors.push(e && e.message ? e.message : String(e)); return fetchToken(i + 1, errors) });
}
fetchToken(0, []);
}
function copy(text, btn) {
var ok = function () { var t = btn.textContent; btn.textContent = 'Copied'; setTimeout(function () { btn.textContent = t }, 1200) };
var fallback = function () { var ta = btn.parentNode.nextSibling; ta.focus(); ta.select(); try { document.execCommand('copy'); ok() } catch (e) {} };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(ok, fallback); else fallback();
}
embed.addEventListener('click', function (e) { var b = e.target.closest ? e.target.closest('[data-copy]') : null; if (b) copy($(b.getAttribute('data-copy')).value, b) });
embBtn.onclick = function () { embed.hidden = !embed.hidden; embBtn.setAttribute('aria-expanded', String(!embed.hidden)) };
menu.onclick = function () { nav.hidden = !nav.hidden; menu.setAttribute('aria-expanded', String(!nav.hidden)) };
try { window.matchMedia('(max-width:700px)').addEventListener('change', function (e) { if (!e.matches) { nav.hidden = false; menu.setAttribute('aria-expanded', 'true') } }) } catch (e) {}
function route() { var h = (location.hash || '').replace(/^#\/?/, ''); if (h) select(h) }
function init(m) {
if (m) { try { REG = compact(m, false) } catch (e) { REG = BAKED } }
draw(); window.addEventListener('hashchange', route); route();
}
// Live manifest first (the page stays right when bundles change); the baked copy is the fallback.
if (typeof fetch === 'function' && location.protocol !== 'file:') {
fetch(TREE + '/manifest.json', { cache: 'no-cache' }).then(function (r) { return r.ok ? r.json() : null }).then(init, function () { init(null) });
} else init(null);
})();
</script>
</body>
</html>
`
const out = join(ROOT, 'public', 'index.html')
writeFileSync(out, html)
console.log(`[generate-spa] public/index.html — ${(html.length / 1024).toFixed(1)} KB · ${baked.length} bundle(s) · ${nProfiles} profile(s) · ${nInstances} instance(s) · ${nOverrides} embed snippet(s) baked verbatim (the rest re-derive)`)

View file

@ -409,7 +409,7 @@ function mapPageHtml(opts) {
${peerBlock}
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, ${esc(tag)} { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -825,16 +825,19 @@ function main() {
// DATA LIVES IN data/, THE WEB-ROOT IS GENERATED (the D1 reorganization):
// source-of-truth folders that used to sit ONLY inside public/ now live in
// data/ and are copied into the served tree here, exactly like boundaries
// above. Served paths are unchanged (public/assets/people-groups, /vendor,
// /external), so no runtime reference moves. public/ stays fully
// regenerable: rm -rf it and `bun run build` rebuilds everything.
// above. Served paths are unchanged (public/assets/people-groups, /vendor),
// so no runtime reference moves. public/ stays fully regenerable: rm -rf it
// and `bun run build` rebuilds everything. The web-root holds only assets/,
// <name>/ and the generated root page (internal/generate-spa.mjs): the
// vendored external bundles (data/external-bundles) were consumed by the
// optional layouts/ shell alone, so they are no longer copied; the frame
// policy is the serving host's (docs/architecture/embeddability.md).
const repoData = resolve(__dirname, '..', 'data')
for (const [srcName, destRel] of [
['people-groups', join(pubAssets, 'people-groups')],
['vendor', join(pubAssets, 'vendor')],
['posters', join(pubAssets, 'posters')],
['tables', join(pubAssets, 'tables')],
['external-bundles', resolve(__dirname, '..', 'public', 'external')],
]) {
const src = join(repoData, srcName)
if (!existsSync(src)) continue
@ -842,29 +845,7 @@ function main() {
mkdirSync(destRel, { recursive: true })
cpSync(src, destRel, { recursive: true })
}
console.log('[generate-tree] copied data/ sources → web-root (people-groups, vendor, external)')
// FRAME-POLICY CONTRACT (docs/architecture/embeddability.md): emit the split
// as a real serving artifact. Netlify/Cloudflare Pages consume /_headers
// automatically when this tree is dropped on them; other hosts ignore the
// file (Traefik reference: serving/traefik-embed-headers.yaml). Direct-embed
// pages are framable anywhere (the product); staging pages are preview-only.
writeFileSync(resolve(__dirname, '..', 'public', '_headers'), `# doxa-maps frame policy — generated by internal/generate-tree.mjs
# (see docs/architecture/embeddability.md — the server owns frame-ancestors)
# Staging surfaces: internal preview only
/${TREE_NAME}/index.html
Content-Security-Policy: frame-ancestors 'self'
/${TREE_NAME}/:bundle/index.html
Content-Security-Policy: frame-ancestors 'self'
# Direct-embed pages (the product): framable on any partner site
/${TREE_NAME}/:bundle/:profile/index.html
Content-Security-Policy: frame-ancestors *
/${TREE_NAME}/:bundle/:profile/:instance/index.html
Content-Security-Policy: frame-ancestors *
`)
console.log('[generate-tree] wrote public/_headers (frame-policy contract)')
console.log('[generate-tree] copied data/ sources → web-root/assets (people-groups, vendor, posters, tables)')
}
for (const dest of dests) {

View file

@ -18,9 +18,13 @@
* staging — preview surfaces: <tree>/index.html and <tree>/<bundle>/index.html (the internal
* staging pages; leaf pages live one level deeper) — reported as WARN unless
* --strict, because the shell root may be product for a host site
* contract — the drop-in contract: a served root holds ONLY index.html (the generated root
* page), assets/ (data) and <tree>/ (the bundle folders + manifest.json). Every other
* root entry — a shell's nav.js, external/, _headers … — is a FINDING under --strict
* ("outside the drop-in contract"); without --strict it is a warning
* Also prints an inventory (bundles, leaf pages, staging pages, assets by folder, total size).
*
* Usage: node internal/public-hygiene-check.mjs [<dir>] [--tree doxa-maps] [--strict] [--json out.json]
* Usage: node internal/public-hygiene-check.mjs [<dir> | --root <dir>] [--tree doxa-maps] [--strict] [--json out.json]
* <dir> defaults to this repo's public/; for the website pass its drop-in folder.
* Exit: 0 clean · 1 findings (or warnings with --strict) · 2 bad arguments
*/
@ -31,7 +35,8 @@ import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const DIR = resolve(args.find(a => !a.startsWith('--') && a !== argOf('--tree') && a !== argOf('--json')) || join(ROOT, 'public'))
const VALUE_FLAGS = new Set(['--tree', '--json', '--root'])
const DIR = resolve(argOf('--root', '') || args.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(args[i - 1])) || join(ROOT, 'public'))
const TREE = argOf('--tree', 'doxa-maps')
const STRICT = args.includes('--strict')
const JSON_OUT = argOf('--json', '')
@ -67,7 +72,16 @@ for (const f of files) {
// staging surfaces: <tree>/index.html and <tree>/<bundle>/index.html (leaf pages are deeper)
const staging = files.filter(f => f.rel === `${TREE}/index.html` || /^[^/]+\/[^/]+\/index\.html$/.test(f.rel) && f.rel.startsWith(`${TREE}/`)).map(f => f.rel)
for (const s of staging) hit(STRICT ? findings : warnings, 'staging', s, 'preview/staging page (internal surface) — exclude from a public mirror or accept knowingly')
if (files.some(f => f.rel === 'index.html')) hit(warnings, 'staging', 'index.html', 'shell root page at the drop-in root — product if the host site uses it, internal otherwise')
// the drop-in contract: only index.html, assets/ and <tree>/ may sit at the root (the same three
// entries vite.doxa-maps-build-config.json dropin.include declares). Strict = every other root
// entry is a finding; otherwise a warning.
const CONTRACT = new Set(['index.html', 'assets', TREE])
const rootEntries = [...new Set(files.map(f => f.rel.split('/')[0]))].sort()
for (const e of rootEntries) {
if (CONTRACT.has(e)) continue
const label = files.some(f => f.rel.startsWith(`${e}/`)) ? `${e}/` : e
hit(STRICT ? findings : warnings, 'contract', label, `outside the drop-in contract (index.html, assets/, ${TREE}/ only)`)
}
// inventory
const inv = {

View file

@ -16,29 +16,47 @@
* removed like any other stale file);
* · staging/preview pages are NEVER copied — <tree>/index.html and <tree>/<bundle>/index.html
* are internal surfaces; partners embed the LEAF pages one level deeper (--keep-staging to
* opt back in; the shell root index.html at the drop-in root is product and stays);
* opt back in);
* · host-owned files can be PROTECTED with --protect (none by default) — never overwritten,
* never deleted.
*
* THE DROP-IN CONTRACT (allowlist): a host does not need the whole web-root. The website takes
* ONLY the bundles it embeds, declared ONCE in vite.doxa-maps-build-config.json under
* "dropin.include" — path prefixes relative to public/ (a folder, or a single file such as
* doxa-maps/manifest.json) — and nothing at the drop-in root.
* --from-config take the allowlist from that build-config (fails if it declares none);
* --only a,b pass an allowlist explicitly (wins over --from-config);
* --prune also delete destination files OUTSIDE the allowlist (an older full mirror:
* the shell, assets/, headers …) so the drop-in ends up exactly the contract.
* --protect is still honoured and nothing outside <dest> is ever touched.
* Without an allowlist the tool is the plain full mirror it always was; --prune then has no
* effect. Files in <dest> outside the allowlist are left alone unless --prune is given.
* Refuses to CREATE <dest>: the drop-in folder is the host's decision, not this script's.
* Gate: `node internal/public-hygiene-check.mjs <dest> --strict` must pass before a public push.
*
* Usage: node internal/sync-dropin.mjs <dest> [--from public] [--tree doxa-maps] [--keep-staging]
* [--protect a.txt,other] [--dry-run]
* e.g. node internal/sync-dropin.mjs /home/qwerty/Workspace/doxa-life/public/js/doxa-maps-build
* Exit: 0 synced (or dry-run printed) · 1 bad arguments / dest missing
* [--only a,b | --from-config] [--prune] [--protect a.txt,other] [--dry-run]
* e.g. node internal/sync-dropin.mjs ../doxa-life/public/js/doxa-maps-build --from-config --prune
* Prints: copied · unchanged · deleted (stale inside the allowlist) · pruned (outside it) ·
* excluded (credentials/docs/staging), then the destination's top-level tree after the sync.
* Exit: 0 synced (or dry-run printed) · 1 bad arguments / dest missing / no allowlist in config
* Review the result with `git -C <host> status` — the host repo is the restore point.
*/
import { readdirSync, statSync, mkdirSync, copyFileSync, rmSync, existsSync, readFileSync } from 'node:fs'
import { readdirSync, statSync, lstatSync, mkdirSync, copyFileSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
import { join, relative, resolve, dirname, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CONFIG = join(ROOT, 'vite.doxa-maps-build-config.json')
const norm = p => p.split(sep).join('/')
const args = process.argv.slice(2)
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
const DEST = args.find(a => !a.startsWith('--') && a !== argOf('--from') && a !== argOf('--protect'))
const VALUE_FLAGS = new Set(['--from', '--tree', '--protect', '--only'])
const argOf = (k, d) => { const i = args.indexOf(k); const v = args[i + 1]; return i >= 0 && v !== undefined && !v.startsWith('--') ? v : d }
const DEST = args.find((a, i) => !a.startsWith('--') && !VALUE_FLAGS.has(args[i - 1]))
const FROM = resolve(ROOT, argOf('--from', 'public'))
const DRY = args.includes('--dry-run')
const PRUNE = args.includes('--prune')
const PROTECT = new Set(String(argOf('--protect', '')).split(',').map(s => s.trim()).filter(Boolean))
// Never ship a credential into a host repo — these are gitignored here for the same reason.
const EXCLUDE = new Set(['api/maps/token.json', 'token.local.js'])
@ -52,12 +70,30 @@ const KEEP_STAGING = args.includes('--keep-staging')
const isStaging = (f) => !KEEP_STAGING && (f === `${TREE}/index.html` || new RegExp(`^${TREE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/[^/]+/index\\.html$`).test(f))
const excluded = (f) => EXCLUDE.has(f) || isDoc(f) || isStaging(f)
if (!DEST) { console.error('[sync-dropin] usage: node internal/sync-dropin.mjs <dest> [--from public] [--protect a,b] [--dry-run]'); process.exit(1) }
const usage = 'usage: node internal/sync-dropin.mjs <dest> [--from public] [--only a,b | --from-config] [--prune] [--protect a,b] [--dry-run]'
if (!DEST) { console.error(`[sync-dropin] ${usage}`); process.exit(1) }
const dest = resolve(DEST)
if (!existsSync(dest) || !statSync(dest).isDirectory()) { console.error(`[sync-dropin] dest does not exist or is not a directory: ${dest} — create the drop-in folder in the host first`); process.exit(1) }
if (!existsSync(FROM)) { console.error(`[sync-dropin] source missing: ${FROM} — run \`bun run build\` first`); process.exit(1) }
const norm = p => p.split(sep).join('/')
// The allowlist: path prefixes relative to <from>. A prefix names a folder (everything under it)
// or one file. --only wins; --from-config reads "dropin.include" from the build-config.
const normPrefix = p => String(p).trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '')
let ALLOW = null, ALLOW_FROM = ''
if (args.includes('--only')) {
ALLOW = String(argOf('--only', '')).split(',').map(normPrefix).filter(Boolean)
ALLOW_FROM = '--only'
} else if (args.includes('--from-config')) {
let cfg
try { cfg = JSON.parse(readFileSync(CONFIG, 'utf8')) } catch (e) { console.error(`[sync-dropin] cannot read ${CONFIG}: ${e.message}`); process.exit(1) }
const inc = cfg?.dropin?.include
if (!Array.isArray(inc) || !inc.length || !inc.every(s => typeof s === 'string')) { console.error(`[sync-dropin] --from-config: ${norm(relative(ROOT, CONFIG))} declares no "dropin": { "include": [ …path prefixes relative to public/… ] }`); process.exit(1) }
ALLOW = inc.map(normPrefix).filter(Boolean)
ALLOW_FROM = `${norm(relative(ROOT, CONFIG))} · dropin.include`
}
if (ALLOW && (!ALLOW.length || ALLOW.some(p => p.split('/').includes('..')))) { console.error(`[sync-dropin] allowlist is empty or climbs out of ${norm(relative(ROOT, FROM))}/: ${JSON.stringify(ALLOW)}`); process.exit(1) }
const allowed = f => !ALLOW || ALLOW.some(p => f === p || f.startsWith(p + '/'))
function walk(dir, base = dir, out = []) {
for (const name of readdirSync(dir)) {
const p = join(dir, name), st = statSync(p)
@ -66,31 +102,63 @@ function walk(dir, base = dir, out = []) {
return out
}
const sha = p => createHash('sha1').update(readFileSync(p)).digest('hex')
// A deletion only ever happens to a path that really lives under <dest> (a symlink that points
// elsewhere is refused, not followed).
const destReal = realpathSync(dest)
const insideDest = f => { try { return realpathSync(join(dest, f)).startsWith(destReal + sep) } catch { return false } }
const allSrc = walk(FROM)
const src = allSrc.filter(f => !excluded(f))
const excludedCount = allSrc.filter(excluded).length
const src = allSrc.filter(f => !excluded(f) && allowed(f))
const outsideSrc = allSrc.length - excludedCount - src.length // built, but not part of the contract
const have = new Set(walk(dest))
let copied = 0, unchanged = 0, deleted = 0, protectedSkips = 0
const excludedCount = allSrc.length - src.length
const result = new Set() // what <dest> holds after the sync
let copied = 0, unchanged = 0, deleted = 0, pruned = 0, kept = 0, protectedSkips = 0, refused = 0
for (const f of src) {
if (PROTECT.has(f)) { protectedSkips++; continue }
if (PROTECT.has(f)) { protectedSkips++; if (have.has(f)) result.add(f); continue }
const s = join(FROM, f), d = join(dest, f)
result.add(f)
if (have.has(f) && statSync(s).size === statSync(d).size && sha(s) === sha(d)) { unchanged++; continue }
if (!DRY) { mkdirSync(dirname(d), { recursive: true }); copyFileSync(s, d) }
copied++
}
const srcSet = new Set(src)
for (const f of have) {
if (srcSet.has(f)) continue
// stale = not in the source, not protected. Credentials and docs found in <dest> are stale too:
// they must not sit in a served folder (a stray README, a token file).
if (srcSet.has(f) || PROTECT.has(f)) continue
if (PROTECT.has(f)) { result.add(f); continue }
if (!allowed(f) && !PRUNE) { kept++; result.add(f); continue }
if (!insideDest(f)) { refused++; result.add(f); continue }
if (!DRY) rmSync(join(dest, f))
deleted++
if (allowed(f)) deleted++; else pruned++
}
// prune directories emptied by deletions (deepest first)
// prune directories emptied by deletions (deepest first; symlinked folders are never entered)
if (!DRY) {
const dirs = []; (function collect(d) { for (const n of readdirSync(d)) { const p = join(d, n); if (statSync(p).isDirectory()) { collect(p); dirs.push(p) } } })(dest)
const dirs = []; (function collect(d) { for (const n of readdirSync(d)) { const p = join(d, n); if (lstatSync(p).isDirectory()) { collect(p); dirs.push(p) } } })(dest)
for (const d of dirs) if (readdirSync(d).length === 0) rmSync(d, { recursive: true })
}
// Top-level tree of the destination (two levels; deeper folders show a file count).
function renderTree(paths, depth = 2) {
const root = {}
for (const p of paths) { let n = root; for (const s of p.split('/')) n = (n[s] ??= {}) }
const isDir = n => Object.keys(n).length > 0
const count = n => Object.values(n).reduce((a, c) => a + (isDir(c) ? count(c) : 1), 0)
const show = (n, indent, level) => {
const names = Object.keys(n).sort((a, b) => (isDir(n[b]) ? 1 : 0) - (isDir(n[a]) ? 1 : 0) || a.localeCompare(b))
for (const name of names) {
if (!isDir(n[name])) { console.log(`${indent}${name}`); continue }
console.log(`${indent}${name}/${level >= depth ? ` (${count(n[name])} files)` : ''}`)
if (level < depth) show(n[name], indent + ' ', level + 1)
}
}
if (!paths.length) console.log(' (empty)'); else show(root, ' ', 1)
}
console.log(`[sync-dropin] ${DRY ? 'DRY RUN — ' : ''}${norm(relative(ROOT, FROM))}/ → ${dest}`)
console.log(`[sync-dropin] copied ${copied} · unchanged ${unchanged} · deleted ${deleted} · protected ${protectedSkips}${PROTECT.size ? ` (${[...PROTECT].join(', ')})` : ''} · credentials/docs excluded ${excludedCount}`)
if (ALLOW) console.log(`[sync-dropin] allowlist (${ALLOW_FROM}): ${ALLOW.join(', ')} — ${PRUNE ? '--prune: everything else in the destination is removed' : 'no --prune: destination files outside it are left alone'}`)
else if (PRUNE) console.log('[sync-dropin] --prune has no effect without an allowlist (--only or --from-config) — full mirror as usual')
console.log(`[sync-dropin] copied ${copied} · unchanged ${unchanged} · deleted ${deleted} · pruned ${pruned} · protected ${protectedSkips}${PROTECT.size ? ` (${[...PROTECT].join(', ')})` : ''} · excluded ${excludedCount} (credentials/docs/staging)${ALLOW ? ` · outside allowlist: ${outsideSrc} built file(s) not shipped${kept ? `, ${kept} destination file(s) left` : ''}` : ''}${refused ? ` · refused ${refused} (resolve outside the destination)` : ''}`)
console.log(`[sync-dropin] destination${DRY ? ' (as it would be)' : ''}: ${result.size} file(s)`)
renderTree([...result])

View file

@ -0,0 +1,155 @@
// vite-plugin-strip-shadow-css-comments — production bundles carry only CSS rules.
//
// Every component injects its shadow-DOM styles from a template literal passed to
// useShadowStyles(`…`). Those literals ship verbatim inside the built IIFE, so any
// CSS block comment written in them would reach the public drop-in as dead weight.
// This build-only plugin rewrites the source of every .vue and .js file under
// library/ and app-profiles/ before the Vue compiler sees it (enforce: 'pre'):
// inside each such literal it removes the block comments and collapses the blank
// lines they leave behind. Nothing outside the literal changes, and dev mode is
// untouched so the comments stay readable while working.
//
// A literal is left exactly as written (with a build warning) when it cannot be
// rewritten safely: it contains a `${…}` substitution, or a comment, string or
// url() inside it never closes. Comment markers that sit inside a CSS string
// (`content:"*/"`) or an unquoted url() are string content and are preserved.
import { resolve, dirname, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SOURCE_ROOTS = ['library', 'app-profiles'].map(d => resolve(ROOT, d) + sep)
const TAG = '[strip-shadow-css-comments]'
// The call site: `useShadowStyles(` then optional whitespace then the opening backtick.
const CALL = /\buseShadowStyles\(\s*`/g
/**
* Find the end of a template literal whose opening backtick is just before `from`.
* Returns { end } = index of the closing backtick, or { reason } when the literal
* contains a substitution or never closes.
*/
function findTemplateEnd(code, from) {
for (let i = from; i < code.length; i++) {
const c = code[i]
if (c === '\\') { i++; continue }
if (c === '`') return { end: i }
if (c === '$' && code[i + 1] === '{') return { reason: 'contains a ${…} substitution', at: i }
}
return { reason: 'template literal never closes', at: code.length }
}
/**
* Remove CSS block comments from one literal's text. Strings ('…' / "…") and
* unquoted url(…) are copied through untouched so a comment marker inside them
* is never mistaken for a comment. Returns { css, comments } or { reason }.
*/
export function stripCssComments(css) {
const n = css.length
let out = ''
let comments = 0
let i = 0
while (i < n) {
const c = css[i]
if (c === '"' || c === "'") {
let j = i + 1
while (j < n && css[j] !== c && css[j] !== '\n') { if (css[j] === '\\') j++; j++ }
if (j >= n) return { reason: 'a CSS string never closes' }
if (css[j] === '\n') { out += css.slice(i, j); i = j; continue } // stray quote: only its own line is treated as a string
out += css.slice(i, j + 1); i = j + 1; continue
}
if (c === '/' && css[i + 1] === '*') {
const j = css.indexOf('*/', i + 2)
if (j < 0) return { reason: 'a CSS comment never closes' }
comments++; i = j + 2; continue
}
if ((c === 'u' || c === 'U') && /^url\(/i.test(css.slice(i, i + 4)) && (i === 0 || !/[\w-]/.test(css[i - 1]))) {
let j = i + 4
while (j < n && (css[j] === ' ' || css[j] === '\t')) j++
if (css[j] === '"' || css[j] === "'") { out += css.slice(i, j); i = j; continue } // quoted url: the string branch owns it
const k = css.indexOf(')', j)
if (k < 0) return { reason: 'an unquoted url() never closes' }
out += css.slice(i, k + 1); i = k + 1; continue
}
out += c; i++
}
if (comments > 0) {
out = out.replace(/[ \t]+(?=\r?\n|$)/g, '') // whitespace left behind at the end of a line
out = out.replace(/\r?\n(?:[ \t]*\r?\n)+/g, '\n') // runs of blank lines
}
return { css: out, comments }
}
/**
* Rewrite every useShadowStyles(`…`) literal in a module's source.
* Returns { code, changed, literals, comments, bytes, skipped: [{ line, reason }] }.
*/
export function stripShadowStyleComments(code) {
const result = { code, changed: false, literals: 0, comments: 0, bytes: 0, skipped: [] }
let out = ''
let last = 0
CALL.lastIndex = 0
let m
while ((m = CALL.exec(code))) {
const open = m.index + m[0].length
const lit = findTemplateEnd(code, open)
if (lit.reason) {
result.skipped.push({ line: lineOf(code, m.index), reason: lit.reason })
CALL.lastIndex = lit.at
continue
}
result.literals++
const original = code.slice(open, lit.end)
const stripped = stripCssComments(original)
if (stripped.reason) {
result.skipped.push({ line: lineOf(code, m.index), reason: stripped.reason })
} else if (stripped.css !== original) {
out += code.slice(last, open) + stripped.css
last = lit.end
result.comments += stripped.comments
result.bytes += original.length - stripped.css.length
result.changed = true
}
CALL.lastIndex = lit.end + 1
}
if (result.changed) result.code = out + code.slice(last)
return result
}
function lineOf(code, index) {
let line = 1
for (let i = 0; i < index; i++) if (code[i] === '\n') line++
return line
}
function isSourceModule(id) {
const q = id.indexOf('?')
if (q >= 0) return false // Vue sub-requests (?vue&type=…) read the already-rewritten descriptor
if (!(id.endsWith('.vue') || id.endsWith('.js'))) return false
return SOURCE_ROOTS.some(root => id.startsWith(root))
}
export function stripShadowCssCommentsPlugin() {
let logger = console
const totals = { files: 0, literals: 0, comments: 0, bytes: 0, skipped: 0 }
return {
name: 'doxa:strip-shadow-css-comments',
apply: 'build',
enforce: 'pre',
configResolved(config) { logger = config.logger },
transform(code, id) {
if (!isSourceModule(id)) return null
const r = stripShadowStyleComments(code)
for (const s of r.skipped) {
totals.skipped++
logger.warn(`${TAG} left untouched ${id.slice(ROOT.length + 1)}:${s.line} — ${s.reason}`)
}
if (!r.changed) return null
totals.files++; totals.literals += r.literals; totals.comments += r.comments; totals.bytes += r.bytes
return { code: r.code, map: null }
},
buildEnd() {
logger.info(`${TAG} removed ${totals.comments} CSS comments (${totals.bytes} bytes) from ${totals.literals} shadow-style literals in ${totals.files} files` +
(totals.skipped ? `; ${totals.skipped} literal(s) left untouched` : ''))
},
}
}

View file

@ -348,7 +348,7 @@ useShadowStyles(`
search bar at top:10, left:10, right:10. The geocoder pill renders ~60px
tall on mobile, so its bottom edge is ~70px. MapToolbar sits at top:80
(10px gap below search). We mirror MapToolbar exactly so the popover's
top edge aligns with the top of the `+` button.
top edge aligns with the top of the '+' button.
right:52 = MapToolbar right(10) + button-width(36) + 6px breathing room. */
@media(max-width:767px){
.share-pop{

View file

@ -22,10 +22,8 @@ function createGoMarkerElement(color = GO_ORANGE) {
el.innerHTML = `
<style>
/* Angled, brush-style pin (qa: 2026-05-06 user feedback — wanted a more
hand-drawn / painterly feel like the reference image they shared).
Slight tilt + diagonal gradient + a stronger drop-shadow gives the pin
a "set down on the map" look without rendering raster art. */
/* Angled, brush-style pin: a slight tilt, a diagonal gradient and a stronger
drop-shadow give it a hand-drawn, "set down on the map" look without raster art. */
.doxa-go-pin-wrapper {
position: relative;
width: 32px;

View file

@ -7,12 +7,13 @@
"type": "module",
"scripts": {
"dev": "bun x vite",
"build": "bun run clean && bun run token && for d in app-profiles/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:drop && bun run check:parse && bun run verify && bun run shell",
"build": "bun run clean && bun run token && for d in app-profiles/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:drop && bun run check:parse && bun run verify && bun run spa",
"build:watch": "bun x vite build --watch",
"preview": "bun x vite preview",
"clean": "node internal/clean.mjs",
"build:bundle": "bun x vite build",
"tree": "node internal/generate-tree.mjs",
"spa": "node internal/generate-spa.mjs # writes public/index.html, the one root page, from public/doxa-maps/manifest.json",
"check:drop": "node internal/drop-portability-check.mjs",
"check:parse": "node internal/inline-parse-check.mjs",
"verify": "node library/scripts/research/verify-all.mjs",
@ -24,7 +25,7 @@
"apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd capacitor/android && ./gradlew assembleDebug",
"apk:install": "adb install -r capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
"phone": "bun run apk:debug && bun run apk:install",
"shell": "node internal/install-shell.mjs --no-token # re-seed public/ from a layouts/ wrapper; --no-token so the local credential is never copied into the served root",
"shell": "node internal/install-shell.mjs --no-token # OPTIONAL, not part of build: overlays a layouts/ wrapper onto public/ for local use (the served root is the generated index.html); --no-token so the local credential is never copied into the served root",
"posters": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs",
"posters:one": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs # ONLY=<zoneKey> to build one",
"posters:shoot": "cd library/scripts/posters && DSF=2 node shoot.mjs",
@ -38,7 +39,7 @@
"smoke:share": "node internal/share-roundtrip-smoke.mjs",
"probe:legend-columns": "node internal/legend-columns-probe.mjs",
"sync:dropin": "node internal/sync-dropin.mjs",
"deploy:dev": "bun run build && node internal/sync-dropin.mjs \"${DOXA_DROPIN_DEST:-../doxa-life/public/js/doxa-maps-build}\" # every bundler slice ends here: the website worktree gets the fresh drop-in; then commit + push BOTH repos",
"deploy:dev": "bun run build && node internal/sync-dropin.mjs \"${DOXA_DROPIN_DEST:-../doxa-life/public/js/doxa-maps-build}\" --from-config --prune # every bundler slice ends here: the website worktree gets exactly the drop-in contract (dropin.include in the build-config); then commit + push BOTH repos",
"check:public-hygiene": "node internal/public-hygiene-check.mjs"
},
"devDependencies": {

View file

@ -1,14 +0,0 @@
# doxa-maps frame policy — generated by internal/generate-tree.mjs
# (see docs/architecture/embeddability.md — the server owns frame-ancestors)
# Staging surfaces: internal preview only
/doxa-maps/index.html
Content-Security-Policy: frame-ancestors 'self'
/doxa-maps/:bundle/index.html
Content-Security-Policy: frame-ancestors 'self'
# Direct-embed pages (the product): framable on any partner site
/doxa-maps/:bundle/:profile/index.html
Content-Security-Policy: frame-ancestors *
/doxa-maps/:bundle/:profile/:instance/index.html
Content-Security-Policy: frame-ancestors *

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
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;

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, cities-internet-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, cities-internet-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, cities-internet-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

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":10,"profileCount":19,"bundles":[{"bundle":"cities-internet","scriptUrl":"./cities-internet/cities-internet.js","output":"doxa-maps/cities-internet/cities-internet.js","integrity":"sha384-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U","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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U\" 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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U\" 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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"bundles":[{"bundle":"cities-internet","scriptUrl":"./cities-internet/cities-internet.js","output":"doxa-maps/cities-internet/cities-internet.js","integrity":"sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1","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-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1\" 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-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1\" 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-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1\" 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).
*

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, country-posters-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, country-posters-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

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":10,"profileCount":19,"bundles":[{"bundle":"country-posters","scriptUrl":"./country-posters/country-posters.js","output":"doxa-maps/country-posters/country-posters.js","integrity":"sha384-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23","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-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23\" 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-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"bundles":[{"bundle":"country-posters","scriptUrl":"./country-posters/country-posters.js","output":"doxa-maps/country-posters/country-posters.js","integrity":"sha384-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI","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-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI\" 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-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI\" 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).
*

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, demo-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-LHGkKIEgv00d9oLnEZtLJJZ24oCTXrgoMItI12/wIOgUxlsefkyIb1bIPQKPkZiV" crossorigin="anonymous"></script>
<script src="../demo-map.js" integrity="sha384-iwx1BbMrXkW8BmQMSfzl4MX4nyH6CDoOFivDM4DsIVr7832ELldLnRqrpaLV3BOY" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -12,7 +12,7 @@
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5/dist/maplibre-gl.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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, demo-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-LHGkKIEgv00d9oLnEZtLJJZ24oCTXrgoMItI12/wIOgUxlsefkyIb1bIPQKPkZiV" crossorigin="anonymous"></script>
<script src="../../demo-map.js" integrity="sha384-iwx1BbMrXkW8BmQMSfzl4MX4nyH6CDoOFivDM4DsIVr7832ELldLnRqrpaLV3BOY" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

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":10,"profileCount":19,"bundles":[{"bundle":"demo-map","scriptUrl":"./demo-map/demo-map.js","output":"doxa-maps/demo-map/demo-map.js","integrity":"sha384-LHGkKIEgv00d9oLnEZtLJJZ24oCTXrgoMItI12/wIOgUxlsefkyIb1bIPQKPkZiV","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-LHGkKIEgv00d9oLnEZtLJJZ24oCTXrgoMItI12/wIOgUxlsefkyIb1bIPQKPkZiV\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"bundles":[{"bundle":"demo-map","scriptUrl":"./demo-map/demo-map.js","output":"doxa-maps/demo-map/demo-map.js","integrity":"sha384-iwx1BbMrXkW8BmQMSfzl4MX4nyH6CDoOFivDM4DsIVr7832ELldLnRqrpaLV3BOY","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-iwx1BbMrXkW8BmQMSfzl4MX4nyH6CDoOFivDM4DsIVr7832ELldLnRqrpaLV3BOY\" 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).
*

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":10,"profileCount":19,"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-v4CfNCIcWcojYz+iZPzigtU7Imb8CPk5YKe+tjv2z++m1IHtXCmo60EkWdFAed9d","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-v4CfNCIcWcojYz+iZPzigtU7Imb8CPk5YKe+tjv2z++m1IHtXCmo60EkWdFAed9d\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"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-K9GWnVJ6jQ4XpL1uat6IkrZ2ODyWEhDZWJ1yUkT0A70ZiLeJ7cD9dBpsG/5juF2b","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-K9GWnVJ6jQ4XpL1uat6IkrZ2ODyWEhDZWJ1yUkT0A70ZiLeJ7cD9dBpsG/5juF2b\" 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).
*

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, doxa-research-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-v4CfNCIcWcojYz+iZPzigtU7Imb8CPk5YKe+tjv2z++m1IHtXCmo60EkWdFAed9d" crossorigin="anonymous"></script>
<script src="../doxa-research-map.js" integrity="sha384-K9GWnVJ6jQ4XpL1uat6IkrZ2ODyWEhDZWJ1yUkT0A70ZiLeJ7cD9dBpsG/5juF2b" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, doxa-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, doxa-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, doxa-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9" crossorigin="anonymous"></script>
<script src="../doxa-simple-map.js" integrity="sha384-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, doxa-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9" crossorigin="anonymous"></script>
<script src="../../doxa-simple-map.js" integrity="sha384-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

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":10,"profileCount":19,"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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9","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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"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-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T","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-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T\" 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).
*

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, gospel-resources-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, gospel-resources-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

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":10,"profileCount":19,"bundles":[{"bundle":"gospel-resources","scriptUrl":"./gospel-resources/gospel-resources.js","output":"doxa-maps/gospel-resources/gospel-resources.js","integrity":"sha384-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL","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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" 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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" 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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" 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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"bundles":[{"bundle":"gospel-resources","scriptUrl":"./gospel-resources/gospel-resources.js","output":"doxa-maps/gospel-resources/gospel-resources.js","integrity":"sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs","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-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" 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-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" 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-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" 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-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" 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).
*

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, gospel-resources-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, gospel-resources-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -33,7 +33,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "doxa-maps/cities-internet/cities-internet.js",
"integrity": "sha384-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U",
"integrity": "sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1",
"elements": [
"cities-internet-map"
],
@ -47,7 +47,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
}
},
{
@ -59,7 +59,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
}
},
{
@ -71,7 +71,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"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-C2lYpjkm+jDkGY+t5UkXn3qQfGKtxi7JTBjo6qZsw1TTXOkGiTsZaz9fOQSf/V9U\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
"script": "<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-1u8zoyJyvUMjdin6Fowou+Lur8Tasmp2jJXC6dzrlhWpUYDvztHtq9FmPGZCi1K1\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></cities-internet-map>"
}
}
]
@ -80,7 +80,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "doxa-maps/country-posters/country-posters.js",
"integrity": "sha384-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23",
"integrity": "sha384-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI",
"elements": [
"country-posters-map"
],
@ -94,7 +94,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"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-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></country-posters-map>"
"script": "<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></country-posters-map>"
}
},
{
@ -106,7 +106,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"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-dBU1y4TEAqdXbhET85eUwE1ahRWktH59Cxk5mgUD2Rixo7MP2HDjGrejidLsSM23\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></country-posters-map>"
"script": "<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-MJMmqcsSWrc61XNGUeI/zT2Ig7vl5yeWEpyLwjkePCj9coEulJhGljDuSP8SLSGI\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></country-posters-map>"
}
}
]
@ -115,7 +115,7 @@
"bundle": "demo-map",
"scriptUrl": "./demo-map/demo-map.js",
"output": "doxa-maps/demo-map/demo-map.js",
"integrity": "sha384-LHGkKIEgv00d9oLnEZtLJJZ24oCTXrgoMItI12/wIOgUxlsefkyIb1bIPQKPkZiV",
"integrity": "sha384-iwx1BbMrXkW8BmQMSfzl4MX4nyH6CDoOFivDM4DsIVr7832ELldLnRqrpaLV3BOY",
"elements": [
"demo-map"
],
@ -129,7 +129,7 @@
"scriptUrl": "./demo-map/demo-map.js",
"embed": {
"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-LHGkKIEgv00d9oLnEZtLJJZ24oCTXrgoMItI12/wIOgUxlsefkyIb1bIPQKPkZiV\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></demo-map>"
"script": "<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-iwx1BbMrXkW8BmQMSfzl4MX4nyH6CDoOFivDM4DsIVr7832ELldLnRqrpaLV3BOY\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></demo-map>"
},
"instances": [
{
@ -156,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-v4CfNCIcWcojYz+iZPzigtU7Imb8CPk5YKe+tjv2z++m1IHtXCmo60EkWdFAed9d",
"integrity": "sha384-K9GWnVJ6jQ4XpL1uat6IkrZ2ODyWEhDZWJ1yUkT0A70ZiLeJ7cD9dBpsG/5juF2b",
"elements": [
"doxa-research-map"
],
@ -170,7 +170,7 @@
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
"embed": {
"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-v4CfNCIcWcojYz+iZPzigtU7Imb8CPk5YKe+tjv2z++m1IHtXCmo60EkWdFAed9d\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></doxa-research-map>"
"script": "<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-K9GWnVJ6jQ4XpL1uat6IkrZ2ODyWEhDZWJ1yUkT0A70ZiLeJ7cD9dBpsG/5juF2b\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></doxa-research-map>"
}
}
]
@ -179,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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9",
"integrity": "sha384-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T",
"elements": [
"doxa-map",
"doxa-simple-map"
@ -194,7 +194,7 @@
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
"embed": {
"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-tG0UdPJ6tjXJV0Hg3OFXY5I4yvu0ZwDvphYurVzw8cx4k4ne8RR7Rr2UP3MkeRy9\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></doxa-map>"
"script": "<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-T815eVSU0j7QECeAlyNEVzQrDGUm7abImmsOukMW8/vTbamGODe3CMYILmlgSH9T\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></doxa-map>"
},
"instances": [
{
@ -274,7 +274,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL",
"integrity": "sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs",
"elements": [
"gospel-resources-map"
],
@ -288,7 +288,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
},
{
@ -300,7 +300,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
},
{
@ -312,7 +312,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
},
{
@ -324,7 +324,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-GGNkQjzHDoMF7Wz6mXQqTNGqzSEDKq4YTOjCwVsGpW+9IoUBcdN6JLbxwGJ05PZL\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
"script": "<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-KDyF/7avg/RzCZufMlCj5Jv5t7ox3nCd4YB21L8UsWQaLTM35/Avt1JxNiuf+tQs\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></gospel-resources-map>"
}
}
]
@ -333,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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs",
"integrity": "sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px",
"elements": [
"my-upg-100-list-map"
],
@ -347,7 +347,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs\" 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>"
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px\" 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>"
}
},
{
@ -359,7 +359,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs\" 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>"
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px\" 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>"
}
},
{
@ -371,7 +371,7 @@
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"embed": {
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></my-upg-100-list-map>"
"script": "<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></my-upg-100-list-map>"
}
}
]
@ -380,7 +380,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "doxa-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-zt/p5jPpNqDg6lcSBIEt0HccyWTMZE6JtY5tH7QdX7johN+8nf7xAHpoXPq1Ls0O",
"integrity": "sha384-KM1+8afDjk+fAFTzbxqv66v2xJtrgihGGkjQd7AFoWr87sctxGvAPpEG0aTsgKiY",
"elements": [
"research-dashboard-map"
],
@ -394,7 +394,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"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-zt/p5jPpNqDg6lcSBIEt0HccyWTMZE6JtY5tH7QdX7johN+8nf7xAHpoXPq1Ls0O\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'></research-dashboard-map>"
"script": "<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-KM1+8afDjk+fAFTzbxqv66v2xJtrgihGGkjQd7AFoWr87sctxGvAPpEG0aTsgKiY\" 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"
}

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":10,"profileCount":19,"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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs","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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs\" 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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs\" 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>"}},{"profile":"upg-100-mfe","file":"upg-100-mfe.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/upg-100-mfe/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-mfe/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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\"}'>\u003c/my-upg-100-list-map>"}}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"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-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px","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-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px\" 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-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px\" 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>"}},{"profile":"upg-100-mfe","file":"upg-100-mfe.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/upg-100-mfe/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-mfe/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-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"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).
*

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, my-upg-100-list-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, my-upg-100-list-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, my-upg-100-list-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-Z1RXSgx5DrgvI618cta+6XbWC0p4P1PSunjEh37wc2w5HZrdzcFdOYsZlUOAr2rs" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-mbEjNg2czbPNcbsd8yqtppwFymur3/+1roDf54PmZCO11f6ekERchOH+F7J10+Px" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

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":10,"profileCount":19,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-zt/p5jPpNqDg6lcSBIEt0HccyWTMZE6JtY5tH7QdX7johN+8nf7xAHpoXPq1Ls0O","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-zt/p5jPpNqDg6lcSBIEt0HccyWTMZE6JtY5tH7QdX7johN+8nf7xAHpoXPq1Ls0O\" 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>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":10,"profileCount":19,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-KM1+8afDjk+fAFTzbxqv66v2xJtrgihGGkjQd7AFoWr87sctxGvAPpEG0aTsgKiY","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-KM1+8afDjk+fAFTzbxqv66v2xJtrgihGGkjQd7AFoWr87sctxGvAPpEG0aTsgKiY\" 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).
*

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, research-dashboard-map { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
@ -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-zt/p5jPpNqDg6lcSBIEt0HccyWTMZE6JtY5tH7QdX7johN+8nf7xAHpoXPq1Ls0O" crossorigin="anonymous"></script>
<script src="../research-dashboard.js" integrity="sha384-KM1+8afDjk+fAFTzbxqv66v2xJtrgihGGkjQd7AFoWr87sctxGvAPpEG0aTsgKiY" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, template-bundle { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;

View file

@ -14,7 +14,7 @@
<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"). */
browser URL shares the map itself. */
html, body { height: 100%; margin: 0; background: #0a0e15; }
#slot, template-bundle { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,62 +0,0 @@
/**
* 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 one or more lines are too long

View file

@ -1,62 +0,0 @@
/**
* 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

@ -1,128 +0,0 @@
/**
* 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

@ -1,176 +0,0 @@
/**
* 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

@ -24,6 +24,7 @@ import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
import { autoSpaDevPlugin } from './internal/playground/dev-plugin.mjs'
import { stripShadowCssCommentsPlugin } from './internal/vite-plugin-strip-shadow-css-comments.mjs'
import { fileURLToPath } from 'node:url'
import { resolve, dirname } from 'node:path'
import { readdirSync, statSync, existsSync, readFileSync } from 'node:fs'
@ -87,7 +88,10 @@ export default defineConfig(({ command, mode }) => {
// artifacts. Default FALSE strips it from every bundle (dead-branch +
// treeshake); an explicitly-flagged platform build sets DOXA_QA=1.
define: { __DOXA_QA__: JSON.stringify(process.env.DOXA_QA === '1') },
plugins: [vue(), cssInjectedByJsPlugin(), autoSpaDevPlugin(env.VITE_MAPBOX_TOKEN || process.env.VITE_MAPBOX_TOKEN || '')],
// Build only: shadow-DOM CSS is injected from useShadowStyles(`…`) literals that
// ship verbatim, so their block comments are stripped before the Vue compiler
// runs (enforce: 'pre'). Dev keeps the comments.
plugins: [stripShadowCssCommentsPlugin(), vue(), cssInjectedByJsPlugin(), autoSpaDevPlugin(env.VITE_MAPBOX_TOKEN || process.env.VITE_MAPBOX_TOKEN || '')],
resolve: {
alias: { '@map': resolve(__dirname, 'library') },
},

View file

@ -1,11 +1,18 @@
{
"//": "THE single build-config for this STANDALONE doxa-maps bundler. Edit this ONE file, then run `bun run build`. FIELDS: 'name' = the output folder name the tree is written as. 'paths' = EXTRA folders to copy the whole tree into. EMPTY and normally stays empty: the build writes into public/<name>/ directly, because public/ is the committed served web-root rather than something assembled at build time. STANDALONE NOTE (2026-07-30): the old value ['../../../public/js'] pointed into the dx-life Nuxt app's public dir \u2014 meaningless in this repo, and the whole point of standing this bundler up on its own is that dx-life consumes maps from the CDN instead of hosting a copy. So 'paths' is now EMPTY: the tree is written to ./app/<name>/ only, and that ONE tree is what all three targets consume (Railway/CDN static host, Capacitor webDir, local staging). If you ever want to ALSO publish into a local dx-life checkout, add its path back here. 'embedBaseUrl' = public URL where the <name>/ tree is served, used to build ABSOLUTE paste-anywhere embed snippets for partner sites ('' = RELATIVE ./bundle/profile/... URLs so the dropped folder is position-independent \u2014 keeps the drop-portability gate green and is what Capacitor needs, since the app loads from https://localhost). 'tokenUrl' = the endpoint every generated map page fetches its Mapbox token from \u2014 a URL, NOT a secret. Set it ABSOLUTE (https://doxa.life/api/maps/token) so both a CDN-hosted folder and the mobile app work; the token stays remote and only the URL ships. window.MAP_TOKEN_URL overrides at runtime. Your endpoint must send CORS headers permitting https://localhost (the Capacitor origin) and any partner domain. 'token' = OPT-IN literal token baked into every page \u2014 LEAVE EMPTY; prefer tokenUrl. 'assetsBaseUrl' = where the SHARED asset tree (previews, posters, boundaries) is served from. Vite no longer copies public/ into each bundle (publicDir:false in vite.config.js), so pages are TOLD where the one shared copy lives. EMPTY = a relative path derived per page depth, which keeps a dropped folder self-contained under one web-root. Set it ABSOLUTE (https://maps.doxa.life/assets) for partner embeds and the mobile app, where there is no shared web-root to climb to. Same modular pattern as tokenUrl and window.MAP_APP_API_URL.",
"//": "THE single build-config for this STANDALONE doxa-maps bundler. Edit this ONE file, then run `bun run build`. FIELDS: 'name' = the output folder name the tree is written as. 'paths' = EXTRA folders to copy the whole tree into. EMPTY and normally stays empty: the build writes into public/<name>/ directly, because public/ is the committed served web-root rather than something assembled at build time. STANDALONE NOTE (2026-07-30): the old value ['../../../public/js'] pointed into the dx-life Nuxt app's public dir \u2014 meaningless in this repo, and the whole point of standing this bundler up on its own is that dx-life consumes maps from the CDN instead of hosting a copy. So 'paths' is now EMPTY: the tree is written to ./app/<name>/ only, and that ONE tree is what all three targets consume (Railway/CDN static host, Capacitor webDir, local staging). If you ever want to ALSO publish into a local dx-life checkout, add its path back here. 'embedBaseUrl' = public URL where the <name>/ tree is served, used to build ABSOLUTE paste-anywhere embed snippets for partner sites ('' = RELATIVE ./bundle/profile/... URLs so the dropped folder is position-independent \u2014 keeps the drop-portability gate green and is what Capacitor needs, since the app loads from https://localhost). 'tokenUrl' = the endpoint every generated map page fetches its Mapbox token from \u2014 a URL, NOT a secret. Set it ABSOLUTE (https://doxa.life/api/maps/token) so both a CDN-hosted folder and the mobile app work; the token stays remote and only the URL ships. window.MAP_TOKEN_URL overrides at runtime. Your endpoint must send CORS headers permitting https://localhost (the Capacitor origin) and any partner domain. 'token' = OPT-IN literal token baked into every page \u2014 LEAVE EMPTY; prefer tokenUrl. 'assetsBaseUrl' = where the SHARED asset tree (previews, posters, boundaries) is served from. Vite no longer copies public/ into each bundle (publicDir:false in vite.config.js), so pages are TOLD where the one shared copy lives. EMPTY = a relative path derived per page depth, which keeps a dropped folder self-contained under one web-root. Set it ABSOLUTE (https://maps.doxa.life/assets) for partner embeds and the mobile app, where there is no shared web-root to climb to. Same modular pattern as tokenUrl and window.MAP_APP_API_URL. 'dropin.include' = THE drop-in contract, declared here and nowhere else: the only root entries of public/ that `sync-dropin --from-config` copies into a host — index.html (the generated root page, internal/generate-spa.mjs), assets/ (data) and <name>/ (the bundle folders + manifest.json); with --prune everything else in the host's drop-in folder is removed, and `public-hygiene-check --strict` fails on any other root entry.",
"name": "doxa-maps",
"paths": [],
"embedBaseUrl": "",
"assetsBaseUrl": "",
"tokenUrl": "https://doxa.life/api/maps/token",
"token": "",
"dropin": {
"include": [
"index.html",
"assets",
"doxa-maps"
]
},
"apiBaseUrl": "https://pray.doxa.life",
"gatedProfiles": [
"key-cities"