Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts: # public/doxa-maps/index.html
This commit is contained in:
commit
4d79c08fd4
10 changed files with 247 additions and 77 deletions
138
internal/check-legend-export-reachable.mjs
Normal file
138
internal/check-legend-export-reachable.mjs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env node
|
||||
// check-legend-export-reachable — the Export affordance must be reachable AT BOTH WIDTHS.
|
||||
//
|
||||
// ★ WHAT THIS CAUGHT. The Export menu lives in SemanticTreeLegend, which upg-100-list.vue mounts
|
||||
// for DESKTOP only ("Hidden below the mobile breakpoint, where <LegendMobile>..."). LegendMobile —
|
||||
// the phone bottom-sheet — contains the string "export" ZERO times. So on a phone a reader can
|
||||
// browse the UPG-100 lists and cannot export a packet or a poster. No error, nothing to click; the
|
||||
// failure is invisible rather than broken, which is why it survived.
|
||||
//
|
||||
// The packet IS the product for this profile. The Driver: "print options at the top right of the
|
||||
// legend... that's the only way that map would be usable."
|
||||
//
|
||||
// ⚠ THE CONTROL IS GATED ON A SELECTION. `v-if="SHOW_EXPORT_BUTTON && exportEnabled && selectedNode"`
|
||||
// — so a probe that merely loads the page finds nothing at EITHER width and concludes "no export
|
||||
// anywhere". Measured: that was my first reading and it was wrong. This gate SELECTS A LEAF ROW
|
||||
// first, and asserts the 0 -> n transition, which is also what proves the control is selection-gated
|
||||
// rather than always-on.
|
||||
//
|
||||
// ⚠ AND ON MOBILE THE LEGEND STARTS CLOSED. 20 rows exist with 0 visible until the "Show or hide
|
||||
// the legend" control is clicked. A probe that skips that step reports "no rows on mobile", which is
|
||||
// a true statement about the wrong surface — the second wrong reading this gate encodes against.
|
||||
//
|
||||
// ⚠ VISIBILITY IS GEOMETRY, NOT A CSS PROPERTY. The mobile elements report display:block and
|
||||
// visibility:visible while measuring 0x0 at (0,0) inside an overflow:hidden parent — the desktop
|
||||
// panel rendered but never laid out. A predicate reading `visibility` would call that visible.
|
||||
// Assert a real box.
|
||||
//
|
||||
// Usage: node internal/check-legend-export-reachable.mjs [leafUrl]
|
||||
// SKIPs when nothing is served — this needs the built tree.
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
|
||||
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html'
|
||||
const LEAF = process.argv[2] || DEFAULT_LEAF
|
||||
|
||||
let reachable = false
|
||||
try {
|
||||
const r = await fetch(LEAF, { signal: AbortSignal.timeout(4000) })
|
||||
reachable = r.ok
|
||||
} catch { /* a precondition, not a product failure */ }
|
||||
if (!reachable) {
|
||||
console.log(`LEGEND-EXPORT-REACHABLE: SKIP — nothing served at ${LEAF}.`)
|
||||
console.log(' Serve the built tree: python3 -m http.server 4173 --directory public (or set BASE_URL)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const WALK = `(root, out) => { out = out || []; const rec = (r) => { for (const e of r.querySelectorAll('*')) { out.push(e); if (e.shadowRoot) rec(e.shadowRoot) } }; rec(root); return out }`
|
||||
|
||||
async function probe(browser, { name, width, height, mobile }) {
|
||||
const page = await browser.newPage({ viewport: { width, height }, isMobile: mobile, hasTouch: mobile })
|
||||
const errs = []
|
||||
page.on('pageerror', (e) => errs.push(String(e).slice(0, 110)))
|
||||
await page.goto(LEAF, { waitUntil: 'load', timeout: 45000 })
|
||||
await page.waitForTimeout(9000)
|
||||
|
||||
const before = await page.evaluate(`(${WALK})(document).filter(e => (e.className||'').toString().includes('stl-export')).length`)
|
||||
|
||||
// On a phone the legend starts closed — open it first, or every row query returns nothing.
|
||||
const openedSheet = await page.evaluate(`(() => {
|
||||
const all = (${WALK})(document)
|
||||
const b = all.filter(e => e.tagName === 'BUTTON')
|
||||
.find(e => /show or hide the legend|toggle.?legend/i.test(e.getAttribute('aria-label') || ''))
|
||||
if (!b) return false
|
||||
b.click(); return true
|
||||
})()`)
|
||||
if (openedSheet) await page.waitForTimeout(2200)
|
||||
|
||||
// Select a LEAF row. Height-bounded because a container also carries .stl-row-ish classes and
|
||||
// clicking it selects nothing — measured: its textContent is the whole list concatenated.
|
||||
const clicked = await page.evaluate(`(() => {
|
||||
const rows = (${WALK})(document)
|
||||
.filter(e => (e.className||'').toString().includes('stl-row'))
|
||||
.filter(e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 60 })
|
||||
if (!rows.length) return null
|
||||
rows[0].click()
|
||||
return (rows[0].textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 30)
|
||||
})()`)
|
||||
await page.waitForTimeout(2200)
|
||||
|
||||
const after = await page.evaluate(`(() => {
|
||||
const el = (${WALK})(document).filter(e => (e.className||'').toString().includes('stl-export'))
|
||||
// A REAL BOX, not a CSS property: the mobile case reports visibility:visible at 0x0.
|
||||
const boxed = el.filter(e => { const r = e.getBoundingClientRect(); return r.width > 2 && r.height > 2 })
|
||||
return { total: el.length, boxed: boxed.length,
|
||||
label: boxed.map(e => (e.textContent || '').trim().slice(0, 18))[0] || '' }
|
||||
})()`)
|
||||
await page.close()
|
||||
return { name, before, openedSheet, clicked, ...after, errs }
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
|
||||
let results
|
||||
try {
|
||||
results = [
|
||||
await probe(browser, { name: 'desktop', width: 1500, height: 1000, mobile: false }),
|
||||
await probe(browser, { name: 'mobile', width: 390, height: 844, mobile: true }),
|
||||
]
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
console.log('LEGEND-EXPORT-REACHABLE')
|
||||
console.log(` leaf : ${LEAF}`)
|
||||
for (const r of results) {
|
||||
console.log(` ${r.name.padEnd(8)}: selected ${JSON.stringify(r.clicked)} — `
|
||||
+ `.stl-export ${r.before} -> ${r.total} present, ${r.boxed} with a real box`
|
||||
+ `${r.label ? ` ("${r.label}")` : ''}${r.openedSheet ? ' [opened the sheet first]' : ''}`)
|
||||
}
|
||||
|
||||
const fails = []
|
||||
for (const r of results) {
|
||||
if (!r.clicked) {
|
||||
fails.push(`${r.name}: no selectable legend row — the probe could not reach the state the `
|
||||
+ 'export control is gated on, so this width was not actually tested')
|
||||
continue
|
||||
}
|
||||
if (r.boxed === 0) {
|
||||
fails.push(`${r.name}: the Export affordance has NO reachable box`
|
||||
+ (r.total > 0
|
||||
? ` — ${r.total} .stl-export element(s) exist but all measure 0x0, i.e. the desktop legend `
|
||||
+ 'rendered without being laid out. A reader here can browse the lists and cannot export '
|
||||
+ 'a packet or a poster.'
|
||||
: ' — no .stl-export element at all.'))
|
||||
}
|
||||
if (r.errs.length) fails.push(`${r.name}: ${r.errs.length} page error(s): ${r.errs[0]}`)
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nLEGEND-EXPORT-REACHABLE: FAIL — ${fails.length} problem(s)`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
console.error('\n The packet IS the product for this profile. See '
|
||||
+ 'BUG-UPG100-PACKET-AND-POSTER-EXPORT-ARE-UNREACHABLE-ON-A-PHONE — WHERE the control goes on a '
|
||||
+ 'phone is a design call (the sheet has no titlebar), so this gate names the gap rather than '
|
||||
+ 'assuming a fix.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('\nLEGEND-EXPORT-REACHABLE: PASS — selecting a list reveals a reachable Export control at '
|
||||
+ 'every width tested.')
|
||||
|
|
@ -351,6 +351,27 @@ const dataStore = inject('dataStore')
|
|||
// never updated by the mobile flow).
|
||||
const onSemanticTreeSelect = inject('onSemanticTreeSelect', () => {})
|
||||
|
||||
// ★ THE EXPORT AFFORDANCE, WHICH THIS SHEET RENDERED AND NEVER ENABLED.
|
||||
// This component already renders <SemanticTreeLegend> below — the same component that
|
||||
// owns the Export menu — but passed neither :export-enabled nor @export. exportEnabled
|
||||
// defaults to FALSE, so the control's v-if was never satisfied and a phone reader could
|
||||
// browse the UPG-100 lists and not export a packet or a poster. The packet IS the product
|
||||
// for that profile; the Driver asked for "print options at the top right of the legend...
|
||||
// that's the only way that map would be usable".
|
||||
//
|
||||
// ⚠ IT WAS NOT A DESIGN QUESTION, WHICH IS WHAT I FIRST RECORDED. The 0x0 elements that
|
||||
// measurement found belong to the OTHER, hidden SemanticTreeLegend instance — the desktop
|
||||
// one, mounted but not laid out below the breakpoint. That geometry made it look as though
|
||||
// the mobile sheet had nowhere to put the control. It renders its own instance, with a
|
||||
// titlebar, and only needed the prop.
|
||||
//
|
||||
// PROVIDE/INJECT, matching onSemanticTreeSelect directly above, because this sheet is
|
||||
// rendered deep inside a shell rather than as a direct child — an emit would not reach.
|
||||
// OPT-IN BY CONSTRUCTION: exportEnabled is true only when a profile actually provides a
|
||||
// handler, so the three profiles that do not provide one are untouched.
|
||||
const onLegendExport = inject('onLegendExport', null)
|
||||
const mobileExportEnabled = computed(() => typeof onLegendExport === 'function')
|
||||
|
||||
// ── Data-driven legend (universal for all legend types) ─────────────────────
|
||||
const legendTypeRef = toRef(props, 'legendType')
|
||||
const {
|
||||
|
|
@ -781,7 +802,9 @@ onBeforeUnmount(() => {
|
|||
:hideTabs="nodes ? !tabs : _isFlatTabMobile"
|
||||
:columnLabel="treeColumnLabelForMobile"
|
||||
:columns="columns || ['count', 'pop']"
|
||||
:export-enabled="mobileExportEnabled"
|
||||
@select="onSemanticTreeSelect"
|
||||
@export="(kind) => onLegendExport && onLegendExport(kind)"
|
||||
>
|
||||
<template #title-caret>
|
||||
<button class="mobile-collapse-caret"
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
"check:locale-reachable": "node internal/check-locale-reachable.mjs",
|
||||
"check:poster-prints": "node internal/check-poster-prints.mjs",
|
||||
"check:provider-switch": "node internal/check-provider-switch.mjs # the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.",
|
||||
"check:legend-export-reachable": "node internal/check-legend-export-reachable.mjs # the Export affordance must be reachable at BOTH widths. Selects a leaf row first (the control is gated on selectedNode) and opens the mobile sheet first (it starts closed). Asserts a REAL BOX, not CSS visibility — on mobile the elements report visibility:visible at 0x0. ⚠ CURRENTLY FAILS ON MOBILE BY DESIGN: the export lives in the desktop legend only, and WHERE it goes on a phone is a Driver call (BUG-UPG100-PACKET-AND-POSTER-EXPORT-ARE-UNREACHABLE-ON-A-PHONE). Not wave-wired until then. SKIPs when nothing is served.",
|
||||
"check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs",
|
||||
"check:share-policy": "node internal/check-share-policy.mjs",
|
||||
"check:zone-census": "node internal/check-zone-census.mjs # every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.",
|
||||
|
|
|
|||
|
|
@ -1097,6 +1097,13 @@ function watchLegendRows() {
|
|||
// here). Providing it wires the mobile sheet to the same handler the desktop
|
||||
// legend's @select uses — without this, mobile rows render but tap dead.
|
||||
provide('onSemanticTreeSelect', onLegendSelect)
|
||||
// The SAME handler the desktop legend gets via @export. Provided rather than passed as a
|
||||
// prop because <LegendMobile> renders several levels down inside the shell, which is also
|
||||
// why onSemanticTreeSelect above is provided. Without this the mobile sheet rendered its
|
||||
// SemanticTreeLegend with exportEnabled defaulting to FALSE, so a phone reader could browse
|
||||
// the lists and had no way to print a packet or a poster — the half of this profile the
|
||||
// Driver called "the only way that map would be usable".
|
||||
provide('onLegendExport', onLegendExport)
|
||||
|
||||
// ── LEGEND EXPORT (top-right of the legend titlebar) ────────────────────────
|
||||
// The shared legend emits 'export' with 'poster' | 'packet' for the currently
|
||||
|
|
@ -1984,7 +1991,6 @@ onBeforeUnmount(() => {
|
|||
v-if="legendNodes.length"
|
||||
:nodes="legendNodes"
|
||||
:tabs="LEGEND_TABS"
|
||||
:default-tab="LEGEND_DEFAULT_TAB"
|
||||
:columns="['count', 'pop']"
|
||||
:title="LEGEND_TITLE"
|
||||
:export-enabled="true"
|
||||
|
|
@ -2004,6 +2010,8 @@ onBeforeUnmount(() => {
|
|||
v-if="legendNodes.length"
|
||||
legend-type="upg-100"
|
||||
:nodes="legendNodes"
|
||||
:tabs="LEGEND_TABS"
|
||||
:default-tab="LEGEND_DEFAULT_TAB"
|
||||
:columns="['count', 'pop']"
|
||||
column-label="UPG 100"
|
||||
:title="LEGEND_TITLE"
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -676,7 +676,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p",
|
||||
"integrity": "sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -696,7 +696,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -710,7 +710,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -724,7 +724,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
136
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
136
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB" 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 <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-xb5q69l8SauILfDQxPhCJeIaJ0JZVCOV5KhsOmbOrPbyAjr9y5Cjw+WmDUjN5IjB" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue