Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
This commit is contained in:
commit
42f48b7cf5
15 changed files with 448 additions and 235 deletions
|
|
@ -297,7 +297,8 @@ export function useMapPoster(getSourceMap) {
|
|||
async function _collectSlotSvgs(spec, layout) {
|
||||
const out = [];
|
||||
if (spec.slots?.title) out.push({ box: layout.title, svg: _stubTitleSvg(spec, layout.title) });
|
||||
if (spec.slots?.legend) out.push({ box: layout.legend, svg: _stubLegendSvg(spec, layout.legend) });
|
||||
// A legend with no rows is an empty white panel over the map: leave it out.
|
||||
if (spec.slots?.legend?.rows?.length) out.push({ box: layout.legend, svg: _stubLegendSvg(spec, layout.legend) });
|
||||
if (spec.slots?.scaleBar) out.push({ box: layout.scaleBar, svg: _stubScaleSvg(spec, layout.scaleBar) });
|
||||
if (spec.slots?.north) out.push({ box: layout.north, svg: _stubNorthSvg(spec, layout.north) });
|
||||
// Footer is ALWAYS present (attribution is mandatory).
|
||||
|
|
@ -362,14 +363,25 @@ async function _spawnHiddenMap(sourceMap, w, h, opts = {}) {
|
|||
// Some environments lock devicePixelRatio; fall back silently.
|
||||
}
|
||||
|
||||
// Lazy-grab mapboxgl off the global — same pattern as useMapInstance.
|
||||
// THE SAME ENGINE AS THE SOURCE MAP. This used to grab window.mapboxgl unconditionally, so on a
|
||||
// keyless page (provider=maplibre) every poster threw "mapboxgl global not found" — the source map
|
||||
// is a MapLibre map and no Mapbox engine is loaded at all. Measured 2026-09-25 while making the
|
||||
// Simple Research export one click. Mapbox behaviour is unchanged.
|
||||
// eslint-disable-next-line no-undef
|
||||
const mapboxgl = (typeof window !== 'undefined' ? window.mapboxgl : null);
|
||||
if (!mapboxgl) throw new Error('useMapPoster: mapboxgl global not found');
|
||||
const W = (typeof window !== 'undefined' ? window : {});
|
||||
const isMaplibre = !!W.maplibregl && sourceMap instanceof W.maplibregl.Map;
|
||||
const gl = isMaplibre ? W.maplibregl : W.mapboxgl;
|
||||
if (!gl) throw new Error('useMapPoster: ' + (isMaplibre ? 'maplibregl' : 'mapboxgl') + ' global not found');
|
||||
|
||||
const styleObj = sourceMap.getStyle();
|
||||
// MapLibre's style spec wants projection as an OBJECT ({ type: 'globe' }); a live MapLibre map can
|
||||
// hand back Mapbox's string spelling ('globe'), which MapLibre then refuses to load — measured
|
||||
// 2026-09-25: every keyless poster failed with "projection: object expected, string found".
|
||||
if (isMaplibre && styleObj && typeof styleObj.projection === 'string') {
|
||||
styleObj.projection = { type: styleObj.projection };
|
||||
}
|
||||
|
||||
const hiddenMap = new mapboxgl.Map({
|
||||
const hiddenMap = new gl.Map({
|
||||
container,
|
||||
style: styleObj,
|
||||
center: sourceMap.getCenter(),
|
||||
|
|
@ -379,24 +391,38 @@ async function _spawnHiddenMap(sourceMap, w, h, opts = {}) {
|
|||
interactive: false,
|
||||
attributionControl: false,
|
||||
preserveDrawingBuffer: true,
|
||||
canvasContextAttributes: { preserveDrawingBuffer: true }, // MapLibre v5 spelling of the same
|
||||
fadeDuration: 0,
|
||||
});
|
||||
|
||||
// Style ready: 'style.load', or 'load', or already loaded by the time we look (an engine may finish
|
||||
// a style OBJECT before a listener is attached). A style that cannot load says why ('error'), so a
|
||||
// failed poster names its cause instead of a bare timeout.
|
||||
let lastError = '';
|
||||
await new Promise((resolve, reject) => {
|
||||
let done = false;
|
||||
const t = setTimeout(() => {
|
||||
const finish = (err) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
reject(new Error('useMapPoster: hidden map style.load timeout'));
|
||||
}, IDLE_TIMEOUT_MS);
|
||||
hiddenMap.once('style.load', () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(t);
|
||||
resolve();
|
||||
});
|
||||
clearTimeout(t); clearInterval(poll);
|
||||
err ? reject(err) : resolve();
|
||||
};
|
||||
const t = setTimeout(() => finish(new Error('useMapPoster: hidden map style.load timeout' + (lastError ? ' (' + lastError + ')' : ''))), IDLE_TIMEOUT_MS);
|
||||
const poll = setInterval(() => { try { if (hiddenMap.isStyleLoaded()) finish(); } catch (_) { /* not yet */ } }, 100);
|
||||
hiddenMap.once('style.load', () => finish());
|
||||
hiddenMap.once('load', () => finish());
|
||||
hiddenMap.on('error', (e) => { lastError = String(e?.error?.message || e?.message || e).slice(0, 160); });
|
||||
});
|
||||
|
||||
// Print the place names in the language the reader sees. Mapbox localizes INSIDE the engine
|
||||
// (setLanguage), so a new instance starts in the style default (English) — measured on an Arabic
|
||||
// poster; carry the source map's language across. MapLibre's localized labels are expressions in
|
||||
// the style itself, which the hidden map already copied.
|
||||
try {
|
||||
const srcLang = typeof sourceMap.getLanguage === 'function' ? sourceMap.getLanguage() : null;
|
||||
if (srcLang && typeof hiddenMap.setLanguage === 'function') hiddenMap.setLanguage(srcLang);
|
||||
} catch (_) { /* a label left in the default language is a nicety lost, never a failed export */ }
|
||||
|
||||
if (opts.preserveBounds && opts.bounds) {
|
||||
hiddenMap.fitBounds(opts.bounds, { padding: 0, animate: false, duration: 0 });
|
||||
}
|
||||
|
|
@ -404,24 +430,30 @@ async function _spawnHiddenMap(sourceMap, w, h, opts = {}) {
|
|||
return { hiddenMap, restoreDpr, container };
|
||||
}
|
||||
|
||||
// The hidden map's 'idle' can arrive late or never on a heavy render (measured 2026-09-25: an
|
||||
// Arabic Letter poster, RTL shaping + a 2x canvas, missed a 15s idle and the export gave NO file).
|
||||
// So: at IDLE_TIMEOUT_MS, capture if every tile is in (something else is only repainting); keep
|
||||
// waiting to IDLE_HARD_MS otherwise, then capture what is drawn with a warning. A poster whose last
|
||||
// tiles are missing is still a file; a rejected promise is a dead Export button.
|
||||
const IDLE_HARD_MS = 60000;
|
||||
function _waitForIdle(map) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const t = setTimeout(() => {
|
||||
const timers = [];
|
||||
const finish = (why) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(new Error('useMapPoster: hidden map idle timeout'));
|
||||
}, IDLE_TIMEOUT_MS);
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(t);
|
||||
timers.forEach(clearTimeout);
|
||||
if (why) console.warn('[useMapPoster] ' + why);
|
||||
requestAnimationFrame(() => resolve());
|
||||
};
|
||||
const tilesIn = () => { try { return typeof map.areTilesLoaded === 'function' && map.areTilesLoaded(); } catch (_) { return false; } };
|
||||
timers.push(setTimeout(() => { if (tilesIn()) finish(); }, IDLE_TIMEOUT_MS));
|
||||
timers.push(setTimeout(() => finish('hidden map not idle after ' + IDLE_HARD_MS / 1000 + 's; capturing what is drawn'), IDLE_HARD_MS));
|
||||
if (map.loaded() && !map.isMoving()) {
|
||||
finish();
|
||||
} else {
|
||||
map.once('idle', finish);
|
||||
map.once('idle', () => finish());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -504,31 +536,49 @@ function mergeSignals(...signals) {
|
|||
// These produce minimal SVG so the headless render still has *something* in
|
||||
// each slot. Live preview uses the real Vue slot components instead.
|
||||
|
||||
// spec.dir === 'rtl' (an Arabic poster): text starts at the RIGHT edge and runs right-to-left.
|
||||
// ⚠ Under direction="rtl" SVG's text-anchor is relative to the text's direction, so the right edge
|
||||
// is "start", not "end" — "end" pushed every Arabic title off the poster (measured 2026-09-25).
|
||||
function _rtl(spec) { return spec?.dir === 'rtl'; }
|
||||
function _textAt(spec, box, inset) {
|
||||
return _rtl(spec)
|
||||
? `x="${box.w - inset}" text-anchor="start" direction="rtl"`
|
||||
: `x="${inset}"`;
|
||||
}
|
||||
|
||||
function _stubTitleSvg(spec, box) {
|
||||
const t = (spec.slots?.title?.text || '').replace(/[<>&]/g, _esc);
|
||||
const s = (spec.slots?.title?.subtitle || '').replace(/[<>&]/g, _esc);
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${box.w}" height="${box.h}" viewBox="0 0 ${box.w} ${box.h}">
|
||||
<rect width="100%" height="100%" fill="rgba(255,255,255,0.92)"/>
|
||||
<text x="20" y="${box.h * 0.55}" font-family="Inter, system-ui, sans-serif"
|
||||
<text ${_textAt(spec, box, 20)} y="${box.h * 0.55}" font-family="Inter, system-ui, sans-serif"
|
||||
font-size="${Math.min(box.h * 0.5, 64)}" font-weight="700" fill="#0a0a0a">${t}</text>
|
||||
${s ? `<text x="20" y="${box.h * 0.85}" font-family="Inter, system-ui, sans-serif"
|
||||
${s ? `<text ${_textAt(spec, box, 20)} y="${box.h * 0.85}" font-family="Inter, system-ui, sans-serif"
|
||||
font-size="${Math.min(box.h * 0.22, 28)}" fill="#444">${s}</text>` : ''}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function _stubLegendSvg(spec, box) {
|
||||
const rows = spec.slots?.legend?.rows || [];
|
||||
const items = rows.slice(0, 24).map((r, i) => {
|
||||
const y = 30 + i * 26;
|
||||
const rows = (spec.slots?.legend?.rows || []).slice(0, 24);
|
||||
const title = (spec.slots?.legend?.title || 'Legend').replace(/[<>&]/g, _esc);
|
||||
const rtl = _rtl(spec);
|
||||
// Type scales with the panel (it is ~600px wide at 200 DPI); the panel is only as TALL as its rows,
|
||||
// so the rest of the column stays map.
|
||||
const fs = Math.max(14, Math.round(box.w / 26)), line = Math.round(fs * 1.9), sw = Math.round(fs * 1.2), pad = Math.round(fs * 1.1);
|
||||
const panelH = Math.min(box.h, pad * 2 + line * (rows.length + 1));
|
||||
const swX = rtl ? box.w - pad - sw : pad;
|
||||
const textX = rtl ? `x="${box.w - pad - sw - Math.round(fs * 0.7)}" text-anchor="start" direction="rtl"` : `x="${pad + sw + Math.round(fs * 0.7)}"`;
|
||||
const items = rows.map((r, i) => {
|
||||
const y = pad + line * (i + 1.5);
|
||||
const c = (r.color || '#999').replace(/[<>&]/g, _esc);
|
||||
const l = (r.label || '').replace(/[<>&]/g, _esc);
|
||||
return `<g><rect x="14" y="${y - 14}" width="18" height="18" fill="${c}" stroke="#222" stroke-width="0.5"/><text x="42" y="${y}" font-family="Inter, sans-serif" font-size="14" fill="#111">${l}</text></g>`;
|
||||
return `<g><rect x="${swX}" y="${y - sw + Math.round(fs * 0.15)}" width="${sw}" height="${sw}" rx="2" fill="${c}" stroke="#222" stroke-width="0.5"/><text ${textX} y="${y}" font-family="Inter, sans-serif" font-size="${fs}" fill="#111">${l}</text></g>`;
|
||||
}).join('');
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${box.w}" height="${box.h}" viewBox="0 0 ${box.w} ${box.h}">
|
||||
<rect width="100%" height="100%" fill="rgba(255,255,255,0.94)" stroke="#ccc"/>
|
||||
<text x="14" y="20" font-family="Inter, sans-serif" font-size="14" font-weight="700" fill="#111">Legend</text>
|
||||
<rect width="${box.w}" height="${panelH}" fill="rgba(255,255,255,0.94)" stroke="#ccc"/>
|
||||
<text ${_textAt(spec, box, pad)} y="${pad + line * 0.6}" font-family="Inter, sans-serif" font-size="${Math.round(fs * 1.1)}" font-weight="700" fill="#111">${title}</text>
|
||||
${items}
|
||||
</svg>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@
|
|||
"check:embedded-rtl": "node internal/check-embedded-rtl.mjs",
|
||||
"check:english-leaks": "node internal/check-gospel-english-leaks.mjs",
|
||||
"check:experimental-shelf": "node internal/check-experimental-shelf.mjs",
|
||||
"check:export-one-click": "node plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs",
|
||||
"check:export-one-click:maplibre": "node plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs --provider maplibre",
|
||||
"check:facet-cascade": "node internal/check-facet-cascade.mjs",
|
||||
"check:facet-cascade:offline": "node internal/check-facet-cascade.mjs --offline",
|
||||
"check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test",
|
||||
|
|
@ -245,6 +247,8 @@
|
|||
"check:embed-targets": "every embed.iframe src in manifest.json must resolve to a file that EXISTS. Those snippets ARE deployment-method 1 (iframe) — a partner pastes them verbatim — and NOTHING else asserts their targets are still emitted, because every other gate walks only files that exist and is blind to ABSENCE. `the `check:embed-targets:self-test` script` poisons a copy of the manifest and asserts the gate NOTICES: a gate that cannot fail is worse than no gate.",
|
||||
"check:english-leaks": "A translated key must not render in English. rega's method: a string is a LEAK when the catalogue has a DIFFERENT value for the locale and the page still shows the English one - a render/wiring fault, not a missing translation. Third-party attributions, data names, placeholder-only strings and the prose this bundle deliberately keeps in English are all excluded, the last by simply not being in the catalogue. Scoped to the LEAF on purpose: six keys were reported leaking in the gallery and the leaf is clean in ar/zh/ru/hi - the gallery builds its iframe src with no lang parameter, so the embed never learns the locale. Embedder defect, not a bundle one.",
|
||||
"check:experimental-shelf": "Every Experimental-folder bundle, leaf + Gallery tab, mapbox + maplibre: mounts with content, 0 errors / failed requests, maps load and DRAW their own data. Self-tests on a deliberately broken bundle first. Run through internal/browser-slot.sh.",
|
||||
"check:export-one-click": "the Simple Research 'Export poster' button gives a FILE on ONE click (no menu, no dialog, no second step): leaf AND Research Gallery, en AND ar; the file is a real print PNG (US Letter @200 DPI); 0 page errors. Run inside the shared browser slot. Exit 2 = SKIP, 75 = NOT MEASURED.",
|
||||
"check:export-one-click:maplibre": "the same four one-click exports on the keyless MapLibre engine (OpenFreeMap tiles): the hidden print map must be built with the engine that drew the screen, and the footer must credit the tiles actually printed.",
|
||||
"check:facet-cascade": "a child facet selection must not outlive its parent: clearing a parent facet must cascade-clear its descendants on EVERY clear path. Asserts on RESULT COUNTS, not DOM — an orphaned child silently ANDs the map to empty. run check:facet-cascade:self-test for the NEGATIVE test (reverts the cascade and requires this gate to FAIL).",
|
||||
"check:facet-changes-map": "a facet selection must CHANGE THE DOTS — and the dots must SAY what they mean. Asserts (1) the rd-bubbles feature set changes by count AND identity, (2) every surviving dot satisfies the selected level, (3) while a prayer filter is active circle-color is a DATA-DRIVEN EXPRESSION reading peoplePraying, not a flat literal. Assertion 3 is the one that matters: the prayer filter ALREADY narrowed 2107->689 correctly while the Driver reported it broken, because every dot painted the same orange. Browser gate (needs `bun run dev` on 5173, or BASE_URL/PROBE_PATH at a served tree), so NOT in build:unlocked. run the `check:facet-changes-map:self-test` script for the NEGATIVE test.",
|
||||
"check:facet-changes-map:built": "the SAME gate against the BUILT tree instead of the dev server. Measured 2026-09-24: the dev default is 1 PASS / 3 FAIL in four runs on a busy box (88 unbundled module requests vs the built leaf's 3), while the built tree passes deterministically. Use this when you need a trustworthy answer; use the dev default when checking uncommitted source.",
|
||||
|
|
|
|||
|
|
@ -1132,7 +1132,9 @@ export default defineComponent({
|
|||
let crossOrigin = false
|
||||
try { crossOrigin = new URL(rootUrl, location.href).origin !== location.origin } catch { /* relative = same */ }
|
||||
const candidates = crossOrigin ? [copy, root] : [root, copy]
|
||||
const devFallback = '/public/doxa-maps/manifest.json'
|
||||
// The tree's name comes from the build config (vite define), never a literal here: this was the
|
||||
// one tree path compiled into any bundle, so a renamed tree would have needed a gallery edit.
|
||||
const devFallback = '/public/' + __DOXA_TREE_NAME__ + '/manifest.json'
|
||||
if (!candidates.some(c => c.url === devFallback)) candidates.push({ url: devFallback, root: dirOf(devFallback) })
|
||||
const tried = []
|
||||
const attempt = (i) => {
|
||||
|
|
|
|||
|
|
@ -63,9 +63,7 @@ import { useGeocoderSearch } from '@map/composables/useGeocoderSearch.js'
|
|||
// below.
|
||||
|
||||
// PERF: useMapPoster pulls in the tile-stitch + PDF composition pipeline
|
||||
// (~117 KB). It's only needed when the user opens the poster dialog. Defer
|
||||
// the import until that happens — see `openPoster()` below.
|
||||
let _posterPromise = null
|
||||
// (~117 KB). It is imported on the first "Export poster" click — see `openPoster()` below.
|
||||
|
||||
// PERF: useMapPacket builds a multi-page PDF (map page + one detail card per
|
||||
// people group). It reuses useMapPoster's pipeline, so it's at least as heavy.
|
||||
|
|
@ -148,17 +146,6 @@ function _syncScrollZoomToHost(m) {
|
|||
m.scrollZoom.enable()
|
||||
} catch (_) { /* an engine without scrollZoom must never break the panel */ }
|
||||
}
|
||||
function loadPoster() {
|
||||
if (!_posterPromise) {
|
||||
_posterPromise = import('@map/composables/poster/useMapPoster.js')
|
||||
.then(m => m.useMapPoster({
|
||||
sourceMap: () => map.value,
|
||||
mapboxToken: mapboxToken.value,
|
||||
styleUrl: () => map.value?.getStyle()?.sprite?.replace(/\/sprite.*$/, '') || profileConfig?.value?.style
|
||||
}))
|
||||
}
|
||||
return _posterPromise
|
||||
}
|
||||
|
||||
// Shared legend pipeline — lives in the library (@map/composables/legends/useLegendData.js).
|
||||
import { useLegendData } from '@map/composables/legends/useLegendData.js'
|
||||
|
|
@ -194,6 +181,7 @@ import ThemeToggleButton from '@map/components/map-controls/ThemeToggleButton.vu
|
|||
import HamburgerButton from '@map/components/map-controls/HamburgerButton.vue'
|
||||
import GeocoderComponent from '@map/components/map-controls/GeocoderComponent.vue'
|
||||
import ShareButton from '@map/components/map-controls/ShareButton.vue'
|
||||
import MapControlButton from '@map/components/map-controls/MapControlButton.vue'
|
||||
|
||||
// PERF: SideMenuDrawer is unused in the current research-map template (the
|
||||
// ResearchMapSideMenu sibling renders the actual menu). Removed entirely from
|
||||
|
|
@ -226,9 +214,7 @@ import { useWagfRegionsLegendData } from '@map/composables/legends/useWagfRegi
|
|||
import { useLegendData as useFlatLegendData } from '@map/composables/legends/useLegendData.js'
|
||||
import { RESEARCH_LEGEND_OPTIONS } from '@map/composables/legends/researchLegendOptions.js'
|
||||
|
||||
// PERF: PosterDialog only renders when the user opens the poster export
|
||||
// flow. Async-import it so the dialog's deps don't load on every map boot.
|
||||
const PosterDialog = retryableAsyncComponent(() => import('@map/components/poster/PosterDialog.vue'))
|
||||
|
||||
|
||||
// ─── Shadow-DOM style injection ──────────────────────────────────────────────
|
||||
useShadowStyles(`
|
||||
|
|
@ -1310,11 +1296,58 @@ watch(() => mapStore.selectedRegion, (key) => {
|
|||
// Instantiated only when the user opens the poster dialog. The composable +
|
||||
// off-screen Mapbox instance + PDF pipeline are ~117 KB; keep them off the
|
||||
// critical boot path.
|
||||
const poster = ref(null)
|
||||
// ★ ONE CLICK, ONE FILE (BUILD-SIMPLE-RESEARCH-EXPORT-ZERO-CLICK). The Driver: "there's an export
|
||||
// button on the simple dashboard ... get it working". Measured 2026-09-25, leaf AND gallery: clicking
|
||||
// "Export poster…" produced NOTHING — no dialog, no file in 60s, no error. Two bugs stacked: the
|
||||
// template passed the poster dialog a `poster` prop it does not have and omitted its v-model and
|
||||
// required getMap, so it mounted invisible; and the old loader called useMapPoster({ sourceMap, … })
|
||||
// with an OBJECT where the shared composable takes a FUNCTION, so a render would have thrown anyway.
|
||||
// Now the button does the export itself, through the SHARED poster path (useMapPoster → render →
|
||||
// download), with a sensible print default: the current view on US Letter portrait at 200 DPI, the
|
||||
// legend title and rows, and a source/date footer. No dialog, no second step.
|
||||
const posterBusy = ref(false)
|
||||
async function openPoster() {
|
||||
if (!poster.value) poster.value = await loadPoster()
|
||||
showPoster.value = true
|
||||
if (posterBusy.value) return
|
||||
posterBusy.value = true
|
||||
try {
|
||||
const [{ useMapPoster }, { defaultPosterSpec }] = await Promise.all([
|
||||
import('@map/composables/poster/useMapPoster.js'),
|
||||
import('@map/constants/posterDefaults.js'),
|
||||
])
|
||||
const exporter = useMapPoster(() => map.value)
|
||||
const spec = defaultPosterSpec({ widthIn: 8.5, heightIn: 11, dpi: 200, orientation: 'portrait' })
|
||||
// The poster is titled by the VIEW on screen ("Prayer", "Religion"…), in the reader's language.
|
||||
// The legend's title bar is the fallback only: on a flat tab it is the generic word "Legend".
|
||||
const title = tabLabel(activeTab.value) || legendTitle.value || 'Doxa Map'
|
||||
// Direction follows the MAP's language, not the host page's: an English map on an Arabic page stays LTR.
|
||||
spec.dir = ['ar', 'fa', 'he', 'ur'].includes(String(uiLocale.value || '').split('-')[0]) ? 'rtl' : 'ltr'
|
||||
spec.slots.title = { ...spec.slots.title, text: title }
|
||||
// The legend the reader sees: the flat tab's rows (and their sub-rows), else the aggregated ones.
|
||||
const flatRows = (flatLegend.items.value || []).flatMap(it => [it, ...(it.children || [])])
|
||||
const rows = (flatRows.length ? flatRows : (legend?.rows?.value || [])).map(r => ({ label: r.label, color: r.color }))
|
||||
spec.slots.legend = { ...spec.slots.legend, rows, title: legendTitle.value }
|
||||
// No scale bar or north arrow: the headless renderer's are placeholders (a fixed bar reading
|
||||
// "scale"), which on a print would be a false statement — and a globe has no single scale.
|
||||
spec.slots.scaleBar = false
|
||||
spec.slots.north = false
|
||||
// Credit the tiles actually drawn: OpenFreeMap on MapLibre, Mapbox otherwise.
|
||||
const onMaplibre = !!(window.maplibregl && map.value instanceof window.maplibregl.Map)
|
||||
spec.slots.footer = { ...spec.slots.footer, left: 'pray.doxa.life', right: new Date().toISOString().slice(0, 10),
|
||||
attribution: onMaplibre ? '© OpenFreeMap © OpenMapTiles © OpenStreetMap' : '© Mapbox © OpenStreetMap' }
|
||||
const blob = await exporter.render(spec, 'png')
|
||||
// File name from the view's id, not its title: an Arabic or Chinese title has no ASCII letters
|
||||
// to keep, and every reader's file would otherwise be the same "doxa-map-poster.png".
|
||||
const slug = String(activeTab.value?.id || '').replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase()
|
||||
_downloadBlob(blob, 'doxa-' + (slug || 'map') + '-poster.png')
|
||||
} catch (err) {
|
||||
console.error('[research-map] poster export failed:', err)
|
||||
} finally {
|
||||
posterBusy.value = false
|
||||
}
|
||||
}
|
||||
// The toolbar's export label: the drawer's translated 'Export poster…' WITHOUT the ellipsis — an
|
||||
// ellipsis promises a dialog, and this button hands over the file directly.
|
||||
const exportPosterLabel = computed(() => String(t('controls.export_poster') || 'Export poster').replace(/\s*…\s*$/, ''))
|
||||
|
||||
// ─── Export menu router (SemanticTreeLegend @export) ─────────────────────────
|
||||
// EXPERIMENTAL — dormant behind the shared kill-switch (SHOW_EXPORT_BUTTON).
|
||||
|
|
@ -1326,7 +1359,7 @@ function onExport(kind) {
|
|||
|
||||
// ─── Packet export (multi-page PDF: map + one card per people group) ─────────
|
||||
// PERF: useMapPacket reuses useMapPoster's render pipeline — heavy. Lazy-load
|
||||
// once, mirroring loadPoster()/_posterPromise. Defensive throughout: never
|
||||
// once, the same lazy-import pattern openPoster() uses. Defensive throughout: never
|
||||
// throw uncaught into the UI; guard against double-run.
|
||||
let _packetBusy = false
|
||||
function _downloadBlob(blob, filename) {
|
||||
|
|
@ -1377,7 +1410,6 @@ async function openPacket() {
|
|||
|
||||
// ─── Side-menu / dialog state ────────────────────────────────────────────────
|
||||
const showSideMenu = ref(false)
|
||||
const showPoster = ref(false)
|
||||
const selectedFeature = computed(() => mapStore.selectedFeature)
|
||||
|
||||
// ─── Theme toggle (clone of doxa-simple-map.vue:368-395) ─────────────────────
|
||||
|
|
@ -2388,7 +2420,6 @@ onBeforeUnmount(() => {
|
|||
window.removeEventListener('legend:highlight', onLegendHighlight)
|
||||
if (_containerObserver) { _containerObserver.disconnect(); _containerObserver = null }
|
||||
if (_scrollZoomObserver) { _scrollZoomObserver.disconnect(); _scrollZoomObserver = null }
|
||||
poster.value?.cleanup?.()
|
||||
mapLayers.stopPrayerGlow()
|
||||
clustering.cleanup?.()
|
||||
selectedPin.cleanup?.()
|
||||
|
|
@ -2459,6 +2490,20 @@ onBeforeUnmount(() => {
|
|||
<LocationButton :map="map" :is-dark="isDark" />
|
||||
<FullscreenButton :map-container="mapContainer" :is-dark="isDark" />
|
||||
<ThemeToggleButton :is-dark="isDark" @toggle="handleToggleTheme" />
|
||||
<!-- EXPORT, ONE CLICK, ALWAYS VISIBLE (BUILD-SIMPLE-RESEARCH-EXPORT-ZERO-CLICK). The only
|
||||
export control used to live in the side drawer's footer, which is CLOSED by default
|
||||
(translated off-screen), so a file took two clicks at best — and then nothing arrived.
|
||||
This control sits in the toolbar every reader sees and exports the current view as a
|
||||
print poster on the first click; busy while it renders, so a second click cannot
|
||||
double-fire. -->
|
||||
<MapControlButton :is-dark="isDark" :active="posterBusy" :disabled="posterBusy"
|
||||
:title="exportPosterLabel" class="export-poster-btn" :aria-busy="posterBusy ? 'true' : 'false'"
|
||||
@click="openPoster">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
|
||||
</svg>
|
||||
</MapControlButton>
|
||||
<!-- Share button hidden behind FEATURES.share (glitch — PR gate). All
|
||||
ShareButton code kept warm; flip FEATURES.share=true to restore. -->
|
||||
<ShareButton v-if="FEATURES.share" :is-dark="isDark" :embed-path="'doxa-research-map/research-map/index.html'" />
|
||||
|
|
@ -2532,14 +2577,6 @@ onBeforeUnmount(() => {
|
|||
@close="mapStore.selectFeature(null)"
|
||||
/>
|
||||
|
||||
<!-- Poster export modal -->
|
||||
<PosterDialog
|
||||
v-if="showPoster"
|
||||
:poster="poster"
|
||||
:legend-rows="legend.rows"
|
||||
:active-tab="activeTab"
|
||||
@close="showPoster = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-export-one-click.mjs — the Simple Research "Export poster" button gives you a FILE on ONE
|
||||
* click, with no menu, dialog or second step, in its own leaf AND inside the Research Gallery, in
|
||||
* en AND ar.
|
||||
*
|
||||
* WHY (card BUILD-SIMPLE-RESEARCH-EXPORT-ZERO-CLICK; the Driver: "there's an export button on the
|
||||
* simple dashboard ... get it working"). Measured 2026-09-25 before the fix: one click produced
|
||||
* NOTHING — no dialog, no file within 60s, no error — in both hosts. A silent dead button reads as
|
||||
* "the export is slow" to a reader, never as "it is broken", so no other gate could see it.
|
||||
*
|
||||
* WHAT IT ASSERTS, per host × language:
|
||||
* 1. exactly ONE click on the ALWAYS-VISIBLE export button in the map toolbar (button.export-poster-btn).
|
||||
* The drawer's "Export poster…" does not count: the drawer is CLOSED by default (translated off
|
||||
* screen), so it costs an extra click to reach — measured 2026-09-25, and in Arabic the button
|
||||
* could not be clicked at all. The toolbar button must be on screen with no preparation;
|
||||
* 2. a download arrives within 120s, WITHOUT any further click;
|
||||
* 3. the file is a real PNG (signature) of the print size (US Letter at 200 DPI = 1700x2200);
|
||||
* 4. 0 page errors.
|
||||
*
|
||||
* Runs through internal/browser-slot.sh like every browser gate. Starts its own servers on
|
||||
* OS-assigned ports and proves the server is THIS checkout's tree before measuring.
|
||||
* --provider maplibre runs the same four on the keyless engine.
|
||||
* EXPORT_SAVE_DIR=<dir> keeps each downloaded poster (<host>-<lang>-<file>) so a person can look at it.
|
||||
* Exit: 0 pass · 1 fail · 2 SKIP (no built tree / no playwright) · 75 NOT MEASURED (server identity)
|
||||
*/
|
||||
import { createServer } from 'node:http'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
||||
if (!existsSync(resolve(ROOT, 'public/doxa-maps/research-map-plus-grid/research-map-plus-grid.js'))) {
|
||||
console.log('SKIP check-export-one-click: no built research-map-plus-grid'); process.exit(2)
|
||||
}
|
||||
let chromium
|
||||
try { ({ chromium } = await import('playwright')) } catch { console.log('SKIP check-export-one-click: playwright not installed'); process.exit(2) }
|
||||
|
||||
const freePort = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const PROVIDER = process.argv.includes('--provider') ? process.argv[process.argv.indexOf('--provider') + 1] : '' // maplibre = the keyless engine
|
||||
const TP = await freePort(), HP = await freePort()
|
||||
const TREE = `http://127.0.0.1:${TP}/doxa-maps`
|
||||
let lang = 'en'
|
||||
const srv = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TP)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end(`<!doctype html><html lang="${lang}"><body style="margin:0"><script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa tab="research-map-plus-grid"${PROVIDER ? ` provider="${PROVIDER}"` : ''} style="height:900px"></doxa-maps-spa></body></html>`) }).listen(HP, 'localhost')
|
||||
const cleanup = () => { try { srv.kill() } catch {} try { host.close() } catch {} }
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
|
||||
// Server identity by CONTENT: the served manifest must be this checkout's.
|
||||
{
|
||||
const disk = readFileSync(resolve(ROOT, 'public/doxa-maps/manifest.json'), 'utf8')
|
||||
let served = ''
|
||||
for (let i = 0; i < 20 && !served; i++) {
|
||||
try { const r = await fetch(`${TREE}/manifest.json`); if (r.ok) served = await r.text() } catch { /* not up yet */ }
|
||||
if (!served) await new Promise(r => setTimeout(r, 300))
|
||||
}
|
||||
if (served !== disk) { console.log(`NOT MEASURED: :${TP} is not serving this checkout's tree. Re-run.`); cleanup(); process.exit(75) }
|
||||
}
|
||||
|
||||
const b = await chromium.launch({ args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'] })
|
||||
const fails = []
|
||||
try {
|
||||
for (lang of ['en', 'ar']) for (const where of ['leaf', 'gallery']) {
|
||||
const url = where === 'leaf' ? `${TREE}/research-map-plus-grid/research-map-plus-grid/index.html?lang=${lang}${PROVIDER ? `&provider=${PROVIDER}` : ''}` : `http://localhost:${HP}/`
|
||||
const ctx = await b.newContext({ acceptDownloads: true, viewport: { width: 1400, height: 900 } })
|
||||
const p = await ctx.newPage(); const errs = []
|
||||
p.on('pageerror', e => errs.push(String(e).slice(0, 160)))
|
||||
// A failed export is CAUGHT and logged (console.error), never thrown: read the console, or a dead
|
||||
// button reports "no file" with no reason.
|
||||
const consoleErrs = []; p.on('console', m => { if (m.type() === 'error' || m.type() === 'warning') consoleErrs.push(m.text().slice(0, 200)) })
|
||||
const downloads = []; p.on('download', d => downloads.push(d))
|
||||
await p.goto(url); await p.waitForTimeout(12000)
|
||||
const btn = p.locator('button.export-poster-btn').first()
|
||||
const onScreen = await btn.evaluate(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.left >= 0 && r.top >= 0 && r.right <= innerWidth && r.bottom <= innerHeight }).catch(() => false)
|
||||
if (!onScreen) errs.push('the export button is not on screen without a prior click')
|
||||
const label = ((await btn.getAttribute('title', { timeout: 20000 }).catch(() => '')) || '').trim()
|
||||
let clicks = 0
|
||||
// ONE PHYSICAL CLICK at the button's centre, as a reader makes it. Not locator.click(): that
|
||||
// re-checks its hit target and RETRIES, and by the retry the button is already disabled (the
|
||||
// export is running), so it reported "timeout" on exports whose file then arrived — measured
|
||||
// 2026-09-25 in gallery-ar and leaf-en. What the click achieved is judged by the file below.
|
||||
try {
|
||||
const box = await btn.boundingBox({ timeout: 20000 })
|
||||
if (!box) throw new Error('the export button has no box')
|
||||
await p.mouse.click(box.x + box.width / 2, box.y + box.height / 2); clicks = 1
|
||||
} catch (e) { errs.push('could not click: ' + String(e.message || e).split('\n')[0]) }
|
||||
for (let i = 0; i < 120 && !downloads.length; i++) await p.waitForTimeout(1000)
|
||||
let file = null, info = ''
|
||||
if (downloads.length) {
|
||||
file = downloads[0].suggestedFilename()
|
||||
const path = await downloads[0].path()
|
||||
const buf = readFileSync(path)
|
||||
if (process.env.EXPORT_SAVE_DIR) writeFileSync(resolve(process.env.EXPORT_SAVE_DIR, `${where}-${lang}${PROVIDER ? '-' + PROVIDER : ''}-${file}`), buf) // to LOOK at the poster
|
||||
const png = buf.length > 24 && buf.slice(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
||||
const w = png ? buf.readUInt32BE(16) : 0, h = png ? buf.readUInt32BE(20) : 0
|
||||
info = `${png ? 'PNG' : 'NOT-PNG'} ${w}x${h} ${Math.round(buf.length / 1024)}KB`
|
||||
if (!png || w < 1600 || h < 2100) errs.push(`file is not the expected print PNG (${info})`)
|
||||
}
|
||||
if (!file) {
|
||||
for (const c of consoleErrs.filter(c => /poster|useMapPoster/i.test(c)).slice(-2)) errs.push(c)
|
||||
// SLOW or DEAD? A button still busy at the deadline is a render still running; an idle one is an
|
||||
// export that ended without a file. The verdict is FAIL either way; the reason says which.
|
||||
const busy = await btn.evaluate(el => el.disabled || el.getAttribute('aria-busy') === 'true').catch(() => null)
|
||||
errs.push(busy ? 'still rendering at the deadline (button busy)' : 'the export ended with no file (button idle)')
|
||||
}
|
||||
const ok = clicks === 1 && !!file && !errs.length
|
||||
console.log(`${ok ? 'PASS' : 'FAIL'} [${where} ${lang}${PROVIDER ? ' ' + PROVIDER : ''}] "${label}" · 1 click → ${file ? `${file} (${info})` : 'NO FILE within 120s'}${errs.length ? ' · ' + [...new Set(errs)].slice(0, 3).join(' | ') : ' · 0 errors'}`)
|
||||
if (!ok) fails.push(`${where} ${lang}`)
|
||||
await ctx.close()
|
||||
}
|
||||
} finally { await b.close(); cleanup() }
|
||||
|
||||
if (fails.length) { console.log(`\ncheck-export-one-click: FAIL (${fails.join(', ')})`); process.exit(1) }
|
||||
console.log('\ncheck-export-one-click: PASS — one click on Export poster gives a print PNG, leaf and gallery, en and ar')
|
||||
|
|
@ -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-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,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-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,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-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
186
public/doxa-maps/country-posters/country-posters.js
generated
186
public/doxa-maps/country-posters/country-posters.js
generated
File diff suppressed because one or more lines are too long
12
public/doxa-maps/doxa-maps-spa/manifest.json
generated
12
public/doxa-maps/doxa-maps-spa/manifest.json
generated
|
|
@ -158,7 +158,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "doxa-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L",
|
||||
"integrity": "sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -178,7 +178,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": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -192,7 +192,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>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -354,7 +354,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-M9zI75lchJ/lMfPAvDUVUed7HZXoOd4RSAq0rF16ohbSfdwG/g0aqK/1QDsIgnfc",
|
||||
"integrity": "sha384-smxkLip3BUQ3NvfPk15gwPfYepcWieirbddwayS0WNI7uVaULvJqqHrlcqhqW++j",
|
||||
"elements": [
|
||||
"doxa-research-map"
|
||||
],
|
||||
|
|
@ -370,7 +370,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": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-M9zI75lchJ/lMfPAvDUVUed7HZXoOd4RSAq0rF16ohbSfdwG/g0aqK/1QDsIgnfc\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-smxkLip3BUQ3NvfPk15gwPfYepcWieirbddwayS0WNI7uVaULvJqqHrlcqhqW++j\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
122
public/doxa-maps/doxa-research-map/doxa-research-map.js
generated
122
public/doxa-maps/doxa-research-map/doxa-research-map.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -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-M9zI75lchJ/lMfPAvDUVUed7HZXoOd4RSAq0rF16ohbSfdwG/g0aqK/1QDsIgnfc" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-research-map.js" integrity="sha384-smxkLip3BUQ3NvfPk15gwPfYepcWieirbddwayS0WNI7uVaULvJqqHrlcqhqW++j" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,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-M9zI75lchJ/lMfPAvDUVUed7HZXoOd4RSAq0rF16ohbSfdwG/g0aqK/1QDsIgnfc" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-research-map.js" integrity="sha384-smxkLip3BUQ3NvfPk15gwPfYepcWieirbddwayS0WNI7uVaULvJqqHrlcqhqW++j" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
12
public/doxa-maps/manifest.json
generated
12
public/doxa-maps/manifest.json
generated
|
|
@ -158,7 +158,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "doxa-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L",
|
||||
"integrity": "sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -178,7 +178,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": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -192,7 +192,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>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-50ZKX8FcRxEmacDuUyngi9KOsmxU0bz7HzRaCS2zcYpPobksg3itqQwgUkxbyO0L\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-6Uk4vKHEvRs1FKY8Ld71vl3azsjXzyPgCXUrg8AeJWgT1FD8OUWKW7wu8PsJxniL\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -354,7 +354,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-M9zI75lchJ/lMfPAvDUVUed7HZXoOd4RSAq0rF16ohbSfdwG/g0aqK/1QDsIgnfc",
|
||||
"integrity": "sha384-smxkLip3BUQ3NvfPk15gwPfYepcWieirbddwayS0WNI7uVaULvJqqHrlcqhqW++j",
|
||||
"elements": [
|
||||
"doxa-research-map"
|
||||
],
|
||||
|
|
@ -370,7 +370,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": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-M9zI75lchJ/lMfPAvDUVUed7HZXoOd4RSAq0rF16ohbSfdwG/g0aqK/1QDsIgnfc\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
"script": "<!-- Mapbox engine, fetched early; remove this line if you set \"provider\":\"maplibre\" -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-smxkLip3BUQ3NvfPk15gwPfYepcWieirbddwayS0WNI7uVaULvJqqHrlcqhqW++j\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -94,7 +94,12 @@ export default defineConfig(({ command, mode }) => {
|
|||
// the maintainer's internal platform and must NOT exist in public
|
||||
// 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') },
|
||||
define: {
|
||||
__DOXA_QA__: JSON.stringify(process.env.DOXA_QA === '1'),
|
||||
// The output tree's name (vite.doxa-maps-build-config.json `name`, the same value OUT_ROOT uses),
|
||||
// for the few runtime paths that must name the tree — so renaming the tree needs no source edit.
|
||||
__DOXA_TREE_NAME__: JSON.stringify(MFE_NAME),
|
||||
},
|
||||
// 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue