diff --git a/internal/check-grid-image-loading.mjs b/internal/check-grid-image-loading.mjs new file mode 100644 index 000000000..24158d752 --- /dev/null +++ b/internal/check-grid-image-loading.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * check-grid-image-loading — the people-group cards must not fetch 24 photos during boot. + * + * THE MEASUREMENT (rega, SR-LOAD on 21ebb4f26): a cold load of Simple Research downloaded 24 + * people-group photos, 5.3 MB, DURING BOOT — competing with the map engine and the API data on the + * same connection. Nothing was wrong with any single image; there was simply no instruction about + * WHEN to fetch them, so the browser fetched them all at once. + * + * ★ WHY A GATE AND NOT JUST THE FIX. This component is being MERGED with the regions list right now + * (a settled decision, regions-plugin building it), and a merge rewrites markup. Four attributes on + * one are exactly the kind of thing that survives a review and not a rewrite — and losing them + * is invisible: the page still works, it just costs 5.3 MB again on every cold load. + * + * ★ AND IT ASSERTS THE FIRST ROW IS EAGER, which is the half a naive "add loading=lazy everywhere" + * would get wrong. Lazy-loading what the reader is already looking at trades a visible flash of empty + * cards for bytes nobody saves. + * + * Exit 0 pass · 1 fail. + */ +import { readFileSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const TAG = 'GRID-IMAGE-LOADING' +const F = 'plugin-designs/people-groups-grid/people-groups-grid.vue' +const fails = [] +const src = readFileSync(join(ROOT, F), 'utf8') + +const m = src.match(/]*class="uupg__image"[^>]*>/s) +if (!m) { + fails.push(`${F}: the card image element was not found — it was renamed or restructured, and this ` + + 'gate would pass while checking nothing. Re-point it before trusting a green run.') +} else { + const tag = m[0] + if (!/:loading=/.test(tag)) { + fails.push('the card image has no loading attribute, so every photo is fetched during boot — ' + + 'rega measured 24 of them, 5.3 MB, competing with the map and the API data.') + } + if (!/decoding="async"/.test(tag)) fails.push('the card image does not decode off the main thread (decoding="async").') + if (!/\bwidth=/.test(tag) || !/\bheight=/.test(tag)) { + fails.push('the card image states no intrinsic size, so the box is unreserved before the ' + + 'stylesheet applies. (.uupg__image\'s aspect-ratio holds it afterwards; these cover the gap.)') + } + if (!/:fetchpriority=/.test(tag)) fails.push('off-screen images are not de-prioritised (fetchpriority).') + // ★ the first row must be EAGER — a bare loading="lazy" would pass every check above and be wrong. + if (/loading="lazy"/.test(tag) && !/:loading=/.test(tag)) { + fails.push('the card image is UNCONDITIONALLY lazy, so the row the reader is already looking at ' + + 'loads late. The first row must stay eager.') + } + if (!/EAGER_IMAGES/.test(tag)) { + fails.push('the eager/lazy split is not keyed on EAGER_IMAGES, so nothing ties it to the column count.') + } +} +// and the constant must exist and be a plausible first-row ceiling +const c = src.match(/const EAGER_IMAGES\s*=\s*(\d+)/) +if (!c) fails.push('EAGER_IMAGES is gone; the first-row exemption has no source.') +else if (Number(c[1]) < 3 || Number(c[1]) > 8) { + fails.push(`EAGER_IMAGES is ${c[1]} — the grid is repeat(auto-fill, minmax(240px, 1fr)), 1 column on a ` + + 'phone and 3-5 on a desktop, so a first-row ceiling outside 3..8 is either short (a visible ' + + 'lazy-load in view) or so generous it undoes the saving.') +} + +if (fails.length) { + console.error(`${TAG}: FAIL`) + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log(`${TAG}: PASS — card photos past the first row load lazily, at low priority, decoded off the main ` + + 'thread, with their box reserved; the first row stays eager.') diff --git a/internal/check-map-control-overlap.mjs b/internal/check-map-control-overlap.mjs index 1c1406b55..8f9b72c22 100644 --- a/internal/check-map-control-overlap.mjs +++ b/internal/check-map-control-overlap.mjs @@ -3,6 +3,8 @@ * check-map-control-overlap — two controls pinned to the same corner of the dashboard map must not * land on top of each other. * + * Reported 2026-09-28 (test round 2, #3): some map controls overlap each other. + * * WHAT HAPPENED, because it is the interesting part. The projection toggle (.rd-proj) sits at * top:8px and the zoom toolbar (.rd-zoomtools) was pinned at top:34px — a hand-tuned number chosen * when the toggle measured 23px tall, which the panel's own comment records as a live measurement diff --git a/internal/check-plural-forms.mjs b/internal/check-plural-forms.mjs index d4205a632..3505331ba 100644 --- a/internal/check-plural-forms.mjs +++ b/internal/check-plural-forms.mjs @@ -13,12 +13,15 @@ * English string written "none | one | {n}" changes MEANING at n=0 under the map. Every pipe * key is rendered at 0 and must not regress. */ -import { readFileSync, readdirSync, existsSync } from 'node:fs' +import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs' import { join } from 'node:path' const LOC_DIR = new URL('../library/i18n/locales/', import.meta.url).pathname const SRC_DIRS = [new URL('../plugin-designs/', import.meta.url).pathname, new URL('../library/', import.meta.url).pathname] -const LOCALES = readdirSync(LOC_DIR).filter((d) => !d.startsWith('.')) +// DIRECTORIES ONLY. This listed every entry, so a README.md beside the locale folders entered +// the locale list, passed the existsSync below (a file does exist) and crashed readdirSync on a +// non-directory. A note explaining a catalogue belongs beside it; the gate has to tolerate one. +const LOCALES = readdirSync(LOC_DIR).filter((d) => !d.startsWith('.') && statSync(join(LOC_DIR, d)).isDirectory()) const fail = [] // Collect every pipe-bearing catalogue value, as ns.key -> { locale: value } diff --git a/internal/check-retry-fetch.mjs b/internal/check-retry-fetch.mjs new file mode 100644 index 000000000..69ebaa87d --- /dev/null +++ b/internal/check-retry-fetch.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/** + * check-retry-fetch — a runtime data fetch survives a transport blip, and a real error still fails. + * + * THE DEFECT (reported 2026-09-28): net::ERR_NETWORK_CHANGED killed an embedded UPG-100's load. + * + * ★ THE URLs IN HIS CONSOLE CANNOT BE FIXED, BECAUSE THEY DO NOT EXIST IN PRODUCTION. They were module + * URLs — locale JSON with ?import, useOptionalT.js, useShadowStyles.js, useMapTheme.js. The build is + * IIFE with inlineDynamicImports (vite.config.js:216-220), so every one of those is inside the single + * bundle and is never fetched separately. Those failures are dev-server module requests. Wrapping them + * in a retry is impossible AND unnecessary; saying so is the useful part of this card. + * + * ★ THE FAILURE CLASS IS REAL ONE LAYER DOWN, and that is what this guards. A built bundle still makes + * runtime requests — boundaries geojson, the people-group list, prayer statistics — and none of them + * retried. The same blip takes out whichever was in flight, and the feature is simply ABSENT: an empty + * map, a missing outline, a statistic that never arrives, with nothing a reader could act on. + * + * Exit 0 pass · 1 fail. + */ +import { readFileSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const { retryFetch } = await import(resolve(ROOT, 'library/composables/retryFetch.js')) +const fails = [] +const ok = (w, c, d = '') => { if (!c) fails.push(`${w}${d ? `\n ${d}` : ''}`) } +const realFetch = globalThis.fetch + +// ── behaviour ──────────────────────────────────────────────────────────────────────────────────── +{ + let n = 0 + globalThis.fetch = async () => { if (++n < 3) throw new TypeError('Failed to fetch'); return { ok: true, attempt: n } } + const r = await retryFetch('u', undefined, { backoff: () => 1 }) + ok('a transport blip is survived', r.attempt === 3, `resolved on attempt ${r.attempt}`) +} +{ + // ★ COUNT THE ATTEMPTS, do not merely assert that it throws. An unbounded loop with a short backoff + // ALSO throws eventually, so "it failed in the end" passes a retry policy that hammers a dead server + // forever — caught by this gate's own negative test, which is the only reason the weak version did + // not ship. The property is BOUNDED give-up, and the bound is the number. + let calls = 0 + globalThis.fetch = async () => { calls++; throw new TypeError('Failed to fetch') } + let threw = false + try { await retryFetch('u', undefined, { backoff: () => 1 }) } catch { threw = true } + ok('a persistently dead endpoint still FAILS — retrying forever would hide real breakage', threw) + ok('...after a BOUNDED number of attempts, not indefinitely', calls === 3, `fetch called ${calls}x, expected 3`) + calls = 0 + try { await retryFetch('u', undefined, { attempts: 5, backoff: () => 1 }) } catch { /* expected */ } + ok('...and the bound is the caller\'s, when they state one', calls === 5, `fetch called ${calls}x, expected 5`) +} +{ + // A 404/500 RESOLVES. It must be handed back untouched, not retried: the server said something. + let calls = 0 + globalThis.fetch = async () => { calls++; return { ok: false, status: 404 } } + const r = await retryFetch('u', undefined, { backoff: () => 1 }) + ok('a 404 is returned, not retried', r.status === 404 && calls === 1, `fetch called ${calls}x`) +} +{ + // An abort is a DECISION, never a blip. + let calls = 0 + globalThis.fetch = async () => { calls++; const e = new Error('aborted'); e.name = 'AbortError'; throw e } + let threw = false + try { await retryFetch('u', undefined, { backoff: () => 1 }) } catch (e) { threw = e.name === 'AbortError' } + ok('an abort is not retried', threw && calls === 1, `fetch called ${calls}x`) +} +globalThis.fetch = realFetch + +// ── and the shared data paths actually use it ──────────────────────────────────────────────────── +// ★ ONE SITE IS DELIBERATELY EXEMPT AND IS NAMED, not silently skipped. usePosterPrints probes for an +// OPTIONAL pre-rendered .webp and falls through to generating the poster when it is absent — a miss is +// the expected path, so retrying would delay the fallback for an asset that is legitimately not there. +// Everything else in that file, and every other runtime request, must retry. +const EXEMPT = new Map([['library/composables/usePosterPrints.js', 1]]) +for (const rel of [ + 'library/composables/useMapLayers.js', + 'library/composables/useCountryOutline.js', + 'library/lists/peopleGroupList.js', + 'library/composables/usePrayerStatistics.js', + 'library/composables/usePosterPrints.js', + 'library/composables/useSharedMapToken.js', + 'library/composables/useCsvExport.js', + 'library/components/PeopleGroupDetail.vue', +]) { + const src = readFileSync(join(ROOT, rel), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') + const bare = (src.match(/(? allowed) { + fails.push(`${rel} has ${bare} bare fetch() call(s) for a runtime request, ${allowed} allowed — a transport\n` + + ' blip there leaves the feature silently absent. Use retryFetch().') + } +} + +if (fails.length) { + console.error('RETRY-FETCH: FAIL') + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log('RETRY-FETCH: PASS — a blip is survived, a dead endpoint still fails, a 404 and an abort are not ' + + 'retried, and the shared data paths all go through it.') diff --git a/internal/fixtures/on-demand-checks.json b/internal/fixtures/on-demand-checks.json index 5c05654f2..fdadac724 100644 --- a/internal/fixtures/on-demand-checks.json +++ b/internal/fixtures/on-demand-checks.json @@ -7,6 +7,7 @@ "check:composite": "rebuilds every bundle with vite to compare its bytes, minutes long by design; on every landing the precondition's fresh step rebuilds the composites that landing reaches", "check:hardcoded-english": "report-only while its findings are triaged (97 user-visible English strings on 2026-09-29); chaining it now would hold every gate red. Its :self-test IS chained, so the matcher stays proven", "check:shot-fonts": "checks that the MACHINE taking README screenshots can render emoji (fc-list): a property of that box, not of the product, so it runs before a screenshot session, not in the gates", - "check:coverage-consistent": "the full run includes clause L, the age of the oldest link verification (45 days), which depends on the CALENDAR: chained, it would turn gates red on a date (first 2026-11-08) with no code change. Run it with the link re-verification; every other clause runs in gates:pre as check:coverage-consistent:chained" + "check:coverage-consistent": "the full run includes clause L, the age of the oldest link verification (45 days), which depends on the CALENDAR: chained, it would turn gates red on a date (first 2026-11-08) with no code change. Run it with the link re-verification; every other clause runs in gates:pre as check:coverage-consistent:chained", + "check:region-paint-single-source": "TEMPORARY, remove when chained. Asserts the regions idle paint expression has one source in the library rather than a copy per map. Half-adopted on purpose: research-map.vue took the library export (2026-09-29), countries-map.vue is the other plugin owner's file and its two assertions are red until they land their half. Chaining it now would hold gates:post red for everyone over another agent's unlanded work. It joins gates:post in the SAME landing as that half, and this entry is deleted in that commit" } } diff --git a/library/charts/LanguageTreeChart.vue b/library/charts/LanguageTreeChart.vue new file mode 100644 index 000000000..ee71f4710 --- /dev/null +++ b/library/charts/LanguageTreeChart.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/library/components/PeopleGroupDetail.vue b/library/components/PeopleGroupDetail.vue index 4a8fcc829..2279e605a 100644 --- a/library/components/PeopleGroupDetail.vue +++ b/library/components/PeopleGroupDetail.vue @@ -108,6 +108,7 @@ * */ -import { defineMapElement } from '@map/elements/defineMapElement.js' const profileModules = import.meta.glob('./*.vue') diff --git a/plugin-designs/cities-internet/index.js b/plugin-designs/cities-internet/index.js index fc7ec9559..883a005f5 100644 --- a/plugin-designs/cities-internet/index.js +++ b/plugin-designs/cities-internet/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * plugin-designs/cities-internet/index.js — bundle entry for CITIES WITH INTERNET. * @@ -17,7 +18,6 @@ * plugin call before a map exists would be dead code implying a capability the * bundle does not have. */ -import { defineMapElement } from '@map/elements/defineMapElement.js' // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. diff --git a/plugin-designs/country-posters/index.js b/plugin-designs/country-posters/index.js index 3f6caaf51..adb27950b 100644 --- a/plugin-designs/country-posters/index.js +++ b/plugin-designs/country-posters/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * plugin-designs/country-posters/index.js * @@ -28,7 +29,6 @@ * import.meta.glob('./*.vue') locally and hand the result to the loader via * app.provide('profileModules', ...). */ -import { defineMapElement } from '@map/elements/defineMapElement.js' // ─── Bundle-private profile registry ───────────────────────────────────────── const profileModules = import.meta.glob('./*.vue') diff --git a/plugin-designs/doxa-research-map/index.js b/plugin-designs/doxa-research-map/index.js index 4d599164d..77196e6d1 100644 --- a/plugin-designs/doxa-research-map/index.js +++ b/plugin-designs/doxa-research-map/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * doxa-research-map / index.js — Bundle entry * @@ -16,7 +17,6 @@ * folder. The bundle evaluates import.meta.glob('./*.vue') HERE and * hands the result to the loader via app.provide('profileModules'). */ -import { defineMapElement } from '@map/elements/defineMapElement.js' // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file — diff --git a/plugin-designs/doxa-research-map/research-map.vue b/plugin-designs/doxa-research-map/research-map.vue index 620fbc602..b5c629fd9 100644 --- a/plugin-designs/doxa-research-map/research-map.vue +++ b/plugin-designs/doxa-research-map/research-map.vue @@ -123,9 +123,16 @@ function _syncScrollZoomToHost(m) { // non-interactive and left the strips redundant. Two fixes for one bug, and the second one // silently undid the feature. // - // So: keep scroll-zoom ON, and let the strips carry the page. `docScrolls`/`ancestorScrolls` - // are still computed because the edge affordance below keys its gray indicator off the same - // question — a map with nothing stacked under it should not advertise a scroll lane. + // So: keep scroll-zoom ON, and let the strips carry the page. + // + // ★ THE TWO SCROLL PROBES THAT USED TO SIT HERE ARE GONE, AND THE COMMENT THAT KEPT THEM WAS + // WRONG. It said docScrolls/ancestorScrolls were still computed because "the edge affordance + // keys its gray indicator off the same question". This profile has no such indicator and never + // calls useEdgeScrollAffordance: its .map-scroll-edge strips are background:transparent lanes + // that are always present (see the rules near the bottom of this stylesheet). They were + // function-local consts, computed and discarded — the comment described a consumer that does + // not exist here, which is worse than no comment, because it makes dead code look load-bearing. + // _findScrollportAncestor STAYS: it is still used to pick a second ResizeObserver target below. m.scrollZoom.enable() } catch (_) { /* an engine without scrollZoom must never break the panel */ } } @@ -428,6 +435,20 @@ useShadowStyles(` .map-scroll-edge, .rm-provider-badge, .rm-loading { display:none !important; } } .rm-map-canvas { position:absolute;inset:0; } + + /* ★ PRINT: GIVE THE MAP AREA A HEIGHT, OR EVERYTHING ANCHORED TO IT RISES. + .rm-map-area is flex:1 1 0 — it has no height of its own and fills a viewport-height ancestor. + Print layout has no viewport height, so that box collapses toward its content height, and the + toolbar and legend (position:absolute against it) move UP with its top edge. That is the reported + print breakage (the map controls and legend jump upward), and it needs no print + stylesheet to cause — the browser's DEFAULT print styles are enough, because nothing here declared + one. A profile with no @media print rules is not neutral in print; it is unspecified. + A definite height restores the anchor. This does not change what the poster prints — that is a + separate document in its own frame (printInPlace) — it stops the LIVE page deforming while the + dialog is open. */ + @media print { + .rm-map-area { flex: 0 0 auto; height: 100vh; min-height: 0; } + } /* MATERIAL LIGHT (2026-09-21). Was background #0f1216 / color #7c8cf8 — same unconditional loading overlay as .dsm-loading, .apg-loading and countries-map's .rm-loading. Rendered on every load via v-if="!appReady", so it was the first surface a visitor saw. @@ -437,6 +458,9 @@ useShadowStyles(` map did manage to render stays visible around it. A spinner that outlives the failure reads as "slow" and the user just waits; this says the map is gone. */ .rm-error { position:absolute;top:50%;left:12px;right:12px;z-index:7;transform:translateY(-50%);text-align:center;font:13px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#fecaca;background:rgba(15,18,22,.95);border:1px solid rgba(254,202,202,.28);border-radius:10px;padding:12px 14px;box-shadow:0 2px 10px rgba(0,0,0,.4); } + /* inset-inline-end, not right: this badge is pinned to the READING edge, so under ar it belongs on + the left. .map-scroll-edge--left/right below stay physical on purpose — those are literal screen + edges for a scroll affordance and do not mirror. */ .rm-badge { position:absolute;top:10px;inset-inline-end:10px;background:rgba(124,140,248,0.12);color:#7c8cf8;border:1px solid rgba(124,140,248,0.3);padding:3px 9px;border-radius:4px;font-size:11px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;pointer-events:none;z-index:50; } /* Engine badge — bottom-left, unobtrusive. Colour differs per engine so a side-by-side comparison is readable at a glance. */ @@ -1469,6 +1493,15 @@ function onExport(kind) { if (base === 'poster') return openPoster() if (base === 'packet') return openPacket(variant || 'full') } +// ★ THE PHONE HAD NO EXPORT AT ALL, AND THE KILL-SWITCH ONLY COVERED THE DESKTOP. +// LegendDesktop takes :export-enabled="EXPORT_FEATURE_ENABLED", so turning that flag off hid the +// desktop menu — but LegendMobile is OPT-IN BY CONSTRUCTION: its exportEnabled is +// `typeof inject('onLegendExport') === 'function'` (LegendMobile.vue:393), and this profile provided +// nothing. So a reader on a phone had no way to export a poster or a packet, while the same map on a +// desktop did. country-posters already carries the guarded line; this is the missing half here. +// Providing it UNGUARDED would be the other error — the phone would then keep an affordance the +// desktop deliberately hides whenever the flag is off, which is the bug exp's card names. +provide('onLegendExport', EXPORT_FEATURE_ENABLED ? onExport : null) // ★ THE PHONE HAD NO EXPORT AT ALL. The desktop legend gets :export-enabled and @export in the // template, but LegendMobile does not read props from here — it reaches for an injected handler, and @@ -1487,7 +1520,19 @@ provide('onLegendExport', EXPORT_FEATURE_ENABLED ? onExport : null) // ─── Packet export (printed, multi-page: map + a card per people group) ────── // Lazy-loaded on first use, the same pattern openPoster() uses. Defensive throughout: never throw // uncaught into the UI; guard against double-run. -let _packetBusy = false +// ★ A REF, NOT A PLAIN LET, AND THAT IS THE WHOLE DEFECT. This guarded re-entrancy correctly and could +// not tell the reader anything, because a non-reactive binding drives no UI. Its sibling openPoster() +// has had `posterBusy` as a ref wired to :active/:disabled/:aria-busy since it was written — so the +// FASTER action showed a busy state and the SLOWER one (dynamic imports, a map render, then a card per +// people group) showed none. One path was given feedback and its sibling was never looked at; upg100 +// found the identical shape on their own surface the same day. +// Widened by me today: providing onLegendExport gave the legend's Export menu a mobile entry point, +// so there is now a second way into this path. +const packetBusy = ref(false) +// The legend's Export menu reaches BOTH builds, so it reports either one. Provided rather than passed +// because the mobile sheet's legend is mounted by LegendMobile, not by this template. +const exportBusy = computed(() => posterBusy.value || packetBusy.value) +provide('legendExportBusy', exportBusy) // Neither export hands over a file any more: the poster prints through printImageBlob and the packet // through the shared usePosterPrints.printPacket, so the download helper that served packet.pdf is gone. async function openPacket(variant = 'full') { @@ -1624,7 +1669,7 @@ async function openPacket(variant = 'full') { } catch (err) { console.error('[research-map] packet export failed:', err) } finally { - _packetBusy = false + packetBusy.value = false } } diff --git a/plugin-designs/gospel-internet/index.js b/plugin-designs/gospel-internet/index.js index 70939fb3f..814fb8019 100644 --- a/plugin-designs/gospel-internet/index.js +++ b/plugin-designs/gospel-internet/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * plugin-designs/gospel-internet/index.js — bundle entry for the GOSPEL x INTERNET BOARD. * @@ -13,7 +14,6 @@ * The RTL text plugin is installed by the map panel itself (installRtlTextPlugin before `new Map`), on * whichever engine resolveMapProvider picked. */ -import { defineMapElement } from '@map/elements/defineMapElement.js' const profileModules = import.meta.glob('./*.vue') diff --git a/plugin-designs/gospel-resources/index.js b/plugin-designs/gospel-resources/index.js index 43756f49a..99c844321 100644 --- a/plugin-designs/gospel-resources/index.js +++ b/plugin-designs/gospel-resources/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * plugin-designs/gospel-resources/index.js — bundle entry for GOSPEL RESOURCES. * @@ -17,7 +18,6 @@ * plugin call before a map exists would be dead code implying a capability the * bundle does not have. */ -import { defineMapElement } from '@map/elements/defineMapElement.js' // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. diff --git a/plugin-designs/key-cities/index.js b/plugin-designs/key-cities/index.js index ad5548a12..65726ddac 100644 --- a/plugin-designs/key-cities/index.js +++ b/plugin-designs/key-cities/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * plugin-designs/key-cities/index.js — bundle entry for KEY CITIES. * @@ -17,7 +18,6 @@ * plugin call before a map exists would be dead code implying a capability the * bundle does not have. */ -import { defineMapElement } from '@map/elements/defineMapElement.js' // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. diff --git a/plugin-designs/my-upg-100-list/index.js b/plugin-designs/my-upg-100-list/index.js index ac2085a19..d372ce1de 100644 --- a/plugin-designs/my-upg-100-list/index.js +++ b/plugin-designs/my-upg-100-list/index.js @@ -1,3 +1,4 @@ +import { defineMapElement } from '@map/elements/defineMapElement.js' /** * plugin-designs/my-upg-100-list/index.js * @@ -26,7 +27,6 @@ * Mapbox RTL text plugin: registered ONCE at module init (before any map * constructs), lazy-loads when Arabic/Hebrew text appears on the basemap. */ -import { defineMapElement } from '@map/elements/defineMapElement.js' // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file — diff --git a/plugin-designs/people-groups-grid/people-groups-grid.vue b/plugin-designs/people-groups-grid/people-groups-grid.vue index 41aab945e..38079304d 100644 --- a/plugin-designs/people-groups-grid/people-groups-grid.vue +++ b/plugin-designs/people-groups-grid/people-groups-grid.vue @@ -388,6 +388,13 @@ const t = computed(() => { }) // ── Cat-2 show/hide (accept the DOM-attribute string 'false') ────────────────── +// How many cards can sit in the FIRST ROW at any supported width. The grid is +// repeat(auto-fill, minmax(240px, 1fr)) unless a host overrides cards-per-row, giving 1 column on a +// phone and 3-5 on a desktop — so five is the upper bound of "already on screen", not a preference. +// Deliberately a constant rather than a read of perRow: the override is the host's state and this +// only needs a SAFE CEILING. Being generous by a card or two costs a few KB; being short by one +// lazy-loads something the reader is looking at, which is the visible failure. +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 @@ -1121,9 +1128,33 @@ onMounted(() => {
-