my-upg-100-list: neutral wording in comments and provenance notes
Rewrites comments and the underscore-prefixed notes in data/adm0/PROVENANCE.json so they keep the technical fact without quoting anyone, naming process roles or speaking in the first person. Comments and doc keys only: with comments (and _-keys) stripped, every changed file is byte-identical to its parent.
This commit is contained in:
parent
3f7d489409
commit
e0ca45d6b0
11 changed files with 104 additions and 108 deletions
|
|
@ -7,9 +7,9 @@
|
|||
"adm0_features": 162,
|
||||
"_duty": "130 of the 162 countries carry a duty that travels with any redistribution of boundaries/world-adm0*.geojson; only 32 are duty-free. The 130 split TWO ways and the split matters more than the total: 65 are attribution-only, and 65 are SHARE-ALIKE CLASS (58 ODbL + 7 explicit ShareAlike), which can constrain how a derivative is licensed rather than merely how it is credited. See _duty_classes. This matters because the repo is intended to be given away: these obligations are not discharged by shipping the file. See data/boundaries/ATTRIBUTION, which references this table by path.",
|
||||
"_duty_classes": {
|
||||
"_why": "THREE CLASSES, NOT TWO — corrected 2026-09-28 after regions-plugin's challenge, which was right. I first gave only the 7 explicitly-named ShareAlike countries their own field on the reasoning that share-alike is a stronger duty than attribution. By that same reasoning the field should hold 65: Open Data Commons ODbL 1.0 is itself a SHARE-ALIKE licence for databases — alongside attribution it requires a Derivative Database be offered under ODbL, and it has a keep-open provision. Separating 7 from 58 buried exactly the thing the field existed to surface.",
|
||||
"_precedent": "THIS REPO ALREADY TREATS ODbL AS CONSEQUENTIAL, which is the evidence rather than my reading of the licence: library/scripts/posters/data/README.md:36-50 records that Natural Earth's 1:10m ROADS were requested and DELIBERATELY NOT SHIPPED because of an ODbL attribution obligation. Someone here dropped a dataset over this class of duty.",
|
||||
"_open": "NOT A LEGAL CONCLUSION. Whether a redistributed boundary file is a 'Derivative Database' under ODbL is exactly the clause a giveaway needs a real answer on, and neither regions-plugin nor I is qualified to give it. Recorded here, and raised with the licensing items, so the question is visible rather than settled by omission.",
|
||||
"_why": "THREE CLASSES, NOT TWO — corrected 2026-09-28 after a review challenge, which was right. The first version gave only the 7 explicitly-named ShareAlike countries their own field on the reasoning that share-alike is a stronger duty than attribution. By that same reasoning the field should hold 65: Open Data Commons ODbL 1.0 is itself a SHARE-ALIKE licence for databases — alongside attribution it requires a Derivative Database be offered under ODbL, and it has a keep-open provision. Separating 7 from 58 buried exactly the thing the field existed to surface.",
|
||||
"_precedent": "THIS REPO ALREADY TREATS ODbL AS CONSEQUENTIAL, which is the evidence rather than a reading of the licence: library/scripts/posters/data/README.md:36-50 records that Natural Earth's 1:10m ROADS were requested and DELIBERATELY NOT SHIPPED because of an ODbL attribution obligation. Someone here dropped a dataset over this class of duty.",
|
||||
"_open": "NOT A LEGAL CONCLUSION. Whether a redistributed boundary file is a 'Derivative Database' under ODbL is exactly the clause a giveaway needs a real answer on, and nobody on this project is qualified to give it. Recorded here, and raised with the licensing items, so the question is visible rather than settled by omission.",
|
||||
"duty_free": {
|
||||
"count": 32,
|
||||
"iso3": [
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const TRACKED = [
|
|||
'gb-IND-ADM1.geojson', 'gb-CHN-ADM1.geojson',
|
||||
];
|
||||
// ★ MOVED TO data/tables/. groupings.json is now in the shared asset pool the tree copies into
|
||||
// public/dx-maps/assets/tables/, because gospel's legend renders IN THE BROWSER and needs it at
|
||||
// public/dx-maps/assets/tables/, because the gospel legend renders IN THE BROWSER and needs it at
|
||||
// runtime — a build-time import path would not reach the bundle. The name stays relative so P()
|
||||
// still resolves it against this directory; only the prefix changed.
|
||||
const GROUPINGS = '../../../data/tables/groupings.json';
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const readJSON = (f) => JSON.parse(readFileSync(P(f), 'utf8'));
|
|||
|
||||
// ── canonical paths ───────────────────────────────────────────────────────────
|
||||
// ★ MOVED TO data/tables/. groupings.json is now in the shared asset pool the tree copies into
|
||||
// public/dx-maps/assets/tables/, because gospel's legend renders IN THE BROWSER and needs it at
|
||||
// public/dx-maps/assets/tables/, because the gospel legend renders IN THE BROWSER and needs it at
|
||||
// runtime — a build-time import path would not reach the bundle. The name stays relative so P()
|
||||
// still resolves it against this directory; only the prefix changed.
|
||||
const GROUPINGS = '../../../data/tables/groupings.json';
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { dirname, join } from 'node:path';
|
|||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const STORE = join(HERE, 'zone-colors.json');
|
||||
// zone-regions.json moved to data/tables/ (the shared asset pool the tree copies into
|
||||
// public/dx-maps/assets/tables/) because gospel's legend renders IN THE BROWSER and needs it at
|
||||
// public/dx-maps/assets/tables/) because the gospel legend renders IN THE BROWSER and needs it at
|
||||
// runtime. HERE is still this plugin's data dir, so the path walks up to the repo root.
|
||||
const REGIONS = join(HERE, '..', '..', '..', 'data', 'tables', 'zone-regions.json');
|
||||
const LOCKS = join(HERE, 'color-locks.json'); // optional
|
||||
|
|
|
|||
|
|
@ -259,9 +259,8 @@ export function buildLegendNodes(clusters, opts = {}) {
|
|||
// ── REGIONAL GROUPING — the SAME sections the poster page uses ─────────────
|
||||
// The legend used to emit 14 LOOSE LEAVES at top level (West Africa, Sudan,
|
||||
// Brazil, Laos, …) beside two odd parents, "Asia (admin-1)" and "Frontier Asia
|
||||
// (uncovered)". Brief: "the legend itself is not really well grouped — it says
|
||||
// Asia and it mentions other groups… find the sections on the poster page and
|
||||
// use those as the legend sections."
|
||||
// (uncovered)". That grouping read poorly beside the poster page, so the legend
|
||||
// now uses the poster page's own sections.
|
||||
//
|
||||
// So the same 28 zones now group into the poster page's nine regions. The
|
||||
// mapping is duplicated from poster-wall.vue rather than imported because this
|
||||
|
|
@ -309,10 +308,10 @@ export function buildLegendNodes(clusters, opts = {}) {
|
|||
const node = {
|
||||
// ★ FOLD DIACRITICS BEFORE STRIPPING, or the mark is DELETED and the id silently becomes a
|
||||
// different id: Gujarāt -> 'region-gujar-t', not 'region-gujarat'. Today's labels are the
|
||||
// fixed ASCII set from regionFor(), so nothing is broken — but the brief's whole point is that
|
||||
// fixed ASCII set from regionFor(), so nothing is broken — but the whole point is that
|
||||
// a fork brings its OWN people-group set and names its own regions, and the first accented
|
||||
// label would split one region into two with no error and no empty state. Same class gospel
|
||||
// measured joining admin-to-list.json: a naive fold matched 514 of 1009 rows and lost THREE
|
||||
// label would split one region into two with no error and no empty state. Same class as a measured
|
||||
// join of admin-to-list.json: a naive fold matched 514 of 1009 rows and lost THREE
|
||||
// whole zones, while every surviving zone still looked populated. Fix the class, not the six
|
||||
// spellings we happen to know.
|
||||
id: 'region-' + label.normalize('NFD').replace(/\p{Mn}/gu, '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { dirname, join } from 'node:path';
|
|||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const STORE = join(HERE, 'zone-colors.json');
|
||||
// zone-regions.json moved to data/tables/ (the shared asset pool the tree copies into
|
||||
// public/dx-maps/assets/tables/) because gospel's legend renders IN THE BROWSER and needs it at
|
||||
// public/dx-maps/assets/tables/) because the gospel legend renders IN THE BROWSER and needs it at
|
||||
// runtime. HERE is still this plugin's data dir, so the path walks up to the repo root.
|
||||
const REGIONS = join(HERE, '..', '..', '..', 'data', 'tables', 'zone-regions.json');
|
||||
const GENERATOR = join(HERE, 'generate-region-shades.mjs');
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ async function main() {
|
|||
// booted dark, so the first move used to be dark→light). This proves nothing
|
||||
// assumed the boot theme: handleToggleTheme flips isDark false→true → setStyle
|
||||
// dark-v11 → style.load re-adds tapestry/pins/isolation. The ThemeToggleButton
|
||||
// is the .mcb control with data-control="theme" (studio L54) — untranslatable,
|
||||
// is the .mcb control with data-control="theme" — untranslatable,
|
||||
// unlike the "Switch to …" title this used to match, which is localized AND dynamic.
|
||||
const themeBtn = page.locator(`${EL} button.mcb[data-control="theme"]`);
|
||||
let toggled = false;
|
||||
|
|
@ -639,7 +639,7 @@ async function main() {
|
|||
await clearFilter(page);
|
||||
|
||||
// ── (4) controls present in DOM (zoom, theme, fullscreen) ──────────────────
|
||||
// ★ QUERY data-control, NEVER THE TITLE (studio L54). Each wrapper writes
|
||||
// ★ QUERY data-control, NEVER THE TITLE. Each wrapper writes
|
||||
// data-control="zoom-in|zoom-out|fullscreen|theme|location|help|share" on
|
||||
// MapControlButton; it is not a declared prop, so it falls through onto the
|
||||
// rendered <button>. Untranslatable, so this works in every locale.
|
||||
|
|
@ -1159,7 +1159,7 @@ async function main() {
|
|||
// ★ FIND BY CLASS, ASSERT THE TITLE SEPARATELY — the title is LOCALIZED.
|
||||
// This used to be `.upg-rail-batch[title="Batch Select"]`, which pinned the gate
|
||||
// to one English string: once the title became `:title="BATCH_SELECT_TITLE"`
|
||||
// (studio L53) the selector matched in `en` and NOWHERE ELSE, and the gate would
|
||||
// the selector matched in `en` and NOWHERE ELSE, and the gate would
|
||||
// report "button not in the toolbar" — a product failure — for what is really a
|
||||
// broken query. `.upg-rail-batch` is already proven unique by railBtns === 1
|
||||
// below, so the class alone is the identity. The title is still checked, as the
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ export function zoneCountryCodes(selector) {
|
|||
* page/packet already loaded AT THE READER'S LOCALE. The preferred source: no alpha-2 mapping,
|
||||
* no second service, and it agrees with Intl where both have an answer.
|
||||
* @param {string} [opts.locale] used only by the Intl fallback.
|
||||
* @param {(iso3:string, locale:string)=>string} [opts.countryName] regions-plugin's coming library
|
||||
* helper (their card BUILD-LOCALIZED-COUNTRY-NAMES-ISO3-TO-ISO2). Injected rather than imported
|
||||
* @param {(iso3:string, locale:string)=>string} [opts.countryName] the coming library
|
||||
* helper (localized country names, ISO-3 → ISO-2). Injected rather than imported
|
||||
* so this module does not depend on an unlanded file; when it lands, pass it and it wins over
|
||||
* the local Intl fallback.
|
||||
* @returns {string|null} null rather than the raw code — see below.
|
||||
|
|
@ -79,13 +79,13 @@ export function zoneCountryCodes(selector) {
|
|||
export function countryLabel(iso3, opts = {}) {
|
||||
const code = String(iso3 || '').toUpperCase()
|
||||
if (!code) return null
|
||||
// ★ ORDER: regions' helper (or Intl) FIRST, the records' label SECOND — and this order is the
|
||||
// opposite of the one I wrote first, corrected by measuring what the two sources actually say.
|
||||
// ★ ORDER: the shared country-name helper (or Intl) FIRST, the records' label SECOND — and this order
|
||||
// is the opposite of the first version, corrected by measuring what the two sources actually say.
|
||||
// The records API returns the FORMAL country name, which is wrong for a poster header:
|
||||
// LAO "Lao People's Democratic Republic" vs Intl "Laos"
|
||||
// IRN "Islamic Republic of Iran" vs Intl "Iran"
|
||||
// CHN "People's Republic of China" vs Intl "China"
|
||||
// 28 of our 175 country labels are long formal forms like these. My first version preferred the
|
||||
// 28 of our 175 country labels are long formal forms like these. The first version preferred the
|
||||
// records because they were already localized and needed no code mapping — true, and it produced
|
||||
// "Lao People's Democratic Republic" as the title of the zone whose hand-written label was "Laos",
|
||||
// i.e. a generated title WORSE than the one it replaced. Intl gives the short common form in every
|
||||
|
|
@ -97,8 +97,8 @@ export function countryLabel(iso3, opts = {}) {
|
|||
}
|
||||
// ⚠ Intl REGIONS TAKE ALPHA-2 AND THROW ON ALPHA-3 — `new Intl.DisplayNames(['en'],{type:'region'})
|
||||
// .of('IND')` is a RangeError, not a miss. So a 3-letter code needs a mapping, and `alpha2` is that
|
||||
// mapping, INJECTED rather than imported: regions-plugin owns the ISO-3→ISO-2 table (their card
|
||||
// BUILD-LOCALIZED-COUNTRY-NAMES-ISO3-TO-ISO2) and it has not landed, so this module must work without
|
||||
// mapping, INJECTED rather than imported: the ISO-3→ISO-2 table belongs to the library's
|
||||
// localized-country-names work and it has not landed, so this module must work without
|
||||
// it and improve when it arrives. With no mapper a 3-letter code simply falls through to the records
|
||||
// label below — the formal name, but a real localized name.
|
||||
// That THROW-vs-ECHO asymmetry with the language type is also why every lookup here ends in null
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ const REGION_OF = {
|
|||
// the region is read as a cultural/linguistic sweep.
|
||||
'maghreb-egypt': 'Middle East',
|
||||
'middle-east-core': 'Middle East',
|
||||
// CENTRAL ASIA is its own section. Brief: "the 55 UPG Pakistan map is wrongly
|
||||
// added to Middle East, it is Central Asia." It had been folded into the Middle
|
||||
// CENTRAL ASIA is its own section: the 55-UPG Pakistan map belongs to Central
|
||||
// Asia, not the Middle East. It had been folded into the Middle
|
||||
// East as "the same sweep continuing east", but Pakistan, Afghanistan,
|
||||
// Uzbekistan, Tajikistan, Kyrgyzstan and Kazakhstan are Central/South Asia —
|
||||
// filing them under the Middle East misnames six countries to save a heading.
|
||||
|
|
@ -120,7 +120,7 @@ const REGION_OF = {
|
|||
// below reads it directly. Passing regionFor straight in built and loaded cleanly and
|
||||
// then threw `c.startsWith is not a function` at first render: the whole wall was blank,
|
||||
// 0 cards, with nothing wrong at the source layer.
|
||||
// ★ THE ENGLISH VALUES ABOVE ARE GROUPING KEYS; THIS TRANSLATES AT RENDER (studio's split).
|
||||
// ★ THE ENGLISH VALUES ABOVE ARE GROUPING KEYS; THIS TRANSLATES AT RENDER.
|
||||
// REGION_OF keeps its English values as stable identity — changing them would re-key the
|
||||
// grouping — and the heading a reader sees is resolved here, per locale, through REGION_KEY
|
||||
// (data/legend-tree.js), the one table the legend's region rows translate through as well. Every
|
||||
|
|
@ -195,7 +195,7 @@ const zoneSheet = makeZoneSheet({ allRecords: () => recordsFor.all(), assetsBase
|
|||
// The wall's own copy. v-html in the shared component, so the <strong> lands as markup —
|
||||
// the emphasis is the point: praying for them BY NAME is the whole brief.
|
||||
//
|
||||
// ★ THROUGH t(), NOT A LITERAL. rega measured ?lang=ar and found this intro, the heading and
|
||||
// ★ THROUGH t(), NOT A LITERAL. Measured at ?lang=ar: this intro, the heading and
|
||||
// the tag rendering in English while the wall's buttons beside them were translated — the
|
||||
// copy a reader reads FIRST was the untranslated part. The <strong> travels inside the
|
||||
// translated string so each language can emphasise its own words, which is not something a
|
||||
|
|
@ -204,26 +204,26 @@ const zoneSheet = makeZoneSheet({ allRecords: () => recordsFor.all(), assetsBase
|
|||
// that guard once (host i18n -> catalogue -> the key) instead of a hand-rolled copy.
|
||||
// No namespace: callers pass FULL keys here, exactly as the raw useI18n().t they used to get.
|
||||
//
|
||||
// ★ ONE BEHAVIOUR GAIN FROM WAVE 3c (regions-plugin, 4c05e589): the old path was the bare
|
||||
// ★ ONE BEHAVIOUR GAIN FROM WAVE 3c (4c05e589): the old path was the bare
|
||||
// useI18n().t, so on a host WITH i18n a missing key rendered the raw dotted key. It now falls through.
|
||||
//
|
||||
// ★ AND THE humanizeKey FALLBACK IS GONE — the one deliberate departure from 4c05e589, which kept it.
|
||||
// It rendered a key's last segment title-cased, so a bare host showed "Upg100 Wall Intro" where a
|
||||
// whole SENTENCE exists: wrong even in English, and untranslatable because it comes from the
|
||||
// identifier rather than a catalogue. It was kept on the theory that these keys were not fully
|
||||
// catalogued — I told regions-plugin exactly that, and it was FALSE: this file's three keys are
|
||||
// catalogued — and that was FALSE: this file's three keys are
|
||||
// present in all ELEVEN locales, 0 gaps (enumerated 2026-09-27; the only three, all string literals).
|
||||
// ⚠ IT GUARDS A BARE HOST, NOT A MISSING KEY. Those are different failures and I had them confused.
|
||||
// Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the standing goal that the
|
||||
// ⚠ IT GUARDS A BARE HOST, NOT A MISSING KEY. Those are different failures, and the two had been confused.
|
||||
// A product decision (2026-09-27), under the standing goal that the
|
||||
// production maps be localized.
|
||||
const t = useOptionalT('')
|
||||
|
||||
// ★ THE MARKUP IS COMPOSED HERE, so the CATALOGUE holds plain text (dx-shepherd UPGHTML, 2026-09-27).
|
||||
// ★ THE MARKUP IS COMPOSED HERE, so the CATALOGUE holds plain text (2026-09-27).
|
||||
// vue-i18n warns "[intlify] Detected HTML in '…'" twice per tab pass, and it is right to: a translated
|
||||
// string carrying <strong> is the only one in this tree that does — I measured every en catalogue area
|
||||
// and upg100WallIntro is 1 of 1, so this was not a convention, it was my outlier.
|
||||
//
|
||||
// ⚠ WHY THE OBVIOUS FIXES DO NOT APPLY HERE. The shepherd's suggestion — markup in the template, or
|
||||
// ⚠ WHY THE OBVIOUS FIXES DO NOT APPLY HERE. The usual suggestions — markup in the template, or
|
||||
// <i18n-t> interpolation — assumes this profile renders the sentence. It does not: `intro` is a STRING
|
||||
// PROP on the library PosterWall, rendered there with v-html (PosterWall.vue:955), and that component
|
||||
// has no slots at all. So the markup has to arrive inside the string, and the only question is who
|
||||
|
|
@ -241,18 +241,18 @@ const t = useOptionalT('')
|
|||
const escHtml = (s) => String(s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
const intro = computed(() => {
|
||||
// The three plain-text keys studio adds (rule 5B — the catalogue is theirs). Until they land, the
|
||||
// The three plain-text keys come from the shared catalogue (rule 5B). Until they land, the
|
||||
// single legacy key is still the source of truth, so this renders identically before AND after: no
|
||||
// flag day, and my landing does not have to be ordered against theirs.
|
||||
// flag day, and this consumer does not have to land in step with the catalogue.
|
||||
const a = t('messages.upg100WallIntroA')
|
||||
const b = t('messages.upg100WallIntroEmph')
|
||||
const c = t('messages.upg100WallIntroB')
|
||||
const missing = (v, key) => !v || v === key
|
||||
// ★ THE LEGACY upg100WallIntro FALLBACK IS GONE (2026-09-27). It existed for exactly one window: the
|
||||
// three plain-text keys were landing in studio's catalogue while this consumer was already written,
|
||||
// three plain-text keys were landing in the shared catalogue while this consumer was already written,
|
||||
// so the branch rendered the old single markup-bearing key until they arrived. They are on main in
|
||||
// all eleven locales, so the branch is now DEAD CODE — and dead code that names a key keeps that key
|
||||
// alive: studio cannot delete upg100WallIntro while this line reads it (their L13U measurement).
|
||||
// alive: the catalogue cannot delete upg100WallIntro while this line reads it (measured).
|
||||
// A fallback whose condition can no longer be true is not safety, it is a reference.
|
||||
// If A or Emph ever went missing the per-key fallback still yields English from the en block, and
|
||||
// check:i18n-keys-exist fails the build before a reader could see it.
|
||||
|
|
|
|||
|
|
@ -247,8 +247,8 @@ let countryNameMap = null // iso3→label, built from live re
|
|||
// plain tap is the existing single-zone isolate (a‴, unchanged). Leaving batch
|
||||
// mode KEEPS the selection STATE so Copy/color still work (D-7); Clear empties it.
|
||||
//
|
||||
// R1.4 (L4 fix — Brief: "should just be a single button and only should show ui
|
||||
// to batch select if you select the button"): the map-rail .upg-rail-batch is the
|
||||
// R1.4 (L4 fix — batch selection is ONE button, and its UI shows only once that
|
||||
// button is selected): the map-rail .upg-rail-batch is the
|
||||
// ONLY entry point (the always-visible HUD .upg-batch-btn is GONE), and ALL batch
|
||||
// chrome — HUD, legend row markers, the amber map highlight — renders ONLY while
|
||||
// batchMode is ON (progressive disclosure). The kept selection is invisible out
|
||||
|
|
@ -370,7 +370,7 @@ const geocoderRef = ref(null) // <GeocoderComponent> exposed ref
|
|||
// `:default-tab="1"` at the call site — the prop country-posters added for exactly
|
||||
// this ("it opens to the country's semantic level in the legend").
|
||||
//
|
||||
// The catalog entries this note used to ask for now EXIST (studio L56): tabs.upg100Region /
|
||||
// The catalog entries this note used to ask for now EXIST: tabs.upg100Region /
|
||||
// tabs.upg100Zone and legend.tabInfo.upg100Region / legend.tabInfo.upg100Zone, in all 11 locales
|
||||
// with exactly the text that used to be inline here. So the labels are bound, not hardcoded.
|
||||
//
|
||||
|
|
@ -479,7 +479,7 @@ watch(() => packetLang(), () => {
|
|||
// the mounting language forever, which is the subtler half of the same bug.
|
||||
const LEGEND_TITLE = computed(() => t('legend.title.upg100List'))
|
||||
const BATCH_SELECT_TITLE = computed(() => t('legend.title.batchSelect'))
|
||||
// ★ THE SEARCH PLACEHOLDER WAS THE LAST ENGLISH STRING ON THE ARABIC SCREEN. repo's ?lang=ar
|
||||
// ★ THE SEARCH PLACEHOLDER WAS THE LAST ENGLISH STRING ON THE ARABIC SCREEN. The ?lang=ar
|
||||
// screenshot was clean except for this one hardcoded literal.
|
||||
// ⚠ A computed, and declared HERE rather than beside geocoderContext, because t() is not
|
||||
// initialised until line 359 — a const at 339 would capture the fallback and never re-evaluate
|
||||
|
|
@ -733,7 +733,7 @@ useShadowStyles(
|
|||
|
||||
// ── RTL: WHAT THE LIBRARY DOES NOT DO ──────────────────────────────────────
|
||||
// ⚠ THE TWO POSITION RULES THAT USED TO LIVE HERE ARE GONE, deleted deliberately rather than left as a
|
||||
// belt-and-braces duplicate. studio's c79aae95 converts BOTH physical insets in SemanticTreeLegend to
|
||||
// belt-and-braces duplicate. c79aae95 converts BOTH physical insets in SemanticTreeLegend to
|
||||
// logical ones — :619 .stl-panel and :925 .stl-reopen are now `inset-inline-start: 8px` — so the panel
|
||||
// and its reopen button mirror for EVERY profile, not just this one. Two mechanisms doing one job is
|
||||
// worse than either: the next reader cannot tell which is load-bearing, and deleting the wrong one
|
||||
|
|
@ -745,11 +745,11 @@ useShadowStyles(
|
|||
// panel's POSITION; these set its CONTENT direction, which is a different property. repo reported
|
||||
// BOTH symptoms — "the panel is on the left" AND "the rows stay LTR" — so dropping these would fix
|
||||
// the first and silently restore the second.
|
||||
// ⚠ AND THIS PAIR MAY YET BECOME REDUNDANT: studio's argument is that dir on .profile-loader
|
||||
// ⚠ AND THIS PAIR MAY YET BECOME REDUNDANT: the argument is that dir on .profile-loader
|
||||
// (ProfileLoader.vue:282) inherits into the legend, since `direction` is an inherited property that
|
||||
// crosses the shadow boundary. That is plausible and they declined to add a library rule without a
|
||||
// measurement — correctly. rega will read the rows on Simple Research and Prayer Progress, which
|
||||
// have NO override, after landing 5. If they read RTL there, DELETE the two rules below too.
|
||||
// crosses the shadow boundary. That is plausible, and a library rule was declined without a
|
||||
// measurement — correctly. The rows on Simple Research and Prayer Progress, which
|
||||
// have NO override, get read after landing 5. If they read RTL there, DELETE the two rules below too.
|
||||
// ── the original finding, kept because it explains the shape ────────────────
|
||||
// Reported via repo, 2026-09-26, from the README's Arabic picture: under ?lang=ar the legend
|
||||
// panel stayed on the LEFT and the map controls sat on top of its top-left corner.
|
||||
|
|
@ -760,11 +760,11 @@ useShadowStyles(
|
|||
// dir entirely. So the toolbar moves left, the panel never leaves, and they land on top of each other.
|
||||
// The overlap is not a z-index or spacing bug; it is two coordinate systems disagreeing.
|
||||
//
|
||||
// ⚠ THIS IS A WORKAROUND FOR A LIBRARY DEFECT AND I AM SAYING SO RATHER THAN QUIETLY PAPERING IT.
|
||||
// ⚠ THIS IS A WORKAROUND FOR A LIBRARY DEFECT, SAID PLAINLY RATHER THAN QUIETLY PAPERED OVER.
|
||||
// The real fix is one character class in SemanticTreeLegend — `left:8px` -> `inset-inline-start:8px` —
|
||||
// which would mirror for EVERY profile automatically. That file is library/, so under rule 5B it is
|
||||
// studio's; reported. Until then every other profile with an RTL locale has this same collision, and
|
||||
// this override fixes only mine.
|
||||
// the library's to change; reported. Until then every other profile with an RTL locale has this same
|
||||
// collision, and this override fixes only this one.
|
||||
//
|
||||
// Scoped to [dir=rtl] so the LTR layout is untouched: ProfileLoader.vue:282 binds dir on the
|
||||
// .profile-loader wrapper from RTL_LOCALES, so this activates exactly when the document says so.
|
||||
|
|
@ -779,7 +779,7 @@ useShadowStyles(
|
|||
// REDUNDANT. It matched real elements (.stl-panel appears 69 times in SemanticTreeLegend) but the
|
||||
// library gets there on its own: it uses the :dir(rtl) PSEUDO-CLASS (:645-646), which reads the
|
||||
// INHERITED direction rather than needing a declaration, and CSS `direction` inherits across the
|
||||
// shadow boundary. rega measured the consequence at main 4ca2dcf9 (row 56b): Simple Research and
|
||||
// shadow boundary. Measured at main 4ca2dcf9 (row 56b): Simple Research and
|
||||
// Prayer Progress, which carry NO override at all, compute direction rtl on their legend rows under
|
||||
// ?lang=ar. A rule whose effect is indistinguishable from its absence on two other profiles is not
|
||||
// load-bearing here either.
|
||||
|
|
@ -788,22 +788,21 @@ useShadowStyles(
|
|||
// ⚠ DEAD CSS, and it always was. The library's count and population cells are `.stl-num` (:1314-1315).
|
||||
// `.stl-count` and `.stl-pop` DO NOT EXIST — `git grep` finds those two class names nowhere in
|
||||
// library/ or any other plugin, only inside this override. So this rule matched zero elements from the
|
||||
// day it was written, and the digit order rega verified ("694", "1,413" keeping their order under ar)
|
||||
// was always the library's doing, never mine.
|
||||
// day it was written, and the verified digit order ("694", "1,413" keeping their order under ar)
|
||||
// was always the library's doing, never this file's.
|
||||
// ★ THE LESSON IS ABOUT THE EVIDENCE, NOT THE CSS: this rule was written to fix a real bidi problem,
|
||||
// was believed to have fixed it, and was cited as the reason the numbers read correctly — while
|
||||
// matching nothing. A style rule fails SILENTLY when its selector is wrong; it does not warn, and the
|
||||
// page looks right if something else is already handling it. The only way to know a rule works is to
|
||||
// check that its selector exists, which is a different act from looking at the rendered page.
|
||||
// (dx-shepherd UPGDEL authorised deleting both as provisional; recording here that one of the two was
|
||||
// (Both were deleted as provisional; recorded here that one of the two was
|
||||
// never provisional but inert, so nobody re-adds it believing it once worked.)
|
||||
// ⚠ AND THE REOPEN BUTTON, which I missed on my first pass and repo caught. SemanticTreeLegend has
|
||||
// TWO physical `left:8px` rules, not one: :619 for the panel and :921 for .stl-reopen, the button that
|
||||
// brings the panel back after it is collapsed. Mirroring only the panel would leave the button on the
|
||||
// opposite side from the thing it opens — a worse state than the original bug, because it looks
|
||||
// deliberate.
|
||||
// The lesson is mine: I told studio to sweep for other physical insets and did not run the sweep
|
||||
// myself before reporting. `grep -nE '(^|[^-])(left|right):' library/components/legends/*.vue`.
|
||||
// The lesson: a sweep for other physical insets was requested but not run before reporting. `grep -nE '(^|[^-])(left|right):' library/components/legends/*.vue`.
|
||||
// Geocoder suggestion metadata layout (clone — bold field labels for PG rows).
|
||||
'.dg-main{font:13px/1.3 ui-monospace,monospace}' +
|
||||
'.dg-meta{margin-top:2px;font-size:11px;color:#475569;display:flex;flex-wrap:wrap;gap:8px}' +
|
||||
|
|
@ -842,11 +841,10 @@ function ensureLayers() {
|
|||
// `if (map.getSource('tapestry')) return`, which returns and adds NOTHING whenever the source
|
||||
// survives while the layers are gone. That state is real: a style reload wipes LAYERS, and a source
|
||||
// can be present without them.
|
||||
// ⚠ THIS IS NOT THE CAUSE OF THE BLANK ENGLISH README SHOT, and I want that on record because I
|
||||
// diagnosed it as such and was wrong. rega settled that one with the measurement I did not have:
|
||||
// the leaf paints 9.5-16.5 s after load and the screenshot tool waited 7 s. A timeout, not a defect.
|
||||
// I had built a confident chain — source-survives-layers, half-built style, en/ar divergence — that
|
||||
// explained the symptom without being true of it.
|
||||
// ⚠ THIS IS NOT THE CAUSE OF THE BLANK ENGLISH README SHOT, though it was first diagnosed as one.
|
||||
// A measurement settled it: the leaf paints 9.5-16.5 s after load and the screenshot tool waited 7 s.
|
||||
// A timeout, not a defect. The earlier chain — source-survives-layers, half-built style, en/ar
|
||||
// divergence — explained the symptom without being true of it.
|
||||
// THE GUARD BUG IS STILL REAL, on its own merits and on a different path: setStyle() (the
|
||||
// light/dark toggle) wipes LAYERS, and a source can outlive them, so the old guard returns having
|
||||
// added nothing. That is why paintAll is re-armed on 'style.load' at :1625 — the re-add is a real
|
||||
|
|
@ -1564,7 +1562,7 @@ const zoneSheet = makeZoneSheet({ allRecords: () => recordsFor.all(), assetsBase
|
|||
// <svg> as if it were a poster.
|
||||
// ⚠ A TRANSIENT STATUS CLEARS BY IDENTITY, NOT BY ITS ENGLISH PREFIX. Each of these messages used to be
|
||||
// cleared by `if (status.value.startsWith('Packet failed'))` — a guard that reads the PROSE. The moment
|
||||
// the prose is translated (exp-shepherd's universal-props row, 2026-09-26) that guard stops matching and
|
||||
// the prose is translated (the universal-props change, 2026-09-26) that guard stops matching and
|
||||
// the status never clears in any non-English locale: a stuck error message, in ten languages, from a fix
|
||||
// that looked purely cosmetic. Same family as counting prayer points by matching the English word
|
||||
// "prayer". So compare against the exact string we set instead — locale-independent by construction.
|
||||
|
|
@ -1613,7 +1611,7 @@ async function localizedZone(zone) {
|
|||
// prayer mobiliser — a GENERATOR that takes a list in and puts posters, packets and cards out, so a
|
||||
// partner can generate their own. Every list-consuming path in this profile goes through THIS
|
||||
// function (allLists -> selectedLists, printPrayerPacket, the legend, the funnel), so when the
|
||||
// universal `list` prop lands (exp-shepherd Tier A: profile-config.list / ?list= / window.MAP_LIST,
|
||||
// universal `list` prop lands (Tier A: profile-config.list / ?list= / window.MAP_LIST,
|
||||
// default 'doxa') it replaces the body here and nothing else moves. Do not read `clusters` directly
|
||||
// anywhere else; that is what makes the swap a one-line change instead of an audit.
|
||||
// ★ AND THIS IS THAT SWAP, taken 2026-09-27 — the body, exactly as the note above predicted, with the
|
||||
|
|
@ -1665,7 +1663,7 @@ function printableZones() {
|
|||
// 2026-09-26: K = 27 (13 + 12 + 2 buckets) with per-list counts from 8 to 118, against a brief that
|
||||
// said "~30 lists of 50-100" — only about half sit in that band, so no copy here claims it. The
|
||||
// 27 lists account for 2,086 of the 2,107, leaving 21 in no list; the footnote states that plainly
|
||||
// rather than letting a partner discover it by adding up rows (shepherd, ENDTOKEN-DXS-UPG-KDECIDE).
|
||||
// rather than letting a partner discover it by adding up rows.
|
||||
// ⚠ IT IS LIST-AWARE, AND IT HAS TO BE — THIS WAS A LIVE WRONG NUMBER, not a future risk. This
|
||||
// profile already honours the universal `list` prop: applyPeopleGroupList() filters the rows at all
|
||||
// four fetch sites, including packetRecords (:1258), and the membership files are on disk. So under
|
||||
|
|
@ -1758,7 +1756,7 @@ function selectedLists() {
|
|||
// allLists().filter((l) => want.has(l.id))
|
||||
// which returns the lists in clusters.json order and silently DISCARDS the order the reader chose —
|
||||
// even though selectedTerritories is documented at :186 as "an ORDERED list … (insertion order)".
|
||||
// rega measured the consequence (NEEDS-EYES row 45, main 52eeb0db): selecting Laos, then West Africa,
|
||||
// Measured (main 52eeb0db): selecting Laos, then West Africa,
|
||||
// then Europe produced a packet headed "West Africa + Laos + Europe", because west-africa is index 0
|
||||
// in the catalogue and laos is index 4. Reproduced exactly from the data.
|
||||
// It matters because the label's whole job is to "name the territories they actually chose rather than
|
||||
|
|
@ -2065,7 +2063,7 @@ function packetLang() {
|
|||
// ★ IT REPORTS WHETHER IT ACTED. Returns true when the selection was applied, false when it bailed.
|
||||
//
|
||||
// WHY: a silent `return` on a precondition is INDISTINGUISHABLE AT THE RENDERED LAYER from the
|
||||
// request never arriving (regions-plugin + upg100, 2026-09-26). The "Show on map" interop bug had
|
||||
// request never arriving (2026-09-26). The "Show on map" interop bug had
|
||||
// three layers stacked under one symptom — an element event with no replay, an id resolved against
|
||||
// unloaded legendNodes, and THIS function no-op'ing on a missing layer — and each fix looked
|
||||
// sufficient because all three produced the same nothing. Two were fixed by retaining the request;
|
||||
|
|
@ -2128,7 +2126,7 @@ function paintAll() {
|
|||
if (!map) return
|
||||
// ★ IT PAINTS WHAT IS READY, rather than refusing until everything is. The old guard was
|
||||
// `if (!map || !tapestry) return`, which meant the PINS could not be drawn until the boundaries had
|
||||
// been fetched AND parsed — and rega's baseline trace showed the parse, not the network, is the
|
||||
// been fetched AND parsed — and a baseline trace showed the parse, not the network, is the
|
||||
// 5.6-6.4 s cost. The pins need only the 68 KB records list. Every step below is idempotent and
|
||||
// guards its own source and layers, so calling this twice (once with pins only, once when the
|
||||
// tapestry lands) adds each thing exactly once.
|
||||
|
|
@ -2215,7 +2213,7 @@ function attachPinHandlers() {
|
|||
// detail.listId = a zoneKey to select, or NULL to deselect.
|
||||
let _selectZoneHandler = null;
|
||||
// ★ A REQUEST THAT ARRIVES TOO EARLY IS NOW REMEMBERED, NOT DROPPED.
|
||||
// rega measured the defect: the FIRST "Show on map" on a poster card did nothing, while a SECOND
|
||||
// Measured: the FIRST "Show on map" on a poster card did nothing, while a SECOND
|
||||
// one after the map had loaded isolated the zone and fitted the camera. So the listener and the
|
||||
// handler both work -- the first request simply died before this component was ready for it.
|
||||
// There were two ways for it to die and the fix has to survive both, which is why it retains the
|
||||
|
|
@ -2231,17 +2229,17 @@ let _pendingZoneId; // undefined = nothing outstanding
|
|||
let _selectZoneObserver = null; // watches the retained `select-zone` attribute
|
||||
|
||||
/** The ONE apply path. Returns true when the request was satisfied (or was a deselect). */
|
||||
// ★ THE APPLY REPORTS ITS OWN OUTCOME. rega's timeline proved delivery works (attribute written
|
||||
// ★ THE APPLY REPORTS ITS OWN OUTCOME. A timeline proved delivery works (attribute written
|
||||
// t=4106, and main already reads it at registration t=4763) and a headless test proved the lookup
|
||||
// works (asia-india-rajasthan -> "Northwest India", 37 legend keys). So the failure is DOWNSTREAM of
|
||||
// onLegendSelect and invisible: nothing throws, and "selected but not visibly selected" is
|
||||
// indistinguishable at the rendered layer from "never selected". Writing the outcome onto the HOST
|
||||
// makes it readable by rega's existing 50 ms poll, with no CDP and no new instrument.
|
||||
// ★ NAMESPACED, on upg100-plugin's point: the host element is SHARED SURFACE. A partner page
|
||||
// makes it readable by an existing 50 ms poll, with no CDP and no new instrument.
|
||||
// ★ NAMESPACED: the host element is SHARED SURFACE. A partner page
|
||||
// embedding this custom element could reasonably own an attribute called `data-select-zone-state`,
|
||||
// and a diagnostic has no business squatting a generic name on someone else's element. The
|
||||
// data-doxa- prefix makes the owner obvious and the collision risk nil.
|
||||
// Renamed AFTER rega's re-shoot rather than before, deliberately: their probe polls these names, and
|
||||
// Renamed AFTER a re-shoot rather than before, deliberately: the probe polls these names, and
|
||||
// renaming mid-flight would have burned a measurement instead of costing a few minutes.
|
||||
function _markZoneState(state, zoneId) {
|
||||
try {
|
||||
|
|
@ -2259,11 +2257,11 @@ function applySelectZone(zoneId) {
|
|||
const applied = onLegendSelect(node);
|
||||
// selectedKey (:163, "currently isolated zoneKey (or null)") is this component's own record of the
|
||||
// isolation, so 'applied-but-no-selection' means the path was REACHED and its EFFECT lost -- a
|
||||
// different bug from delivery, and the one rega's timeline points at.
|
||||
// different bug from delivery, and the one the timeline points at.
|
||||
// ★ I first wrote selectedZoneKey here, which does not exist; the try/catch would have swallowed
|
||||
// the ReferenceError and reported 'applied' every time. A diagnostic that cannot report failure is
|
||||
// worse than none. main's check:no-free-identifiers (6b05f513) now catches this class.
|
||||
// ★ IF THE EFFECT DID NOT TAKE, THE REQUEST IS STILL OUTSTANDING. rega's re-shoot on 27eed341
|
||||
// ★ IF THE EFFECT DID NOT TAKE, THE REQUEST IS STILL OUTSTANDING. A re-shoot on 27eed341
|
||||
// read exactly this state twice, 13 ms apart, with the right id -- so the node WAS found and
|
||||
// onLegendSelect WAS called, and the selection still did not hold. The cause is onLegendSelect's
|
||||
// own first line: `if (!map || !map.getLayer('tapestry-fill')) return`. It bails silently when the
|
||||
|
|
@ -2280,8 +2278,8 @@ function applySelectZone(zoneId) {
|
|||
// !selectedKey.value as "it failed". That is a proxy for "did it ISOLATE", not for "did it work",
|
||||
// and the two differ in exactly the two paths above — batch mode and the un-isolate both leave
|
||||
// selectedKey unset while having succeeded. Retaining on that would have re-applied the zone and
|
||||
// toggled it back OUT of the selection union. upg100-plugin caught that before it bit anyone, and
|
||||
// added the boolean so the applier could be ASKED instead of inferred from.
|
||||
// toggled it back OUT of the selection union. That was caught before it bit anyone, and
|
||||
// the boolean was added so the applier could be ASKED instead of inferred from.
|
||||
if (!applied) { _pendingZoneId = zoneId; _markZoneState('applied-but-no-selection', zoneId); return false; }
|
||||
// ★ CLEAR THE RETAINED REQUEST ON SUCCESS. Every FAILING path above sets _pendingZoneId and the
|
||||
// no-op path clears it, but this one — the one that WORKED — left it set. So a request that was
|
||||
|
|
@ -2299,8 +2297,8 @@ function _onSelectZone(ev) {
|
|||
const zoneId = ev?.detail?.listId ?? null;
|
||||
// REUSE THE CANONICAL SELECT PATH. onLegendSelect(node) is what a legend row
|
||||
// tap and the search bar already call: it isolates the zone, DIMS everything
|
||||
// outside it, and fits the camera. Brief: "when I click Show on map it should
|
||||
// be basically the same as if I searched that result in the search bar."
|
||||
// outside it, and fits the camera. "Show on map" should behave exactly as if
|
||||
// that result had been chosen in the search bar.
|
||||
//
|
||||
// Writing a second selection path here would have diverged from the legend and
|
||||
// the search bar the first time any of them changed — so this resolves the zone
|
||||
|
|
@ -2411,14 +2409,14 @@ onMounted(async () => {
|
|||
// ⚠ AND THE FALLBACK COULD NOT EVEN WORK: `gl.accessToken = token` a few lines below is gated on
|
||||
// `if (!isMaplibre)`, so a maplibre request that fell through to Mapbox would get Mapbox WITHOUT a token
|
||||
// — 401s on every tile instead of a legible failure. Two defects in one expression.
|
||||
// Found by regions' C3 read, routed via dx-shepherd (END-SHEP-C3C). Same shape in all three raw-map
|
||||
// Found by a read of the regions map. Same shape in all three raw-map
|
||||
// plugins.
|
||||
const gl = engineGlobal(upgProvider)
|
||||
// ★ SHAPE RTL BASEMAP LABELS — HERE, not in index.js, and not after the map exists.
|
||||
// This profile builds its map RAW (`new gl.Map` below) instead of going through
|
||||
// useMapInstance, so it inherits none of the entry's side effects. It appeared to work only
|
||||
// because the leaf head PRELOADED the Mapbox peer, which made an entry-time registration land
|
||||
// in time; studio is removing that preload, and with the engine injected lazily an index.js
|
||||
// in time; that preload is being removed, and with the engine injected lazily an index.js
|
||||
// call runs before `gl` exists and does nothing at all.
|
||||
//
|
||||
// ⚠ IT IS NOT A UI-LANGUAGE FEATURE, which is why it matters more than it looks. A basemap
|
||||
|
|
@ -2474,8 +2472,8 @@ onMounted(async () => {
|
|||
// mapbox-under-maplibre reference in this profile, carried as a named exemption in the
|
||||
// maplibre-embed gate; guarding it lets that exemption be deleted and enforced instead.
|
||||
// MapLibre ships its own ctrl CSS via ensureEngineScript, so nothing is lost.
|
||||
// ★ THE LIBRARY'S ensureEngineCSS, NOT A HAND-ROLLED COPY (rega END-REGA-SRV / END-DXS-U100C,
|
||||
// 2026-09-27). Mapbox logged "missing CSS declarations" on the BUILT leaf and in the gallery, and the
|
||||
// ★ THE LIBRARY'S ensureEngineCSS, NOT A HAND-ROLLED COPY (2026-09-27).
|
||||
// Mapbox logged "missing CSS declarations" on the BUILT leaf and in the gallery, and the
|
||||
// cause was this block, not a missing <link>:
|
||||
// (a) it only ran when the map element was inside a ShadowRoot — `root instanceof ShadowRoot` — so on
|
||||
// a standalone leaf, where the element is in LIGHT DOM, no stylesheet was ensured at all; and
|
||||
|
|
@ -2589,13 +2587,13 @@ onMounted(async () => {
|
|||
// map-highlight feature count independently.
|
||||
// ⚠ THIS IS THE BATCH-UNION TEST HANDLE, NOT A CLICK SIMULATOR — it calls toggleZoneSelection() directly,
|
||||
// so OUTSIDE batch mode it toggles a union nobody is displaying and the isolate filter stays
|
||||
// undefined. rega measured exactly that (NEEDS-EYES row 45) and asked whether it should isolate for a
|
||||
// undefined. Measured exactly that, which raised whether it should isolate for a
|
||||
// single select.
|
||||
// IT SHOULD NOT, and widening it would be worse than the narrowness. A REAL click goes through
|
||||
// onLegendSelect(), which branches on batchMode — isolate when off, feed the union when on. If this
|
||||
// handle isolated on its own it would no longer EXERCISE that branch, and a gate built on it could go
|
||||
// green while the real click path was broken. A test handle that does more than the UI hides the UI.
|
||||
// So: use a real click to test isolation (rega's row-45 test A does, and it passes), and use this only
|
||||
// So: use a real click to test isolation (the row-45 test A does, and it passes), and use this only
|
||||
// to drive batch selection. The name says Toggle, and toggling the union is what it does.
|
||||
toggleLegendRow: (label) => {
|
||||
const find = (nodes) => {
|
||||
|
|
@ -2682,7 +2680,7 @@ onMounted(async () => {
|
|||
// ★ THE API REQUEST IS ISSUED NOW, NOT AFTER THE BOUNDARIES. Measured 2026-09-26: this chain was
|
||||
// strictly serial — `await Promise.all([3 boundaries])` and only THEN `await fetch(API)` — so
|
||||
// 3,549 KB of geojson (world-adm0 1,866 + gb-IND-ADM1 1,628 + gb-CHN-ADM1 54) had to download AND
|
||||
// JSON.parse before the API request was even sent. rega measured the first paint at 9.5-16.5 s.
|
||||
// JSON.parse before the API request was even sent. The first paint measured 9.5-16.5 s.
|
||||
// Starting the fetch here overlaps the two; nothing below needs the boundaries to BUILD the URL.
|
||||
// ⚠ NOT awaited here — the promise is created and awaited further down, so a rejection surfaces at
|
||||
// the same place in the same try/catch as before. Creating it early changes WHEN the request goes
|
||||
|
|
@ -2722,8 +2720,8 @@ onMounted(async () => {
|
|||
// full fetch, no per-slice field subsets.
|
||||
//
|
||||
// ★ THE RECORDS ARE AWAITED BEFORE THE BOUNDARIES, so the pins can be drawn while MapLibre is still
|
||||
// parsing 3.6 MB of GeoJSON. rega traced the baseline (main ccc2b1bb, tokenless MapLibre, software
|
||||
// GL) and the finding overturned my own profile: every key byte arrives by 1.2-2.9 s, but the first
|
||||
// parsing 3.6 MB of GeoJSON. A baseline trace (main ccc2b1bb, tokenless MapLibre, software
|
||||
// GL) overturned an earlier profile: every key byte arrives by 1.2-2.9 s, but the first
|
||||
// territory draws 5.6-6.4 s LATER, and only ~2.6 s of that gap is main-thread long tasks. The rest
|
||||
// is MapLibre's WORKERS parsing and tiling the boundaries. I had profiled file sizes and request
|
||||
// order — the artifact — and concluded the download was the cost; on localhost those 3.6 MB arrive
|
||||
|
|
@ -2802,7 +2800,7 @@ onMounted(async () => {
|
|||
// ⚠ SAME BUG, SAME FIX as ensureLayers():606 — guard on the LAYER, not only the source. The old
|
||||
// `if (map.getSource('upg')) return` adds nothing whenever the source survives a style reload
|
||||
// while 'upg-pins' does not. NOT the cause of the blank English README shot (that was a 7 s
|
||||
// screenshot wait against a 9.5-16.5 s first paint — rega measured it); this is the
|
||||
// screenshot wait against a 9.5-16.5 s first paint — measured); this is the
|
||||
// setStyle/theme-toggle path, where the re-add is real and could silently no-op.
|
||||
if (map.getSource('upg') && map.getLayer('upg-pins') && map.getLayer('upg-pins-hit')) return
|
||||
if (!map.getSource('upg')) map.addSource('upg', { type: 'geojson', data: pinFC })
|
||||
|
|
@ -2891,11 +2889,10 @@ onMounted(async () => {
|
|||
// ── NOW the boundaries, and the territories behind the pins ───────────────
|
||||
// ★ EVERYTHING ABOVE THIS LINE NEEDS ONLY THE 68 KB RECORDS LIST. The boundary await sits HERE so
|
||||
// the pin layer can be added while MapLibre's workers are still parsing 3.6 MB of GeoJSON —
|
||||
// rega's baseline trace showed the first territory drawing 5.6-6.4 s after the last key byte
|
||||
// A baseline trace showed the first territory drawing 5.6-6.4 s after the last key byte
|
||||
// arrived, and only ~2.6 s of that is main-thread work. Parsing is the bottleneck, not the network,
|
||||
// so putting SOMETHING useful on screen means drawing what does not need parsing.
|
||||
// (dx-shepherd ENDTOKEN-DXS-UPG-PINSFIRST; it replaced my two-pass-zones plan, which would still
|
||||
// have waited on world-adm0.)
|
||||
// (This replaced a two-pass-zones plan, which would still have waited on world-adm0.)
|
||||
status.value = t('messages.loadingBoundaries')
|
||||
const [adm0, adm1Chn] = await Promise.all([boundaryPromises.adm0, boundaryPromises.adm1Chn])
|
||||
const adm1Ind = await boundaryPromises.adm1Ind
|
||||
|
|
@ -3041,7 +3038,7 @@ onMounted(async () => {
|
|||
// When the legend data finally lands, satisfy anything that was asked for too early. This is the
|
||||
// half that makes retention real -- without it the request is remembered and never applied.
|
||||
//
|
||||
// ★ ON nextTick, AND NOT AS A PRECAUTION. upg100-plugin flagged the race from experience with this
|
||||
// ★ ON nextTick, AND NOT AS A PRECAUTION. The race is a known one in this
|
||||
// file: watch([batchMode, isDark]) at :222 calls markLegendBatchRows() on nextTick, and this watcher
|
||||
// would otherwise fire PRE-FLUSH -- so the selection would be applied against the legend rows that
|
||||
// are about to be replaced by the re-render from these very legendNodes, and its row highlight
|
||||
|
|
@ -3050,7 +3047,7 @@ onMounted(async () => {
|
|||
// the bug this whole change exists to fix. Draining after the flush lets the rows exist and be
|
||||
// marked first.
|
||||
// ★ DRAIN ON EITHER READINESS SIGNAL, AND RE-READ THE ATTRIBUTE RATHER THAN THE BOOKKEEPING.
|
||||
// rega measured legendNodes populated at t=1017 and the request at t=4106, so a legendNodes-change
|
||||
// Measured: legendNodes populated at t=1017 and the request at t=4106, so a legendNodes-change
|
||||
// drain can never re-fire for it. Re-reading the host attribute makes the outcome independent of
|
||||
// which hook fired when, which is the whole reason state was chosen over an event.
|
||||
const _hostEl = () => { try { return mapEl.value?.getRootNode?.()?.host || null } catch (_) { return null } }
|
||||
|
|
@ -3312,8 +3309,8 @@ onBeforeUnmount(() => {
|
|||
<div v-if="selCount" class="upg-hud-head">
|
||||
<!-- ★ A LABEL FORM ("Territories: {count}"), NOT A PLURAL KEY. This was
|
||||
`territor{{ selCount === 1 ? 'y' : 'ies' }}` — English pluralisation welded
|
||||
into the template. I proposed a vue-i18n plural key instead; studio was right
|
||||
that it would be WORSE. Tested: with no pluralRules registered, vue-i18n v9
|
||||
into the template. A vue-i18n plural key was proposed instead; it would
|
||||
be WORSE. Tested: with no pluralRules registered, vue-i18n v9
|
||||
applies an English-shaped choice (count===1 ? 0 : 1) to EVERY locale, so ru
|
||||
with 3 forms supplied picks wrong at 1, 2 and 21 — confidently wrong grammar
|
||||
that LOOKS translated. Real plural support needs a pluralRules map at
|
||||
|
|
@ -3321,7 +3318,7 @@ onBeforeUnmount(() => {
|
|||
agreement entirely and is correct in all 11 today. -->
|
||||
<span>{{ t('messages.territoriesSelected', { count: selCount }) }} ·</span>
|
||||
<span class="upg-hud-total">{{ selTotal }} UUPG</span>
|
||||
<!-- TWO KEYS, not a ternary over one (studio's call, and it is the right one): these are two
|
||||
<!-- TWO KEYS, not a ternary over one: these are two
|
||||
STATES of one label, and a locale may not phrase "in range" and "out of range" as a tick
|
||||
plus the same words. The ✓ lives in the catalogue string so a locale can drop it. -->
|
||||
<span class="upg-hud-band" :class="{ ok: selTotal >= 50 && selTotal <= 100 }">
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import ShareButton from '@map/components/map-controls/ShareButton.vue'
|
|||
// through the control bridge (tabInfo 'upg-100' trigger:'control'), which then keeps its floating ⓘ off.
|
||||
import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
|
||||
|
||||
// ★ THE SHELL'S OWN CHROME WAS ENGLISH IN EVERY LOCALE. rega measured ?lang=ar and found
|
||||
// ★ THE SHELL'S OWN CHROME WAS ENGLISH IN EVERY LOCALE. Measured at ?lang=ar:
|
||||
// 'Select View', 'Prayer Resources' and 'Map' rendering in English while the poster wall's own
|
||||
// buttons inside the panes translated correctly — because the panes each install i18n and this
|
||||
// wrapper never did. The chrome a reader sees FIRST was the untranslated part.
|
||||
|
|
@ -53,7 +53,7 @@ import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
|
|||
// that guard once — host i18n first, then tStatic, then the key — instead of a hand-rolled copy here.
|
||||
// No namespace: callers pass FULL keys here, exactly as the raw useI18n().t they used to get.
|
||||
//
|
||||
// ★ ONE BEHAVIOUR GAIN FROM WAVE 3c, stated rather than smuggled (regions-plugin, 4c05e589): the old
|
||||
// ★ ONE BEHAVIOUR GAIN FROM WAVE 3c, stated rather than smuggled (4c05e589): the old
|
||||
// i18n path was the bare useI18n().t, so on a host WITH i18n a missing key rendered the raw dotted key.
|
||||
// It now falls through to the catalogue instead.
|
||||
//
|
||||
|
|
@ -65,7 +65,7 @@ import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
|
|||
// reader's locale and still ends at English.
|
||||
// The four keys this file uses (tabs.map / prayerResources / selectView / views) are present in all
|
||||
// ELEVEN locales, 0 gaps, enumerated 2026-09-27 — nothing here was relying on the fallback.
|
||||
// Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the standing goal that the
|
||||
// A product decision (2026-09-27), under the standing goal that the
|
||||
// production maps be localized; it changes what seven vue-i18n-less bundles render, which is not a
|
||||
// decision a migration gets to make for itself.
|
||||
const t = useOptionalT('')
|
||||
|
|
@ -89,7 +89,7 @@ const spec = computed(() => ({
|
|||
// pane's own lookup: ProfileLoader.vue:142 resolves
|
||||
// config.dataSource || sourcesConfig.activeSource || 'doxa-csv'
|
||||
// and sources.json's activeSource is 'pray-tools' today, so the two AGREE and this is a no-op refactor
|
||||
// (verified from source, per dx-shepherd END-SHEP-UPG-CONTRACT, not assumed).
|
||||
// (verified from source, not assumed).
|
||||
// It matters anyway, for two reasons. (a) The day activeSource changes, these panes would silently keep
|
||||
// using 'pray-tools' while every other consumer moved — a divergence nothing would report. (b) The
|
||||
// comment at ProfileLoader.vue:135-141 records this EXACT bug already happening once: a host embedding
|
||||
|
|
@ -180,8 +180,8 @@ const paneEls = {}
|
|||
// before it: the inner map cannot honour a key the shell never passed down.
|
||||
// ⚠ tokenUrl IS FORWARDED EXPLICITLY. The panes already RECEIVE a token in practice — the outer
|
||||
// ProfileLoader fetches profile-config.tokenUrl and publishes a page-wide shared token — so this was
|
||||
// working by a side effect of something else rather than by this contract. exp-shepherd's
|
||||
// universal-props row (2026-09-26) flagged it for exactly that reason: a prop that arrives because a
|
||||
// working by a side effect of something else rather than by this contract. The
|
||||
// universal-props change (2026-09-26) flagged it for exactly that reason: a prop that arrives because a
|
||||
// neighbour happens to publish it is not forwarded, it is lucky, and it breaks silently the day the
|
||||
// neighbour stops. Naming it here makes the panes' token a consequence of this list.
|
||||
const INHERITED_PAGE_KEYS = ['assetsBase', 'apiBaseUrl', 'tokenUrl', 'locale', 'lang', 'isDark', 'qa', 'provider', 'list']
|
||||
|
|
@ -204,9 +204,9 @@ function paneElement(pane) {
|
|||
// reported screenshots show it over a page with no coloured dots on it at all.
|
||||
// ⚠ THIS SUPERSEDES THE inert WORK FOR THIS PROFILE, and that is worth saying plainly rather than
|
||||
// quietly leaving both in: PILLRISK was "the pill shows over the posters", and I fixed WHERE it shows.
|
||||
// The ruling is that it should not show HERE, full stop — a narrower and more correct claim
|
||||
// than mine. The inert signal stays because it is still right for its own reasons (a stacked pane
|
||||
// should not be in tab order or the a11y tree, and studio's PinHint change serves profiles that DO
|
||||
// It should not show HERE, full stop — a narrower and more correct claim
|
||||
// than the earlier one. The inert signal stays because it is still right for its own reasons (a stacked pane
|
||||
// should not be in tab order or the a11y tree, and the PinHint change serves profiles that DO
|
||||
// want the pill), but it is no longer what keeps this pill off this app.
|
||||
// Opt-out via the contract that already existed — ProfileLoader.vue:150 reads
|
||||
// `config.pinHint !== false` — so this is a config value from my side, not a library change.
|
||||
|
|
@ -228,10 +228,10 @@ function paneElement(pane) {
|
|||
//
|
||||
// ⚠ inert, NOT display:none OR v-show, and this is the constraint that decides the whole approach:
|
||||
// hiding the pane would tear down the live map. A MapLibre/Mapbox canvas in a zero-size container
|
||||
// loses its size and re-initialises on the way back, which is exactly the cold-start path rega had to
|
||||
// verify in row 52. inert leaves the pane mounted, painted and warm; it only declares it non-interactive
|
||||
// loses its size and re-initialises on the way back, which is exactly the cold-start path that had to
|
||||
// be verified in row 52. inert leaves the pane mounted, painted and warm; it only declares it non-interactive
|
||||
// — and, unlike aria-hidden, also removes it from tab order and the accessibility tree, so a keyboard
|
||||
// user cannot tab into a map they cannot see. (studio's contract, END-STU-PILL: PinHint walks up through
|
||||
// user cannot tab into a map they cannot see. (The library contract: PinHint walks up through
|
||||
// shadow roots for inert/hidden/aria-hidden and watches that chain with a MutationObserver, so no event
|
||||
// is needed and switching views brings the pill straight back.)
|
||||
//
|
||||
|
|
@ -280,7 +280,7 @@ function onShowOnMap(e) {
|
|||
go('map')
|
||||
// ★ RECORD THE REQUEST, THEN ANNOUNCE IT — in that order, and the order is the fix.
|
||||
// The comment that used to sit here called the miss "the degradation we want": a pane that had
|
||||
// not mounted got nothing, and the scroll still happened. rega measured what that actually costs
|
||||
// not mounted got nothing, and the scroll still happened. What that actually cost, measured
|
||||
// — the FIRST "Show on map" did nothing at all, and only a SECOND click, after the map had
|
||||
// loaded, selected the zone. A control the user must press twice reads as broken.
|
||||
//
|
||||
|
|
|
|||
Loading…
Reference in a new issue