diff --git a/internal/check-group-set-standard.mjs b/internal/check-group-set-standard.mjs new file mode 100644 index 000000000..e1273741f --- /dev/null +++ b/internal/check-group-set-standard.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * check-group-set-standard — a FORK's group set generates packets end-to-end. + * + * THE CONTRACT UNDER TEST (documentation/STANDARD-GROUP-SET.md in the plugin): a fork supplies ONE + * FOLDER — clusters.json with a flat lists[] ({ id, label, selector, unlisted? }), records.json + * (its own people rows, so no API), selector-evaluator.js (resolvable from the folder) — and points + * the packet generator at it with DATA_DIR= and PACKET_OUT=. Nothing else. If that sentence stops + * being true, this gate goes red before a fork finds out. + * + * ★ THE FIXTURE IS THE STANDARD'S FALSIFIER, deliberately synthetic (every slug and number is + * invented — no real group data ships in a test fixture). It exercises exactly the generic shape: + * FLAT lists[] (not upg100's bucketed one), an unlisted:true entry that must NOT generate, records + * from the folder, and no posters directory — the map page must degrade to a cover, not kill the + * run, because a fork's first run will not have posters yet. + * + * Written RED-FIRST: on the tree before the standard landed, packet.mjs knew only series.json + * dataDirs, bucketed clusters, cached/API records and a fixed out/ — this test failed on all four. + * + * Headless, no network (NOPHOTOS + folder records). Exit 0 pass · 1 fail. + */ +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { existsSync, readFileSync, readdirSync, rmSync, mkdtempSync } from 'node:fs' +import { spawnSync } from 'node:child_process' +import { tmpdir } from 'node:os' + +const HERE = dirname(fileURLToPath(import.meta.url)) +const REPO = resolve(HERE, '..') +const FIXTURE = join(HERE, 'fixtures', 'toy-group-set') +const GEN = join(REPO, 'library', 'scripts', 'posters', 'packet.mjs') + +const fails = [] +const ok = (c, m) => { if (!c) fails.push(m) } + +// ⚠ OUTPUT GOES TO A TEMP DIR, NEVER THE REAL out/packets — toy packets landing beside the real 28 +// would make check:packets-all count fiction as coverage. +const OUT = mkdtempSync(join(tmpdir(), 'toy-packets-')) +const env = { + ...process.env, + DATA_DIR: FIXTURE, + PACKET_OUT: OUT, + NOPHOTOS: '1', + OFFLINE: '1', +} +const run = spawnSync('node', [GEN], { env, encoding: 'utf8', timeout: 120000 }) +const outText = String(run.stdout || '') + String(run.stderr || '') + +ok(run.status === 0, + `the generator exited ${run.status} on the toy dataDir — a fork cannot generate at all.\n tail: ${outText.trim().split('\n').slice(-4).join('\n ')}`) + +if (run.status === 0) { + const files = existsSync(OUT) ? readdirSync(OUT).filter((f) => f.endsWith('.html')).sort() : [] + // toy-hidden is unlisted:true — TWO packets, and the third's absence is as load-bearing as the + // two's presence: an unlisted list that generates anyway is the reachability bug inverted. + ok(files.includes('toy-north.html') && files.includes('toy-south.html'), + `expected toy-north + toy-south packets, got: ${files.join(', ') || '(none)'}`) + ok(!files.some((f) => f.startsWith('toy-hidden')), + 'the unlisted:true list generated a packet — unlisted must mean unlisted') + + for (const f of ['toy-north.html', 'toy-south.html']) { + if (!files.includes(f)) continue + const html = readFileSync(join(OUT, f), 'utf8') + const groups = (html.match(/class="card"/g) || []).length + const want = f === 'toy-north.html' ? 3 : 2 + ok(groups === want, `${f}: ${groups} group rows, expected ${want} — the folder's records.json did not drive the join`) + // The synthetic names must actually print — a packet of the right COUNT with the wrong rows + // would pass a bare count. + ok(f !== 'toy-north.html' || /Alpha People/.test(html), 'toy-north is missing its own synthetic people') + // No posters exist for the toy: the map page must be the documented cover, and the run must + // not have died for it (already asserted by exit 0). + } +} +rmSync(OUT, { recursive: true, force: true }) + +if (fails.length) { + console.error('GROUP-SET-STANDARD: FAIL') + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log('GROUP-SET-STANDARD: PASS — a one-folder synthetic fork generates exactly its listed packets (and not its unlisted one) from its own records, with no API, no posters and no series.json edit.') diff --git a/internal/check-list-pattern-doc.mjs b/internal/check-list-pattern-doc.mjs new file mode 100644 index 000000000..e79839650 --- /dev/null +++ b/internal/check-list-pattern-doc.mjs @@ -0,0 +1,163 @@ +#!/usr/bin/env node +/** + * check-list-pattern-doc — the numbers in PATTERN-A-LIST-OF-ZONES.md must still be true. + * + * That document tells the next agent how to turn a project list (7000/4700/2000/700) into ~100 + * zones, and it does it by QUOTING MEASUREMENTS: 28 leaf zones, 5 registry lists, 7 selector types, + * four membership files with specific slug counts. Prose like that is the first thing to rot — the + * data moves, the sentence does not, and a confident wrong number in a how-to is worse than no + * how-to, because the reader has no reason to doubt it. + * + * ★ IT RE-DERIVES EVERY NUMBER FROM THE SOURCE AND COMPARES TO THE DOC'S OWN TEXT. It does not + * assert the numbers are 28/5/7 — hardcoding the expected value on both sides is the tautology this + * whole class of gate falls into, where the check restates the doc instead of testing it. One side + * is parsed out of the markdown, the other is computed from clusters.json / lists.json / the + * membership files. They must agree. + * + * So this gate fails in BOTH useful directions: + * · someone adds a zone or a list and does not update the doc → mismatch + * · someone edits the doc's numbers without the data changing → mismatch + * + * ⚠ The deaf collapse (29 cluster entries → 28 leaves) is asserted explicitly, because it is the + * exact discrepancy that has already caused two real errors: a zone count of 27 that forgot `deaf`, + * and a delete of 30 files in the belief there were 28. + * + * Exit 0 pass · 1 fail · 75 NOT MEASURED. + */ +import { readFileSync, existsSync, readdirSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const HERE = dirname(fileURLToPath(import.meta.url)) +const REPO = resolve(HERE, '..') +const PLUGIN = join(REPO, 'plugin-designs/my-upg-100-list') +const DOC = join(PLUGIN, 'documentation/PATTERN-A-LIST-OF-ZONES.md') + +const fails = [] +const notes = [] +const ok = (label, got) => notes.push(` ok ${label.padEnd(58)} ${got}`) +const bad = (label, expected, got) => fails.push(`${label}\n doc says: ${expected}\n source is: ${got}`) + +if (!existsSync(DOC)) { + console.error('LIST-PATTERN: NOT MEASURED (exit 75) — the document is absent; nothing to check.') + process.exit(75) +} +const doc = readFileSync(DOC, 'utf8') + +// ── 1. leaf zones. The doc's headline count, and the one with a history of being wrong. ────────── +const clusters = JSON.parse(readFileSync(join(PLUGIN, 'data/clusters.json'), 'utf8')) +const tree = await import(join(PLUGIN, 'data/legend-tree.js')) + +// ⚠ buildLegendNodes THROWS on a malformed zone (assertUniformLeafShape guards the consumer shape), +// and an uncaught throw here would exit 1 — the same code as a real FAIL, so "the doc is wrong" and +// "the data will not even load" would look identical to a reader and to CI. They need different +// answers: one is a doc to fix, the other is a broken clusters.json that this gate cannot speak to +// at all. Found by mutation-testing: adding a zone without its `count` field crashed the run with a +// stack trace and exit 1, which I would otherwise have read as the gate correctly failing. +let leaves +try { + leaves = tree.leafZones(tree.buildLegendNodes(clusters)) +} catch (e) { + console.error('LIST-PATTERN: NOT MEASURED (exit 75) — clusters.json does not build a legend tree,') + console.error(' so nothing can be said about whether the document matches it. Fix the data first;') + console.error(' the zone-shape gate (assertUniformLeafShape) is the one describing the problem:') + console.error(` ${e.message.split('\n').slice(0, 3).join('\n ')}`) + process.exit(75) +} + +const docLeaves = Number((doc.match(/⇒\s*(\d+)/) || [])[1]) +if (docLeaves !== leaves.length) bad('leaf-zone count', docLeaves, leaves.length) +else ok('leaf zones, re-derived through leafZones()', leaves.length) + +// The entry count, and that the difference is exactly the deaf collapse. +const entries = Object.values(clusters).filter(Array.isArray).flat().length +const docEntries = Number((doc.match(/(\d+)\s+entries,\s+\d+\s+zones/) || [])[1]) +if (docEntries !== entries) bad('clusters.json entry count', docEntries, entries) +else ok('clusters.json list entries', entries) + +const deafLeaves = leaves.filter((z) => /deaf/i.test(z.id || z.key || '')) +const deafEntries = (clusters.deaf_lists || []).length +if (deafLeaves.length !== 1) bad('the deaf collapse yields ONE leaf', 1, deafLeaves.length) +else if (entries - leaves.length !== deafEntries - 1) { + bad('entries minus leaves is explained by the deaf collapse alone', `${deafEntries - 1}`, `${entries - leaves.length}`) +} else ok('the 29→28 collapse is exactly the deaf buckets merging', `${deafEntries} entries → 1 leaf`) + +// ── 2. the registry. Every list the doc tables must exist, with those aliases. ──────────────────── +const registry = JSON.parse(readFileSync(join(REPO, 'library/lists/lists.json'), 'utf8')) + +/** + * Slice the markdown to ONE table, by the heading it sits under. + * ⚠ NOT OPTIONAL. The first version of this gate matched `^| \`id\` |` across the WHOLE document + * and swept up the SELECTOR table too, reporting `union` as an unknown registry list — a false + * failure against a correct doc. Two tables in one file share a row shape; the only thing that + * distinguishes them is where they are. + */ +function section(fromHeading, toHeading) { + const a = doc.indexOf(fromHeading) + if (a < 0) return '' + const b = toHeading ? doc.indexOf(toHeading, a + 1) : -1 + return doc.slice(a, b < 0 ? undefined : b) +} +const listSection = section('## Half 1', '## Half 2') +const zoneSection = section('## Half 2', '## What you get') + +const docIds = [...listSection.matchAll(/^\|\s*`([a-z0-9-]+)`\s*\|/gm)].map((m) => m[1]) +const realIds = registry.lists.map((l) => l.id) +const missing = realIds.filter((id) => !docIds.includes(id)) +const extra = docIds.filter((id) => !realIds.includes(id)) +if (missing.length) bad('every registry list appears in the doc table', `missing ${missing.join(', ')}`, realIds.join(', ')) +else if (extra.length) bad('the doc table lists only real registry ids', `unknown ${extra.join(', ')}`, realIds.join(', ')) +else ok('registry lists, all present in the doc table', realIds.length) + +if (registry.default !== 'doxa') bad('the default list', 'doxa', registry.default) +else ok('the default list is the unfiltered one', registry.default) + +// ── 3. selector types. The doc's table is the vocabulary the pattern hands the next author. ─────── +const selCounts = {} +for (const v of Object.values(clusters)) { + if (!Array.isArray(v)) continue + for (const l of v) { + const t = l?.selector?.type + if (t) selCounts[t] = (selCounts[t] || 0) + 1 + } +} +for (const [type, n] of Object.entries(selCounts)) { + const row = zoneSection.match(new RegExp(`^\\|\\s*\`${type}\`\\s*\\|\\s*(\\d+)\\s*\\|`, 'm')) + if (!row) bad(`selector type "${type}" has a row in the doc`, 'a row', `${n} zones use it, undocumented`) + else if (Number(row[1]) !== n) bad(`selector "${type}" zone count`, row[1], n) +} +const docSelTypes = [...zoneSection.matchAll(/^\|\s*`(\w+)`\s*\|\s*\d+\s*\|/gm)].map((m) => m[1]) + .filter((t) => t in selCounts || /^(wagf_|country_|union|pg_slugs|deaf_)/.test(t)) +for (const t of docSelTypes) { + if (!(t in selCounts)) bad(`doc documents selector "${t}"`, 'in use', 'no zone uses it') +} +if (!fails.length) ok('selector types and their zone counts', Object.keys(selCounts).length + ' types') + +// ── 4. membership files. The doc quotes four slug counts; doxa must have none. ──────────────────── +const MEM = join(REPO, 'public/dx-maps/assets/people-groups/lists') +if (!existsSync(MEM)) { + console.error('LIST-PATTERN: NOT MEASURED (exit 75) — membership directory absent, so the counts') + console.error(` in the doc cannot be checked either way: ${MEM}`) + process.exit(75) +} +for (const f of readdirSync(MEM).filter((f) => f.endsWith('.json'))) { + const id = f.replace(/\.json$/, '') + const n = (JSON.parse(readFileSync(join(MEM, f), 'utf8')).slugs || []).length + const row = listSection.match(new RegExp(`^${id}\\s+(\\d+)`, 'm')) + if (!row) bad(`membership "${id}" is quoted in the doc`, 'a row', `${n} slugs, undocumented`) + else if (Number(row[1]) !== n) bad(`membership "${id}" slug count`, row[1], n) +} +if (existsSync(join(MEM, 'doxa.json'))) { + bad('doxa has NO membership file (it is the unfiltered default)', 'absent', 'present') +} else ok('doxa has no membership file, as the default requires', 'absent') + +// ── report ─────────────────────────────────────────────────────────────────────────────────────── +for (const n of notes) console.log(n) +if (fails.length) { + console.error('LIST-PATTERN: FAIL — PATTERN-A-LIST-OF-ZONES.md disagrees with the data it describes.') + for (const f of fails) console.error(` ✗ ${f}`) + console.error('\n Fix whichever is wrong. If the DATA changed, update the doc; if the DOC drifted,') + console.error(' correct the number. Do not delete the claim to make this pass.') + process.exit(1) +} +console.log(`LIST-PATTERN: PASS — ${notes.length} claims in PATTERN-A-LIST-OF-ZONES.md re-derived from clusters.json, lists.json and the membership files.`) diff --git a/internal/fixtures/toy-group-set/clusters.json b/internal/fixtures/toy-group-set/clusters.json new file mode 100644 index 000000000..9fd1d6a81 --- /dev/null +++ b/internal/fixtures/toy-group-set/clusters.json @@ -0,0 +1,9 @@ +{ + "_synthetic": "Toy group set for check:group-set-standard. Every slug, name and number is invented — no real people-group data. The FLAT lists[] shape is the fork contract; unlisted:true keeps an entry out of generation.", + "source_total": 8, + "lists": [ + { "id": "toy-north", "label": "Toy North", "selector": { "type": "pg_slugs", "values": ["toy-alpha", "toy-bravo", "toy-charlie"] } }, + { "id": "toy-south", "label": "Toy South", "selector": { "type": "pg_slugs", "values": ["toy-delta", "toy-echo"] } }, + { "id": "toy-hidden", "label": "Toy Hidden", "unlisted": true, "selector": { "type": "pg_slugs", "values": ["toy-foxtrot"] } } + ] +} diff --git a/internal/fixtures/toy-group-set/records.json b/internal/fixtures/toy-group-set/records.json new file mode 100644 index 000000000..6107654d3 --- /dev/null +++ b/internal/fixtures/toy-group-set/records.json @@ -0,0 +1,8 @@ +[ + { "slug": "toy-alpha", "name": "Alpha People", "country_code": "AAA", "country_code_label": "Toyland", "population": 1000, "religion": "SYN", "religion_label": "Synthetic", "primary_language_label": "Toyish", "imb_gsec": 1 }, + { "slug": "toy-bravo", "name": "Bravo People", "country_code": "AAA", "country_code_label": "Toyland", "population": 2000, "religion": "SYN", "religion_label": "Synthetic", "primary_language_label": "Toyish", "imb_gsec": 2 }, + { "slug": "toy-charlie", "name": "Charlie People", "country_code": "BBB", "country_code_label": "Boxland", "population": 3000, "religion": "SYN", "religion_label": "Synthetic", "primary_language_label": "Boxish", "imb_gsec": 0 }, + { "slug": "toy-delta", "name": "Delta People", "country_code": "BBB", "country_code_label": "Boxland", "population": 4000, "religion": "SYN", "religion_label": "Synthetic", "primary_language_label": "Boxish", "imb_gsec": 4 }, + { "slug": "toy-echo", "name": "Echo People", "country_code": "AAA", "country_code_label": "Toyland", "population": 5000, "religion": "SYN", "religion_label": "Synthetic", "primary_language_label": "Toyish", "imb_gsec": 6 }, + { "slug": "toy-foxtrot", "name": "Foxtrot People", "country_code": "AAA", "country_code_label": "Toyland", "population": 6000, "religion": "SYN", "religion_label": "Synthetic", "primary_language_label": "Toyish", "imb_gsec": 3 } +] diff --git a/internal/fixtures/toy-group-set/selector-evaluator.js b/internal/fixtures/toy-group-set/selector-evaluator.js new file mode 100644 index 000000000..1f95e0aa9 --- /dev/null +++ b/internal/fixtures/toy-group-set/selector-evaluator.js @@ -0,0 +1,4 @@ +// The toy dataDir carries its own evaluator per the contract. Re-exported rather than copied: +// the CONTRACT is "the folder resolves selector-evaluator.js"; the toy proves resolution, and a +// copy here would be a second evaluator that drifts (the exact defect class the standard avoids). +export { evaluate } from '../../../plugin-designs/my-upg-100-list/data/selector-evaluator.js' diff --git a/library/layouts/pane-wrapper/pane-wrapper.js b/library/layouts/pane-wrapper/pane-wrapper.js index 3832ab6d2..6603fff20 100644 --- a/library/layouts/pane-wrapper/pane-wrapper.js +++ b/library/layouts/pane-wrapper/pane-wrapper.js @@ -56,9 +56,31 @@ export const PANE_GRID_STYLES = ` .pane{ position:relative; width:100%; height:100%; scroll-snap-align:start; scroll-snap-stop:always; + /* ★ A PANE NEVER PAINTS OUTSIDE ITS OWN BOX. The UPG-100 wall's first card row runs past the list + pane's bottom (rega's census: cards to y 142 against a pane ending at 57, 14 wall elements inside + the map area at 1440, on every surface), and on the standalone leaf the tapes' z-index painted + that overflow OVER the map pane below. Whatever stacking context differs between surfaces, content + outside a pane is not the reader's to see: clip at the pane. overflow, NOT contain/isolation — + those would make each pane the containing block / stacking context for position:fixed modal + scrims and trap them under a host's nav rail (VerticalTabsLayout records that regression). + clip, with hidden as the fallback for engines without it: hidden alone leaves the pane a scroll + container, so focus() or scrollIntoView() on content in the overflowing row would scroll the PANE + and shift the wall inside it. clip only clips paint. Neither makes a containing block for fixed. */ + overflow:hidden; + overflow:clip; } /* The live custom element fills its pane. */ -.pane > .pane-mount{ position:absolute; inset:0; } +.pane > .pane-mount{ + position:absolute; inset:0; + /* ★ THE PANE ALREADY SIZES ITS ELEMENT, SO THE ELEMENT OPTS OUT OF THE VIEWPORT FLOOR. ProfileLoader's + wrapper has min-height: var(--profile-min-h, 100dvh) (library/ProfileLoader.vue), and a host that + gives the element a definite height sets --profile-min-h:0 — the Gallery (.dms-pane), VerticalTabs + and plugin-studio all do. The pane column never did, so on the standalone leaf each pane's wrapper + was 100dvh (900) inside an 843 pane: the wall scroller ran 57 px past its pane (its tapes painted + over the map, and Tab into row 1 scrolled the COLUMN instead of the wall). Inherited, so it crosses + into each element's shadow root, where no selector here can reach. */ + --profile-min-h:0; +} /* Placeholder copy for a pane whose profile is not built yet — same card styling as the wrapper's own placeholder so an unbuilt pane never looks broken. */ .pane-placeholder{ diff --git a/library/scripts/posters/packet.mjs b/library/scripts/posters/packet.mjs index bd455c069..911e001de 100644 --- a/library/scripts/posters/packet.mjs +++ b/library/scripts/posters/packet.mjs @@ -37,7 +37,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO = join(__dirname, '../../..'); @@ -59,13 +59,23 @@ if (!SERIES_ALL[SERIES_ID] || SERIES_ID.startsWith('_')) { const SERIES = SERIES_ALL[SERIES_ID]; // The data folder is a SERIES property, not a constant. It was hard-wired to my-upg-100-list, // which is the other half of why a country run could only ever be a UPG-100 run. -const DATA = join(REPO, SERIES.dataDir); +// ★ DATA_DIR overrides the series folder entirely — the FORK CONTRACT (see the plugin's +// documentation/STANDARD-GROUP-SET.md): one folder with clusters.json + records.json + +// selector-evaluator.js generates packets with NO series.json edit. Series flags (funnel, +// strings, countryMode) still come from SERIES_ID, defaulting to upg100's. +const DATA = process.env.DATA_DIR ? resolve(process.env.DATA_DIR) : join(REPO, SERIES.dataDir); +if (process.env.DATA_DIR && !existsSync(join(DATA, 'clusters.json'))) { + console.error(`[packet] DATA_DIR set but ${join(DATA, 'clusters.json')} does not exist.`); + process.exit(1); +} // Generated zone titles (zone-title.js) and the ISO-3 to ISO-2 table Intl needs for them. countryName.js // cannot load under Node (it imports JSON without an attribute), so the table is read here and INJECTED, // which is the path zone-title.js was built for. A data set without zone-title.js keeps its labels. const ALPHA2 = JSON.parse(readFileSync(join(REPO, 'library/data/iso3-to-alpha2.json'), 'utf8')).map; const ZONE_TITLE = await import(join(DATA, 'zone-title.js')).then((m) => m.zoneTitle).catch(() => null); -const OUT = join(__dirname, 'out', 'packets'); +// PACKET_OUT keeps a fork's (or a test's) output out of the real artifact directory — toy packets +// beside the real ones would make check:packets-all count fiction as coverage. +const OUT = process.env.PACKET_OUT ? resolve(process.env.PACKET_OUT) : join(__dirname, 'out', 'packets'); mkdirSync(OUT, { recursive: true }); // ⚠ NOT process.env.LANG — that is the SHELL's POSIX locale ("en_US.UTF-8"), which @@ -104,7 +114,8 @@ const API = process.env.VITE_API_BASE_URL || process.env.MAP_APP_API_URL; // the program refuses. Any env var that makes the API unnecessary must be named HERE, next to the // guard, or it is decoration. const CACHE_OK = process.env.OFFLINE && existsSync(join(__dirname, 'out', '_records.json')); -if (!API && !process.env.PACKET_RECORDS && !CACHE_OK) { +const DATA_RECORDS_EARLY = process.env.DATA_DIR && existsSync(join(resolve(process.env.DATA_DIR), 'records.json')); +if (!API && !process.env.PACKET_RECORDS && !CACHE_OK && !DATA_RECORDS_EARLY) { console.error('[packet] No API base — set VITE_API_BASE_URL, or PACKET_RECORDS for a fixture build,'); console.error(' or OFFLINE=1 to reuse out/_records.json if a previous run cached it.'); if (process.env.OFFLINE) { @@ -253,16 +264,31 @@ function buildFunnel(membership) { // ("13 + 12 + 2 + 1 deaf = 28"). The packet's triangle tip prints this number as "1 of N", so a // 27 there is a visible wrong fact on the page. Same omission as allZones() had before 3ef164c0; // this was the last copy of it. - const listCount = allZones().length; + const listCount = ZONES_ALL.length; const listTotal = Number(raw.steps[raw.steps.length - 1].groups) || 0; return { ...raw, listCount, listTotal }; } catch (_) { return null; } } const { evaluate } = await import(join(DATA, 'selector-evaluator.js')); -const { normalizeRecords, extractRecords } = await import(join(DATA, 'normalize.js')); +// normalize.js is OPTIONAL in the fork contract: a fork's records.json is its own shape, already +// flat. Absent -> identity, said OUT LOUD once — a silent fallback would let a TYPO'd normalize.js +// quietly skip normalization on the real series, which is the fail-open trap. +const { normalizeRecords, extractRecords } = await (async () => { + try { return await import(join(DATA, 'normalize.js')); } + catch { + console.log('[packet] no normalize.js in the data folder — records are used as-is (fork contract).'); + return { normalizeRecords: (r) => r, extractRecords: (r) => (Array.isArray(r) ? r : (r?.posts || r?.records || [])) }; + } +})(); // The zone enumeration itself, so this generator, the poster generator and upg-100-list.vue cannot // disagree about which zones exist (see allZones below). -const { buildLegendNodes, leafZones, zonesWithSelectors } = await import(join(DATA, 'legend-tree.js')); +// legend-tree.js is loaded LAZILY, only for the bucketed (upg100) shape — a flat fork's file IS +// its enumeration and must not be forced to ship a legend module it never uses. +let _legend = null; +async function legendModules() { + if (!_legend) _legend = await import(join(DATA, 'legend-tree.js')); + return _legend; +} async function loadRecords() { // ★ A COMMITTED FIXTURE, so CI can build a packet at all. out/ is gitignored — both the @@ -280,6 +306,10 @@ async function loadRecords() { if (!existsSync(fixture)) throw new Error(`PACKET_RECORDS set but missing: ${fixture}`); return JSON.parse(readFileSync(fixture, 'utf8')); } + // The FORK CONTRACT's records: a dataDir may carry its own rows, so a fork needs no API and no + // cache. Detected, not configured — the folder IS the interface. + const folderRecords = join(DATA, 'records.json'); + if (existsSync(folderRecords)) return JSON.parse(readFileSync(folderRecords, 'utf8')); const cache = join(__dirname, 'out', '_records.json'); if (process.env.OFFLINE && existsSync(cache)) return JSON.parse(readFileSync(cache, 'utf8')); // ★ THE FIELD LIST IS PART OF THE SELECTOR CONTRACT — every field a selector READS must be asked @@ -337,7 +367,7 @@ async function loadRecords() { // // leafZones() carries id/zoneKey/label/count but NOT selector (leaves do not), so the selector is // re-attached by id exactly as generate.mjs:2801-2808 does it, deaf included. -function allZones() { +async function allZones() { // ★ COUNTRY MODE HAS ITS OWN ZONE SOURCE, and leaving it out broke the packet half of // SERIES=country for every country. Measured by exp-shepherd on main, 2026-09-28: // `SERIES=country ONLY=country-yem node generate.mjs` made a poster, and the matching @@ -375,9 +405,21 @@ function allZones() { selector: { type: 'country_codes', values: [c.iso3] }, })); } + // ★ THE FORK CONTRACT'S GENERIC SHAPE: a FLAT lists[] with an optional unlisted:true per entry. + // Detected by the ABSENCE of upg100's extra buckets, so the 28-zone catalogue keeps its legend- + // tree enumeration (the deaf collapse and its reachability semantics) byte-for-byte, and a fork + // never needs a legend tree at all — its file IS the enumeration. unlisted:true is the same + // reachability idea as data: defined, selectable by ONLY=, but not generated by default. + const BUCKETED = !!(clusters.asia_lists || clusters.uncovered_lists || clusters.deaf_lists); + if (!BUCKETED) { + return (clusters.lists || []) + .filter((l) => !l.unlisted || (process.env.ONLY && process.env.ONLY === l.id)) + .map((l) => ({ id: l.id, zoneKey: l.id, label: l.label || l.id, count: l.count, selector: l.selector })); + } // The legend's leaves with their selectors, the unified deaf leaf included — ONE enumeration // (legend-tree.js zonesWithSelectors) shared with the browser packet and the map, replacing the // id->selector table and deaf literal this function used to type out itself. + const { zonesWithSelectors } = await legendModules(); return zonesWithSelectors(clusters, { records: null }); } @@ -490,9 +532,15 @@ if (LIST_MEMBERSHIP) { console.log(`[packet] list=${LIST} — ${records.length} of ${allRecords.length} record(s) ` + `belong to it (list total ${LIST_MEMBERSHIP.total})`); } +// Resolved ONCE, and it MUST precede FUNNEL: packetFunnel reads ZONES_ALL.length inside a +// try/catch whose catch returns null — so a use-before-init here does not crash, it silently +// deletes the funnel page from every packet. That exact failure shipped for one run: the tip +// probe found no "1 of 28" anywhere, which is how a swallowed TDZ was caught as missing CONTENT +// rather than as an error. +const ZONES_ALL = await allZones(); const FUNNEL = buildFunnel(LIST_MEMBERSHIP); const bySlug = new Map(records.map((r) => [r.slug, r])); -const zones = allZones().filter((z) => !ONLY || z.id === ONLY); +const zones = ZONES_ALL.filter((z) => !ONLY || z.id === ONLY); if (!zones.length) { console.error(`[packet] no zone matches ONLY=${ONLY}`); process.exit(1); } let made = 0; diff --git a/package.json b/package.json index 773ec3a1b..54ffae408 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "check:popup-escaping": "node internal/check-popup-escaping.mjs", "check:ported-globals-identical": "node internal/check-ported-globals-identical.mjs", "check:one-scroll-container": "node internal/check-one-scroll-container.mjs", + "check:list-pattern-doc": "node internal/check-list-pattern-doc.mjs", "check:pin-selection": "node internal/check-pin-selection.mjs", "check:poster-action-print": "node internal/check-poster-action-print.mjs", "check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs", @@ -335,7 +336,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", + "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", "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", diff --git a/plugin-designs/my-upg-100-list/documentation/PATTERN-A-LIST-OF-ZONES.md b/plugin-designs/my-upg-100-list/documentation/PATTERN-A-LIST-OF-ZONES.md new file mode 100644 index 000000000..900866cac --- /dev/null +++ b/plugin-designs/my-upg-100-list/documentation/PATTERN-A-LIST-OF-ZONES.md @@ -0,0 +1,122 @@ +# PATTERN — a project list becomes ~100 zones (what UPG-100 actually does) + +Project 7000, 4700, 2000 and 700 each need their own clusters of roughly 100 groups. This document +is so the next one is built by FOLLOWING a pattern rather than by copying this plugin. + +Two halves, and they are independent — that separation is the pattern's main structural claim: + +| half | what it decides | where it lives | gated by | +|---|---|---|---| +| **which groups** | which people groups are in the list at all | `library/lists/` (shared) | `internal/check-list-prop-unit.mjs` | +| **which zones** | how those groups are cut into ~100-group territories | your plugin's `data/clusters.json` | `internal/check-group-set-standard.mjs` | + +A new list needs the SECOND half only if it wants its own territories. The first half already +carries all five lists. + +--- + +## Half 1 — the list prop. Already built; do not rebuild it. + +`library/lists/peopleGroupList.js` is shared machinery, not UPG-100's. The registry +(`library/lists/lists.json`) already defines every list in scope: + +| id | aliases | label | +|---|---|---| +| `doxa` | `2000`, `project-2000` | Project 2000: DOXA's people groups (the default) | +| `unreached` | `7000`, `project-7000` | Project 7000: unreached people groups | +| `frontier` | `4700`, `project-4700` | Project 4700: frontier people groups | +| `frontier-500k` | `700`, `project-700` | Project 700: frontier people groups of 500,000 or more | +| `unengaged` | — | Unengaged unreached people groups | + +`resolvePeopleGroupList(profileConfig)` picks one, **first answer wins**: profile-config `list` → +`?list=` → `window.MAP_LIST` → `doxa`. An unknown value falls back to the default and warns once, +naming every known id. + +MEASURED, so you can trust the counts rather than re-derive them — membership files under +`public/dx-maps/assets/people-groups/lists/`: + +``` +unreached 2082 slugs +unengaged 1929 +frontier 1745 +frontier-500k 71 +doxa (no file, and correctly so — the default is the unfiltered API result) +``` + +★ **The filter FAILS CLOSED.** If a non-default list's membership cannot be loaded, the map shows +NO rows and the console names the list and the URL. A map that silently showed all 2,107 while the +embed asked for Project 700 would be showing the wrong list with no sign of it — the failure mode +this design exists to prevent. Do not "improve" this into a fall-back-to-everything. + +**To add a list:** a registry entry + a membership file from +`library/scripts/people-groups/build-lists.mjs` + label/definition keys in every +`library/i18n/locales//lists.json`. The gate checks all three (24 cases) and will tell you which +locale you missed. + +--- + +## Half 2 — the zoning. This is the judgement, and it cannot be automated. + +**The rule:** geography FIRST so the poster maps make sense, balanced with +LANGUAGE — because languages usually cluster geographically, the zones are already a mixture of the +two, grouped manually. + +That balance is a human call. What the pattern can do is give you the vocabulary to express the call +you made, so the next reader can see it. UPG-100's 29 entries use **seven selector types**, and the +type you pick IS the statement of how you zoned: + +| selector | zones | what it says | +|---|---|---| +| `wagf_block` | 3 | a named regional bloc (west/east/central Africa) — pure geography | +| `wagf_region` | 1 | a whole region (europe) — geography, coarsest | +| `country_code` | 3 | one country is the territory (SDN, BRA, LAO) | +| `country_codes` | 6 | several countries make one territory (middle-east-core, maghreb-egypt) | +| `union` | 2 | a composite that no single rule expresses (oceania-indonesia) | +| `pg_slugs` | 12 | an explicit roster — every Indian state and Chinese province | +| `deaf_predicate` | 2 | not geographic at all (see the exception below) | + +★ **THE `pg_slugs` BLOCK IS THE HONEST PART, NOT THE LAZY PART.** All 12 of India's states and +China's provinces are enumerated group-by-group rather than derived from a rule. That is not +technical debt: below the country level there is no ADM1 field on the records to select by, so a +rule would be a *fiction* that looks principled. An explicit roster is checkable; an invented +predicate is not. Expect a sub-national list to need the same, and do not let a reviewer talk you +into a clever selector that cannot be verified. + +### 29 entries, 28 zones — the collapse is real, count the LEAVES + +``` +buildLegendNodes(clusters) → leafZones(...) ⇒ 28 +``` +The two `deaf_lists` entries (`deaf-old-world`, `deaf-new-world-asia-pacific`) collapse into ONE +`deaf` leaf. So `clusters.json` has 29 list entries and the product has 28 posters/packets/zones. + +⚠ **Never count zones from `clusters.json`'s buckets.** I did, called it 27 (forgetting `deaf` +entirely), and later deleted 30 files believing there were 28. `leafZones(buildLegendNodes(...))` +is the one answer that poster, packet and legend all agree on — ask it, don't count by hand. + +### The deaf poster is the known exception. Leave it unsolved. + +The deaf zone is global and non-geographic by construction (affinity bloc A017, 123 groups spanning +every region), so it breaks the geography-first rule the rest of the pattern rests on. **It is +recorded as not yet solved.** It is recorded here as an open exception, not papered over with +an invented zoning. If your list has a similar affinity-not-territory group, do the same: give it +its own bucket, let it collapse to one leaf, and say plainly that it is not geographic. + +--- + +## What you get for free once your `clusters.json` exists + +`documentation/STANDARD-GROUP-SET.md` is the fork contract for the offline generator: point it at +one folder and it produces a packet per zone, localized, with no code change. Its falsifier +(`internal/check-group-set-standard.mjs`) runs a synthetic 3-list toy set end-to-end on every gate +pass, so the contract cannot rot silently. Read it next; it covers data shapes, `PACKET_LANG`, +`ONLY=`, and which pages omit cleanly when optional repo data is absent. + +## Not solved by this pattern + +- **Posters for arbitrary sets.** The poster generator needs geo/bbox inputs this pattern does not + define. Packets are generative today; posters are not. +- **Zone TITLES in other languages.** Tracked separately — only 10 of 28 titles are composable from + already-translated parts, and `Intl.DisplayNames({type:'language'})` resolves just 9.5% of ISO + 639-3 codes and ECHOES the code on a miss, so it would print raw codes on a poster silently. +- **Multi-territory maps** and any authoring UI. diff --git a/plugin-designs/my-upg-100-list/documentation/STANDARD-GROUP-SET.md b/plugin-designs/my-upg-100-list/documentation/STANDARD-GROUP-SET.md new file mode 100644 index 000000000..addc99f0f --- /dev/null +++ b/plugin-designs/my-upg-100-list/documentation/STANDARD-GROUP-SET.md @@ -0,0 +1,63 @@ +# STANDARD — generate packets for ANY group set (the fork contract) + +The UPG-100 machinery is generative: point the packet generator at **one folder** and it produces +a packet per list, localized, with no code change and no `series.json` edit. This document is the +contract; `internal/check-group-set-standard.mjs` is its falsifier (a synthetic 3-list toy set runs +end-to-end on every gate pass, so the contract cannot rot silently). + +## What a fork supplies — one folder + +``` +my-set/ + clusters.json REQUIRED the group-set file (schema below) + records.json OPTIONAL your own people rows — with it, no API and no cache are needed + selector-evaluator.js REQUIRED resolvable from the folder (re-export ours, or ship your own) + normalize.js OPTIONAL absent -> records are used as-is (logged once, never silent) + legend-tree.js ONLY for the bucketed legacy shape; a flat set never needs it +``` + +### clusters.json — the generic (flat) shape + +```json +{ "lists": [ + { "id": "my-north", "label": "My North", "selector": { "type": "pg_slugs", "values": ["a","b"] } }, + { "id": "my-draft", "label": "Draft", "unlisted": true, "selector": { … } } +] } +``` + +- `selector` — any type your evaluator handles (`pg_slugs` needs nothing but slugs). +- `unlisted: true` — defined and selectable with `ONLY=`, but not generated by default. + This is the reachability idea as data. +- The bucketed shape (`lists` + `asia_lists` + `uncovered_lists` + `deaf_lists`) is the UPG-100 + catalogue's legacy form; its enumeration goes through the legend tree to keep the deaf collapse. + New sets use the flat form. + +## Running it + +``` +DATA_DIR=path/to/my-set PACKET_OUT=out/my-packets PACKET_LANG=es node library/scripts/posters/packet.mjs +ONLY=my-north … # one list NOPHOTOS=1 … # skip photo fetch +``` + +- **Locale**: `PACKET_LANG` (never the shell's `LANG`); strings come from `packet-strings.json` + per-locale blocks with per-key fallback — a new language is a new block, not a code change. +- **Titles**: the per-list `label` + your strings block. Nothing else to configure. +- `PACKET_OUT` keeps fork/test output away from the real artifact directory. + +## Pages that depend on optional repo data (they omit cleanly, never fail the run) + +| page | needs | absent → | +|---|---|---| +| map (page 1) | `out/posters/.html` (`bun run posters`) | cover without the map | +| triangle | `data/people-groups/vision-funnel.json` | page omitted | +| %-evangelical | `data/people-groups/christian-share.json` | page omitted | +| bounded/centered sets | `public/dx-maps/assets/tables/near-neighbors.json` joined by slug | page omitted | + +A fork's first run therefore yields: cover + profiles (+ triangle if the repo file is present) — +correct and smaller, per the rule that a page which cannot state true numbers must not appear. + +## Not in scope + +Posters for arbitrary sets (the poster generator needs geo/bbox inputs this contract does not +define yet), multi-territory maps, and authoring UI. The in-app catalogue is the plugin's own +`data/clusters.json` — a fork edits its copy; the file is the interface. diff --git a/public/dx-maps/country-posters/countries-map/index.html b/public/dx-maps/country-posters/countries-map/index.html index 6ffe5924b..2cf1ed71b 100644 --- a/public/dx-maps/country-posters/countries-map/index.html +++ b/public/dx-maps/country-posters/countries-map/index.html @@ -70,7 +70,7 @@ - + - + - + \n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -240,7 +240,7 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -256,8 +256,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] @@ -880,7 +880,7 @@ "bundle": "my-upg-100-list", "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "output": "dx-maps/my-upg-100-list/my-upg-100-list.js", - "integrity": "sha384-dkMVx+R6LBxJppU8H4Xh17N7lSnzQYge8sSJT/OlKCgv/h609sD2hJmWKySdukXF", + "integrity": "sha384-ypVn5CInyWDq+MKYgqlXuJ3nOKMxX18ZhFugR1e7Ne+I4DuR1kuce+MPMf8DwgQV", "elements": [ "my-upg-100-list-map" ], @@ -903,7 +903,7 @@ "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -919,8 +919,8 @@ "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -936,8 +936,8 @@ "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] @@ -1126,7 +1126,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-ZvJGcuWsTs4ZvI11xU4ugS85NMHR57ddkjh2134qmd77FNq438z9KHHP4L2pSrDl", + "integrity": "sha384-ADyor00XF8YBITkubbaYI01lOxb61a/2u0YdZ2wbT9hd6xDDw9gI8iaJj+tM8009", "elements": [ "regions-dashboard" ], @@ -1149,8 +1149,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -1166,7 +1166,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1182,8 +1182,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index 0c7749d9e..71a8c3df8 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -200,7 +200,7 @@ "bundle": "country-posters", "scriptUrl": "./country-posters/country-posters.js", "output": "dx-maps/country-posters/country-posters.js", - "integrity": "sha384-59m8oHdofbBqClmRtUIa9IDDV8Bb7ONCyFhq3Sz0V6nGdWIL6DvyCY26YSehFGul", + "integrity": "sha384-0TXSOdeBj0zqNGTJhzFrcnz9ndaoyYh3ljbLnamDVzRp5rvLZGXxhnLzh3YdzaqP", "elements": [ "country-posters-map" ], @@ -223,8 +223,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -240,7 +240,7 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -256,8 +256,8 @@ "scriptUrl": "./country-posters/country-posters.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] @@ -880,7 +880,7 @@ "bundle": "my-upg-100-list", "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "output": "dx-maps/my-upg-100-list/my-upg-100-list.js", - "integrity": "sha384-dkMVx+R6LBxJppU8H4Xh17N7lSnzQYge8sSJT/OlKCgv/h609sD2hJmWKySdukXF", + "integrity": "sha384-ypVn5CInyWDq+MKYgqlXuJ3nOKMxX18ZhFugR1e7Ne+I4DuR1kuce+MPMf8DwgQV", "elements": [ "my-upg-100-list-map" ], @@ -903,7 +903,7 @@ "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -919,8 +919,8 @@ "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -936,8 +936,8 @@ "scriptUrl": "./my-upg-100-list/my-upg-100-list.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] @@ -1126,7 +1126,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-ZvJGcuWsTs4ZvI11xU4ugS85NMHR57ddkjh2134qmd77FNq438z9KHHP4L2pSrDl", + "integrity": "sha384-ADyor00XF8YBITkubbaYI01lOxb61a/2u0YdZ2wbT9hd6xDDw9gI8iaJj+tM8009", "elements": [ "regions-dashboard" ], @@ -1149,8 +1149,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } }, { @@ -1166,7 +1166,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1182,8 +1182,8 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n\n", - "scriptMapbox": "\n\n\n\n\n\n" + "script": "\n\n\n\n\n\n", + "scriptMapbox": "\n\n\n\n\n\n" } } ] diff --git a/public/dx-maps/my-upg-100-list/my-upg-100-list.js b/public/dx-maps/my-upg-100-list/my-upg-100-list.js index d7e7eb40f..81cbaa059 100644 --- a/public/dx-maps/my-upg-100-list/my-upg-100-list.js +++ b/public/dx-maps/my-upg-100-list/my-upg-100-list.js @@ -877,8 +877,13 @@ .pane{ position:relative; width:100%; height:100%; scroll-snap-align:start; scroll-snap-stop:always; + overflow:hidden; + overflow:clip; +} +.pane > .pane-mount{ + position:absolute; inset:0; + --profile-min-h:0; } -.pane > .pane-mount{ position:absolute; inset:0; } .pane-placeholder{ position:absolute; inset:0; display:grid; place-items:center; padding:24px; text-align:center; diff --git a/public/dx-maps/my-upg-100-list/poster-wall/index.html b/public/dx-maps/my-upg-100-list/poster-wall/index.html index e9012843a..b970dabd2 100644 --- a/public/dx-maps/my-upg-100-list/poster-wall/index.html +++ b/public/dx-maps/my-upg-100-list/poster-wall/index.html @@ -61,7 +61,7 @@ - + - + - + - + - + - +