Every shadow stylesheet gets its own label, and a gate keeps it that way
useShadowStyles keys its <style> by the label and overwrites an existing tag with the same key, so two components sharing a label keep only the last one's CSS in a shared shadow root. The regions list lost its sheet this way to the region card, which had kept the list's label when it was extracted. check:shadow-style-labels-unique (gates:pre) fails any label two calls share, globally rather than per shadow root, so a copied or forked component is caught when it is copied. It reads the real parsers, classifies every reference to the function (a rebinding, .call, a re-export or a namespace import fails instead of hiding a call), cross-checks the AST against the token stream, and covers the other ways two calls share a key: unlabelled calls under the dev server's per-component key, and a fixed label over CSS that varies per caller. countries-map.vue, a fork of research-map.vue, carried the same label; it is now 'countries-map', with the country-posters bundle rebuilt and the tree regenerated. The composable's JSDoc called the label a debugging name; it now says it is the key.
This commit is contained in:
parent
b5e4be4355
commit
99816626af
10 changed files with 351 additions and 19 deletions
324
internal/check-shadow-style-labels-unique.mjs
Normal file
324
internal/check-shadow-style-labels-unique.mjs
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
#!/usr/bin/env node
|
||||
// check-shadow-style-labels-unique.mjs — `bun run check:shadow-style-labels-unique`. Source-only, headless, no network.
|
||||
//
|
||||
// Every useShadowStyles(css, label) call must use its OWN label.
|
||||
//
|
||||
// WHY A SHARED LABEL IS A BUG, not a naming nit: useShadowStyles keys its <style> by the label, and when a
|
||||
// tag with that key is already in the shadow root and its text differs, it OVERWRITES the text (the
|
||||
// hot-update branch in library/composables/useShadowStyles.js). Two components that share a label and
|
||||
// mount in one root therefore keep only the LAST one's CSS, silently. The regions list lost its whole
|
||||
// sheet this way on 2026-09-29: RegionSectionCard.vue, extracted from regions-list.vue, kept the label
|
||||
// 'regions-list'. The cards mount after the list's fetch, so they overwrote .rl-root/.rl-scroll and the
|
||||
// list stopped scrolling. It was latent for two days, because the list's own label was being swallowed
|
||||
// into its CSS by a trailing comma; fixing that comma made the label real and the collision live.
|
||||
//
|
||||
// ★ GLOBAL, NOT PER SHADOW ROOT, on purpose. The live defect needs two labels in the SAME root, but proving
|
||||
// which components can co-mount needs an import graph (aliases, import.meta.glob, dynamic imports), and
|
||||
// every edge such a resolver misses UNDER-reports — the "0 collisions" false green this gate exists to
|
||||
// prevent. Both collisions found so far were born the same way: a copy (an extracted component, a forked
|
||||
// profile) kept its source's label. A global rule catches that at the moment of the copy; a per-root rule
|
||||
// catches it only once the two happen to co-mount. Compliance costs a one-word rename.
|
||||
//
|
||||
// THE OTHER WAYS TWO CALLS END UP WITH ONE KEY, each an arm below:
|
||||
// - no label: the key is 'hmr-' + the COMPONENT's HMR id under the dev server (a hash of the CSS in a
|
||||
// build), so two unlabelled calls in one component, or one in a composable its caller also makes,
|
||||
// share a key in dev;
|
||||
// - a label that is fixed while the CSS is not: a wrapper `useX(css) { useShadowStyles(css, 'x') }`, or
|
||||
// CSS built from setup(props), gives every caller the same key with different text.
|
||||
//
|
||||
// ★ A CALL THE WALK CANNOT SEE MUST NOT READ AS "NO CALL". A hand-rolled scan of this question
|
||||
// under-reported twice on 2026-09-29, and a first version of this gate cross-checked its AST walk with a
|
||||
// token scan that used the SAME rule ("the name followed by '('"), so `useShadowStyles?.(…)`, `.call(…)`,
|
||||
// a rebinding and a re-export slipped past both. So: every REFERENCE to the function (by name or by an
|
||||
// import alias) must be classified as an import, a call, or the definition, and anything else FAILS; and
|
||||
// the count of references in Babel's TOKEN stream must equal the count in its AST.
|
||||
// The parsers are the real ones (the SFC parser and Babel), as in check:shadow-css-parses.
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parse as sfcParse } from '@vue/compiler-sfc'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
|
||||
// Each copy of this repo grades ITS OWN tree (check:no-abs-paths).
|
||||
const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const COMPOSABLE = 'library/composables/useShadowStyles.js'
|
||||
const NAME = 'useShadowStyles'
|
||||
const FROM_COMPOSABLE = /(^|\/)useShadowStyles(\.js)?$/
|
||||
const SOURCE_EXT = /\.(vue|js|mjs|cjs|jsx|ts|tsx|mts|cts)$/
|
||||
|
||||
// Keys useShadowStyles itself writes. A component using one would overwrite, or be overwritten by, the injector.
|
||||
const RESERVED = [{ test: (l) => l === 'reduced-motion', why: 'the per-root reduced-motion reset uses this key' },
|
||||
{ test: (l) => l.startsWith('hmr-'), why: 'dev-server hot-update keys use this prefix' }]
|
||||
|
||||
let fails = 0
|
||||
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
||||
|
||||
function pluginsFor(lang) {
|
||||
if (lang === 'ts' || lang === 'mts' || lang === 'cts') return ['typescript']
|
||||
if (lang === 'tsx') return ['typescript', 'jsx']
|
||||
return ['jsx']
|
||||
}
|
||||
|
||||
function scriptsOf(path, src) {
|
||||
const ext = path.split('.').pop()
|
||||
if (ext !== 'vue') return [{ code: src, offset: 0, lang: ext }]
|
||||
const { descriptor } = sfcParse(src, { filename: path })
|
||||
return [descriptor.script, descriptor.scriptSetup].filter(Boolean)
|
||||
.map((b) => ({ code: b.content, offset: b.loc.start.line - 1, lang: b.lang || 'js' }))
|
||||
}
|
||||
|
||||
// Names every identifier a pattern binds (function parameters).
|
||||
function boundNames(p, out = new Set()) {
|
||||
if (!p) return out
|
||||
if (p.type === 'Identifier') out.add(p.name)
|
||||
else if (p.type === 'AssignmentPattern') boundNames(p.left, out)
|
||||
else if (p.type === 'RestElement') boundNames(p.argument, out)
|
||||
else if (p.type === 'ObjectPattern') p.properties.forEach((q) => boundNames(q.type === 'RestElement' ? q : q.value, out))
|
||||
else if (p.type === 'ArrayPattern') p.elements.forEach((q) => boundNames(q, out))
|
||||
else if (p.type === 'TSParameterProperty') boundNames(p.parameter, out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Identifiers READ inside an expression (not property names, not object keys).
|
||||
function readNames(node, out = new Set()) {
|
||||
if (!node || typeof node !== 'object') return out
|
||||
if (Array.isArray(node)) { node.forEach((n) => readNames(n, out)); return out }
|
||||
if (node.type === 'Identifier') { out.add(node.name); return out }
|
||||
for (const k of Object.keys(node)) {
|
||||
if (k === 'loc' || /Comments$/.test(k)) continue
|
||||
if (k === 'property' && node.type === 'MemberExpression' && !node.computed) continue
|
||||
if (k === 'key' && node.type === 'ObjectProperty' && !node.computed) continue
|
||||
readNames(node[k], out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The token-stream count of references, compared with the AST's. Pure, so the self-tests can show it FAILS.
|
||||
function referenceCountsAgree(tokens, astStarts, names) {
|
||||
let n = 0
|
||||
for (const t of tokens) if (t.type && t.type.label === 'name' && names.has(t.value)) n++
|
||||
return { agree: n === astStarts.size, tokens: n, ast: astStarts.size }
|
||||
}
|
||||
|
||||
// One file: the calls with their labels, and every problem that stops the file being graded honestly.
|
||||
function gradeFile(path, src) {
|
||||
const blocks = scriptsOf(path, src)
|
||||
const parsed = blocks.map((b) => ({ ...b, ast: babelParse(b.code, { sourceType: 'module', plugins: pluginsFor(b.lang), tokens: true }) }))
|
||||
|
||||
// Aliases are collected across BOTH blocks: <script> and <script setup> share their imports.
|
||||
const names = new Set([NAME])
|
||||
const problems = []
|
||||
for (const b of parsed) {
|
||||
for (const n of b.ast.program.body) {
|
||||
if (n.type === 'ImportDeclaration' && FROM_COMPOSABLE.test(n.source.value)) {
|
||||
for (const s of n.specifiers) {
|
||||
if (s.type === 'ImportSpecifier' && (s.imported.name || s.imported.value) === NAME) names.add(s.local.name)
|
||||
else problems.push(`${path}:${n.loc.start.line + b.offset}: import useShadowStyles BY NAME ({ useShadowStyles }); a default or namespace import hides its calls from this gate`)
|
||||
}
|
||||
}
|
||||
if (n.type === 'ExportAllDeclaration' && FROM_COMPOSABLE.test(n.source.value)) {
|
||||
problems.push(`${path}:${n.loc.start.line + b.offset}: re-exports the composable (export *); its callers would call it by another name this gate cannot follow`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const calls = []
|
||||
for (const b of parsed) {
|
||||
const at = (node) => `${path}:${node.loc.start.line + b.offset}`
|
||||
const refStarts = new Set()
|
||||
const calleeOf = new Set() // identifiers that are a callee (or a callee's property)
|
||||
;(function walk(node, parent, fnParams) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (Array.isArray(node)) { node.forEach((n) => walk(n, parent, fnParams)); return }
|
||||
if (node.type === 'ImportExpression' || (node.type === 'CallExpression' && node.callee.type === 'Import')) {
|
||||
const s = node.source || node.arguments[0]
|
||||
if (s && s.type === 'StringLiteral' && FROM_COMPOSABLE.test(s.value)) problems.push(`${at(node)}: imports the composable dynamically; import { useShadowStyles } statically so its calls can be graded`)
|
||||
}
|
||||
if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
|
||||
const c = node.callee
|
||||
const direct = c.type === 'Identifier' && names.has(c.name)
|
||||
const member = (c.type === 'MemberExpression' || c.type === 'OptionalMemberExpression') && !c.computed && c.property.name === NAME
|
||||
if (direct || member) {
|
||||
calleeOf.add(direct ? c : c.property)
|
||||
const args = node.arguments
|
||||
let label
|
||||
if (args.slice(0, 2).some((a) => a.type === 'SpreadElement')) label = { kind: 'computed', text: '...spread' }
|
||||
else {
|
||||
const a = args[1]
|
||||
if (!a || (a.type === 'Identifier' && a.name === 'undefined') || a.type === 'NullLiteral' ||
|
||||
(a.type === 'StringLiteral' && a.value === '') ||
|
||||
(a.type === 'TemplateLiteral' && !a.expressions.length && a.quasis[0].value.cooked === '')) label = { kind: 'absent' }
|
||||
else if (a.type === 'StringLiteral') label = { kind: 'literal', text: a.value }
|
||||
else if (a.type === 'TemplateLiteral' && !a.expressions.length) label = { kind: 'literal', text: a.quasis[0].value.cooked }
|
||||
else label = { kind: 'computed', text: b.code.slice(a.start, a.end).slice(0, 60) }
|
||||
}
|
||||
// A fixed label over CSS that varies per caller: the CSS reads a parameter of an enclosing function.
|
||||
const varying = [...readNames(args[0])].filter((n) => fnParams.has(n))
|
||||
calls.push({ at: at(node), label, varying })
|
||||
}
|
||||
}
|
||||
if (node.type === 'Identifier' && names.has(node.name)) refStarts.add(node.start)
|
||||
let params = fnParams
|
||||
if (/Function|Method/.test(node.type) && node.params) {
|
||||
params = new Set(fnParams)
|
||||
boundNames({ type: 'ArrayPattern', elements: node.params }, params)
|
||||
}
|
||||
for (const k of Object.keys(node)) {
|
||||
if (k === 'loc' || /Comments$/.test(k)) continue
|
||||
walk(node[k], node, params)
|
||||
}
|
||||
})(b.ast.program, null, new Set())
|
||||
|
||||
// CLASSIFY EVERY REFERENCE. An import specifier, a callee and the composable's own definition are the only
|
||||
// positions a reference may take; anything else (rebinding, .call/.apply, a destructured key, a re-export,
|
||||
// passing it as a value) is a call this gate cannot follow.
|
||||
;(function classify(node, parent) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (Array.isArray(node)) { node.forEach((n) => classify(n, parent)); return }
|
||||
const isRef = (node.type === 'Identifier' && names.has(node.name))
|
||||
if (isRef && !calleeOf.has(node)) {
|
||||
const fine = (parent?.type === 'ImportSpecifier') ||
|
||||
(path === COMPOSABLE && parent?.type === 'FunctionDeclaration' && parent.id === node)
|
||||
if (!fine) {
|
||||
problems.push(`${at(node)}: '${node.name}' is used as ${parent?.type || 'a value'}, not called; this gate can only grade direct calls ` +
|
||||
'(a rebinding, .call/.apply, a destructured key, a re-export or a method of that name all hide a call)')
|
||||
}
|
||||
}
|
||||
for (const k of Object.keys(node)) {
|
||||
if (k === 'loc' || /Comments$/.test(k)) continue
|
||||
classify(node[k], node)
|
||||
}
|
||||
})(b.ast.program, null)
|
||||
|
||||
const cc = referenceCountsAgree(b.ast.tokens, refStarts, names)
|
||||
if (!cc.agree) problems.push(`${path}: the token stream holds ${cc.tokens} reference(s) to useShadowStyles and the AST ${cc.ast}; the file cannot be graded`)
|
||||
}
|
||||
return { calls, problems }
|
||||
}
|
||||
|
||||
// Grades a set of { path, src } files. Returns the problems found (empty = pass) and what was measured.
|
||||
function grade(files) {
|
||||
const byLabel = new Map()
|
||||
const problems = []
|
||||
let calls = 0, absent = 0
|
||||
for (const { path, src } of files) {
|
||||
if (!/useShadowStyles/.test(src)) continue
|
||||
let r
|
||||
try { r = gradeFile(path, src) } catch (e) { problems.push(`${path}: could not be parsed (${e.message.slice(0, 80)})`); continue }
|
||||
problems.push(...r.problems)
|
||||
const unlabelled = r.calls.filter((c) => c.label.kind === 'absent')
|
||||
if (unlabelled.length && !path.endsWith('.vue')) {
|
||||
problems.push(`${unlabelled.map((c) => c.at).join(' , ')}: an unlabelled call outside a .vue file is keyed by its CALLER's HMR id under the dev server, ` +
|
||||
'so it overwrites the caller\'s own unlabelled sheet; give it a label')
|
||||
} else if (unlabelled.length > 1) {
|
||||
problems.push(`${unlabelled.map((c) => c.at).join(' , ')}: ${unlabelled.length} unlabelled calls in one component share its HMR key under the dev server; label them`)
|
||||
}
|
||||
for (const c of r.calls) {
|
||||
calls++
|
||||
if (c.varying.length) {
|
||||
problems.push(`${c.at}: the CSS reads ${c.varying.join(', ')}, a parameter of the enclosing function, under one label; ` +
|
||||
'every caller (or instance) then shares the key with different CSS. Pass fixed CSS, or give each caller its own label')
|
||||
}
|
||||
if (c.label.kind === 'absent') { absent++; continue }
|
||||
if (c.label.kind === 'computed') {
|
||||
problems.push(`${c.at}: the label is computed (${c.label.text}); pass a string literal so its uniqueness can be checked`)
|
||||
continue
|
||||
}
|
||||
const res = RESERVED.find((x) => x.test(c.label.text))
|
||||
if (res) problems.push(`${c.at}: label '${c.label.text}' is reserved (${res.why})`)
|
||||
if (!byLabel.has(c.label.text)) byLabel.set(c.label.text, [])
|
||||
byLabel.get(c.label.text).push(c.at)
|
||||
}
|
||||
}
|
||||
for (const [label, sites] of byLabel) {
|
||||
if (sites.length > 1) {
|
||||
problems.push(`label '${label}' is used by ${sites.length} calls: ${sites.join(' , ')}\n` +
|
||||
' whichever mounts last in a shadow root overwrites the other\'s CSS; give each call its own label (its file\'s name)')
|
||||
}
|
||||
}
|
||||
return { problems, calls, absent, labels: byLabel.size }
|
||||
}
|
||||
|
||||
// ── SELF-TESTS: the grader must FAIL on the damage it exists to catch, and pass its absence ──────────
|
||||
console.log('SELF-TESTS (a check that cannot fail proves nothing)')
|
||||
const f = (path, src) => ({ path, src })
|
||||
const imp = "import { useShadowStyles } from '@map/composables/useShadowStyles.js'\n"
|
||||
const sfc = (body) => `<script setup>\n${imp}${body}\n</script>\n<template><div/></template>`
|
||||
const one = (files, re, m) => { const r = grade(files); ok(r.problems.length === 1 && re.test(r.problems[0]), `${m}${r.problems.length === 1 ? '' : ` (got ${r.problems.length} problems)`}`) }
|
||||
{
|
||||
const r = grade([f('a.js', imp + "useShadowStyles('.a{}', 'aa')"), f('b.vue', sfc("useShadowStyles('.b{}', 'bb')"))])
|
||||
ok(!r.problems.length && r.calls === 2, 'distinct labels in two files pass')
|
||||
}
|
||||
one([f('list.vue', sfc("useShadowStyles('.rl-root{}', 'regions-list')")), f('Card.vue', sfc("useShadowStyles('.card{}', 'regions-list')"))],
|
||||
/'regions-list' is used by 2 calls/, 'NEG CONTROL: the regions-list collision (two .vue files, one label) FAILS')
|
||||
one([f('a.js', imp + 'useShadowStyles(`\n' + '.x{ color:red; }\n'.repeat(263) + "`, 'research-map')"), f('b.js', imp + "useShadowStyles('.b{}', 'research-map')")],
|
||||
/'research-map'/, 'NEG CONTROL: a label 263 lines after its call, after a template literal, is still read')
|
||||
one([f('a.js', "import { useShadowStyles as inject } from './useShadowStyles.js'\ninject('.a{}', 'dup')"), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
|
||||
/'dup' is used by 2/, 'NEG CONTROL: a call through an import ALIAS is graded')
|
||||
one([f('a.vue', `<script>\nimport { useShadowStyles as inject } from './useShadowStyles.js'\n</script>\n<script setup>\ninject('.a{}', 'dup')\n</script>`), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
|
||||
/'dup' is used by 2/, 'NEG CONTROL: an alias imported in <script> and called in <script setup> is graded')
|
||||
one([f('a.js', imp + "useShadowStyles?.('.a{}', 'dup')"), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
|
||||
/'dup' is used by 2/, 'NEG CONTROL: an optional call ?.() is graded')
|
||||
one([f('a.js', imp + "useShadowStyles('.a{}', `dup`)"), f('b.js', imp + "useShadowStyles('.b{}', 'dup')")],
|
||||
/'dup' is used by 2/, 'NEG CONTROL: a template-literal label is read as its text')
|
||||
one([f('a.js', imp + "useShadowStyles('.a{}', 'dup')\nuseShadowStyles('.b{}', 'dup')")], /'dup' is used by 2/, 'NEG CONTROL: two calls in ONE file with one label FAIL')
|
||||
one([f('a.js', imp + "const L = 'x'\nuseShadowStyles('.a{}', L)")], /computed/, 'NEG CONTROL: a computed label FAILS (it cannot be checked)')
|
||||
one([f('a.js', imp + "const args = ['.a{}', 'x']\nuseShadowStyles(...args)")], /computed/, 'NEG CONTROL: spread arguments FAIL as computed, not pass as unlabelled')
|
||||
one([f('a.js', imp + "useShadowStyles('.a{}', 'reduced-motion')")], /reserved/, 'NEG CONTROL: a reserved key FAILS')
|
||||
one([f('a.js', imp + "const inject = useShadowStyles\ninject('.a{}', 'x')")], /not called/, 'NEG CONTROL: a rebinding FAILS (the call behind it is invisible)')
|
||||
one([f('a.js', imp + "useShadowStyles.call(null, '.a{}', 'x')")], /not called/, 'NEG CONTROL: .call() FAILS')
|
||||
one([f('a.js', "export { useShadowStyles as injectCss } from './useShadowStyles.js'")], /not called/, 'NEG CONTROL: a re-export under another name FAILS')
|
||||
one([f('a.js', "import * as S from './useShadowStyles.js'\nS.useShadowStyles('.a{}', 'x')")], /BY NAME/, 'NEG CONTROL: a namespace import FAILS')
|
||||
one([f('a.js', imp + "const o = { useShadowStyles(css) { return css } }\nuseShadowStyles('.a{}', 'x')")], /not called/, 'NEG CONTROL: a method NAMED useShadowStyles is refused, not counted as a call')
|
||||
one([f('a.js', imp + "export function useX(css) { useShadowStyles(css, 'x') }")], /parameter of the enclosing function/,
|
||||
'NEG CONTROL: a wrapper forwarding its caller\'s CSS under one fixed label FAILS')
|
||||
{
|
||||
const r = grade([f('a.js', imp + "const CSS = '.a{}'\nexport function useCard() { useShadowStyles(CSS, 'card') }")])
|
||||
ok(!r.problems.length, 'a wrapper injecting FIXED CSS under its label passes (useCardSurface\'s shape)')
|
||||
}
|
||||
one([f('a.vue', sfc("useShadowStyles('.a{}')\nuseShadowStyles('.b{}')"))], /2 unlabelled calls in one component/, 'NEG CONTROL: two unlabelled calls in one component FAIL (one dev HMR key)')
|
||||
one([f('a.js', imp + "useShadowStyles('.a{}')")], /outside a \.vue file/, 'NEG CONTROL: an unlabelled call in a composable FAILS (it takes its caller\'s HMR key)')
|
||||
{
|
||||
const r = grade([f('a.vue', sfc("useShadowStyles('.a{}')")), f('b.vue', sfc("useShadowStyles('.b{}', '')"))])
|
||||
ok(!r.problems.length && r.absent === 2, "one unlabelled call per component passes, and '' is read as no label (the runtime's label || …)")
|
||||
}
|
||||
{
|
||||
const r = grade([f('a.js', imp + "// useShadowStyles('.a{}', 'dup')\nconst s = \"useShadowStyles('.a{}', 'dup')\"\nuseShadowStyles /* why */ ('.b{}', 'dup')")])
|
||||
ok(!r.problems.length && r.calls === 1, 'a call in a comment or a string is not a call, and a comment before the ( does not trip the count')
|
||||
}
|
||||
{
|
||||
const good = referenceCountsAgree([{ type: { label: 'name' }, value: NAME }], new Set([7]), new Set([NAME]))
|
||||
const bad = referenceCountsAgree([{ type: { label: 'name' }, value: NAME }, { type: { label: 'name' }, value: NAME }], new Set([7]), new Set([NAME]))
|
||||
ok(good.agree && !bad.agree, 'NEG CONTROL: the token-vs-AST reference count FAILS when the two disagree (2 tokens, 1 node)')
|
||||
}
|
||||
|
||||
// ── the tree ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Tracked files plus untracked-but-not-ignored ones: a gitignored local copy (an _archive/ backup of a
|
||||
// component, keeping its label) must not turn one machine red. Outside a git checkout (DOXA_REPO pointed at
|
||||
// an exported tree), every source file is read.
|
||||
function sourceFiles() {
|
||||
try {
|
||||
const top = execFileSync('git', ['-C', REPO, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
||||
if (resolve(top) === resolve(REPO)) {
|
||||
return execFileSync('git', ['-C', REPO, 'ls-files', '--cached', '--others', '--exclude-standard', '--', 'library', 'plugin-designs'],
|
||||
{ encoding: 'utf8', maxBuffer: 1 << 26 }).split('\n').filter((p) => SOURCE_EXT.test(p))
|
||||
}
|
||||
} catch { /* not a checkout of its own: walk it */ }
|
||||
const walk = (d) => readdirSync(d).flatMap((n) => {
|
||||
const p = join(d, n)
|
||||
if (statSync(p).isDirectory()) return n === 'node_modules' || n.startsWith('.') ? [] : walk(p)
|
||||
return SOURCE_EXT.test(n) ? [p.slice(REPO.length + 1)] : []
|
||||
})
|
||||
return [...walk(join(REPO, 'library')), ...walk(join(REPO, 'plugin-designs'))]
|
||||
}
|
||||
const files = sourceFiles().sort().flatMap((p) => { try { return [f(p, readFileSync(join(REPO, p), 'utf8'))] } catch { return [] } })
|
||||
|
||||
console.log(`\nSHADOW-STYLE LABELS across library/ and plugin-designs/ (${files.length} source files)`)
|
||||
const r = grade(files)
|
||||
for (const p of r.problems) console.log(` FAIL - ${p}`)
|
||||
fails += r.problems.length
|
||||
ok(r.calls > 0, `${r.calls} useShadowStyles call(s) graded: ${r.labels} distinct label(s), ${r.absent} unlabelled (0 calls would mean this measured nothing)`)
|
||||
|
||||
console.log(fails ? `\nSHADOW-STYLE-LABELS: ${fails} FAIL(s)` : `\nSHADOW-STYLE-LABELS: every one of ${r.calls - r.absent} labelled call(s) has its own label`)
|
||||
process.exit(fails ? 1 : 0)
|
||||
|
|
@ -25,7 +25,10 @@ import { getCurrentInstance, onMounted } from 'vue'
|
|||
* Idempotent — uses a data-attribute key to avoid duplicate injection.
|
||||
*
|
||||
* @param {string} css - Raw CSS text to inject
|
||||
* @param {string} [label] - Optional label for the <style> tag (for debugging)
|
||||
* @param {string} [label] - The <style> tag's KEY in its shadow root, not a debugging name: a second call
|
||||
* with the same label and different CSS OVERWRITES the first one's sheet. Use a
|
||||
* label no other call uses (the file's own name); a copied or forked component
|
||||
* must get a new one. Enforced by check:shadow-style-labels-unique.
|
||||
*/
|
||||
export function useShadowStyles(css, label) {
|
||||
// Captured in setup (onMounted's callback cannot see it reliably).
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@
|
|||
"check:setup-order": "node internal/check-setup-order.mjs",
|
||||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
|
||||
"check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs",
|
||||
"check:shadow-style-labels-unique": "node internal/check-shadow-style-labels-unique.mjs",
|
||||
"check:shadow-reduced-motion": "node internal/check-shadow-reduced-motion.mjs",
|
||||
"check:regions-uses-library": "node plugin-designs/regions-dashboard/scripts/check-regions-uses-library.mjs",
|
||||
"check:religion-label-not-value": "node plugin-designs/regions-dashboard/scripts/check-religion-label-not-value.mjs",
|
||||
|
|
@ -338,7 +339,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:auto-engine check:engine-contract 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:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"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 check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc",
|
||||
"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 check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique",
|
||||
"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",
|
||||
|
|
@ -586,6 +587,7 @@
|
|||
"check:setup-order": "a line's POSITION in a setup block is not the MOMENT it runs, and two P0s from the Driver's console came from that gap. ARM A: `watch(..., { immediate: true })` runs during setup, so anything it reaches must be declared ABOVE it - research-map.vue threw `Cannot access '_lastFitKey' before initialization` and blanked the whole map, because the watcher called a HOISTED function 350 lines down whose own constants were still in their temporal dead zone. A hoisted function promises that its NAME resolves early, not that its BODY can run early, and that asymmetry is invisible from both ends. ARM B: a lifecycle registrar closes over Vue's current instance, which exists only while setup is on the stack, so `onMounted(async () => { await init(); onBeforeUnmount(clean) })` drops the registration silently - DashboardMapPanel.vue leaked a ResizeObserver on document.body plus a window listener, warning only, feature working, and research-map.vue had a second copy this gate found. The fix is never 'move the line': declare the handle at setup scope and tear it down in a hook registered before the first await. Its own false positives are the interesting part - it went 6 hits -> 1 over three rounds, and every one it dropped was prose or shadowing read as code: `onBeforeUnmount` inside a console.error STRING, `table` inside `new Error(`table not found (${r.status})`)` in four files, and a `const [pins, byCountry]` destructured INSIDE the function, shadowing a computed 220 lines away. So it blanks comments, string text and template text (keeping ${...}, which is code) and subtracts every name a span declares for itself. Self-tests on both shipped defects, both fixes, and all three decoys; exits 3 if any arm goes blind.",
|
||||
"check:sfc-parse": "every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
|
||||
"check:shadow-css-parses": "the CSS we assemble as JS STRINGS must parse as CSS — brace balance and declarations stranded outside any rule. check:sfc-parse proves only that the JS parses, so it passed a stylesheet with 9 orphaned declarations and braces at 12 open / 21 close (REGLIB batch 1); nothing else in the tree validates CSS syntax. Reads every useShadowStyles() argument off the @babel/parser AST the build already uses, because three hand-rolled lexers each produced a confident FALSE RED: a /* CSS comment */ read as a declaration, an apostrophe in a .vue prose header opening a phantom string literal, and a regex literal containing a quote. 9 self-tests run inline on every invocation, including the exact batch-1 damage, so it cannot silently stop being able to fail.",
|
||||
"check:shadow-style-labels-unique": "every useShadowStyles call has its OWN label: the label keys the <style>, and a second component with the same label overwrites the first one's CSS in a shared shadow root (the regions list lost its sheet this way). Global, not per-root, so a copied component is caught when it is copied. AST walk cross-checked by a token count.",
|
||||
"check:shadow-reduced-motion": "every animating library component honours prefers-reduced-motion: a page's own reset never crosses a shadow boundary, so useShadowStyles injects one per shadow root, and a component styled any other way must carry its own @media (prefers-reduced-motion: reduce) block. Self-test runs every time.",
|
||||
"check:shared-reached": "a shared library fix is only as deployed as the last rebuild of EVERY bundle that consumes it. Each bundle ships its own COMPILED copy, so a one-line fix in library/ is live in whichever bundles happened to be rebuilt after it and silently absent from the rest — and every other gate measures ONE leaf, which renders the old behaviour perfectly. Asserts a distinctive marker string per tracked change; consumers are DERIVED from imports, never hardcoded.",
|
||||
"check:shot-distinctness": "two README slots must not show the SAME view. README shipped \"List — page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT — same url at a different vp (deploy2 mobile) is intended.",
|
||||
|
|
|
|||
|
|
@ -183,6 +183,9 @@ import { RESEARCH_LEGEND_OPTIONS } from '@map/composables/legends/researchLegend
|
|||
const PosterDialog = retryableAsyncComponent(() => import('@map/components/poster/PosterDialog.vue'))
|
||||
|
||||
// ─── Shadow-DOM style injection ──────────────────────────────────────────────
|
||||
// Labelled 'countries-map', its own name. This file began as a fork of research-map.vue and kept that
|
||||
// file's label; a shared label lets whichever mounts last overwrite the other's sheet
|
||||
// (check:shadow-style-labels-unique).
|
||||
useShadowStyles(`
|
||||
/* ── Canonical geocoder + legend container CSS (clone of doxa-simple-map.vue
|
||||
lines 50-138) so tabs 1-3 render the geocoder + legend at the same
|
||||
|
|
@ -362,7 +365,7 @@ useShadowStyles(`
|
|||
.map-scroll-edge { position:absolute;top:0;bottom:0;width:40px;z-index:2;touch-action:pan-y;pointer-events:auto;background:transparent; }
|
||||
.map-scroll-edge--left { left:0; }
|
||||
.map-scroll-edge--right { right:0; }
|
||||
`, 'research-map')
|
||||
`, 'countries-map')
|
||||
|
||||
// ─── Config from ProfileLoader (provide/inject — never direct imports) ──────
|
||||
const profileConfig = inject('profileConfig')
|
||||
|
|
|
|||
|
|
@ -70,7 +70,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-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,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-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -81,7 +81,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-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -200,7 +200,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o",
|
||||
"integrity": "sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -223,8 +223,8 @@
|
|||
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -240,7 +240,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-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"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-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -256,8 +256,8 @@
|
|||
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "dx-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o",
|
||||
"integrity": "sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -223,8 +223,8 @@
|
|||
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -240,7 +240,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-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"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-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -256,8 +256,8 @@
|
|||
"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": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-M7n3YBWyBkWNuKmNudN3WeaxoZwLf92KQykox7kf1UOFhFMDBKPOQ2XEB/OVhv4o\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<!-- MapLibre engine (the default), fetched early -->\n<link rel=\"preload\" as=\"script\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.js\" integrity=\"sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp\" crossorigin=\"anonymous\">\n<link rel=\"preload\" as=\"style\" href=\"https://cdn.jsdelivr.net/npm/maplibre-gl@5.24.0/dist/maplibre-gl.css\" integrity=\"sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- MapLibre draws this map and needs no token. For Mapbox, paste this map's embed.scriptMapbox from manifest.json instead: it sets \"provider\":\"mapbox\" and preloads the Mapbox engine; fill \"tk\" with your token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>",
|
||||
"scriptMapbox": "<!-- Mapbox engine, fetched early -->\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<link rel=\"preload\" as=\"style\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css\" integrity=\"sha384-rp1X/jJ9MpWPef9OimfXhOaOc4Gzs87yQvF+ajwY6X2H4PTeHNycNlLWiWvj7ehV\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Y33AViPgLgZs1/CPCDU3g/rq20VIg7cYGXtFuweV2m/f0wJa+pqYmWmmwiqlVrvt\" crossorigin=\"anonymous\"></script>\n<!-- Mapbox draws this map: fill \"tk\" with your Mapbox token. -->\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"provider\":\"mapbox\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue