348 lines
34 KiB
HTML
348 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>doxa-maps · staging</title>
|
|
<link rel="icon" href="data:,">
|
|
<link rel="preconnect" href="https://api.mapbox.com" crossorigin />
|
|
<style>/* staging-forge.css — the ONE-PAGE semantic staging theme (root + bundle pages).
|
|
design brief 2026-07-11: tree sidebar left on desktop, tree-top + scroll-to-map
|
|
on mobile, GEM FRAMES as the hero (each map in its colored jewel frame), quiet
|
|
architectural dark, zero folder-hopping. doxa-maps design language. */
|
|
:root {
|
|
--bg:#0a0e15; --surf:#121a27; --surf2:#0e1623; --line:#223047;
|
|
--ink:#eef2f7; --mut:#9aa7b8; --dim:#6b7787;
|
|
--cy:#46d4ff; --vi:#b9a6f5; --gold:#ffc24b; --grn:#5ee0a0;
|
|
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
|
|
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
}
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; }
|
|
body {
|
|
background:
|
|
radial-gradient(1100px 480px at 82% -12%, rgba(70,212,255,.10), transparent 60%),
|
|
radial-gradient(820px 460px at -8% 112%, rgba(185,166,245,.10), transparent 60%),
|
|
var(--bg);
|
|
color: var(--ink); font-family: var(--sans); -webkit-font-smoothing: antialiased;
|
|
}
|
|
code { font-family: var(--mono); }
|
|
.hidden { display: none !important; }
|
|
.empty { color: var(--dim); font: 12px/1.5 var(--mono); padding: 18px; }
|
|
|
|
/* ── Shell: header strip, then sidebar|stage grid (desktop) ── */
|
|
header.top { padding: 14px 18px 10px; border-bottom: 1px solid var(--surf2); }
|
|
header.top h1 { font: 800 15px/1.2 var(--sans); letter-spacing: -.01em; }
|
|
header.top h1 .ac { color: var(--cy); font-weight: 600; }
|
|
header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut); }
|
|
.strip { padding: 7px 18px; font: 11px/1.4 var(--mono); color: var(--mut);
|
|
border-bottom: 1px solid var(--surf2); background: rgba(255,255,255,.012); }
|
|
.strip .pat { color: var(--cy); } .strip .err { color: #f87171; } .strip strong { color: var(--ink); }
|
|
|
|
/* token bar — visible only while no token resolves */
|
|
#tokenBar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
|
padding: 9px 18px; font: 12px/1.4 var(--sans); color: var(--gold);
|
|
border-bottom: 1px solid var(--surf2); background: rgba(255,194,75,.06); }
|
|
#tokenBar input { flex: 1 1 220px; max-width: 420px; font: 12px/1 var(--mono);
|
|
color: var(--ink); background: var(--surf2); border: 1px solid var(--line);
|
|
border-radius: 8px; padding: 8px 10px; }
|
|
#tokenBar button { font: 700 11px/1 var(--mono); color: #0a0e15; cursor: pointer;
|
|
background: linear-gradient(90deg, var(--cy), var(--vi)); border: 0;
|
|
border-radius: 999px; padding: 8px 14px; }
|
|
|
|
/* ── The one-page layout ── */
|
|
#shell { display: grid; grid-template-columns: minmax(280px, 340px) 1fr;
|
|
height: calc(100vh - 118px); min-height: 420px; }
|
|
|
|
/* sidebar: THE semantic tree — navigation and architecture in one */
|
|
#treeNav { overflow-y: auto; border-right: 1px solid var(--surf2);
|
|
padding: 12px 10px 28px; background: rgba(255,255,255,.008); }
|
|
.bgroup { margin-bottom: 8px; border: 1px solid var(--line); border-radius: 10px;
|
|
background: var(--surf); overflow: hidden; }
|
|
.bgroup summary { list-style: none; cursor: pointer; display: flex; align-items: baseline;
|
|
gap: 8px; padding: 10px 12px; font: 700 13px/1.2 var(--sans); }
|
|
.bgroup summary::-webkit-details-marker { display: none; }
|
|
.bgroup summary .bdot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
|
|
align-self: center; background: var(--gem, var(--cy)); box-shadow: 0 0 8px var(--gem, var(--cy)); }
|
|
.bgroup summary .bmeta { margin-left: auto; font: 10px/1.3 var(--mono); color: var(--dim); font-weight: 400; }
|
|
.bgroup[open] summary { border-bottom: 1px solid var(--surf2); }
|
|
|
|
.node { display: block; width: 100%; text-align: left; cursor: pointer; color: inherit;
|
|
background: transparent; border: 0; border-left: 2px solid transparent;
|
|
padding: 8px 12px 8px 22px; transition: background .12s; }
|
|
.node:hover { background: rgba(255,255,255,.03); }
|
|
.node.active { background: color-mix(in srgb, var(--gem, var(--cy)) 10%, transparent);
|
|
border-left-color: var(--gem, var(--cy)); }
|
|
.node .nlabel { display: block; font: 600 12.5px/1.3 var(--sans); }
|
|
.node .nsub { display: block; font: 10.5px/1.4 var(--mono); color: var(--mut); margin-top: 1px; }
|
|
.node .nedit { display: block; font: 9.5px/1.4 var(--mono); color: var(--dim); margin-top: 2px;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
/* kind glyphs */
|
|
.node.k-instance .nlabel::before { content: "◇ "; color: var(--grn); }
|
|
.node.k-profile .nlabel::before { content: "● "; color: var(--vi); }
|
|
.node.k-nested .nlabel::before { content: "◉ "; color: var(--gold); }
|
|
|
|
/* ── Stage: the ONE live map inside its GEM FRAME (the recurring hero theme) ── */
|
|
#stage { display: flex; flex-direction: column; min-width: 0; padding: 12px 16px 16px; }
|
|
.stage-head { display: flex; align-items: center; gap: 10px; padding: 0 2px 9px; }
|
|
.stage-head .sctag { font: 800 13px/1 var(--mono); color: var(--gem, var(--cy)); }
|
|
.stage-head .sckind { font: 8.5px/1 var(--mono); color: var(--dim); border: 1px solid var(--line);
|
|
border-radius: 999px; padding: 3px 8px; text-transform: uppercase; letter-spacing: .08em; }
|
|
.stage-head .popout { margin-left: auto; font: 600 11px/1 var(--mono); color: var(--mut);
|
|
text-decoration: none; border: 1px solid var(--line); border-radius: 999px; padding: 7px 12px; }
|
|
.stage-head .popout:hover { color: var(--ink); border-color: #3a4a63; }
|
|
/* the gem frame: colored jewel border + inner ring + soft glow in the node's gem color */
|
|
.map-slot { flex: 1 1 0; min-height: 0; position: relative; border-radius: 12px;
|
|
border: 1px solid var(--gem, var(--cy));
|
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--gem, var(--cy)) 45%, transparent) inset,
|
|
0 0 26px rgba(var(--gem-rgb, 70,212,255), .16),
|
|
0 8px 30px rgba(0,0,0,.35);
|
|
background: #0d1622; overflow: hidden; padding: 8px; }
|
|
.map-slot iframe { position: absolute; inset: 8px; width: calc(100% - 16px);
|
|
height: calc(100% - 16px); border: 0; border-radius: 8px; }
|
|
|
|
/* ── Mobile: single column — tree on top, the map takes the FULL SCREEN below ──
|
|
(2026-07-11: mobile map = pretty much full screen; scroll must reach it.
|
|
100dvh = dynamic viewport height, tracks the mobile URL bar; 100vh = fallback.) */
|
|
@media (max-width: 899px) {
|
|
html, body { height: auto; } /* let the page actually scroll */
|
|
#shell { display: block; height: auto; }
|
|
#treeNav { border-right: 0; border-bottom: 1px solid var(--surf2); max-height: 44vh; }
|
|
#stage { height: 100vh; height: 100dvh; padding: 8px; } /* full-screen map pane */
|
|
.stage-head { padding-bottom: 6px; }
|
|
.map-slot { padding: 4px; border-radius: 10px; } /* slimmer gem frame = max map */
|
|
.map-slot iframe { inset: 4px; width: calc(100% - 8px); height: calc(100% - 8px); }
|
|
.node .nedit { display: none; } /* edit paths are a desktop affordance */
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="top">
|
|
<h1>doxa-maps<span class="ac"></span></h1>
|
|
<div class="sub">one shared core → many self-contained, paste-anywhere map bundles</div>
|
|
</header>
|
|
<div class="strip" id="frameInfo">loading…</div>
|
|
<div id="tokenBar" class="hidden"></div>
|
|
<div id="shell">
|
|
<nav id="treeNav" aria-label="semantic tree"></nav>
|
|
<main id="stage"></main>
|
|
</div>
|
|
|
|
<script>window.FORGE = {"mode":"built","scope":"root","manifestUrl":"./manifest.json","bundle":null,"hrefBase":"./"};
|
|
/* file:// contract: manifest inlined at build — no fetch needed to render. */
|
|
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"app-profiles/*/*.vue","embedBase":".","bundleCount":9,"profileCount":17,"bundles":[{"bundle":"cities-internet","scriptUrl":"./cities-internet/cities-internet.js","output":"doxa-maps/cities-internet/cities-internet.js","integrity":"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg","elements":["cities-internet-map"],"profiles":[{"profile":"connectivity-list","file":"connectivity-list.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/connectivity-list/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/cities-internet-map>"}},{"profile":"connectivity-map","file":"connectivity-map.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/connectivity-map/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/connectivity-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/cities-internet-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"","element":"cities-internet-map","iframeUrl":"./cities-internet/data-tables/index.html","scriptUrl":"./cities-internet/cities-internet.js","embed":{"iframe":"\u003ciframe src=\"./cities-internet/data-tables/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-rJEeD6vD8BPEh2Ab2sprUPjIYzkledq5jMjNzpVXJNDlnDzX8ejRbACR4xRQaxAg\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/cities-internet-map>"}}]},{"bundle":"country-posters","scriptUrl":"./country-posters/country-posters.js","output":"doxa-maps/country-posters/country-posters.js","integrity":"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX","elements":["country-posters-map"],"profiles":[{"profile":"countries-map","file":"countries-map.vue","description":"","element":"country-posters-map","iframeUrl":"./country-posters/countries-map/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/countries-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/country-posters-map>"}},{"profile":"country-poster-wall","file":"country-poster-wall.vue","description":"","element":"country-posters-map","iframeUrl":"./country-posters/country-poster-wall/index.html","scriptUrl":"./country-posters/country-posters.js","embed":{"iframe":"\u003ciframe src=\"./country-posters/country-poster-wall/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./country-posters/country-posters.js\" integrity=\"sha384-FBtv267lu0P9eg9CMAttrXz1NOkbktgrwvUDjBJvRIEkpqxHCy1UD7ceJeIfAvjX\" crossorigin=\"anonymous\">\u003c/script>\n\u003ccountry-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/country-posters-map>"}}]},{"bundle":"demo-map","scriptUrl":"./demo-map/demo-map.js","output":"doxa-maps/demo-map/demo-map.js","integrity":"sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2","elements":["demo-map"],"profiles":[{"profile":"demo-map","file":"demo-map.vue","description":"Simple two-tab demo map. Tab 1 colors pins by religion (shared","element":"demo-map","iframeUrl":"./demo-map/demo-map/index.html","scriptUrl":"./demo-map/demo-map.js","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./demo-map/demo-map.js\" integrity=\"sha384-B+SyViDxVGRi34FHMkYac/xvZ43ATdtjC581+MYPyim2Z8wk+6uAWj7DNt6GA+C2\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdemo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/demo-map>"},"instances":[{"id":"maplibre","title":"Demo map — MapLibre (token-free)","apiBaseUrl":"","props":{"profile":"demo-map","provider":"maplibre","instanceId":"demo-maplibre"},"element":"demo-map","iframeUrl":"./demo-map/demo-map/maplibre/index.html","embed":{"iframe":"\u003ciframe src=\"./demo-map/demo-map/maplibre/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"demo-map"}]},{"bundle":"doxa-research-map","scriptUrl":"./doxa-research-map/doxa-research-map.js","output":"doxa-maps/doxa-research-map/doxa-research-map.js","integrity":"sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL","elements":["doxa-research-map"],"profiles":[{"profile":"research-map","file":"research-map.vue","description":"","element":"doxa-research-map","iframeUrl":"./doxa-research-map/research-map/index.html","scriptUrl":"./doxa-research-map/doxa-research-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-research-map/research-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ucQyb81oBIScE6DAnHkfssGlIPACxSPP/BoC6mtdkuRn/XNSgyGDssUDTKtodUCL\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/doxa-research-map>"}}]},{"bundle":"doxa-simple-map","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","output":"doxa-maps/doxa-simple-map/doxa-simple-map.js","integrity":"sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a","elements":["doxa-map","doxa-simple-map"],"profiles":[{"profile":"doxa-simple-map","file":"doxa-simple-map.vue","description":"","element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/index.html","scriptUrl":"./doxa-simple-map/doxa-simple-map.js","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-bhqFr7F7wpWDfWVigz0L7MH4tqQObm/W3xhcuzx82x9C64JFGpHDO8QgCRojX56a\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdoxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/doxa-map>"},"instances":[{"id":"prayer","title":"Prayer Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-prayer","dataSource":"pray-tools","tabs":[{"id":"prayer","colorStrategy":"prayer","legend":"prayer","popup":"prayer"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/prayer/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/prayer/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"engagement","title":"Engagement Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-engagement","dataSource":"pray-tools","tabs":[{"id":"engagement","colorStrategy":"engagement","legend":"engagement","popup":"engagement"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/engagement/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/engagement/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"adoption","title":"Adoption Map","apiBaseUrl":"https://pray.doxa.life","props":{"profile":"doxa-simple-map","instanceId":"embed-adoption","dataSource":"pray-tools","tabs":[{"id":"adoption","colorStrategy":"adoption","legend":"adoption","popup":"adoption"}]},"element":"doxa-map","iframeUrl":"./doxa-simple-map/doxa-simple-map/adoption/index.html","embed":{"iframe":"\u003ciframe src=\"./doxa-simple-map/doxa-simple-map/adoption/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]},{"bundle":"gospel-resources","scriptUrl":"./gospel-resources/gospel-resources.js","output":"doxa-maps/gospel-resources/gospel-resources.js","integrity":"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx","elements":["gospel-resources-map"],"profiles":[{"profile":"charts","file":"charts.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/charts/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/charts/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}},{"profile":"data-tables","file":"data-tables.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/data-tables/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/data-tables/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-list","file":"resources-list.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/resources-list/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}},{"profile":"resources-map","file":"resources-map.vue","description":"","element":"gospel-resources-map","iframeUrl":"./gospel-resources/resources-map/index.html","scriptUrl":"./gospel-resources/gospel-resources.js","embed":{"iframe":"\u003ciframe src=\"./gospel-resources/resources-map/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-0njmRPwldwfw1NeqYab3u/cQVuoqBH67iFau1BeBrR81ouB32h8nTw7CbRktgvtx\" crossorigin=\"anonymous\">\u003c/script>\n\u003cgospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/gospel-resources-map>"}}]},{"bundle":"my-upg-100-list","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","output":"doxa-maps/my-upg-100-list/my-upg-100-list.js","integrity":"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi","elements":["my-upg-100-list-map"],"profiles":[{"profile":"poster-wall","file":"poster-wall.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/poster-wall/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/poster-wall/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/my-upg-100-list-map>"}},{"profile":"upg-100-list","file":"upg-100-list.vue","description":"","element":"my-upg-100-list-map","iframeUrl":"./my-upg-100-list/upg-100-list/index.html","scriptUrl":"./my-upg-100-list/my-upg-100-list.js","embed":{"iframe":"\u003ciframe src=\"./my-upg-100-list/upg-100-list/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-EmL+y2EzMjdr+l2gPQNQ0WVmiSatcqpr50yMftD6vTaYYnkHyZL90vN2OIb4XXfi\" crossorigin=\"anonymous\">\u003c/script>\n\u003cmy-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/my-upg-100-list-map>"}}]},{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-lZ7gBho2rDVPcokskvREISv+36u3aHZnZZ/cX9sy180aP+qyCxdgmnd8VacYOfqq\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]},{"bundle":"template-bundle","scriptUrl":"./template-bundle/template-bundle.js","output":"doxa-maps/template-bundle/template-bundle.js","integrity":"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI","elements":["template-bundle"],"profiles":[{"profile":"template-map-a","file":"template-map-a.vue","description":"Minimal example profile A — a colored placeholder, no Mapbox or data.","element":"template-bundle","iframeUrl":"./template-bundle/template-map-a/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-a/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-a\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"},{"profile":"template-map-b","file":"template-map-b.vue","description":"Minimal example profile B — a second profile in the same bundle.","element":"template-bundle","iframeUrl":"./template-bundle/template-map-b/index.html","scriptUrl":"./template-bundle/template-bundle.js","embed":{"iframe":"\u003ciframe src=\"./template-bundle/template-map-b/index.html\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript src=\"./template-bundle/template-bundle.js\" integrity=\"sha384-ThNZrcpzvq928ggsPM27BNm0El0/e8ZLi2Gf8XdzQFWUPxdiP/LmBbq9np5+SZDI\" crossorigin=\"anonymous\">\u003c/script>\n\u003ctemplate-bundle profile-config='{\"profile\":\"template-map-b\",\"tk\":\"YOUR_MAPBOX_TOKEN\"}'>\u003c/template-bundle>"},"bundle":"template-bundle"}]}]};</script>
|
|
<script>/**
|
|
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
|
|
*
|
|
* design brief (2026-07-11, the merger of all prior staging files):
|
|
* - ONE page. No folder-hopping: the semantic tree IS the navigation.
|
|
* - Desktop: hierarchical tree SIDEBAR on the left (bundle → profile → instance,
|
|
* kind-badged, "where do I edit" hints) · selected map fills the stage on the right.
|
|
* - Mobile: tree on top, map below — scroll down to the map. Native responsive,
|
|
* no simulator toggle.
|
|
* - Token UX: resolve ?tk= → localStorage → none. If none, show a token bar
|
|
* (paste once, stored locally in the browser, never in files/links unless you
|
|
* use ?tk= on purpose). Maps must Just Work after one paste.
|
|
* - Auto-generated: whatever app-profiles exist, the tree detects and shows them.
|
|
*
|
|
* file:// contract: classic script, manifest inlined at build (window.FORGE.manifest),
|
|
* fetch only as dev fallback. No import/export. The DEV-HMR block at the bottom is
|
|
* stripped by generate-tree.mjs for built pages.
|
|
*/
|
|
|
|
/* global window, document */
|
|
const FORGE = window.FORGE || {}
|
|
const SCOPE = FORGE.scope || 'root'
|
|
const MODE = FORGE.mode || 'built'
|
|
const MANIFEST_URL = FORGE.manifestUrl || './manifest.json'
|
|
const HREF_BASE = FORGE.hrefBase || './'
|
|
|
|
const GEMS = ['#46d4ff', '#b9a6f5', '#ffc24b', '#5ee0a0', '#f97fb5', '#8ef0e4']
|
|
const $ = (sel) => document.querySelector(sel)
|
|
|
|
let tree = null
|
|
let flat = [] // flat list of selectable map nodes (profiles + instances)
|
|
let activeKey = null // selected node key
|
|
|
|
// ── Token resolution (maintainer ladder: ?tk= → localStorage → ask once) ─────────
|
|
const TK_STORE = 'MAPS_STAGING_TK'
|
|
function resolveToken() {
|
|
const qp = new URLSearchParams(location.search).get('tk')
|
|
if (qp) { try { localStorage.setItem(TK_STORE, qp) } catch (e) {} return qp }
|
|
try { return localStorage.getItem(TK_STORE) || '' } catch (e) { return '' }
|
|
}
|
|
let TK = resolveToken()
|
|
|
|
function withTk(href) {
|
|
if (!TK) return href
|
|
return href + (href.includes('?') ? '&' : '?') + 'tk=' + encodeURIComponent(TK)
|
|
}
|
|
// Carry the page's ?provider= into the embedded map iframe so the engine choice propagates.
|
|
function withProvider(href) {
|
|
const p = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('provider') : null
|
|
return p ? href + (href.includes('?') ? '&' : '?') + 'provider=' + encodeURIComponent(p) : href
|
|
}
|
|
|
|
// ── Load the semantic tree (inline-first; fetch = dev fallback) ──────────────
|
|
async function loadTree() {
|
|
if (FORGE.manifest) return FORGE.manifest
|
|
const res = await fetch(MANIFEST_URL, { cache: 'no-cache' })
|
|
if (!res.ok) throw new Error('manifest ' + res.status + ' from ' + MANIFEST_URL)
|
|
return res.json()
|
|
}
|
|
|
|
const treeHref = (rel) => HREF_BASE + rel
|
|
const hexRgb = (hex) => {
|
|
const h = hex.replace('#', '')
|
|
return parseInt(h.slice(0,2),16) + ',' + parseInt(h.slice(2,4),16) + ',' + parseInt(h.slice(4,6),16)
|
|
}
|
|
|
|
function profileKind(p) {
|
|
if (Array.isArray(p.instances) && p.instances.length) return 'parameterized'
|
|
if (p.nested || /nested|tabs|web components/i.test(p.description || '')) return 'nested'
|
|
return 'individual'
|
|
}
|
|
|
|
// ── Build the flat selectable list + the sidebar tree ────────────────────────
|
|
// Selectable = anything that renders as ONE map page: individual/nested profiles
|
|
// and each instance of a parameterized profile.
|
|
function buildFlat() {
|
|
flat = []
|
|
;(tree.bundles || []).forEach((b, bi) => { b.__gem = GEMS[bi % GEMS.length] })
|
|
for (const b of tree.bundles || []) {
|
|
for (const p of b.profiles || []) {
|
|
const kind = profileKind(p)
|
|
if (kind === 'parameterized') {
|
|
for (const inst of p.instances) {
|
|
flat.push({
|
|
key: b.bundle + '/' + p.profile + '/' + inst.id,
|
|
kind: 'instance', label: inst.title || inst.id,
|
|
sub: p.profile + ' · preset "' + inst.id + '"',
|
|
edit: b.bundle + '/' + p.profile + '.instances.json',
|
|
href: treeHref(b.bundle + '/' + p.profile + '/' + inst.id + '/index.html'),
|
|
bundle: b.bundle, element: p.element || (b.elements || [])[0], gem: b.__gem,
|
|
})
|
|
}
|
|
} else {
|
|
flat.push({
|
|
key: b.bundle + '/' + p.profile,
|
|
kind: kind === 'nested' ? 'nested' : 'profile',
|
|
label: p.profile,
|
|
sub: p.description || (kind === 'nested' ? 'embeds child web components' : 'one web component = one map'),
|
|
edit: 'app-profiles/' + b.bundle + '/' + (p.file || p.profile + '.vue'),
|
|
href: treeHref(b.bundle + '/' + p.profile + '/index.html'),
|
|
bundle: b.bundle, element: p.element || (b.elements || [])[0], gem: b.__gem,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
|
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
}
|
|
|
|
// Sidebar: bundles are <details> groups (open by default), each map node a row.
|
|
function renderSidebar() {
|
|
const el = $('#treeNav')
|
|
if (!el) return
|
|
const parts = []
|
|
const bundles = tree.bundles || []
|
|
bundles.forEach((b, bi) => {
|
|
const gem = b.__gem || GEMS[bi % GEMS.length]
|
|
const rows = flat.filter((n) => n.bundle === b.bundle).map((n) => `
|
|
<button class="node k-${n.kind}${n.key === activeKey ? ' active' : ''}" data-key="${esc(n.key)}" style="--gem:${gem}">
|
|
<span class="nlabel">${esc(n.label)}</span>
|
|
<span class="nsub">${esc(n.sub)}</span>
|
|
<code class="nedit">${esc(n.edit)}</code>
|
|
</button>`).join('')
|
|
parts.push(`
|
|
<details class="bgroup" open style="--gem:${gem}">
|
|
<summary><span class="bdot"></span>${esc(b.bundle)}
|
|
<span class="bmeta">${(b.profiles || []).length} profile${(b.profiles || []).length === 1 ? '' : 's'} · one .js</span>
|
|
</summary>
|
|
${rows}
|
|
</details>`)
|
|
})
|
|
el.innerHTML = parts.join('') || '<p class="empty">no bundles built yet</p>'
|
|
el.querySelectorAll('.node').forEach((btn) =>
|
|
btn.addEventListener('click', () => select(btn.dataset.key)))
|
|
}
|
|
|
|
// ── Stage: the ONE live map (iframe of the node's self-contained page) ───────
|
|
function renderStage() {
|
|
const st = $('#stage')
|
|
if (!st) return
|
|
const nd = flat.find((n) => n.key === activeKey) || flat[0]
|
|
if (!nd) { st.innerHTML = '<p class="empty">no maps in this tree</p>'; return }
|
|
activeKey = nd.key
|
|
const gem = nd.gem || '#46d4ff'
|
|
st.style.setProperty('--gem', gem)
|
|
st.style.setProperty('--gem-rgb', hexRgb(gem))
|
|
st.innerHTML = `
|
|
<div class="stage-head">
|
|
<span class="sctag"><${esc(nd.element || nd.label)}></span>
|
|
<span class="sckind">${esc(nd.kind)}</span>
|
|
<a class="popout" href="${esc(withProvider(withTk(nd.href)))}" target="_blank" rel="noopener">open ↗</a>
|
|
</div>
|
|
<div class="map-slot"><iframe allowfullscreen src="${esc(withProvider(withTk(nd.href)))}"></iframe></div>`
|
|
// keep the sidebar highlight in sync
|
|
document.querySelectorAll('#treeNav .node').forEach((b) =>
|
|
b.classList.toggle('active', b.dataset.key === activeKey))
|
|
}
|
|
|
|
function select(key) {
|
|
activeKey = key
|
|
renderStage()
|
|
// Mobile: after picking from the tree, bring the map into view (scroll down to it).
|
|
if (window.matchMedia('(max-width: 899px)').matches) {
|
|
$('#stage')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
}
|
|
}
|
|
|
|
// ── Token bar: shown only while no token resolves; paste once, stored locally ─
|
|
function renderTokenBar() {
|
|
const bar = $('#tokenBar')
|
|
if (!bar) return
|
|
if (TK) { bar.classList.add('hidden'); return }
|
|
bar.classList.remove('hidden')
|
|
bar.innerHTML = `
|
|
<span>maps need a Mapbox token —</span>
|
|
<input id="tkInput" type="password" placeholder="paste pk.… (stays in this browser only)" autocomplete="off" />
|
|
<button id="tkSave">use it</button>`
|
|
$('#tkSave').addEventListener('click', () => {
|
|
const v = $('#tkInput').value.trim()
|
|
if (!v) return
|
|
TK = v
|
|
try { localStorage.setItem(TK_STORE, v) } catch (e) {}
|
|
bar.classList.add('hidden')
|
|
renderStage()
|
|
})
|
|
}
|
|
|
|
// ── Boot ─────────────────────────────────────────────────────────────────────
|
|
async function boot() {
|
|
const info = $('#frameInfo')
|
|
try {
|
|
tree = await loadTree()
|
|
buildFlat()
|
|
if (info) {
|
|
info.innerHTML = '<span class="pat">semantic tree</span> — '
|
|
+ (tree.bundles || []).length + ' bundle(s) · ' + flat.length + ' map(s) · '
|
|
+ '<strong>' + esc(MODE) + '</strong>'
|
|
}
|
|
renderSidebar()
|
|
renderTokenBar()
|
|
if (flat.length) { activeKey = flat[0].key; renderStage() }
|
|
} catch (e) {
|
|
if (info) info.innerHTML = '<span class="err">failed to load semantic tree: ' + esc(e.message) + '</span>'
|
|
}
|
|
}
|
|
|
|
boot()
|
|
|
|
/* dev-hmr stripped for built pages */
|
|
</script>
|
|
</body>
|
|
</html>
|