P: merge 3aa3d8dde
This commit is contained in:
commit
ea6ba0e126
23 changed files with 113 additions and 116 deletions
|
|
@ -194,7 +194,7 @@ const mapData = useMapData({
|
|||
// all 12,379 features, 3.7 MB of GeoJSON built and re-tiled TWICE (first paint, then the DOXA recolour);
|
||||
// slim, it is 1.25 MB, built once. The popup reads the row by id. The DOXA recolour no longer rebuilds
|
||||
// the 12,379 with all their fields: DOXA pins are a small source of their own drawn on top, and the base
|
||||
// is re-sent once, slim, WITHOUT them (an invisible twin per DOXA group still cost a draw: rega measured).
|
||||
// is re-sent once, slim, WITHOUT them (an invisible twin per DOXA group still cost a draw: measured).
|
||||
let rows = [] // the decoded rows; a feature's id is its index here
|
||||
let doxaIds = new Set() // row indexes in the DOXA set (empty until the prayer API answers)
|
||||
let doxaBySlugRop = new Map() // rop3 → { slug, name } for the pray link
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ watch(textDir, reflectDir)
|
|||
|
||||
// The host's people-group LIST (library/lists/), handed to both children as the only key of their
|
||||
// profile-config, so nothing else about them changes. Absent = the default list.
|
||||
// ★ THE LOCALE TRAVELS WITH THE LIST (rega, row 47: big-picture rendered 31 Arabic text nodes
|
||||
// ★ THE LOCALE TRAVELS WITH THE LIST (measured: big-picture rendered 31 Arabic text nodes
|
||||
// through profile-config against 79 through ?lang=, and check:host-locale-contract measured 402
|
||||
// against 888). This handed each child ONLY the list, so the one documented channel a partner has
|
||||
// for asking a nested element to speak Arabic — its profile-config — arrived empty of any locale.
|
||||
|
|
@ -155,7 +155,7 @@ watch(textDir, reflectDir)
|
|||
// Threading it costs nothing and is what ProfileLoader would have done had one existed beneath this
|
||||
// composite, which is exactly the (C) failure mode: a composite that BAKES its children in has no
|
||||
// ProfileLoader under it, so it must do that job itself.
|
||||
// This is ONE identified gap, not a claim to have closed the whole 402/888 difference; rega
|
||||
// This is ONE identified gap, not a claim to have closed the whole 402/888 difference; a later pass
|
||||
// re-measures.
|
||||
const listConfig = computed(() => {
|
||||
const cfg = {}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
* resolution: "city" an actual city measurement (none yet)
|
||||
*
|
||||
* and rows with no data say `unknown` rather than defaulting to a plausible number.
|
||||
* MEASURED 2026-08-01: 129 of 175 countries resolved, 46 unknown. Showing the 46 as
|
||||
* MEASURED: 129 of 175 countries resolved, 46 unknown. Showing the 46 as
|
||||
* "unknown" is the point of the app, not a gap in it — an unknown is a research
|
||||
* target, and a fabricated figure is a lie a reader cannot detect.
|
||||
*
|
||||
|
|
@ -43,14 +43,14 @@ const profileConfig = inject('profileConfig', ref(null))
|
|||
// i18n is OPTIONAL: useI18n() throws on a host that never installed it, so English from the
|
||||
// shipped catalog is the fallback, never a blank or a key. Region and country names are data.
|
||||
// C4 wave 3b: the guard is library/composables/useOptionalT.js now, and the fallback resolves through
|
||||
// tStatic — the RUNTIME LOCALE first, English as the last resort. exp-shepherd's call as this
|
||||
// plugin's owner (2026-09-26), on the same reasoning gospel-plugin gave: the bundle already carries
|
||||
// tStatic — the RUNTIME LOCALE first, English as the last resort. This plugin owns that
|
||||
// choice, on the same reasoning the other map plugins use: the bundle already carries
|
||||
// all 11 catalogues, so a partner embedding without vue-i18n being served English is a bug, not a
|
||||
// decision. The old enLookup was English-only, which is exactly what that served.
|
||||
// The local fill() is gone with the guard: fillParams applies the same interpolation, except it leaves
|
||||
// a placeholder for a NULL value where this fill() rendered the string "null".
|
||||
const t = useOptionalT('connectivity')
|
||||
// NUMERALS ARE HELD fleet-wide (2026-09-26): a locale-following format would switch
|
||||
// NUMERALS ARE HELD by design: a locale-following format would switch
|
||||
// 'ar' to Arabic-Indic digits. Until that is decided, numbers keep the browser-default format they
|
||||
// had before this pass; only the TEXT is translated.
|
||||
const num = (n) => (Number(n) || 0).toLocaleString()
|
||||
|
|
@ -152,7 +152,7 @@ const bandLabel = (a) => t('connectivity.list.band.' + (['yes', 'partial', 'no']
|
|||
useShadowStyles(
|
||||
// ★ THE LIST MUST STAY INSIDE ITS HOST BOX. This element had NO :host rule, so a custom element
|
||||
// falls back to display:inline with no height of its own, and .ci below — position:absolute;inset:0
|
||||
// — resolved against whatever the PARTNER page happened to have positioned. rega measured the result
|
||||
// — resolved against whatever the PARTNER page happened to have positioned. The result was measured
|
||||
// on a partner page: content painting to y=16,799, which is 16,355px BELOW a 420px host box.
|
||||
// display:block gives it a box; position:relative makes that box the containing block .ci resolves
|
||||
// against; height:100% lets a definite-height slot decide; min-height is the floor for an AUTO-height
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function setGlobal(name, value) {
|
|||
* social platform on tap — the connectivity view of where DX people groups can be reached online.
|
||||
*
|
||||
* Every country shaded by measured internet penetration, with its NAMED carriers
|
||||
* and dominant social platform on tap. Brief: "all these are missing maps."
|
||||
* and dominant social platform on tap. One of the data maps this project adds.
|
||||
*
|
||||
* CLONE of gospel-resources/resources-map.vue with the data and bands swapped —
|
||||
* profiles duplicate, the pattern does not get re-derived. Same mobile-first
|
||||
|
|
@ -131,8 +131,8 @@ const rows = ref([])
|
|||
const selected = ref(null)
|
||||
const aiNoticeOpen = ref(true) // dismissible AI-research disclaimer in the detail sheet
|
||||
const err = ref(null)
|
||||
// Starts TRUE: the notice must be on screen for the first frame, not switched on after a tick. rega
|
||||
// measured 7-30s to the first data paint, and for that whole window the map was an empty basemap with
|
||||
// Starts TRUE: the notice must be on screen for the first frame, not switched on after a tick.
|
||||
// Measurement showed 7-30s to the first data paint, and for that whole window the map was an empty basemap with
|
||||
// nothing to say it was still working — indistinguishable from a broken embed.
|
||||
const loading = ref(true)
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ const selectedAdm1 = ref(null)
|
|||
// The DOXA list from pray-tools, the same list Prayer Progress draws. It carries the slug the
|
||||
// detail panel fetches by. The shared pin table (tables/people-group-pins.json) has no slug,
|
||||
// so its dots could not open that panel.
|
||||
// Dark slate with a white ring (2026-09-28: the red dots looked wrong on the mostly green
|
||||
// Dark slate with a white ring (red dots looked wrong on the mostly green
|
||||
// map). A neutral dark reads on every fill here, the green/amber/red/grey bands AND India's pale-to-deep
|
||||
// blues, and it carries no "good/bad" meaning the way red does on a map whose red means under 25%.
|
||||
// The white ring keeps it apart from the deepest blue and from the dark basemap.
|
||||
|
|
@ -190,7 +190,7 @@ const uiStore = inject('uiStore', null)
|
|||
const _lang = (() => { const v = inject('lang', null)?.value; return (v && v !== 'en') ? v : null })()
|
||||
// The reader's language for the ENGINE'S OWN labels (the canvas's "Map" name, "Zoom in", attribution…).
|
||||
// This map is built raw, not through useMapInstance, so it must hand the engine that locale itself;
|
||||
// without it they stayed English under ar on both engines (rega, row 34). Same ladder as
|
||||
// without it they stayed English under ar on both engines (measured). Same ladder as
|
||||
// useMapInstance's readerLang: ProfileLoader's lang, then profile-config, then ?lang=, then <html lang>.
|
||||
const readerLang = () => {
|
||||
if (_lang) return _lang
|
||||
|
|
@ -302,8 +302,8 @@ function paintGroups(map) {
|
|||
// installed it, so English from the shipped catalog is the fallback, never a blank or a key.
|
||||
// Carrier and platform names are data and are not translated.
|
||||
// C4 wave 3b: the guard is library/composables/useOptionalT.js now, and the fallback resolves through
|
||||
// tStatic — the RUNTIME LOCALE first, English as the last resort. exp-shepherd's call as this
|
||||
// plugin's owner (2026-09-26), on the same reasoning gospel-plugin gave: the bundle already carries
|
||||
// tStatic — the RUNTIME LOCALE first, English as the last resort. This plugin owns that
|
||||
// choice, on the same reasoning the other map plugins use: the bundle already carries
|
||||
// all 11 catalogues, so a partner embedding without vue-i18n being served English is a bug, not a
|
||||
// decision. The old enLookup was English-only, which is exactly what that served.
|
||||
// The local fill() is gone with the guard: fillParams applies the same interpolation, except it leaves
|
||||
|
|
@ -565,7 +565,7 @@ onMounted(async () => {
|
|||
paintGroups(map)
|
||||
const geo = await adm1Loaded
|
||||
if (geo && !unmounted) paintAdm1(map, geo)
|
||||
// ★ CLEARED HERE, NOT ON 'load'. rega measured 7-30s from mount to the first data paint under
|
||||
// ★ CLEARED HERE, NOT ON 'load'. Measurement showed 7-30s from mount to the first data paint under
|
||||
// SwiftShader: the style fires 'load' early, while the shaded countries, the adm1 provinces and
|
||||
// the 2,105 dots all arrive later. Clearing on 'load' would retire the notice during exactly the
|
||||
// wait it exists to explain, leaving the reader with an empty basemap and no reason for it.
|
||||
|
|
@ -603,7 +603,7 @@ const groupOpen = computed(() => !!uiStore?.selectedPeopleGroup)
|
|||
useShadowStyles(
|
||||
// ⚠ THE MAP MUST FILL ITS HOST, NOT THE VIEWPORT. This element had NO :host rule at all, so a
|
||||
// custom element defaults to display:inline with no height of its own, and .gm's absolute inset:0
|
||||
// resolved against whatever ancestor happened to be positioned. rega measured the result in the
|
||||
// resolved against whatever ancestor happened to be positioned. The result was measured in the
|
||||
// /embed-test re-shoot at 1440: a 1198x900 canvas inside a 1198x560 embed card — ~340px, the
|
||||
// attribution among it, drawn below the card and clipped. Same class as the board's, and the same
|
||||
// fix it already carries: height:100% so a height-bounded slot decides, min-height as a floor so an
|
||||
|
|
@ -612,7 +612,7 @@ useShadowStyles(
|
|||
// .gm below is position:absolute;inset:0, and absolute positioning resolves against the nearest
|
||||
// POSITIONED ancestor — which :host was not. So on a partner page .gm resolved against whatever the
|
||||
// PARTNER happened to have positioned, and the map sized itself to that rather than to this element.
|
||||
// rega measured it in the hostbox run on ca07fc3c8: the element was CORRECT at 420px in a 420px box,
|
||||
// It was measured in a host-box run: the element was CORRECT at 420px in a 420px box,
|
||||
// while the canvas inside it stayed 900px and painted the pill and the attribution 478px below the
|
||||
// box (538px in a 360px box), over the partner's own text. Giving the host a HEIGHT without making it
|
||||
// a CONTAINING BLOCK fixes the box and not what is drawn inside it.
|
||||
|
|
@ -635,7 +635,7 @@ useShadowStyles(
|
|||
// control rail by construction: MapToolbar is inset-inline-end:3px with 32px
|
||||
// buttons, so ANY box reaching right:8px intersects it at every viewport. The
|
||||
// rail's z-index:10 beats this z-index:4, so the buttons stayed clickable and
|
||||
// the defect read as "the legend underlaps the controls" — reported 3x.
|
||||
// the defect read as "the legend underlaps the controls".
|
||||
// The fix imitates the shared SemanticTreeLegend, which has always done it
|
||||
// correctly: `left:8px; width:var(--map-legend-width,380px)`. Bounded and
|
||||
// left-anchored cannot reach the right rail, so the two can never overlap
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@
|
|||
* @description Four validated research tables, one tab each, with sortable and filterable columns, CSV and JSON
|
||||
* export, and each table's caveats rendered in the UI rather than hidden in a footnote.
|
||||
*
|
||||
* Four validated Tabulator tables from the flight's research corpora, each with
|
||||
* Four validated Tabulator tables from the research corpora, each with
|
||||
* sortable/filterable columns, CSV+JSON export, and its caveats rendered IN the UI.
|
||||
*
|
||||
* > design note 2026-08-01: "all the tabulator data and trees for all the multiple
|
||||
* > various tabulator tables for all this data all organized in list format with
|
||||
* > full granularity."
|
||||
* Design intent: present all the tabulator data and trees for the various tables
|
||||
* in list format, at full granularity.
|
||||
*
|
||||
* The tables are produced by library/scripts/research/build-tabulator-tables.mjs
|
||||
* (the M+V of IEMV) — flat scalar rows, one row per fact, validated before write.
|
||||
* — flat scalar rows, one row per fact, validated before write.
|
||||
*
|
||||
* WHY THE CAVEATS RENDER: a list view strips context. Someone reading
|
||||
* "Internet 12.6%" needs to know it is a NATIONAL figure, and someone reading a
|
||||
|
|
@ -37,14 +36,14 @@ const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
|
|||
// i18n is OPTIONAL: useI18n() throws on a host that never installed it, so English from the
|
||||
// shipped catalog is the fallback, never a blank or a key. Region and country names are data.
|
||||
// C4 wave 3b: the guard is library/composables/useOptionalT.js now, and the fallback resolves through
|
||||
// tStatic — the RUNTIME LOCALE first, English as the last resort. exp-shepherd's call as this
|
||||
// plugin's owner (2026-09-26), on the same reasoning gospel-plugin gave: the bundle already carries
|
||||
// tStatic — the RUNTIME LOCALE first, English as the last resort. This plugin owns that
|
||||
// choice, on the same reasoning the other map plugins use: the bundle already carries
|
||||
// all 11 catalogues, so a partner embedding without vue-i18n being served English is a bug, not a
|
||||
// decision. The old enLookup was English-only, which is exactly what that served.
|
||||
// The local fill() is gone with the guard: fillParams applies the same interpolation, except it leaves
|
||||
// a placeholder for a NULL value where this fill() rendered the string "null".
|
||||
const t = useOptionalT('connectivity')
|
||||
// NUMERALS ARE HELD fleet-wide (2026-09-26): a locale-following format would switch
|
||||
// NUMERALS ARE HELD by design: a locale-following format would switch
|
||||
// 'ar' to Arabic-Indic digits. Until that is decided, numbers keep the browser-default format they
|
||||
// had before this pass; only the TEXT is translated.
|
||||
const num = (n) => (Number(n) || 0).toLocaleString()
|
||||
|
|
@ -90,7 +89,7 @@ const table = computed(() => cache.value[current.value?.file] || null)
|
|||
useShadowStyles(
|
||||
// ★ THE TABLE MUST STAY INSIDE ITS HOST BOX — the same defect and the same fix as connectivity-list.
|
||||
// With no :host rule the element is inline with no height, and .ct (position:absolute;inset:0)
|
||||
// resolved against the partner page's positioned ancestor instead of this element. rega measured
|
||||
// resolved against the partner page's positioned ancestor instead of this element. Measurement showed
|
||||
// content painting to y=1,698, which is 1,254px BELOW a 420px host box.
|
||||
// .ct already has overflow-y:auto, so containing it is enough to make the table scroll in the box.
|
||||
':host{display:block;position:relative;width:100%;height:100%;min-height:320px}' +
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
* data/boundaries/gb-IND-ADM1-full.geojson the boundary file the map draws (all 36)
|
||||
* WRITES data/tables/internet-by-adm1.json served as assets/tables/internet-by-adm1.json
|
||||
*
|
||||
* THE TABLE IS GENERIC ON PURPOSE (other plugins and go-int reuse it). One row per
|
||||
* THE TABLE IS GENERIC ON PURPOSE (other plugins reuse it). One row per
|
||||
* (iso3, adm1, measure, sex, area): no India-specific shape, so a second country or a second
|
||||
* measure is more rows, not a new format. `meta.boundaries[iso3]` names the geometry file and the
|
||||
* property its `adm1` joins on; `meta.measures` and `meta.sources` say what each number is.
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ useShadowStyles(`
|
|||
container, which would otherwise stretch the column across the map once the start edge moves. */
|
||||
.mapboxgl-ctrl-top-left { top:12px!important;inset-inline-start:8px!important;inset-inline-end:auto!important;width:var(--map-legend-width,380px)!important;max-width:var(--map-legend-width,380px)!important;z-index:1200!important; }
|
||||
.mapboxgl-ctrl-top-left .mapboxgl-ctrl { margin:0!important;float:none!important; }
|
||||
/* Pill geometry + PPLR-matched focus ring (qa: 2026-05-02). border-radius
|
||||
/* Pill geometry + PPLR-matched focus ring. border-radius
|
||||
999px matches PPLR's .pgc-input-wrap; focus-within ring uses PPLR's
|
||||
accent rgba(59,70,61,0.18) so the search bar reads as one column with
|
||||
the SemanticTreeLegend below it. Input font-size:13px / placeholder
|
||||
|
|
@ -315,8 +315,8 @@ useShadowStyles(`
|
|||
/* isolation:isolate forces .rm-root to form its own stacking context so
|
||||
internal z-indexes (geocoder 1200, legend 1000, toolbar 10) compare only
|
||||
against each other and do NOT leak out to compete with page-level
|
||||
elements like the site nav / hamburger menu (qa: 2026-05-04 user
|
||||
reported the geocoder pill rendering above the page menu). Without
|
||||
elements like the site nav / hamburger menu (the geocoder pill otherwise
|
||||
rendered above the page menu). Without
|
||||
this, the host element doesn't establish a stacking context and the
|
||||
internal 1200 wins against any page elements with z-index < 1200. */
|
||||
.rm-root { position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden;background:#ffffff;color:#1f2937;isolation:isolate; }
|
||||
|
|
@ -342,7 +342,7 @@ useShadowStyles(`
|
|||
top-left corner than the standard 16px gutter; end side stays 16px.
|
||||
Mobile @media block below resets both sides back to 16px since the
|
||||
mobile slot uses a smaller radius and the existing 16px is correct
|
||||
there (qa: 2026-05-04 user feedback — desktop only). Active-tab
|
||||
there (desktop only). Active-tab
|
||||
scrollIntoView still uses 16px scroll-padding which is fine — the
|
||||
tab is already visible, just nudged by the static padding. */
|
||||
padding-inline-start:36px;
|
||||
|
|
@ -529,7 +529,7 @@ const legendTopVar = computed(() => {
|
|||
// Languages tabs it reserved 56px for a toolbar it never draws, and only those two tabs, because
|
||||
// they are exactly the set showsFlyTools() returns true for. Every other tab took the 14px branch
|
||||
// and looked right, which is why it was reported as two tabs misbehaving.
|
||||
// MEASURED by rega at 1440x900 on main e1d0bc20: the band from the map's top edge (y 44) to the
|
||||
// MEASURED at 1440x900 on main e1d0bc20: the band from the map's top edge (y 44) to the
|
||||
// legend card (y 100) is 56px of EMPTY map on both tabs, with no LegendTools anywhere in the strip.
|
||||
// So the gap was dead space, not a toolbar the reader had not registered.
|
||||
const toolsShown = showLegendTools.value && showsFlyTools(activeLegendType?.value ?? activeLegendType)
|
||||
|
|
@ -603,7 +603,7 @@ const FEATURES = {
|
|||
}
|
||||
|
||||
// Tab order: Prayer → Adoption → Engagement → Language Families
|
||||
// (qa: 2026-05-04 user feedback — Adoption swapped before Engagement so
|
||||
// Adoption swapped before Engagement so
|
||||
// the order reads as the user-flow ladder Prayer → Adopt → Engage rather
|
||||
// than the previous alphabetical Engagement-before-Adoption.)
|
||||
// No `label:` here: tabLabel() reads tabs.<i18nKey|id> behind a te() guard, and all seven keys
|
||||
|
|
@ -698,7 +698,7 @@ watch(() => activeTabId?.value, async (newTabId, oldTabId) => {
|
|||
// ★ SCROLL THE TAB BAR, NOT THE PAGE. This was
|
||||
// btn.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' })
|
||||
// and 'nearest' does not mean "do not scroll the page" — it means "scroll every scrollable
|
||||
// ancestor the least amount needed", and the HOST PAGE is an ancestor. MEASURED by rega's embed
|
||||
// ancestor the least amount needed", and the HOST PAGE is an ancestor. MEASURED in the embed
|
||||
// sweep: a partner page carrying this map below the fold jumps to y≈640 the moment it mounts, on
|
||||
// a phone, where Simple Research is the default tab. The reader asked for nothing; the page moved
|
||||
// under them.
|
||||
|
|
@ -788,10 +788,10 @@ const mapData = useMapData({
|
|||
// then force it visible (country search stays on the current tab — d7e741b — so
|
||||
// the polygon layers, normally hidden off the Regions tab, must be shown here).
|
||||
// ★ EVERY region paint write goes through this, or the pre-warm above is undone the first time a
|
||||
// highlight is cleared. Found by cross-reading exp-shepherd's country-posters port (73438b43b), which
|
||||
// highlight is cleared. Found by cross-reading the country-posters port (73438b43b), which
|
||||
// wraps all eighteen of its clears while mine still wrote nine RAW constants — a raw value makes the
|
||||
// property constant again, so the next highlight recompiles the shader variant the pre-warm exists to
|
||||
// have already compiled. rega measured 0 ms on a fresh tab; a reader who clears a selection first was
|
||||
// have already compiled. Measurement showed 0 ms on a fresh tab; a reader who clears a selection first was
|
||||
// paying it again, which no run so far has exercised.
|
||||
// regionIdle now comes from the library (useLegendMapBridge), which already called it — this file
|
||||
// was defining a copy and then passing it in. See check:region-paint-single-source.
|
||||
|
|
@ -932,7 +932,7 @@ const legend = useLegendData({
|
|||
// ★ REMOVED: provide('legendRows', legend.rows). useLegendData returns `items`, never `rows`, so
|
||||
// this provided undefined from the day it was written, and nothing in the repo injects 'legendRows'
|
||||
// — LegendDesktop and LegendMobile read the `legend` key instead, which is what the comment above
|
||||
// used to claim was happening. exp-shepherd found the same line in countries-map, where it was not
|
||||
// used to claim was happening. the same line was found in countries-map, where it was not
|
||||
// merely dead: it made Export poster throw (NE-17, their d6916e05e). A provide that is undefined and
|
||||
// a comment that describes a contract nobody implements are worse together than either alone, so
|
||||
// both go. If a consumer ever needs these rows, it should provide legend.items and say so then.
|
||||
|
|
@ -991,7 +991,6 @@ const {
|
|||
// Default the SemanticTreeLegend internal tab per outer-tab (legend type):
|
||||
// • language-families → 1 (Language) — users know "Language" better than "Language Family"
|
||||
// • affinity-blocks → 0 (Bloc) — users know affinity blocks better than clusters
|
||||
// (qa: 2026-05-08 user feedback)
|
||||
// Reset on every outer-tab change so switching between Languages and Affinity
|
||||
// Blocks lands on the right inner tab regardless of where the previous tab left it.
|
||||
pplrInstance.activeTab.value = 1
|
||||
|
|
@ -1101,11 +1100,11 @@ const legendTitle = computed(() => {
|
|||
// @select to the same handler as the desktop instance — without this,
|
||||
// mobile row-clicks updated only instance.selection (shared via inject)
|
||||
// and never reached mapStore.selectFamily → applyDimFilter, so the
|
||||
// dim-on-select / restore-on-deselect path never ran (qa: 2026-05-03 iter-18).
|
||||
// dim-on-select / restore-on-deselect path never ran.
|
||||
provide('onSemanticTreeSelect', (node) => onSemanticTreeSelect(node))
|
||||
// Resolves the active tab's strategy → color for a given feature, used by
|
||||
// useSelectedPin.js so the selection-pin matches the underlying dot color
|
||||
// (qa: 2026-05-06 user feedback). research-mfe tab strategyKeys already
|
||||
// research-mfe tab strategyKeys already
|
||||
// match COLOR_MODES values directly, so no mapping needed.
|
||||
provide('getActivePinColor', (properties) => {
|
||||
const strat = getColorStrategy(activeTab.value?.colorStrategy ?? 'languageFamily')
|
||||
|
|
@ -1117,14 +1116,14 @@ provide('getActivePinColor', (properties) => {
|
|||
// already (fam:/lang:/dial: + region:/block:/country: + bloc:/cluster:/pg:/pgic:),
|
||||
// so this returns null for them and zoomToLegendRow falls back to node.filter.
|
||||
// ─── ONE WRITER FOR THE REGION POLYGONS ──────────────────────────────────────
|
||||
// ★ MEASURED (rega, main 373b7979b): a single WAGF row click produced NINE regions-* paint writes —
|
||||
// ★ MEASURED (main 373b7979b): a single WAGF row click produced NINE regions-* paint writes —
|
||||
// fill-opacity, line-color and line-width, three times each inside ~110 ms — because two paths painted
|
||||
// the same highlight from DIFFERENT sources: onSemanticTreeSelect from node.isoCodes, applyDimFilter
|
||||
// from mapLayers.regionIsoMap, each triggering the other through mapStore. Two sources for one visual
|
||||
// is not just wasted work; it is how they drift, and it already cost a debugging cycle when we had to
|
||||
// establish WHICH of them had cleared the highlight.
|
||||
//
|
||||
// ★ AND THE IDLE STATE IS DATA-DRIVEN ON PURPOSE. rega measured the row click compiling a new shader
|
||||
// ★ AND THE IDLE STATE IS DATA-DRIVEN ON PURPOSE. Measurement showed the row click compiling a new shader
|
||||
// variant: 5.5 s on MapLibre, 11.1 s on Mapbox (SwiftShader), because that click was the first moment
|
||||
// these three properties stopped being constants. Same defect as the pins, same fix: the idle value is
|
||||
// ['case', ['has', 'iso_3166_1_alpha_3'], x, x] — both branches identical, so not one pixel changes —
|
||||
|
|
@ -1143,7 +1142,7 @@ function prewarmRegionPaint(m) {
|
|||
// Was a THIRD local copy of the idle expression, under a different name — byte-identical body.
|
||||
// The library's regionIdle replaces it exactly.
|
||||
const wrap = regionIdle
|
||||
// ★ SKIP WHAT IS ALREADY DATA-DRIVEN, and note the test is not "is it an array". exp-shepherd wrote
|
||||
// ★ SKIP WHAT IS ALREADY DATA-DRIVEN, and note the test is not "is it an array". The country-posters port wrote
|
||||
// their copy to skip any array, reasoning that an expression is already data-driven so there is no
|
||||
// variant to warm. That is right for this layer — line-color arrives as a match on a feature property
|
||||
// — but it is right by accident: a ZOOM expression like ['interpolate',['linear'],['zoom'],…] is also
|
||||
|
|
@ -1163,10 +1162,10 @@ function prewarmRegionPaint(m) {
|
|||
// exactly that reverse. Mapbox then classifies the property as composite and reaches for an
|
||||
// interpolation factor the expression cannot supply — the class of
|
||||
// this.expression.interpolationFactor is not a function
|
||||
// rega saw once on Mapbox at L54, on the clear → other-row path. A missed pre-warm costs a frame; an
|
||||
// a run saw it once on Mapbox at L54, on the clear → other-row path. A missed pre-warm costs a frame; an
|
||||
// illegal paint costs the layer, so when the two rules disagree this one wins.
|
||||
//
|
||||
// exp-shepherd's port skips ANY array and is immune to this by accident; the difference between us
|
||||
// the country-posters port skips ANY array and is immune to this by accident; the difference between us
|
||||
// was never about zoom expressions being data-driven, it was about which mistake is recoverable.
|
||||
const readsZoom = (e) => Array.isArray(e) && (e[0] === 'zoom' || e.some(readsZoom))
|
||||
const warm = (layer, prop) => {
|
||||
|
|
@ -1185,9 +1184,9 @@ function prewarmRegionPaint(m) {
|
|||
|
||||
function paintRegionScope(m, isoCodes) {
|
||||
if (!m || !m.getLayer('regions-fill')) return
|
||||
// ★ CANONICAL FIRST, OR THE COMPARE BELOW IS A LIE (rega, L43: still 9 writes per click, not 3).
|
||||
// ★ CANONICAL FIRST, OR THE COMPARE BELOW IS A LIE (measured at L43: still 9 writes per click, not 3).
|
||||
// The two sources build the same region from different lists — node.isoCodes and
|
||||
// mapLayers.regionIsoMap[slug] — and they do not agree on ORDER. rega had already told me that in
|
||||
// mapLayers.regionIsoMap[slug] — and they do not agree on ORDER. The measurement had already shown that in
|
||||
// the very first stack trace ("a case over ['IND',…]" then "a case over ['MYS',…], a different
|
||||
// literal order") and I wrote a JSON-string compare anyway, which can never match two orderings of
|
||||
// the same set. Sorting and de-duplicating makes the expression a function of the SET, which is what
|
||||
|
|
@ -1656,7 +1655,7 @@ async function openPacket(variant = 'full') {
|
|||
// professional translation of legend.progress.short renders exactly this relationship as
|
||||
// "{covered} / {total}". The word IS available per locale there (ar "من", ru "из", fr "sur"), but that
|
||||
// key carries "· {pct}%" with it, so reusing it would mean either a wrong suffix or slicing a
|
||||
// translated string on a separator I cannot guarantee survives translation.
|
||||
// translated string on a separator that may not survive translation.
|
||||
// Wrapped in U+2066/U+2069 isolates, the convention the ar catalogue already uses for numeric
|
||||
// ranges (five times, e.g. "30–59%"): without them the pair reorders against Arabic text
|
||||
// beside it and reads as "214 / 50". NUMERALS ARE HELD, so the digits stay as they are.
|
||||
|
|
@ -1703,8 +1702,8 @@ const isDark = computed(() => uiStore.isDarkTheme)
|
|||
const isDarkBasemap = computed(() =>
|
||||
THEME_TOGGLE ? uiStore.isDarkTheme : FIXED_BASEMAP.lightPreset === 'night')
|
||||
|
||||
// Theme-aware data-pin outline (qa: 2026-05-06 user feedback — Joshua Project
|
||||
// uses a thin near-black outline that's barely visible; reverse-engineered).
|
||||
// Theme-aware data-pin outline. Joshua Project
|
||||
// uses a thin near-black outline that's barely visible; reverse-engineered.
|
||||
// In dark mode the outline goes white so pins stay visible against dark
|
||||
// basemaps. Boot style is light-v11, so the initial circle-stroke-color
|
||||
// in addLanguageFamilyLayer is already 'rgba(0,0,0,0.45)' — this watcher
|
||||
|
|
@ -1755,7 +1754,7 @@ async function ensureRegionsLoaded() {
|
|||
if (regionsData && map.value) {
|
||||
mapLayers.addRegionsLayer?.(regionsData, 'doxa-regions')
|
||||
// ★ PRE-WARM HERE, which is the only moment that helps. These layers are added on first visit
|
||||
// to the Regions tab, and rega measured the first ROW click paying 5.5 s (MapLibre) / 11.1 s
|
||||
// to the Regions tab, and measurement showed the first ROW click paying 5.5 s (MapLibre) / 11.1 s
|
||||
// (Mapbox) of shader compile because that click was when their paint first became data-driven.
|
||||
// Making the paint data-driven at layer-add moves that compile here, where a 6.4 MB GeoJSON is
|
||||
// already loading, instead of into the middle of a gesture.
|
||||
|
|
@ -1904,7 +1903,7 @@ function pinBaseAlpha() {
|
|||
// matchExpr: an expression to dim by, null to clear dimming, undefined to re-apply what is current
|
||||
// (which is what the slider's watcher needs — it changes the scale, not the selection).
|
||||
// ★ THE UNDIMMED VALUE IS DATA-DRIVEN TOO, AND THAT IS THE WHOLE POINT (reported bug: "the research map
|
||||
// isn't connecting with the legend"; rega's CPU profile, 2026-09-28). A legend click on a big affinity
|
||||
// isn't connecting with the legend"; a CPU profile). A legend click on a big affinity
|
||||
// row appeared to do nothing for ~2 s. It was not the echo guard, not the legend's DOM (34 rows before,
|
||||
// 34 after) and not expression evaluation (the JS filter scan over 2,106 features against a 700-id list
|
||||
// measures 14 ms). It was ONE task of 3.2-3.5 s whose self time was getShaderParameter + getProgram-
|
||||
|
|
@ -1918,7 +1917,7 @@ function pinBaseAlpha() {
|
|||
// and the program compiles during boot alongside every other one. A selection then only re-uploads
|
||||
// attribute values, which is the cheap path.
|
||||
//
|
||||
// The trade, stated: boot compiles one more program (tens of ms on a real GPU; rega measured seconds
|
||||
// The trade, stated: boot compiles one more program (tens of ms on a real GPU; measurement showed seconds
|
||||
// only because that box rasterises on the CPU via SwiftShader). Paying it at load beats freezing the
|
||||
// map mid-gesture, and the variant was going to be compiled on first interaction anyway.
|
||||
function applyPinOpacity(m, matchExpr) {
|
||||
|
|
@ -1928,7 +1927,7 @@ function applyPinOpacity(m, matchExpr) {
|
|||
// ['has','uniqueId'] reads a feature property, which is what makes it data-driven; both outcomes are
|
||||
// `a`, so a pin without the property renders exactly as before.
|
||||
const value = _pinMatchExpr ? ['case', _pinMatchExpr, a, 0] : ['case', ['has', 'uniqueId'], a, a]
|
||||
// ★ ONE WRITE PER DISTINCT VALUE. rega counted circle-opacity rewritten 4-8 times for a single click
|
||||
// ★ ONE WRITE PER DISTINCT VALUE. Measurement counted circle-opacity rewritten 4-8 times for a single click
|
||||
// (+1.52, +1.57, +1.94, +5.17 s): the selection handler, the glow sync and the slider watcher all end
|
||||
// up here, and each write re-uploads the per-feature attribute. Every writer already routes through
|
||||
// this function, so the collapse belongs here and nowhere else. Compared as text because the value is
|
||||
|
|
@ -2325,7 +2324,7 @@ watch(() => profileConfig?.value?.externalFilters, (raw) => {
|
|||
try {
|
||||
// ★ AN EMPTY VALUE THE HOST DID NOT SET MUST NOT CLEAR WHAT THIS PAYLOAD JUST LIT. This is the
|
||||
// The report "the research map isn't connecting with the legend" on the WAGF Regions tab, found by
|
||||
// rega's stack trace (l40): the highlight painted at +51 ms and was wiped at +438 ms by
|
||||
// a stack trace (l40): the highlight painted at +51 ms and was wiped at +438 ms by
|
||||
// clearAllHighlights, reached from applyDimFilter({kind:null}) through the selectedRegion watcher —
|
||||
// so selectedRegion had gone FALSY while externalRegion still read "asia".
|
||||
//
|
||||
|
|
@ -2426,7 +2425,7 @@ watch(() => profileConfig?.value?.externalFilters, (raw) => {
|
|||
// ★ EACH HOOK IS ALSO HELD LOCALLY, so the unmount path can release ONLY WHAT IS STILL OURS.
|
||||
// Two of these maps can share a page (research-map-plus-grid composes one beside a grid, and the
|
||||
// gallery can show another): B mounts and overwrites A's hooks, then A unmounts. An unconditional
|
||||
// delete would strip the hooks belonging to the map still on screen, and rega would report a verify
|
||||
// delete would strip the hooks belonging to the map still on screen, and it would report a verify
|
||||
// hook missing on a map that is perfectly healthy — a false failure costing more to chase than this
|
||||
// costs to write. Declared out here rather than inside the block below because a const inside it is
|
||||
// invisible to onBeforeUnmount. Same pattern gospel used in d3597f3d1.
|
||||
|
|
@ -2534,7 +2533,7 @@ function emitFilterState() {
|
|||
//
|
||||
// The NAME still goes out because this map's own legend selects by name; the CODE goes out for
|
||||
// anyone matching rows. Both halves get the form they can actually use.
|
||||
// ★ THE LOOKUP HAS TO MATCH THE WAY THE LEGEND BUILT THE LABEL (rega, 2026-09-26, main 49992a5d:
|
||||
// ★ THE LOOKUP HAS TO MATCH THE WAY THE LEGEND BUILT THE LABEL (measured on main 49992a5d:
|
||||
// MapLibre, BOTH locales, Languages > Language > "Arabic" -> languageCode:"" -> grid 2,107 -> 0).
|
||||
//
|
||||
// WHAT I GOT WRONG. This found a row whose language LABEL equals the selected name with `===`.
|
||||
|
|
@ -2543,7 +2542,7 @@ function emitFilterState() {
|
|||
// splitting the API label at the comma. MEASURED against the live source (2,107 rows, all 2,107
|
||||
// carrying primary_language as {value,label}): there is NO row whose label is "Arabic". What
|
||||
// exists is "Arabic, Sudanese", "Arabic, Levantine", "Arabic, Gulf", ... So the exact match could
|
||||
// never hit, in any locale — which is why rega saw it fail identically in en and ar, and why the
|
||||
// never hit, in any locale — which is why it failed identically in en and ar, and why the
|
||||
// locale fix that prompted this code was right about the DISEASE and wrong about this line.
|
||||
//
|
||||
// This is the derived-value trap, one tier down from where I last wrote it up: a value that
|
||||
|
|
@ -2586,7 +2585,7 @@ function emitFilterState() {
|
|||
const detail = {
|
||||
region: mapStore?.selectedRegion || '',
|
||||
religion: religionName,
|
||||
// ★ A DIALECT'S NAME COMES FROM ITS LABELS, NOT ITS KEY (rega, row 43 case 3: this reported
|
||||
// ★ A DIALECT'S NAME COMES FROM ITS LABELS, NOT ITS KEY (a measured case: this reported
|
||||
// ", Afro-Asiatic__Arabic__Sudanese"). `selectedDialect.key` is the TREE NODE ID —
|
||||
// family__baseLang__dialect — and the watcher fifteen lines above says so in as many words;
|
||||
// I read that comment and then used the key as a display name anyway. Worse, selectedLanguage
|
||||
|
|
@ -2605,7 +2604,7 @@ function emitFilterState() {
|
|||
languageCodes: langCodes,
|
||||
}
|
||||
// ★ AN EMPTY REPORT AFTER A NON-EMPTY ONE IS AMBIGUOUS, SO IT IS NOT SENT IMMEDIATELY.
|
||||
// rega, row 43 case 3 at 52eeb0db: coalescing worked — one event per click — and in 3 of 4 runs
|
||||
// measured at 52eeb0db: coalescing worked — one event per click — and in 3 of 4 runs
|
||||
// that one event was EMPTY, so the grid stayed at 2,107. The cascade behind a dialect click
|
||||
// passes through a fully-cleared state: mapStore.selectLanguage() sets selectedDialect = null
|
||||
// BEFORE it sets the language, and the clear was the last write my watcher saw. The settle timer
|
||||
|
|
@ -2642,7 +2641,7 @@ function emitFilterState() {
|
|||
host.dispatchEvent(new CustomEvent('map-filtered', { detail, bubbles: true, composed: true }))
|
||||
}
|
||||
|
||||
// ★ ONE EVENT PER SETTLED SELECTION (rega, row 43 case 3: a dialect click narrowed the grid in
|
||||
// ★ ONE EVENT PER SETTLED SELECTION (a measured case: a dialect click narrowed the grid in
|
||||
// only ONE of four runs, with a correct languageCode in all four).
|
||||
//
|
||||
// THE CAUSE IS A BURST MEETING A GUARD. Clicking a dialect does not change one store value; it
|
||||
|
|
@ -2665,7 +2664,7 @@ const EMIT_SETTLE_MS = 60
|
|||
// of delay on a clear — about a fifth of a second, well under what a reader notices, and far below
|
||||
// the composite's own 1.2s push window so it cannot collide with that.
|
||||
// ★ A CEILING ON BELIEVING A CLEAR — NOT A GUESS AT HOW LONG THE CASCADE TAKES.
|
||||
// rega, row 43 case 3 at 835ec1ac: the retry budget catches the transient on MapLibre and NOT
|
||||
// measured at 835ec1ac: the retry budget catches the transient on MapLibre and NOT
|
||||
// reliably on Mapbox (and not at all in ar/mapbox), because 3 x 60ms is still a duration I picked
|
||||
// without being able to measure the thing it has to outlast. That is the THIRD time I have tuned a
|
||||
// number on one machine for this one defect, so the number's JOB changes rather than its value.
|
||||
|
|
@ -2682,7 +2681,7 @@ let _emptyWaitStartedAt = 0
|
|||
let _lastEmitWasFilled = false
|
||||
function emitFilterStateSettled() {
|
||||
// ★ THE ECHO GUARD MUST BE READ WHEN THE CHANGE HAPPENS, NOT WHEN THE TIMER FIRES.
|
||||
// rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event, on both
|
||||
// measured: a region row click emitted {region:"asia"} and then an EMPTY event, on both
|
||||
// engines. It passed only because the composite's 1.2s push window happened to swallow the
|
||||
// second one — a pass that depends on a swallow is not a pass.
|
||||
//
|
||||
|
|
@ -2809,7 +2808,7 @@ function applyFitSlugs(slugs) {
|
|||
: null
|
||||
|
||||
// ★ REDUCED MOTION DRIVES THE MAP DIRECTLY, and that is not belt-and-braces.
|
||||
// MEASURED by rega with the browser's prefers-reduced-motion: reduce set: the framing move still
|
||||
// MEASURED with the browser's prefers-reduced-motion: reduce set: the framing move still
|
||||
// animated for ~5s on MapLibre and ~6.7s on Mapbox — essentially the same as without it, where a
|
||||
// jump is ~0ms. So the preference was being honoured in THIS file and thrown away downstream:
|
||||
// useMapFly's flyToCoords(coords, zoom) takes NO options argument at all, so the `{ duration: 0 }`
|
||||
|
|
@ -2840,7 +2839,7 @@ function applyFitSlugs(slugs) {
|
|||
//
|
||||
// The checkbox force-set 'mst' "regardless of what's in the store", and the picker wrote to a
|
||||
// property mapStore never declared — so choosing Native drew nothing, and had it worked, one tick
|
||||
// of the checkbox would have snapped it back to MST anyway. MEASURED as inert by exp-shepherd; the
|
||||
// of the checkbox would have snapped it back to MST anyway. MEASURED as inert; the
|
||||
// second half is what makes fixing only the picker the wrong fix, because the control would have
|
||||
// worked until the reader touched the other one.
|
||||
//
|
||||
|
|
@ -2916,7 +2915,7 @@ async function onMapReady(normalizedPeopleGroups) {
|
|||
// added with a CONSTANT "circle-opacity": 1 (see addLanguageFamilyLayer), and applyPinOpacity is
|
||||
// otherwise first reached from a legend click — so without this line the expression shape below is
|
||||
// only ever the CLEARED state and the first selection is still the moment the variant is compiled,
|
||||
// which is the 3.2-3.5s freeze rega profiled. Found by reading the built bundle rather than the
|
||||
// which is the 3.2-3.5s freeze seen in profiling. Found by reading the built bundle rather than the
|
||||
// source: the addLayer constant is right there in the shipped JS.
|
||||
// matchExpr is left UNDEFINED on purpose — "re-apply what is current" — so a selection already
|
||||
// restored from a share-link hash is preserved instead of being cleared at boot.
|
||||
|
|
@ -2986,8 +2985,8 @@ function applyContainerScale(width) {
|
|||
el.style.setProperty('--map-font-md', '13px')
|
||||
el.style.setProperty('--map-font-sm', '11px')
|
||||
// One width for the legend, the pin-detail card and the geocoder pill
|
||||
// (qa: 2026-05-02 — a 400px detail card over a 380px legend read as two
|
||||
// cards). SemanticTreeLegend's .stl-panel follows this variable too.
|
||||
// A 400px detail card over a 380px legend read as two
|
||||
// cards. SemanticTreeLegend's .stl-panel follows this variable too.
|
||||
// 400px (was 380) since the desktop type scale widened the numeric
|
||||
// columns; keeps "Full Prayer Coverage" on one line (2026-09-16).
|
||||
el.style.setProperty('--map-legend-width', '380px')
|
||||
|
|
@ -3022,7 +3021,7 @@ const reannounceInfoPending = () => { if (!appReady.value && !mapError.value) an
|
|||
watch(mapError, (e) => { if (e) announceInfoPending(false) })
|
||||
// ★ THE PRAYER LINE'S TABLE IS FETCHED AFTER THE MAP MOUNTS, when the browser is idle, not when a print starts: the
|
||||
// print waits at most 3 s for it (openPoster), and on a slow phone network a fetch begun at the tap missed that window
|
||||
// (rega: the line dropped in 1 of 2 runs). loadPacketStrings caches the promise, so the print reads it at once; it
|
||||
// (the line dropped in 1 of 2 runs). loadPacketStrings caches the promise, so the print reads it at once; it
|
||||
// never rejects, and a failed fetch is retried by the print. 48 KB gzipped, once per page.
|
||||
onMounted(() => {
|
||||
const prefetch = () => import('@map/packet/packetStrings.js').then(({ loadPacketStrings }) => loadPacketStrings(resolveAssetsBase(profileConfig?.value))).catch(() => {})
|
||||
|
|
@ -3045,7 +3044,7 @@ onMounted(async () => {
|
|||
// any code branching on isMobile (LegendDesktop's internal LegendMobile
|
||||
// mount, mobile-only feature gates, etc.) runs the desktop path.
|
||||
// The CSS @media path on .rm-legend-mobile-slot still works either way,
|
||||
// but isMobile-driven branches don't. (qa: 2026-05-02 mobile audit.)
|
||||
// but isMobile-driven branches don't.
|
||||
uiStore.init?.()
|
||||
activeTabId.value = tabs.value[0]?.id ?? 'doxa-regions'
|
||||
if (uiStore.isMobile) uiStore.collapseLegend?.()
|
||||
|
|
@ -3135,7 +3134,7 @@ onMounted(async () => {
|
|||
window.addEventListener('resize', _resyncScrollZoom)
|
||||
})
|
||||
|
||||
// ★ THE MAP IS TORN DOWN NO MATTER WHAT ELSE FAILS. rega's heap snapshots (leak-722ac7789) found
|
||||
// ★ THE MAP IS TORN DOWN NO MATTER WHAT ELSE FAILS. heap snapshots (leak-722ac7789) found
|
||||
// every unmounted research map still alive, held by window.__doxaLogoOwner -> Map -> div.rm-map-canvas
|
||||
// -> ShadowRoot -> app, and that global is released only when the map's 'remove' event fires. So a
|
||||
// single throw anywhere in this hook stranded the entire application in memory, one copy per mount.
|
||||
|
|
@ -3158,7 +3157,7 @@ onBeforeUnmount(() => {
|
|||
step(() => selectedPin.cleanup?.())
|
||||
step(() => mapStore.unregisterMap?.(mapId))
|
||||
// Every verify hook this component plants is an arrow closing over it, so each one left behind
|
||||
// keeps the component, its shadow root and the whole app alive (rega, leak-722ac7789). They are
|
||||
// keeps the component, its shadow root and the whole app alive (leak-722ac7789). They are
|
||||
// released as a set: they are installed as a set, and releasing three of four leaks just as much
|
||||
// as releasing none. Each is checked before deleting so a later mount's hook is never removed.
|
||||
// Written out one per line, not looped over an array of names, SO THE CHECK CAN READ IT.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* different. A control that does nothing is worse than a control that is absent: it tells the
|
||||
* reader the map cannot do what it can.
|
||||
*
|
||||
* THE DEFECT, MEASURED BY exp-shepherd AND CONFIRMED HERE. The side menu offers four clustering
|
||||
* THE DEFECT, MEASURED AND CONFIRMED HERE. The side menu offers four clustering
|
||||
* options; choosing "Native" changed nothing on the map. Two separate reasons, and only fixing both
|
||||
* makes the control real:
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* check-language-code-link.mjs — the map's language selection must reach the grid, or narrow nothing.
|
||||
*
|
||||
* THE BUG (rega, 2026-09-26, main 49992a5d, MapLibre, BOTH locales). Languages > Language >
|
||||
* THE BUG (measured on main 49992a5d, MapLibre, BOTH locales). Languages > Language >
|
||||
* "Arabic": the map emitted `map-filtered` with {language:"Arabic", languageCode:""}, the composite
|
||||
* handed the grid an empty code, and the grid went 2,107 -> 0. An empty grid reads as "no results",
|
||||
* which is the worst shape a wiring bug can take — the reader believes the data.
|
||||
|
|
@ -48,11 +48,11 @@
|
|||
* E. no tier may produce a value that selects zero rows. That is the user-visible invariant and
|
||||
* it is asserted directly, not inferred.
|
||||
* F. a DIALECT reports a NAME and not a tree node id, and the emitter sends ONE event per
|
||||
* settled selection rather than a burst. Both from rega's row 43 case 3.
|
||||
* settled selection rather than a burst. Both from a measured case.
|
||||
* G. that ONE event is not the cascade's transient EMPTY state. Same case, second measurement:
|
||||
* the coalescing worked and the single event was EMPTY in 3 of 4 runs.
|
||||
*
|
||||
* NO NETWORK, NO BROWSER (Playwright is rega's alone). The fixture is committed and was taken from
|
||||
* NO NETWORK, NO BROWSER (Playwright is the browser-QA lane's alone). The fixture is committed and was taken from
|
||||
* the live source; refresh it with --refresh-fixture when the source's language set changes.
|
||||
*
|
||||
* NEGATIVE-TESTED 2026-09-26, each FAILS:
|
||||
|
|
@ -221,7 +221,7 @@ if (matchesLanguage) {
|
|||
}
|
||||
|
||||
// ── E. the dialect tier: a name, and one event ───────────────────────────────────────────────
|
||||
// rega, row 43 case 3, on BOTH engines in BOTH locales: clicking "Arabic, Sudanese" emitted the
|
||||
// Measured on BOTH engines in BOTH locales: clicking "Arabic, Sudanese" emitted the
|
||||
// right languageCode and narrowed the grid in only ONE of four runs, while the `language` slot read
|
||||
// ", Afro-Asiatic__Arabic__Sudanese".
|
||||
// THE NAME: selectedDialect.key is the TREE NODE ID (family__baseLang__dialect) and
|
||||
|
|
@ -258,7 +258,7 @@ if (matchesLanguage) {
|
|||
}
|
||||
|
||||
// ── G. an empty report after a filled one is re-taken, and the re-take is BOUNDED ────────────
|
||||
// rega, row 43 case 3 at 52eeb0db: coalescing landed — one event per click — and in 3 of 4 runs
|
||||
// measured at 52eeb0db: coalescing landed — one event per click — and in 3 of 4 runs
|
||||
// that one event was EMPTY, so the grid stayed at 2,107. The cascade behind a dialect click passes
|
||||
// through a fully-cleared store state (mapStore.selectLanguage sets selectedDialect = null BEFORE
|
||||
// setting the language), and the settle timer fired inside it.
|
||||
|
|
@ -272,7 +272,7 @@ if (matchesLanguage) {
|
|||
if (!/_lastEmitWasFilled/.test(src) || !/const isEmpty/.test(src)) {
|
||||
fails.push('G research-map.vue no longer guards an all-empty report. A dialect click passes through a fully-cleared store state, so the settled read can be taken mid-cascade and ship {language:"",languageCode:""} — which the composite correctly reads as "the reader cleared everything".')
|
||||
} else if (!/EMPTY_SETTLE_CEILING_MS/.test(src)) {
|
||||
// ★ THE BOUND'S SHAPE CHANGED AND THE INVARIANT DID NOT. It used to be a retry COUNT; rega
|
||||
// ★ THE BOUND'S SHAPE CHANGED AND THE INVARIANT DID NOT. It used to be a retry COUNT; it was
|
||||
// measured that 3 x 60ms catches the cascade on MapLibre and not on Mapbox, so the count became
|
||||
// a TIME CEILING — the difference being what the number is FOR. A count is a guess at how long
|
||||
// the cascade takes, which cannot be known from here; a ceiling is how long before a clear is
|
||||
|
|
@ -285,7 +285,7 @@ if (matchesLanguage) {
|
|||
}
|
||||
|
||||
// ── H. the echo guard survives the coalescing ────────────────────────────────────────────────
|
||||
// rega, row 50: a region click emitted the selection and then an EMPTY event, passing only because
|
||||
// Measured: a region click emitted the selection and then an EMPTY event, passing only because
|
||||
// the composite's 1.2s push window swallowed the second. emitFilterState has always opened with
|
||||
// `if (applyingExternal) return` — a change pushed DOWN by the host is not a change to report back
|
||||
// up — and that worked while the emit ran synchronously inside the watcher. Debouncing moved the
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* ★ A READER OF THIS MAP IS PHOTOSENSITIVE. This is not a polish item: a camera that sweeps the globe every
|
||||
* time a list is filtered is the strobe class this repo has already paid for once.
|
||||
*
|
||||
* THE DEFECT, measured by rega in a real browser with the preference set: the framing move still
|
||||
* THE DEFECT, measured in a real browser with the preference set: the framing move still
|
||||
* animated for ~5.0s on MapLibre and ~6.7s on Mapbox — indistinguishable from no preference at all,
|
||||
* where a jump is ~0ms. The preference WAS being read correctly in research-map.vue and then thrown
|
||||
* away one layer down: useMapFly's `flyToCoords(coords, zoom)` takes NO options argument, so the
|
||||
|
|
@ -13,13 +13,13 @@
|
|||
* options)` does accept one — two functions in one file, different signatures, and nothing at the
|
||||
* call site to show it.
|
||||
*
|
||||
* ★ WHY THIS RUNS WITHOUT A BROWSER, WHICH IS THE POINT. rega found it by watching movestart →
|
||||
* moveend for six seconds; only rega can do that, and only occasionally. What can be checked any
|
||||
* ★ WHY THIS RUNS WITHOUT A BROWSER, WHICH IS THE POINT. it was found by watching movestart →
|
||||
* moveend for six seconds; only a real browser can do that, and only occasionally. What can be checked any
|
||||
* time, in milliseconds, is the DECISION: given the preference, which camera call does the code
|
||||
* make? So this lifts the real camera block out of the SFC, runs it against a mocked matchMedia and
|
||||
* a recording map, and asserts the call that comes out.
|
||||
*
|
||||
* It cannot prove the engine honours duration 0 — that is rega's half. It proves we ASK for a jump,
|
||||
* It cannot prove the engine honours duration 0 — that is the browser half. It proves we ASK for a jump,
|
||||
* which is the half that was wrong.
|
||||
*
|
||||
* ASSERTIONS:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* cities-map.vue — THE KEY CITIES GLOBAL MAP.
|
||||
*
|
||||
* Every located population centre as a POINT, sized by how many people groups it
|
||||
* holds. Brief: "all these are missing maps."
|
||||
* holds. One of the data maps this project adds.
|
||||
*
|
||||
* POINTS, NOT COUNTRY SHADING — this is the one dataset with real coordinates
|
||||
* (1,646 clusters, 277 of them gazetteer-named), so shading a country would throw
|
||||
|
|
@ -86,7 +86,7 @@ async function load() {
|
|||
const res = await fetch(`${assetsBase.value}/tables/key-cities.json`)
|
||||
if (!res.ok) throw new Error(`key-cities table not found (${res.status})`)
|
||||
rows.value = (await res.json()).rows || []
|
||||
// CITY-STAR LAYER (Brief: "better city display"). Fetched SEPARATELY — never a
|
||||
// CITY-STAR LAYER (a richer city display). Fetched SEPARATELY — never a
|
||||
// static import, which would inline the table into the IIFE bundle (that trap
|
||||
// once took two bundles to 2.0/2.9MB). Failure here must NOT break the base map,
|
||||
// so it is caught: the city layer is an ENHANCEMENT, not a dependency.
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@
|
|||
/**
|
||||
* data-tables.vue — the CITIES DATA list view: every research table, one tab each.
|
||||
*
|
||||
* Four validated Tabulator tables from the flight's research corpora, each with
|
||||
* Four validated Tabulator tables from the research corpora, each with
|
||||
* sortable/filterable columns, CSV+JSON export, and its caveats rendered IN the UI.
|
||||
*
|
||||
* > design note 2026-08-01: "all the tabulator data and trees for all the multiple
|
||||
* > various tabulator tables for all this data all organized in list format with
|
||||
* > full granularity."
|
||||
* Design intent: present all the tabulator data and trees for the various tables
|
||||
* in list format, at full granularity.
|
||||
*
|
||||
* The tables are produced by library/scripts/research/build-tabulator-tables.mjs
|
||||
* (the M+V of IEMV) — flat scalar rows, one row per fact, validated before write.
|
||||
* — flat scalar rows, one row per fact, validated before write.
|
||||
*
|
||||
* WHY THE CAVEATS RENDER: a list view strips context. Someone reading
|
||||
* "Internet 12.6%" needs to know it is a NATIONAL figure, and someone reading a
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ const filtersBackdrop = computed(() => {
|
|||
})
|
||||
const prayBaseUrl = computed(() => props.prayBaseUrl || cfg.value.prayBaseUrl || 'https://pray.doxa.life')
|
||||
// ★ THE URL IS A SOURCE OF TRUTH FOR THE DATA TOO, not just for the chrome.
|
||||
// MEASURED by rega at ?lang=ar and ?lang=es: this grid's CHROME translated and its DATA stayed
|
||||
// MEASURED at ?lang=ar and ?lang=es: this grid's CHROME translated and its DATA stayed
|
||||
// English — country names, affinity names, religion labels — because the `&lang=` on the fetch was
|
||||
// built from props and profile-config ONLY. A reader who asked for Arabic got Arabic buttons around
|
||||
// English content, which reads as a half-finished translation rather than the wiring bug it is.
|
||||
|
|
@ -420,7 +420,7 @@ const t = computed(() => {
|
|||
const EAGER_IMAGES = 5
|
||||
const truthy = (v) => !(v === false || v === 'false' || v === '0' || v === 0)
|
||||
|
||||
// ★ THE PRESET'S KEYS WERE NEVER REACHING THESE (rega RQ20). The "driven" preset asks for
|
||||
// ★ THE PRESET'S KEYS WERE NEVER REACHING THESE (measured, RQ20). The "driven" preset asks for
|
||||
// show-search:false and show-filters:false and the page rendered both anyway, with 0 console
|
||||
// errors — because nothing was failing. ProfileLoader PROVIDES profile-config; it does not bind
|
||||
// its keys as props. So props.showSearch stayed at its default of true and the preset was inert.
|
||||
|
|
@ -1153,7 +1153,7 @@ onMounted(() => {
|
|||
<div id="results" class="grid uupgs-list" :style="gridStyle">
|
||||
<template v-for="(uupg, ui) in displayedUUPGs" :key="uupg.slug">
|
||||
<div class="card uupg__card">
|
||||
<!-- ★ THE PHOTOS WERE COMPETING WITH THE MAP FOR THE FIRST THREE SECONDS. rega measured a
|
||||
<!-- ★ THE PHOTOS WERE COMPETING WITH THE MAP FOR THE FIRST THREE SECONDS. Measurement showed a
|
||||
cold load of Simple Research pulling 24 people-group photos, 5.3 MB, DURING BOOT —
|
||||
alongside the map engine and the API data, on the same connection. Nothing was wrong
|
||||
with any one image; there was simply no instruction about WHEN to fetch them.
|
||||
|
|
|
|||
|
|
@ -713,7 +713,7 @@ useShadowStyles(
|
|||
{{ tr('finish', { n: drawPoints.length }) }}
|
||||
</button>
|
||||
<!-- PHONE: these three collapse behind "More" (they ran past x=390 in a sideways row with no
|
||||
scroll cue, rega ka3). Desktop: the toggle is hidden and they sit inline as before. -->
|
||||
scroll cue, measured ka3). Desktop: the toggle is hidden and they sit inline as before. -->
|
||||
<button type="button" class="pwp-btn pwp-more-btn" :aria-expanded="moreOpen ? 'true' : 'false'" @click="moreOpen = !moreOpen">⋯ {{ tr('more') }}</button>
|
||||
<div class="pwp-listops" :class="{ 'is-open': moreOpen }" @click="moreOpen = false">
|
||||
<button type="button" class="pwp-btn is-danger" :disabled="selectedCount === 0"
|
||||
|
|
|
|||
|
|
@ -311,8 +311,8 @@ const bundleSrc = (bundle, file) =>
|
|||
// link matching nothing the moment the grid's data followed the URL's language — and an empty
|
||||
// intersection reads as "no results", not as a broken wire.
|
||||
// `language.codes` is every ISO code the current language selection covers — one for a real
|
||||
// language, many for a legend TIER like "Arabic". It is what lets gridLanguage() tell "I can
|
||||
// express this" from "I cannot", instead of guessing from an empty code.
|
||||
// language, many for a legend TIER like "Arabic". It is what lets gridLanguage() tell a coverable selection
|
||||
// from an uncoverable one, instead of guessing from an empty code.
|
||||
const linked = reactive({ region: { value: '', label: '' }, religion: '', language: { value: '', label: '', codes: [] } })
|
||||
|
||||
// ★ WHAT THE LIST IS SHOWING, so the map can go there (BUILD-GRID-SELECTION-FLIES-THE-MAP).
|
||||
|
|
@ -421,7 +421,7 @@ function onMapFiltered(ev) {
|
|||
})
|
||||
}
|
||||
|
||||
// ★ AN AXIS THIS GRID CANNOT EXPRESS MUST NOT NARROW IT TO NOTHING (rega, 2026-09-26: Languages >
|
||||
// ★ AN AXIS THIS GRID CANNOT EXPRESS MUST NOT NARROW IT TO NOTHING (measured: Languages >
|
||||
// Language > "Arabic" took the grid 2,107 -> 0 in BOTH locales, reading as "no results").
|
||||
//
|
||||
// This line used to be `linked.language.value || linked.language.label`, and the fallback was the
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ for (const p of catalogues) {
|
|||
const files = listFiles((p) => /\.(vue|js|mjs)$/.test(p) && !p.includes('/scripts/') && !p.includes('/locales/'))
|
||||
|
||||
// ── ★ A TRANSLATOR CAN ARRIVE FROM A COMPOSABLE, NOT ONLY FROM A LOCAL WRAPPER ───────────────
|
||||
// go-int planted a poison key behind `const T = useGiT()` and this gate PASSED. Three failures of
|
||||
// a poison key was planted behind `const T = useGiT()` and this gate PASSED. Three failures of
|
||||
// the same shape stacked up: (1) the file never writes the word tStatic, so the precondition below
|
||||
// skipped it outright; (2) `useGiT` is not `tStatic`, so aliasesIn() found no alias at all; and
|
||||
// (3) the alias is PREFIXED — useOptionalT carries a namespace — while its call sites pass BARE
|
||||
|
|
@ -267,7 +267,7 @@ function aliasesIn(src) {
|
|||
}
|
||||
|
||||
// ── ★ SELF-TEST, BECAUSE THIS GATE ALREADY PASSED A PLANTED POISON KEY ────────────────────────
|
||||
// go-int planted `T('poison_key')` behind `const T = useGiT()` and this gate reported the tree
|
||||
// a poison key `T('poison_key')` was planted behind `const T = useGiT()` and this gate reported the tree
|
||||
// clean. That is the failure a check cannot be forgiven for: a red gate gets fixed, a green one that
|
||||
// should be red gets BELIEVED, and this one guards goal (d). Measured before and after, with the
|
||||
// same poison on disk: the version on main PASSES it, this version FAILS it as
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ try {
|
|||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
||||
// ★ LISTEN BEFORE THE PAGE RUNS. The grid reports its total on its FIRST filter pass, which can happen
|
||||
// before an evaluate() after goto attaches a listener; the gate then read "the grid never reported a
|
||||
// total" while the grid had reported one (rega, ded4b3c1). Every total is recorded from the start.
|
||||
// total" while the grid had reported one (measured, ded4b3c1). Every total is recorded from the start.
|
||||
await page.addInitScript(() => {
|
||||
window.__lvTotals = []
|
||||
document.addEventListener('groups-filtered', (e) => { window.__lvTotals.push(e.detail?.total ?? null) }, true)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*
|
||||
* So this reads what the compiler reads and asks the one question those gates cannot: is every name
|
||||
* this file USES actually BOUND somewhere — imported, declared, a parameter, or a known global?
|
||||
* No browser, no mount, milliseconds per file. It is the cheap half of the pair; exp-shepherd's
|
||||
* No browser, no mount, milliseconds per file. It is the cheap half of the pair; the browser lane's
|
||||
* check:composites-mount is the expensive half that proves the thing actually runs.
|
||||
*
|
||||
* SCOPE: every .vue under plugin-designs/ and library/ — fleet-wide, because the mistake was a
|
||||
|
|
|
|||
|
|
@ -534,7 +534,7 @@ function connect() {
|
|||
clearTimeout(retry)
|
||||
try { es?.close() } catch {}
|
||||
// No EventSource (a headless mount, an old WebView): show the engine-not-running state instead of throwing
|
||||
// in the mounted hook, which failed check:profiles-mount for every lane (go-int, END-DXS-ESRC).
|
||||
// in the mounted hook, which failed check:profiles-mount for every lane.
|
||||
if (typeof window === 'undefined' || typeof window.EventSource !== 'function') {
|
||||
engineError.value = 'The desktop engine is not running.'
|
||||
return
|
||||
|
|
@ -665,7 +665,7 @@ function releaseMic() {
|
|||
media = null
|
||||
actx = null
|
||||
}
|
||||
// Stop: the mic goes off and the widget is idle at once (rega: it read "Stop" for 12 s+ while a loaded
|
||||
// Stop: the mic goes off and the widget is idle at once (it read "Stop" for 12 s+ while a loaded
|
||||
// server caught up). The socket stays open so the last words still land in the box; the engine closes the
|
||||
// session with "recording false". Record again meanwhile simply lets go of that tail.
|
||||
function stopBrowser() {
|
||||
|
|
@ -790,7 +790,7 @@ function onKey(ev) {
|
|||
if (ev.ctrlKey && ev.shiftKey && ev.code === 'Space') { ev.preventDefault(); toggle() }
|
||||
}
|
||||
|
||||
// The status also by plain request: a proxy that buffers the event stream (rega's harness did) must not
|
||||
// The status also by plain request: a proxy that buffers the event stream (as a test harness did) must not
|
||||
// leave the page guessing whether this server is meant to have an engine.
|
||||
async function fetchStatus() {
|
||||
try { const r = await fetch(apiBase.value + '/status'); if (r.ok) applyStatus(await r.json()) } catch {}
|
||||
|
|
@ -1160,7 +1160,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
|
|||
.compact .notice, .compact .hint { order: 1; font-size: 12.5px; padding: 8px 10px; }
|
||||
/* ★ AND THE NOTICE YIELDS. Ordering it below the draft fixes the reading order but NOT the height:
|
||||
.notice is flex:0 1 auto and .compact .compose is flex:1 1 0, so the draft still gives up whatever
|
||||
the notice's content asks for. rega measured the result at 375x450 — a 20px draft whose wheel moves
|
||||
the notice's content asks for. Measurement showed the result at 375x450 — a 20px draft whose wheel moves
|
||||
nothing. The engine-offline notice is the long one (apiBase, Go 1.22, the setup command), ~132px at
|
||||
375 and ~179px at 390, and it is REFERENCE text: a reader needs it once, not a third of a short
|
||||
panel forever. Capping it and letting it scroll gives the draft 58px at 375x450 and 124px at
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const fc = (features) => ({ type: 'FeatureCollection', features })
|
|||
// Hover label: after a language is picked, its believers (green) and the unreached they reach (red)
|
||||
// were anonymous until clicked, and a green dot could not be clicked at all. Same label as Mobilize.
|
||||
const hover = ref(null) // { x, y, text }
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (rega saw "Ko…"
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (a run saw "Ko…"
|
||||
// at the right edge in ar); max-width keeps it inside half the map on a phone. A tip is stale as soon as
|
||||
// the map moves (a tap, then the fly to the pick), so moving clears it.
|
||||
const hoverStyle = computed(() => {
|
||||
|
|
@ -110,7 +110,7 @@ function repaint() {
|
|||
const m = map.value
|
||||
if (!m) return
|
||||
// isStyleLoaded() is also false while TILES load (MapLibre), so a data load that lands then used to
|
||||
// be dropped for good: the start view showed no dots at all (rega, upgn6). Try again once idle.
|
||||
// be dropped for good: the start view showed no dots at all (measured, upgn6). Try again once idle.
|
||||
if (!m.isStyleLoaded?.()) { if (!repaintPending) { repaintPending = true; m.once('idle', () => { repaintPending = false; repaint() }) } return }
|
||||
const b = active.value
|
||||
setSrc(m, SRC_B, bridgeFeatures())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// useNeighborT — My UPG Neighbors' UI text: library/i18n/locales/<lc>/neighbor.json (11 locales).
|
||||
// Chrome only, by decision (dx-shepherd, 2026-09-26): research terms, provenance lines and proper
|
||||
// Chrome only, by decision: research terms, provenance lines and proper
|
||||
// nouns (SEND, doxa-2000, Bounded set, JP, PPLR…) stay in English as named sources.
|
||||
// i18n is OPTIONAL: useI18n() throws on a host that never installed it, so the catalogue is the
|
||||
// fallback, never a blank or a raw key. Call it inside setup().
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const believersNear = computed(() => neighbors.value.reduce((a, n) => a + n.g.be
|
|||
const peopleReachable = computed(() => neighbors.value.reduce((a, n) => a + n.g.pop, 0))
|
||||
|
||||
// A pick raises the phone sheet first, so the frame below is computed for the sheet that will show
|
||||
// the pick's detail (rega, 390: a pick with the list lowered showed only a tooltip).
|
||||
// the pick's detail (measured at 390: a pick with the list lowered showed only a tooltip).
|
||||
function select(g) { if (g) { nbState.sheetDown = false; nbState.pgid = g.pgid; query.value = '' } }
|
||||
|
||||
// The neighbor in focus: a dot click picks its card, a card click flies to its dot. Both ways, so the
|
||||
|
|
@ -95,7 +95,7 @@ const activeId = ref(null)
|
|||
// Hover label: 7,000 red dots are anonymous until clicked, so name the one under the pointer.
|
||||
const hover = ref(null) // { x, y, text }
|
||||
function hoverAt(ev, text) { hover.value = text ? { x: ev.point.x, y: ev.point.y, text } : null }
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (rega saw "Ko…"
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (a run saw "Ko…"
|
||||
// at the right edge in ar); max-width keeps it inside half the map on a phone. A tip is stale as soon as
|
||||
// the map moves (a tap, then the fly to the pick), so moving clears it.
|
||||
const hoverStyle = computed(() => {
|
||||
|
|
@ -332,7 +332,7 @@ function repaint() {
|
|||
const m = map.value
|
||||
if (!m) return
|
||||
// isStyleLoaded() is also false while TILES load (MapLibre), so a data load that lands then used to
|
||||
// be dropped for good: the start view showed no dots at all (rega, upgn6). Try again once idle.
|
||||
// be dropped for good: the start view showed no dots at all (measured, upgn6). Try again once idle.
|
||||
if (!m.isStyleLoaded?.()) { if (!repaintPending) { repaintPending = true; m.once('idle', () => { repaintPending = false; repaint() }) } return }
|
||||
ensureLayers(m)
|
||||
}
|
||||
|
|
@ -357,7 +357,7 @@ function framePadding() {
|
|||
const rtl = !!el && getComputedStyle(el).direction === 'rtl'
|
||||
return w > 760 ? { top: 60, bottom: 60, left: rtl ? 60 : 400, right: rtl ? 400 : 60 } : { top: 40, bottom: (nbState.sheetDown ? 56 : Math.round(h * 0.55)) + 20, left: 30, right: 30 }
|
||||
}
|
||||
// PHONE: the list covered 54% of the map with no way to put it down (rega, 390). A handle on the sheet
|
||||
// PHONE: the list covered 54% of the map with no way to put it down (measured at 390). A handle on the sheet
|
||||
// lowers it to a bar and raises it again; the choice is shared with Campaigns.
|
||||
function toggleSheet() { nbState.sheetDown = !nbState.sheetDown; nextTick(() => frame()) }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* MapLibre throws "Style is not done loading" from setProjection before that, and setProjectionOn
|
||||
* swallows the throw, so the lenses that set 'mercator' right after initializeMap() never got it on
|
||||
* MapLibre (rega, C2: lang-cluster, send-map, the bloom cockpit's MapCell). Mapbox accepts it early,
|
||||
* MapLibre (measured, C2: lang-cluster, send-map, the bloom cockpit's MapCell). Mapbox accepts it early,
|
||||
* which is why this only showed on one engine.
|
||||
*/
|
||||
export function whenStyleReady(map, fn) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue