Update from source 3c988b30c
This commit is contained in:
parent
b103bcea24
commit
2759acb556
17 changed files with 184 additions and 155 deletions
|
|
@ -47,6 +47,8 @@
|
|||
* BASE_URL=http://127.0.0.1:4173 PROBE_PATH=/<TREE_NAME>/... node internal/check-facet-changes-map.mjs
|
||||
*/
|
||||
import { chromium } from './lib/playwright-guarded.mjs'
|
||||
// The in-page helper lives in a browser-free module so check:facet-shape can test the same string headlessly.
|
||||
import { WALK } from './lib/facet-walk.mjs'
|
||||
|
||||
// Defaults to the DEV server, because this gate drives a live interaction and the dev surface is
|
||||
// the one that always reflects current source. Point BASE_URL/PROBE_PATH at the built tree
|
||||
|
|
@ -110,96 +112,6 @@ const GL_ARGS = [
|
|||
'--no-sandbox',
|
||||
]
|
||||
|
||||
// Shadow DOM DOES NOT PIERCE — recursive walk with a seen-Set, re-injected after every interaction.
|
||||
const WALK = `
|
||||
function __walk(sel) {
|
||||
const seen = new Set(); const out = [];
|
||||
const visit = (root) => {
|
||||
if (!root || seen.has(root)) return; seen.add(root);
|
||||
let els = []; try { els = Array.from(root.querySelectorAll('*')) } catch (e) { return }
|
||||
for (const el of els) {
|
||||
try { if (el.matches && el.matches(sel)) out.push(el) } catch (e) {}
|
||||
if (el.shadowRoot) visit(el.shadowRoot);
|
||||
}
|
||||
};
|
||||
visit(document); return out;
|
||||
}
|
||||
// THE MEASUREMENT: the map's own bubble source — the DOTS the agent looks at — plus the paint
|
||||
// property that decides what each one LOOKS like.
|
||||
// ★ THE MAP HANDLE CAN GO STALE. The panel destroys and re-creates its Map on a re-render (and
|
||||
// the dev server's HMR does this freely), which leaves window.__rdashMap pointing at a torn-down
|
||||
// instance whose getSource() throws "Cannot read properties of undefined (reading 'getOwnSource')".
|
||||
// Measured: one run in three failed that way on the dev surface while the product was fine.
|
||||
// A gate that reports a product bug when its own handle went stale is a gate that gets ignored, so
|
||||
// __live() proves the handle still works before any assertion reads through it.
|
||||
// ★ READ A GEOJSON SOURCE'S FEATURES WITHOUT ASSUMING ONE ENGINE'S PRIVATE SHAPE.
|
||||
// This gate could only ever measure Mapbox (BUG-FACET-GATE-CANNOT-MEASURE-MAPLIBRE), and the cause
|
||||
// is four reads of 'source._data'. The two engines store it differently — cited from the bundles
|
||||
// this repo actually loads, same constructor in each:
|
||||
// mapbox-gl v3.24.0 this._data = i.data (the raw GeoJSON)
|
||||
// maplibre-gl this._data = "string"==typeof i.data ? {url:…} : {geojson: i.data}
|
||||
// So '_data.features' is undefined under MapLibre and every read fell to an empty list. A GATE that
|
||||
// cannot see MapLibre's features PASSES A MAP THAT IS BROKEN THERE, which is worse than a plugin
|
||||
// having the same bug.
|
||||
//
|
||||
// ⚠ THE 's._data || s.serialize().data' FALLBACK DOES NOT FIX IT, and that is why this is a helper
|
||||
// rather than a copied one-liner: '||' only fires when the left side is FALSY, and '{geojson: …}'
|
||||
// is TRUTHY. The old __state() carried that form and was broken under MapLibre exactly like the
|
||||
// three unguarded reads. The wrapper has to be UNWRAPPED, not fallen back from.
|
||||
//
|
||||
// ⚠ AND IT LIVES HERE, IN THE PAGE, not as an import of library/maps/pin-selection.js. All four
|
||||
// call sites run inside page.evaluate / page.waitForFunction bodies, which are serialised to the
|
||||
// browser — a Node-side import does not exist there. This is the same logic as readSourceFeatures(),
|
||||
// deliberately duplicated across the process boundary, which is the one place duplication is not a
|
||||
// smell. If that helper's rules change, this must follow.
|
||||
function __srcFeatures(m, id) {
|
||||
let s; try { s = m.getSource(id) } catch (e) { return null }
|
||||
if (!s) return null;
|
||||
let data = s._data !== undefined ? s._data : (s.serialize && s.serialize().data);
|
||||
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||
if (data.geojson !== undefined) data = data.geojson;
|
||||
// A REMOTE SOURCE IS NOT AN EMPTY ONE. Its features live on a server and cannot be enumerated
|
||||
// here; returning [] would read as "the map drew nothing", which is the bug this gate exists to
|
||||
// catch. null means "could not measure", and the caller must not treat it as zero.
|
||||
else if (data.url !== undefined) return null;
|
||||
}
|
||||
return (data && data.features) || null;
|
||||
}
|
||||
function __live() {
|
||||
const m = window.__rdashMap;
|
||||
if (!m) return null;
|
||||
try { m.getSource('rd-bubbles'); return m } catch (e) { return null }
|
||||
}
|
||||
function __state() {
|
||||
const m = __live();
|
||||
if (!m) return { stale: true, err: 'map handle is stale (torn down and re-created)' };
|
||||
let s; try { s = m.getSource('rd-bubbles') } catch (e) { return { stale: true, err: 'map torn down: ' + e.message } }
|
||||
if (!s) return { err: 'no rd-bubbles source' };
|
||||
const feats = __srcFeatures(m, 'rd-bubbles') || [];
|
||||
let color = null; try { color = m.getPaintProperty('rd-bubble-layer', 'circle-color') } catch (e) {}
|
||||
return {
|
||||
n: feats.length,
|
||||
slugs: feats.map((f) => f.properties && f.properties.slug),
|
||||
praying: feats.map((f) => (f.properties ? f.properties.peoplePraying : undefined)),
|
||||
color,
|
||||
colorIsExpression: Array.isArray(color),
|
||||
colorJson: JSON.stringify(color),
|
||||
};
|
||||
}
|
||||
function __openPrayer() {
|
||||
const dd = __walk('.filter-dropdown').find((d) => (d.textContent || '').includes('Prayer'));
|
||||
if (!dd) return false;
|
||||
const b = dd.querySelector('button'); if (!b) return false;
|
||||
b.click(); return true;
|
||||
}
|
||||
function __pickPrayer(label) {
|
||||
const dd = __walk('.filter-dropdown').find((d) => (d.textContent || '').includes('Prayer'));
|
||||
if (!dd) return 'no dropdown';
|
||||
const o = Array.from(dd.querySelectorAll('[role=option]')).find((x) => (x.textContent || '').trim().startsWith(label));
|
||||
if (!o) return 'no option: ' + label;
|
||||
o.click(); return 'ok';
|
||||
}
|
||||
`
|
||||
|
||||
// ★ THE SELF-TEST INVERTS THE EXIT CODE, and it must do so HERE rather than at the end.
|
||||
// Every assertion below calls fail() and exits immediately, so a self-test run that correctly
|
||||
|
|
@ -225,40 +137,6 @@ const LEVELS = [
|
|||
{ label: 'Needs Prayer', holds: (n) => !(Number(n) > 0) },
|
||||
]
|
||||
|
||||
// ── SHAPE TEST (headless, no browser) ────────────────────────────────────────────────────────────
|
||||
// ★ NEGATIVE-TESTS __srcFeatures AGAINST BOTH ENGINE SHAPES, in sr's check-pin-selection style.
|
||||
// ⚠ NOT `--self-test`: that already means "run the browser gate with the bug put back, it MUST
|
||||
// fail". This one never opens a browser, so it can run anywhere and in gates:pre.
|
||||
// It evaluates the helper OUT OF THE WALK STRING ITSELF, so the thing tested is the thing injected
|
||||
// into the page — a copy pasted here would pass while the shipped helper rotted.
|
||||
if (process.argv.includes('--shape-test')) {
|
||||
const FC = { type: 'FeatureCollection', features: [{ type: 'Feature', properties: { slug: 'a' } },
|
||||
{ type: 'Feature', properties: { slug: 'b' } }] }
|
||||
const srcFeatures = new Function(`${WALK}\nreturn __srcFeatures`)()
|
||||
const mapWith = (src) => ({ getSource: (id) => (id === 'rd-bubbles' ? src : null) })
|
||||
const cases = [
|
||||
['mapbox shape (_data = FeatureCollection)', () => srcFeatures(mapWith({ _data: FC }), 'rd-bubbles'), 2],
|
||||
['maplibre shape (_data = { geojson })', () => srcFeatures(mapWith({ _data: { geojson: FC } }), 'rd-bubbles'), 2],
|
||||
['public fallback (no _data, serialize())', () => srcFeatures(mapWith({ serialize: () => ({ data: FC }) }), 'rd-bubbles'), 2],
|
||||
// A REMOTE SOURCE MUST NOT READ AS EMPTY. null means "could not measure"; 0 would mean "the map
|
||||
// drew nothing", which is the finding this gate exists to report.
|
||||
['remote (_data = { url }) -> null, not 0', () => srcFeatures(mapWith({ _data: { url: 'https://x.invalid/a.json' } }), 'rd-bubbles'), null],
|
||||
['absent source -> null', () => srcFeatures(mapWith(null), 'rd-bubbles'), null],
|
||||
]
|
||||
let ok = 0
|
||||
for (const [name, run, want] of cases) {
|
||||
const got = run()
|
||||
const n = got === null || got === undefined ? null : got.length
|
||||
const pass = n === want
|
||||
console.log(` ${pass ? '✓' : '✗'} ${name} -> ${n === null ? 'null' : n + ' feature(s)'}`)
|
||||
if (pass) ok++
|
||||
}
|
||||
console.log(ok === cases.length
|
||||
? `FACET-SHAPE-TEST: PASS — ${ok}/${cases.length}, both engine shapes measured`
|
||||
: `FACET-SHAPE-TEST: FAIL — ${ok}/${cases.length}`)
|
||||
process.exit(ok === cases.length ? 0 : 1)
|
||||
}
|
||||
|
||||
async function reinject(page) { await page.addScriptTag({ content: WALK }) }
|
||||
|
||||
// Read __state(), tolerating a STALE map handle: if the panel re-created its Map mid-interaction
|
||||
|
|
|
|||
40
internal/check-facet-shape.mjs
Normal file
40
internal/check-facet-shape.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-facet-shape — the facet gate's in-page source reader, tested headlessly on both engines' shapes.
|
||||
* Imports the SAME WALK string check:facet-changes-map injects (internal/lib/facet-walk.mjs), so a copy cannot
|
||||
* drift from what ships. No browser. Negative-tested by upg100: without the { geojson } unwrap it is 4/5.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { WALK } from './lib/facet-walk.mjs'
|
||||
|
||||
// ── SHAPE TEST (headless, no browser) ────────────────────────────────────────────────────────────
|
||||
// ★ NEGATIVE-TESTS __srcFeatures AGAINST BOTH ENGINE SHAPES, in sr's check-pin-selection style.
|
||||
// ⚠ NOT `--self-test`: that already means "run the browser gate with the bug put back, it MUST
|
||||
// fail". This one never opens a browser, so it can run anywhere and in gates:pre.
|
||||
// It evaluates the helper OUT OF THE WALK STRING ITSELF, so the thing tested is the thing injected
|
||||
// into the page — a copy pasted here would pass while the shipped helper rotted.
|
||||
const FC = { type: 'FeatureCollection', features: [{ type: 'Feature', properties: { slug: 'a' } },
|
||||
{ type: 'Feature', properties: { slug: 'b' } }] }
|
||||
const srcFeatures = new Function(`${WALK}\nreturn __srcFeatures`)()
|
||||
const mapWith = (src) => ({ getSource: (id) => (id === 'rd-bubbles' ? src : null) })
|
||||
const cases = [
|
||||
['mapbox shape (_data = FeatureCollection)', () => srcFeatures(mapWith({ _data: FC }), 'rd-bubbles'), 2],
|
||||
['maplibre shape (_data = { geojson })', () => srcFeatures(mapWith({ _data: { geojson: FC } }), 'rd-bubbles'), 2],
|
||||
['public fallback (no _data, serialize())', () => srcFeatures(mapWith({ serialize: () => ({ data: FC }) }), 'rd-bubbles'), 2],
|
||||
// A REMOTE SOURCE MUST NOT READ AS EMPTY. null means "could not measure"; 0 would mean "the map
|
||||
// drew nothing", which is the finding this gate exists to report.
|
||||
['remote (_data = { url }) -> null, not 0', () => srcFeatures(mapWith({ _data: { url: 'https://x.invalid/a.json' } }), 'rd-bubbles'), null],
|
||||
['absent source -> null', () => srcFeatures(mapWith(null), 'rd-bubbles'), null],
|
||||
]
|
||||
let ok = 0
|
||||
for (const [name, run, want] of cases) {
|
||||
const got = run()
|
||||
const n = got === null || got === undefined ? null : got.length
|
||||
const pass = n === want
|
||||
console.log(` ${pass ? '✓' : '✗'} ${name} -> ${n === null ? 'null' : n + ' feature(s)'}`)
|
||||
if (pass) ok++
|
||||
}
|
||||
console.log(ok === cases.length
|
||||
? `FACET-SHAPE-TEST: PASS — ${ok}/${cases.length}, both engine shapes measured`
|
||||
: `FACET-SHAPE-TEST: FAIL — ${ok}/${cases.length}`)
|
||||
process.exit(ok === cases.length ? 0 : 1)
|
||||
96
internal/lib/facet-walk.mjs
Normal file
96
internal/lib/facet-walk.mjs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* internal/lib/facet-walk.mjs — the in-page helper string check:facet-changes-map injects into the page
|
||||
* (__walk through open shadow roots, __srcFeatures reading a GeoJSON source in BOTH engines' shapes).
|
||||
* Browser-free on purpose: check:facet-shape imports this SAME string and evaluates it headlessly, so the
|
||||
* thing tested is the thing injected, and the test runs in gates:pre on any seat.
|
||||
*/
|
||||
// Shadow DOM DOES NOT PIERCE — recursive walk with a seen-Set, re-injected after every interaction.
|
||||
export const WALK = `
|
||||
function __walk(sel) {
|
||||
const seen = new Set(); const out = [];
|
||||
const visit = (root) => {
|
||||
if (!root || seen.has(root)) return; seen.add(root);
|
||||
let els = []; try { els = Array.from(root.querySelectorAll('*')) } catch (e) { return }
|
||||
for (const el of els) {
|
||||
try { if (el.matches && el.matches(sel)) out.push(el) } catch (e) {}
|
||||
if (el.shadowRoot) visit(el.shadowRoot);
|
||||
}
|
||||
};
|
||||
visit(document); return out;
|
||||
}
|
||||
// THE MEASUREMENT: the map's own bubble source — the DOTS the agent looks at — plus the paint
|
||||
// property that decides what each one LOOKS like.
|
||||
// ★ THE MAP HANDLE CAN GO STALE. The panel destroys and re-creates its Map on a re-render (and
|
||||
// the dev server's HMR does this freely), which leaves window.__rdashMap pointing at a torn-down
|
||||
// instance whose getSource() throws "Cannot read properties of undefined (reading 'getOwnSource')".
|
||||
// Measured: one run in three failed that way on the dev surface while the product was fine.
|
||||
// A gate that reports a product bug when its own handle went stale is a gate that gets ignored, so
|
||||
// __live() proves the handle still works before any assertion reads through it.
|
||||
// ★ READ A GEOJSON SOURCE'S FEATURES WITHOUT ASSUMING ONE ENGINE'S PRIVATE SHAPE.
|
||||
// This gate could only ever measure Mapbox (BUG-FACET-GATE-CANNOT-MEASURE-MAPLIBRE), and the cause
|
||||
// is four reads of 'source._data'. The two engines store it differently — cited from the bundles
|
||||
// this repo actually loads, same constructor in each:
|
||||
// mapbox-gl v3.24.0 this._data = i.data (the raw GeoJSON)
|
||||
// maplibre-gl this._data = "string"==typeof i.data ? {url:…} : {geojson: i.data}
|
||||
// So '_data.features' is undefined under MapLibre and every read fell to an empty list. A GATE that
|
||||
// cannot see MapLibre's features PASSES A MAP THAT IS BROKEN THERE, which is worse than a plugin
|
||||
// having the same bug.
|
||||
//
|
||||
// ⚠ THE 's._data || s.serialize().data' FALLBACK DOES NOT FIX IT, and that is why this is a helper
|
||||
// rather than a copied one-liner: '||' only fires when the left side is FALSY, and '{geojson: …}'
|
||||
// is TRUTHY. The old __state() carried that form and was broken under MapLibre exactly like the
|
||||
// three unguarded reads. The wrapper has to be UNWRAPPED, not fallen back from.
|
||||
//
|
||||
// ⚠ AND IT LIVES HERE, IN THE PAGE, not as an import of library/maps/pin-selection.js. All four
|
||||
// call sites run inside page.evaluate / page.waitForFunction bodies, which are serialised to the
|
||||
// browser — a Node-side import does not exist there. This is the same logic as readSourceFeatures(),
|
||||
// deliberately duplicated across the process boundary, which is the one place duplication is not a
|
||||
// smell. If that helper's rules change, this must follow.
|
||||
function __srcFeatures(m, id) {
|
||||
let s; try { s = m.getSource(id) } catch (e) { return null }
|
||||
if (!s) return null;
|
||||
let data = s._data !== undefined ? s._data : (s.serialize && s.serialize().data);
|
||||
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||
if (data.geojson !== undefined) data = data.geojson;
|
||||
// A REMOTE SOURCE IS NOT AN EMPTY ONE. Its features live on a server and cannot be enumerated
|
||||
// here; returning [] would read as "the map drew nothing", which is the bug this gate exists to
|
||||
// catch. null means "could not measure", and the caller must not treat it as zero.
|
||||
else if (data.url !== undefined) return null;
|
||||
}
|
||||
return (data && data.features) || null;
|
||||
}
|
||||
function __live() {
|
||||
const m = window.__rdashMap;
|
||||
if (!m) return null;
|
||||
try { m.getSource('rd-bubbles'); return m } catch (e) { return null }
|
||||
}
|
||||
function __state() {
|
||||
const m = __live();
|
||||
if (!m) return { stale: true, err: 'map handle is stale (torn down and re-created)' };
|
||||
let s; try { s = m.getSource('rd-bubbles') } catch (e) { return { stale: true, err: 'map torn down: ' + e.message } }
|
||||
if (!s) return { err: 'no rd-bubbles source' };
|
||||
const feats = __srcFeatures(m, 'rd-bubbles') || [];
|
||||
let color = null; try { color = m.getPaintProperty('rd-bubble-layer', 'circle-color') } catch (e) {}
|
||||
return {
|
||||
n: feats.length,
|
||||
slugs: feats.map((f) => f.properties && f.properties.slug),
|
||||
praying: feats.map((f) => (f.properties ? f.properties.peoplePraying : undefined)),
|
||||
color,
|
||||
colorIsExpression: Array.isArray(color),
|
||||
colorJson: JSON.stringify(color),
|
||||
};
|
||||
}
|
||||
function __openPrayer() {
|
||||
const dd = __walk('.filter-dropdown').find((d) => (d.textContent || '').includes('Prayer'));
|
||||
if (!dd) return false;
|
||||
const b = dd.querySelector('button'); if (!b) return false;
|
||||
b.click(); return true;
|
||||
}
|
||||
function __pickPrayer(label) {
|
||||
const dd = __walk('.filter-dropdown').find((d) => (d.textContent || '').includes('Prayer'));
|
||||
if (!dd) return 'no dropdown';
|
||||
const o = Array.from(dd.querySelectorAll('[role=option]')).find((x) => (x.textContent || '').trim().startsWith(label));
|
||||
if (!o) return 'no option: ' + label;
|
||||
o.click(); return 'ok';
|
||||
}
|
||||
`
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
"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",
|
||||
"check:facet-shape": "node internal/check-facet-changes-map.mjs --shape-test",
|
||||
"check:facet-shape": "node internal/check-facet-shape.mjs",
|
||||
"check:facet-changes-map": "node internal/check-facet-changes-map.mjs",
|
||||
"check:facet-changes-map:built": "sh -c 'BASE_URL=${BASE_URL:-http://127.0.0.1:4173} PROBE_PATH=${PROBE_PATH:-/doxa-maps/research-dashboard/research-dashboard/index.html} exec node internal/check-facet-changes-map.mjs'",
|
||||
"check:facet-changes-map:self-test": "node internal/check-facet-changes-map.mjs --self-test",
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@ watch(textDir, reflectDir)
|
|||
const listConfig = computed(() => {
|
||||
const cfg = {}
|
||||
if (config.value.list) cfg.list = config.value.list
|
||||
// ⚠ THE SPELLING IS LOAD-BEARING AND THE TWO SIDES DO NOT MATCH BY NAME. This writes `lang`; the
|
||||
// grid's prop is `languageCode`. It works because people-groups-grid.vue accepts three spellings —
|
||||
// languageCode || locale || lang — and `lang` is the THIRD of them. Rename or "tidy" either side
|
||||
// without the other and a composed grid silently renders in the wrong language on a profile-config
|
||||
// embed, which is the exact failure 8b40f6c58 was written to fix and which no gate would catch:
|
||||
// ?lang= embeds keep working, so only a host passing profile-config sees it.
|
||||
if (hostLang.value) cfg.lang = hostLang.value
|
||||
return Object.keys(cfg).length ? JSON.stringify(cfg) : ''
|
||||
})
|
||||
|
|
|
|||
|
|
@ -120,7 +120,11 @@ const MOBILE_TABS = computed(() => [
|
|||
// The centre shows the geographic map or the language tree; the tree (its own whiteboard map) is built
|
||||
// on first use and then kept, so switching back and forth costs nothing after the first time.
|
||||
const treeEver = ref(false)
|
||||
watch(() => board.centre.value, (c) => { if (c === 'tree') treeEver.value = true }, { immediate: true })
|
||||
// The geographic map mounts the same way, on first show: the board opens on the tree, and a hidden map
|
||||
// still built a second WebGL context, its tiles and 1,700 pins on every open (it froze /embed-test when
|
||||
// several heavy cards mounted together). Once shown it stays mounted, so switching back is instant.
|
||||
const mapEver = ref(false)
|
||||
watch(() => board.centre.value, (c) => { if (c === 'tree') treeEver.value = true; if (c === 'map') mapEver.value = true }, { immediate: true })
|
||||
const CENTRE_TABS = computed(() => [
|
||||
{ id: 'map', label: T('gospelInternet.centre.map', 'Map') },
|
||||
{ id: 'tree', label: T('gospelInternet.centre.tree', 'Language tree') },
|
||||
|
|
@ -241,7 +245,7 @@ useShadowStyles(
|
|||
|
||||
<div v-show="!isNarrow || mobilePanel === 'facets'" class="gb-facets"><GiFacetRail /></div>
|
||||
<div v-show="!isNarrow || mobilePanel === 'map'" class="gb-map">
|
||||
<GiMap v-show="board.centre.value === 'map'" />
|
||||
<GiMap v-if="mapEver" v-show="board.centre.value === 'map'" />
|
||||
<GiTree v-if="treeEver" v-show="board.centre.value === 'tree'" />
|
||||
<div class="gb-seg gb-centre-tabs" role="group" :aria-label="T('gospelInternet.centre.aria', 'Show')">
|
||||
<button v-for="c in CENTRE_TABS" :key="c.id" type="button" :aria-pressed="board.centre.value === c.id ? 'true' : 'false'"
|
||||
|
|
|
|||
|
|
@ -261,6 +261,11 @@ const prayBaseUrl = computed(() => props.prayBaseUrl || cfg.value.prayBaseUrl |
|
|||
// localeFromUrl() is the library's own reader, imported rather than re-implemented, so the fetch and
|
||||
// the chrome can never disagree about what the URL asked for. Same ladder regions uses
|
||||
// (region-dashboards.vue:95), and a host that passes languageCode still wins.
|
||||
// ⚠ ALL THREE SPELLINGS ARE IN USE AND `lang` HAS A CALLER. big-picture composes this grid and threads
|
||||
// the host's locale into its profile-config as `lang` (big-picture.vue, listConfig) — it cannot reach
|
||||
// this bundle any other way, because getRuntimeLocale is module state and a separately-built child gets
|
||||
// its own copy. Dropping `cfg.value.lang` from this chain would leave a composed grid in the wrong
|
||||
// language on a profile-config embed while ?lang= embeds carry on working, which is how it would ship.
|
||||
const languageCode = computed(() =>
|
||||
props.languageCode || cfg.value.languageCode || cfg.value.locale || cfg.value.lang || localeFromUrl() || '')
|
||||
|
||||
|
|
|
|||
|
|
@ -615,7 +615,7 @@
|
|||
"bundle": "gospel-internet",
|
||||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"output": "dx-maps/gospel-internet/gospel-internet.js",
|
||||
"integrity": "sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr",
|
||||
"integrity": "sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC",
|
||||
"elements": [
|
||||
"gospel-internet-board"
|
||||
],
|
||||
|
|
@ -635,7 +635,7 @@
|
|||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-internet-board> custom element. -->
|
||||
<script src="../../gospel-internet.js" integrity="sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr" crossorigin="anonymous"></script>
|
||||
<script src="../../gospel-internet.js" integrity="sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -615,7 +615,7 @@
|
|||
"bundle": "gospel-internet",
|
||||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"output": "dx-maps/gospel-internet/gospel-internet.js",
|
||||
"integrity": "sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr",
|
||||
"integrity": "sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC",
|
||||
"elements": [
|
||||
"gospel-internet-board"
|
||||
],
|
||||
|
|
@ -635,7 +635,7 @@
|
|||
"scriptUrl": "./gospel-internet/gospel-internet.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-internet/board/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-ZbZNhak8LAbwgVY5TvxEsHY2/86zM29DtbU90M7vgR7b3CpSkJOnEN8S8WO2CKsr\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-internet/gospel-internet.js\" integrity=\"sha384-zYSACr93raEk3DVTgSFgElLv93oXIpwS1CApMXdzLdVf7c+QjTvxiLmEICC4b0DC\" crossorigin=\"anonymous\"></script>\n<gospel-internet-board profile-config='{\"profile\":\"board\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-internet-board>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue