The Regions header takes Big Picture's header recipe: its own MapInfoButton and ShareButton in the header form
(shape rounded-square, the vertical tabs' radius), side by side at the header's end, 28 px with a 44 px tap
target; on a phone the ⓘ takes the header's start edge, since the centred view switcher leaves no room for
two at one end. The gallery hands the tab's MapInfo to that button (trigger 'control'), with the floating ⓘ as
the fallback. The map-print progress card no longer shows the renderer's English phase messages.
On the all-regions map a pin tap jumped to the country's dashboard; a pin tap now opens the people
group's popup everywhere, and a tap on a country with no pin under it picks that country. The
invisible tap target is at least 7 px at every zoom, so pins answer a finger. On phones the maps are
half again as tall as they are wide (the square was too small for a popup), the left scroll-past
strip is off, and the popup is compact and lets a pinch through to the map. Desktop is unchanged.
Picking a region from the all-regions table, or a country from a region, kept the old scroll
height, so the new page opened at the bottom; the dashboard now scrolls its own container to the
top when the subject changes (switching to the list and back changes no subject and keeps its
place). The region and country maps take the all-regions map's square shape under 768px; the shared
map component keeps its portrait default for the country page.
The Regions tab handed the sheet generator its localised card, so an Arabic reader's poster read
"Pray for السودان" and Administrative areas "السودان" beside an English "Countries: Sudan". The poster
is English by recorded decision; the generator now gets the data's own record by id, as the wall does.
Reported from a phone: on the Regions tab, "2 posters per row" for the country
posters did not work -- a 390px phone showed ONE per row.
THE CAUSE is a deliberate clamp in PosterWall. The per-row setting is 2, but the
grid is Math.min(perRow, fits) with fits = floor((viewport - 36) / 210), and at
390 that is floor(354 / 210) = 1. So the setting said two and the phone silently
showed one.
★ WHY THE CLAMP EXISTED, AND WHY IT IS SAFE TO RELAX NOW. It was added when a
two-up card (~162-170px) CLIPPED its action labels -- 'Prayer List' and 'Poster
Map' both read 'Pra...', and Romanian's 'Pachet de cartonase' was measured at 54
of 81 controls clipped at ro/390. That was then fixed separately: the labels WRAP
(.pfs > .pbtn, "a label too long for its box wraps"). With wrapping in place the
clamp no longer protects anything; it only costs the phone a column.
★ IT IS OPT-IN, NOT A GLOBAL CHANGE. PosterWall is shared by twelve files,
several of them frozen experimental maps. A new minCardPx prop defaults to 0,
which means the existing 210px clamp, so every wall that does not set it lays out
byte-for-byte as before. Only regions-list sets 150, which gives two per row at
360, 390 and 414, still one at 320 (which genuinely cannot hold two), and no
change on desktop (already capped at the per-row setting of 2).
MEASURED on the regions-list leaf page, this branch served locally, against
FINAL on :4611 -- by hit test and by eye:
FINAL @390 en 1 per row, 367px cards
AFTER @390 en 2 per row, 169px cards, 0/123 labels clipped
AFTER @390 ro 2 per row, 0/123 labels clipped <- the old clipping corner
AFTER @390 ar 2 per row, RTL, 0/84 labels clipped
AFTER @1440 en 2 per row, unchanged
0 horizontal overflow and 0 console errors in every cell. The print buttons now
stack three high on a phone card instead of sharing a line.
KNOWN, COSMETIC, NOT CHANGED: at two-up the card's "53 UPG · Show on map" pill
wraps "Show on / map" onto a second line. Readable, not clipped.
Every country poster printed from the Regions tab read "map 1 of 0" and "This
map is one of a 0-map set called the Country Maps" -- nonsense on a printed
poster, on the demo's showcase flow (Regions -> a country -> Poster Map). Found
by the served check on FINAL.
WHY: makeCountrySheet called generateCountrySheet with no series, so
generateSheet fell back to its default { totalGroups:0, totalLists:0, index:1 }.
NOT A REGRESSION from the one-definition change, and worth recording because it
was first reported as one. The plugin-local copy regions-list used before that
change passed no series either; across it regions-list.vue changed by exactly
one line, the import. The defect has been in the Regions print since 168b1f1bd.
The Country Posters wall was never affected because it computes its own.
THE FIX computes the series exactly as the wall does (country-poster-wall.vue):
the tier is every country printed at the same sheet size, the total is that
tier's length, and the index is the country's position in it by id. It lives in
ONE place, seriesFor() in the library, rather than as a second inline copy of the
wall's formula -- the same duplication this branch's parent removed for the sheet
itself. regions-list passes the FULL country list, not a region's sublist:
indexing a region's own list would print "map 3 of 12" beside a thumbnail that
says "map 57 of 173".
PROVED, rendered with the values seriesFor returns:
Sudan index 1 of 173 -> "map 1 of 173" | "one of a 170-map set"
Chad index 4 of 173 -> "map 4 of 173" | "one of a 170-map set"
which is exactly the text the wall prints for the same two countries on FINAL,
measured on the served page, and what their thumbnails say. A row with no sheet
tier gets undefined, keeping the renderer's own default rather than a fabricated
position.
KNOWN, NOT CHANGED: China is the only 3x4 sheet, so its tier has one member and
it prints "map 1 of 1" / "a 0-map set" -- here AND on the wall, identically,
because this matches the wall by design. Its thumbnail says "174 of 174". Fixing
it means choosing whole-series numbering over per-tier, which is the carded
174-vs-173 question, not a freeze-night change.
exp's second read of 1ab8524cf found a real defect on the demo path, and a census
of the tree found the cause was worse than the symptom.
THE SYMPTOM: the Regions tab printed a TEAL country poster and logged a warn,
while Country Posters printed the same country HUED. That is precisely the "two
different posters for one country" the hue change exists to refuse.
THE CAUSE: regions-list.vue:73 imported a plugin-local copy,
src/country-sheet-print.js, byte-identical to library/packet/countrySheet.js
except for the region key. The region forwarding was added to the LIBRARY file,
so countryRegionColor(undefined) returned the old flat teal and warned.
★ AND THE LIBRARY FILE HAD NO IMPORTERS AT ALL. Its own comment claimed two
consumers -- the country-poster wall and <doxa-country-map> -- and a census found
NEITHER was true: regions-list imported the local copy, and
country-poster-wall.vue:94 defines its own inline countrySheet that calls
generateCountrySheet directly (passing the whole record, which is why THAT path
was correctly hued). So the module was an orphan wearing a comment that said
otherwise, and the region key was added where nothing read it. A move that was
never finished, and the duplicate stayed invisible until one copy was edited.
regions-list now imports the library module, the copy is deleted, and the
comment says what is true. Not re-copied: a duplicate of that file is
indistinguishable from it until one side is edited, and then it is a silent
divergence.
CHAIN VERIFIED TO SOURCE, since the hue is only right if the region survives
every hop: POSTER_BY_ISO3 holds country-posters.json records, which all carry a
region; localisedPoster SPREADS the record in both its branches, so the key
survives a locale switch; postersFor passes it; makeCountrySheet forwards it.
ARTIFACT PROOF: the region forwarding in the rebuilt regions-dashboard bundle
goes from 1 occurrence to 2, and POSTER_REGIONS plus the full region-name list
are present in it.
The no-poster fallback branch in postersFor still builds an object without a
region, deliberately: it has no sheet to draw, and it already carries
unsupportedReason.
NOTE ON HISTORY, not rewritten: f6a3f85a2 is titled "Rebuild the three
consumers…" but it also carries SOURCE changes -- the ink .pin-num-bg in the
shared renderer and the warn-once in posterSheets.js. The title understates it.
It is pushed, so it stands; recorded here instead.
Studio's 5B review found a dependency-tracking hole by testing the helper with
real Vue rather than reading it. entries was a plain array, so the watchEffect
tracked only what the FIRST registered getter read: with two panes reading
different refs, changing pane 2's source produced ZERO writes, and pane 2 caught
up only when pane 1's source happened to change. That is this helper's own bug
reappearing for any adopter whose panes read different sources. regions-mfe is
safe today because every getter reads the same cfg and spec -- which is exactly
why a reading could not find it.
entries is now shallowReactive, so register() re-runs the effect and every
getter is tracked. Also from the review:
- stale entries are PRUNED, not skipped: an adopter that rebuilds its pane
column would otherwise keep detached elements and re-run their getters on
every pass forever;
- a getter that throws is reported once per entry instead of being swallowed,
since silence recreates the symptom this helper exists to fix;
- the unused `tag` parameter is gone -- a documented check that does not exist
misleads the next adopter;
- the host argument now documents that it must be in the SAME tree as the
panes, because Element.contains() does not cross a shadow boundary and
passing the custom element would silently skip every write.
regions-dashboard rebuilt so public/ is servable at this sha for the browser
A/B. It is the only bundle that compiles the helper.
regions-mfe built each pane by creating the child element and writing its
profile-config once, inside onMounted, and nothing rewrote it. The pane is handed
a JSON STRING rather than a reactive object, so a later change to the OUTER
element's profile-config updated the composite's own cfg and reached nothing
below it. regions-dashboard is kept alive by v-show rather than remounted, so the
one code path that would have rewritten the attribute never runs again and the
panes stay in the old language for the whole session.
The leaf was never at fault: ProfileLoader's config is a computed over its prop,
lang is a computed over config, and a watchEffect drives appI18n on every change.
Every link is reactive -- it simply never receives a new prop.
library/composables/usePaneConfig.js owns the missing half: a composite registers
each pane element with a getter for its config, and a watchEffect re-forwards
whenever any reactive source that getter reads changes. It writes only when the
serialised config actually differs, because setting profile-config re-parses it
in the child and can remount what it drives -- an unconditional write per tick
would be a rerender loop. Stale elements (a rebuilt pane column) are skipped.
It is a library helper rather than three copies because regions-mfe, upg-100-mfe
and country-posters-mfe all assemble a config and set the attribute once; they
differ only in which keys they inherit, so each keeps its own
INHERITED_PAGE_KEYS and hands the result to the helper. This commit adopts it in
regions-mfe only; the other two are upg100's to adopt.
Two-sided, headlessly: a check that fails when a composite writes profile-config
on a child without re-forwarding names all THREE composites on development, and
on this branch regions-dashboard reports "re-forwarding=usePaneConfig" while the
other two still fail.
NOT VERIFIED IN A BROWSER YET. rega's LIVEDIAG is the falsifier: the nested
[profile-config] should show the OLD locale on development and the NEW one here.
Resolving package.json conflicts by unioning unique lines is right for a file
that holds one entry per line. Applied to ordinary source it duplicates
declarations, because both sides of a hunk keep their copy and nothing
de-duplicates across the join.
Eleven plugin entry points ended with the same import twice, and the poster
renderer with two identical `const sheet`. Each is a SyntaxError: every one of
those bundles would have failed to build, and the poster export would not run.
Found by parse-checking every changed .js against the merge base, not by the
gates -- gates:pre passed 101 of 102 with all twelve files broken, because its
parse gates do not cover plugin entry points.
One measurement trap worth recording. `node --check` on a .js file parses it as
CommonJS, so a bare `import` is a syntax error and ALL 28 entry points appear
broken, on development too. That reading says the tree is on fire and is simply
wrong. Copying each file to a .mjs and checking that parses it as a module,
which separates the eleven that are genuinely broken here from the rest that
are fine.
Each of these shadow roots is position:absolute; inset:0, while its :host had
display:block with no position. An absolutely positioned element resolves against
its nearest POSITIONED ancestor, and an unpositioned host is not one -- so the
root fell through to whatever the partner page happened to have, or to the
viewport. It only looked right standalone, where the page fills the viewport
anyway, and inside a composite's .pane, which is itself positioned. A partner
embedding the element directly gets it painted outside its box.
All three are the shadow root's own root element, with only comments between
<template> and them, so none of them has a positioned ancestor to fall back on:
regions-list.vue:408 .rl-root at :409
region-dashboards.vue:733 .rd-root at :734
regions-mfe.vue:194 .rmfe at :199
dx-shepherd's census named the first as confirmed and the other two as
candidates to check. Checked: both are roots, not children of something already
positioned, so all three are real.
The fix is the house pattern -- position:relative on :host -- matching the
already-fixed cities-internet/connectivity-map byte for byte in shape, and
prayer-walk-planner.vue:628.
One line changed per file. Verified in the ARTIFACT rather than the source: the
rebuilt regions-dashboard.js carries all three corrected host rules (two at
min-height 320px, one at 420px). check:sfc-parse, check:synced-css and
check:one-scroll-container pass; the manifest diff is integrity hashes only,
0 other lines.
check:no-new-driver-references matched "Driver" case-sensitively, so lines in
capitals passed unseen -- and my own greps were blind the same way, which is why
I reported this plugin clean twice. A case-insensitive sweep finds four, and one
of them carries a verbatim quote.
All four keep their engineering reason and lose the speaker:
· the reuse check states the negotiating position rather than whose it is;
· the substituted button font is blocked on a licensing decision, not a person;
· the code, not just the screenshot, is required to match;
· the table's ring and gridlines live in a global stylesheet, which is why
reading RegionDetail.vue alone could not show them -- the misspelt verbatim
quote is gone, the finding it pointed at is not.
Verified with grep -wi rather than the grep that missed them: 0 remain across
regions-dashboard and country-map. The four reasons are each still present by
their own text. sfc-parse, mjs-parse and no-new-driver-references all pass.
Two untranslated strings on a production print surface, both reported by the
Country Posters plugin after it fixed the same pair on its own wall.
THE CARD NAMES. postersFor returned the country-posters.json record unchanged,
and that record carries its label in English only -- so every card with a poster
was named in English in every locale. label is not decoration: it is the card's
accessible name, the print HUD's title, and the title on the printed list, deck
and packet. It now goes through countryName(iso3, locale) for a non-English
reader. English keeps the data's own names, because Intl's English differs from
them for 32 of these 175 countries and that swap is a recorded product decision.
The sheet generator still receives the original record; the poster itself is
English by the same decision. The locale REF is read, not a snapshot, so a
runtime language switch renames the cards.
INDIA'S REASON. posterUnsupported returned the record's own English note, so one
untranslated sentence sat on the wall in every locale. The record still decides
WHETHER a country is unsupported; only the wording now comes from the catalogue.
The key needs no number in it because the card shows the count directly above.
★ AND IT NEEDED A SECOND TRANSLATOR, which is the part that would have shipped
broken. This page's t() is useRegionsI18n's: it resolves DX-life's English
sentences as keys inside the `regions` namespace, because that is what the page
is cloned from. messages.countryPosterTooMany lives in another area, so t()
would have missed and rendered the literal string "messages.countryPosterTooMany"
on the wall. tKey is vue-i18n's own lookup, added for keys outside this page's
namespace.
Verified in the built bundle rather than the source: DisplayNames present,
countryPosterTooMany present. A probe over the real alpha-2 table confirms the
localisation across ar/ru/zh/de/fr and that an unknown iso3 falls back instead
of throwing. gates:pre 75 PASS / 0 FAIL / 1 NOT MEASURED of 76; i18n-parity and
i18n-keys-exist both pass. Cost +244 B.
From the fleet census: 620 function bodies indexed across 29 plugin trees,
16 identical in two or more. These are the four cheapest — none closes over
anything beyond its own parameters, so each is a file move plus an import
rather than a new dependency to thread.
hostElement resolves the custom element a component is mounted inside, one
hop out through the shadow root. It was identical in three plugins, and it
is the piece worth sharing for a reason other than line count: every plugin
ships through defineCustomElement, a composite nests two shadow roots, and
code that forgets the hop operates on the wrong node while looking correct.
A document-level query for a node inside a shadow root returns null on a
working build, which reads as a broken feature rather than a wrong query.
Three copies were three chances to write that.
findScrollportAncestor walks up to the nearest ancestor that actually
scrolls, crossing shadow boundaries on the way — parentElement stops at a
root, so a plugin in a custom element would otherwise find no scrollport and
conclude the page does not scroll.
findNodeInTree and buildFlatLegendMatchExpr join useLegendMapBridge, which
already holds four functions from the same seam. They sit at module level
rather than inside the factory because they close over nothing: a pure
function hidden in a closure can only be reached by constructing the whole
bridge.
Call sites keep their old names through a one-line local binding, so no
call changed and the diff is the declaration plus an import.
Rebuild-and-compare, all five affected bundles, before -> after:
regions-dashboard 1,098,469 -> 1,098,479 +10
country-posters 1,484,053 -> 1,484,028 -25
doxa-research-map 1,354,051 -> 1,354,061 +10
dashboard-plus-grid 915,660 -> 915,644 -16
big-picture 366,907 -> 366,719 -188
net -209, and country-posters gains 25 bytes against the 1.5 MB ceiling.
Every behaviour-bearing token counted, not set-compared, and unchanged in
all three map bundles: regions-fill, peoplePraying, engagementStatus,
adoptionStatus, overflowY, scrollHeight, getRootNode, _flat_filter, religion.
Owners cleared their own files first: regions for regions-list.vue, upg100
for countries-map.vue. check:selection-kinds, check:legend-matches-source,
check:legend-data-contract, check:packet-variants, check:no-dead-library and
check:no-cross-plugin-imports all pass.
The regions list showed a poster per country and could print the poster alone.
Supplying a records provider is what makes the Prayer List, Card Deck and Prayer
Packet buttons appear at all -- PosterWall hides them when a wall cannot supply
rows, rather than failing at click time -- and this wall never supplied any.
Records come from the shared provider, not a copy of anyone's fetch. It caches
one request per language and every country in every region shares it, so opening
five regions is still one request, and the same rows feed the sheet, so the
printed map and the printed list cannot disagree about which groups are in a
country.
Page 1 is GENERATED, because there is nothing to load. No country sheet ships:
data/posters holds 28 .html and all 28 are zones, and emit-countries.mjs writes
only country-posters.json, whose `sheet` field is a paper size rather than a
filename. So the packet's default lift asked for posters/country-<iso3>.html and
404ed on all 175. country-sheet-print.js draws it instead, in the same shape as
the zone sheet next door so the two surfaces cannot draw the same sheet two
ways, and returns '' when there is no country or no rows so the caller falls
through rather than printing an empty country.
There is ONE loading path for the generator, not two: this calls
generateCountrySheet, and posterSheets resolves the served, sha384-pinned
poster-render.js itself. Nothing here fetches or imports the generator directly.
MEASURED against the development baseline, both built here: 1044 -> 1061 KB,
+17 KB. An earlier measurement of this same wiring on the pre-pinned base read
+208 KB; almost all of that was the printing subsystem that is now served rather
than bundled, so the cost fell by about 190 KB without this file changing.
createPosterGenerator does not appear in the bundle.
item-count-key replaces nothing: the noun pair still names the section, and the
count now goes through the plural-aware key.
sr's census found item-noun="country", item-noun-plural="countries" and
unit="UPG" passed as literals. All three reach the DOM: PosterWall renders the
noun pair at its section count (n === 1 ? itemNoun : plural) and the unit beside
each row's count, so in Arabic the chrome translated and these stayed English.
The noun pair now goes through t(). Country and Countries were already in the
catalogue in all eleven locales -- Land/Länder, الدولة/الدول, Страна/Страны --
so this adds no strings, and PosterWall keeps making its own singular/plural
choice with the caller supplying both forms, which is what its props are for.
unit stays "UPG" on purpose. It is DX-life's term of art, and their own Arabic
common.json carries the literal "UPG" rather than translating it; matching them
is the point of the port. It is the one entry the census still reports here.
A fourth literal the census did not flag went with them: the prayer-list
"Coming soon" sentence, which reaches exportState.message and so renders too.
That one had no key, so it lands as an English-source key -- the convention
useRegionsI18n exists to serve -- translated in all eleven locales, 77 keys
each, parity green.
MEASURED, this plugin's entries in check:hardcoded-english: 3 before, 1 after.
The rule: never quote or cite the Driver in shipped code. Every one of these
comments had a real engineering reason attached, and the reason is what a future
reader needs — so the reason stays and the attribution goes.
COUNT IS THE CHECK: 31 before, 0 after, across 8 files.
I was pointed at 18 in region-dashboards. That was one file. Sweeping my own area
as instructed found 31 in eight files: region-dashboards 7, RegionCountryMap 13,
regions-list 5, UupgsList 2, plus regions-mfe.vue, regions-mfe.meta.json,
index.js and check-regions-uses-library.mjs with one each. Reporting the wider
number rather than the one I was given.
HOW EACH WAS REWRITTEN, by kind:
· a REPORTED SYMPTOM keeps the symptom, loses the reporter — "the pins were
reported too small at rest", "the list page was reported slow, with the
posters suspected", "instead of flying to the region, which is exactly the
reported symptom".
· a REQUIREMENT becomes a requirement — the region key is "a map that shows the
regions and countries AND lets a reader select one" rather than a quotation;
the missing fullscreen and zoom controls are "part of the cloned surface"
rather than someone's complaint about a full clone.
· an OPEN DECISION becomes provisional. Two of these: the stat-card divergence
from RegionDetail.vue:176 is now "carried back as a product question", and
the numeral-grouping parity reads "PROVISIONAL, pending a product decision"
instead of waiting on a named person's ruling.
· a CARD ID goes with the name: BUILD-REGIONS-MAP-RESTORE-EDGE-SCROLL-STRIPS
and the round-2 test numbering are internal handles that mean nothing to a
reader of a published bundle.
· a SCOPE RULING is stated as scope: "improve the map's LOOK and add the
all-regions map, which must be intuitive" — the constraint survives, the
quotation does not.
Nothing lost that a reader needs: every measurement, pixel value, file:line
citation into DX-life's source and engine-specific caveat is untouched. What went
is a name, four dates and two internal ids.
SAME PROOF AS THE CITATION COMMIT, because a prose sweep across eight files is
exactly where a stray edit hides:
· the only non-comment changes are an HTML comment in a template and the "note"
field of regions-mfe.meta.json — and `note` is read by nothing: generate-tree
never touches it, and the string does not appear in the built manifest.
· meta.json still parses.
· rebuilding regions-dashboard and country-map reproduces the committed bytes
EXACTLY, and a full `bun run tree` leaves public/ clean — so NO bundle needs
rebuilding for this.
· gates:pre 66/66.
gospel's citation rule, applied to mine: a comment citing a PROBE or MEASUREMENT
(_probe-*, _chk-*) states the FINDING and drops the path. A reader cannot open
_qa-cycle from a published bundle, and the path outlives the file — so the number
has to be in the comment or it is lost.
FOUR CITATIONS, all of the probe kind, so all get the same treatment. Every one
already carried its finding; only the dead path is removed:
RegionCountryMap:36 three engineFor implementations "decide identically in
every engine combination" — now says WHICH combinations
were checked (mapbox-only, maplibre-only, both globals
present) rather than naming the script that checked them.
RegionCountryMap:462 the globe-fill measurement keeps its numbers (zoom 1.229
-> 390.61px = 50.1% of the frame, half the frame dead
space) and loses the probe path.
RegionCountryMap:552 the fit-at-construction comparison keeps both readings
(bounds-at-construction zoom 3.210 / mercator at t=0 vs
fitBounds zoom 2.777 / globe) and the engine version.
region-dashboards:522 "4 of 10 app properties OK", with the five failures that
were one missing thing already spelled out below it.
I did NOT only fix the one line I was pointed at: I swept my own plugin files,
the six library files I own or moved this session, and the seven gates I wrote.
Four citations total, all in these two files; the gates and the other library
files carry none.
COMMENTS ONLY, and proved rather than asserted: the diff filtered to non-comment
lines is empty; check:ported-globals-identical still reports 4/4 byte-identical
emitted CSS; and rebuilding regions-dashboard and country-map reproduces the
committed bytes exactly — comments are stripped at build, so NO bundle needs
rebuilding for this and public/ stays untouched. gates:pre 66/66.
scripts/strip-code.mjs:5 is deliberately left alone: per the same ruling it
explains why a committed gate cannot import from the scratch area, which is a
fact about the boundary rather than a citation across it.
One sha instead of two, per the shepherd: both branches fixed the SAME
regions-list rule, because a gate is useless on a tree where its poison case is
still live. Landing them separately would have applied that fix twice.
★ I HAD CLAIMED THE TWO COPIES WERE IDENTICAL. They were not — I checked before
folding rather than after, and the hashes differed. The CODE line is the same in
both (`cyrillicHeading('.rl-title')`, one occurrence each); only the explanatory
comment differed. I kept the css-dangling wording because it names the arm that
now catches the bug, which is what the next reader needs. The emitted CSS is
unchanged either way — comments are not CSS — and the byte-compare baseline still
passes, which is the assertion that would have caught me if I were wrong.
WHAT THIS SHA CARRIES, both halves verified after combining rather than assumed:
THE MIGRATION (was 4a4bc96fc). The harness stub's `.value` is now the empty
string, so an injected-config read and a locale interpolation both behave as
they do in the browser. Measured components 2 -> 4, no NOT MEASURED left.
region-dashboards is byte-identical (a pure de-duplication); regions-list
changed because its old bytes were the bug, so its baseline was re-taken.
strongMedium() deliberately NOT adopted — the helper emits bare `strong{...}`
where region-dashboards scopes `.rd-root strong{...}`, and swapping would widen
the rule.
THE GATE (was 023ca2d08). check:shadow-css-parses now fails on a selector that
never receives a block, gated on `endsLiteral` — the LAST fragment — rather than
on `complete`, which is false for nearly every sheet here and therefore
suppressed the very bug the arm was written for. Four self-tests live in the
file: the exact regions-list string is caught, a last rule WITH a block is not,
a trailing comment is not, whitespace is not.
Re-verified on the combined tree: ported-globals 4/4 byte-identical, shadow-css
80/80, the poison case still produces exactly 1 dangling hit when reintroduced,
gates:pre 56/56, and the broken form is absent from the shipped minified bundle.
upg100's harness recorded my two files as NOT MEASURED because its stubs threw on
them. Both were FIDELITY gaps — the stub modelled states that cannot occur — so
they are fixed by making the stub more like the browser, never by relaxing a
comparison:
· `.value` was undefined, so a component reading an INJECTED ref threw
"Cannot read properties of undefined" (region-dashboards.vue:199,
cfg.value.regionSlug — ProfileLoader always provides that object).
· the stub threw on ANY string coercion, because a Proxy over a function has no
primitive conversion. That alone produced regions-list's "Cannot convert
object to primitive value".
`.value` is now the EMPTY STRING, which is the one shape that serves both real
cases honestly: a locale ref's .value IS a string, so `${locale.value || 'en'}`
correctly yields 'en'; and reading a property off '' returns undefined instead of
throwing, so an injected-config read falls through its own `|| ''` exactly as in
the browser. Symbol.toPrimitive makes any remaining stub render empty.
Measured components: 2 -> 4, with no NOT MEASURED left.
★ AND THE MIGRATION FOUND A REAL BUG, which is the part worth reading.
regions-list's Cyrillic heading rule ended with a trailing comma and NO
declaration block, so the concatenation emitted:
[lang="ru"] .rl-title, [lang="ru-RU"] .rl-title,regions-list
A selector list with nothing to apply — the Russian heading font was silently
never applied — AND it swallowed useShadowStyles' style-id argument as CSS,
leaving that sheet unlabelled. Braces stayed balanced, so
check:shadow-css-parses could not see it, and it passed 80/80 both before and
after. cyrillicHeading('.rl-title') replaces it and emits a real rule.
SO THE BYTE-COMPARE SPLITS, deliberately, and I did not paper over it:
region-dashboards.vue IDENTICAL — cyrillicHeading('.rd-title','.rd-h2')
emits exactly the literal it replaced, verified
character-for-character before the swap. A pure
de-duplication.
regions-list.vue CHANGED, and the change IS the fix. The old bytes were
the bug, so its baseline is re-taken rather than
preserved. Preserving it would have pinned a broken
rule forever, which is the one thing a byte-identical
gate must not be used for.
NOT MIGRATED, on purpose: strongMedium(). The helper emits bare `strong{...}`
while region-dashboards emits `.rd-root strong{...}` — a narrower selector.
Swapping it would widen the rule to every <strong> in the shadow root, a
behaviour change disguised as a de-duplication. scrollbarGutter() is likewise
left inline: both files carry it inside a larger .rd-scroll/.rl-scroll rule, not
as a standalone declaration the helper could replace.
Both files now gated (owned:true) instead of recorded-only, so a future edit that
changes their emitted CSS fails rather than being noted.
country-map (the previous commit) moves RegionCountryMap.vue from regions-dashboard/src to
library/components/maps so a page can mount the country map alone. check:region-fill-repaint read the
old path and crashed on the move; it reads the library file now and still PASSes, with the repaint
after setData intact (the move carried the hardening that landed in 608e8ec29). check:universal-props
held the file's fixed en-US population format by its old per-bundle path; the same held decision now
sits on the library file for every bundle that imports it (regions-dashboard and country-map), which
the allowlist permits only for library/. One comment still pointed at src/useRegionsI18n.js.
DX-life's country pages render nothing where a map belongs. The map they need
has been in this repo since the regions clone landed — it was just unreachable
without loading the whole regions dashboard, a bundle that fetches the people
group list, builds four stat cards, a region chooser and a table, none of which
a country page wants. So this is not a new map. It is the same component, given
its own element and its own bundle.
<doxa-country-map profile-config='{"countryCodes":["TCD"],"colorByPrayer":true}'>
RegionCountryMap and useRegionsI18n move to library/ because they now have a
second consumer, which is the only thing that earns a place there. Six import
sites updated in this commit; nothing else moved.
THE CONTRACT arrives through profile-config, the channel every element here
uses, so a generated page and a hand-written host page are configured the same
way: countryCodes, center, zoom, bounds, colorByPrayer, researchUrl,
prayBaseUrl. Two translations happen at the edge rather than by widening the
component — colorByPrayer is a boolean here and colorMode a string inside, and
countryCodes accepts an array OR "TCD,NER,MLI", because an HTML attribute is a
string and JSON-inside-an-attribute is the shape people get wrong.
THE ELEMENT FETCHES ITS OWN PINS, and that is what makes prayBaseUrl part of the
contract rather than decoration. An element handed a country code, a base URL
and no way to fetch would draw a country outline with nothing inside it — the
blank page it exists to fix, in a new disguise. Same endpoint, same fields, same
mapping as the dashboard; it asks for a country instead of the world.
THE FIELD LIST TRAVELS WITH THE MAPPING, in peopleGroupPoints.js. The obvious
version of this commit asks for PEOPLE_GROUP_LIST_FIELDS — the list the stats
need — and every pin still draws, every popup comes up empty, and nothing logs
anything. The popup half is what makes it a map you can read, so the two are
one export and neither can be requested without the other. The dashboard's own
request is unchanged: the concatenation produces the identical string.
countryUrl is additive, not in the confirmed contract. The component's country
link needs a base and DX-life's country pages live under /regions/; a host that
sets nothing gets that documented default.
HARDENING, NOT THE FIX for the grey Regions tab. That tab has been seen loading
grey on first load and coloured after a reload. This closes one ordering that
can reach a flat-grey fill. On built bundles that ordering did not reproduce the
grey tab (0 of 20 loads grey, the fill present with its expression at addLayer),
so the reported bug stays open; the lead is the dev/HMR re-mount path.
THE ORDERING. regionFillExpression builds a ['match'] from props.points; with
no points it returns a FLAT no-data grey. addLayers SNAPSHOTS that value into
the layer's paint when it creates the layer. applySubject does repaint the fill
correctly, but it guards every write on getLayer() — deliberately — so a change
arriving BEFORE the layer exists is silently dropped and never retried. And on
MapLibre, the DEFAULT engine, the country boundaries are added as an EMPTY
source and populated by a later fetch's setData(), which nothing repainted
after. In that ordering the fill keeps the colour it was born with.
THE CHANGE is one call: repaint after the boundaries setData, which is the first
moment the layer has any features on the MapLibre path and the only repaint
guaranteed to run with both inputs present. applySubject is idempotent and
getLayer-guarded, so it is harmless when the colour was already right — it only
removes the ordering's power to be the last word.
★ TWO THINGS I GOT WRONG FIRST, both caught by the gate rather than by review,
and both recorded because the wrong version was plausible:
1. MY MODEL COULD NOT REPRODUCE THE BUG. The first version's negative control
fired: no arrival order greyed even with no repaint, so it could not have
detected what it existed for. The greying order is not "data before the
layer" — by then the snapshot is already coloured. It is points arriving
AFTER both addLayers and its mount repaint, with the watch's repaint not
landing. Corrected, exactly 1 of 3 orders greys without the fix and none
with it.
2. I BLAMED THE EMPTY SOURCE, WHICH CANNOT CAUSE GREY. An empty source draws
NOTHING; fill-color is an expression over feature properties. A grey
landmass therefore means features ARE present and the expression came back
flat. The repaint after setData is still required, for the narrower reason
above. The gate carries that correction so the wrong reason cannot be
inherited from a comment.
THE GATE'S OWN INSTRUMENT WAS WRONG TWICE TOO. A 400-char window between
setData and the repaint failed on the repaint's own comment. Widening it to
1600 then PASSED when I deleted the real repaint and left an unrelated
applySubject() 1200 chars downstream — measured, not feared. A distance is the
wrong instrument, so the .then() block is now extracted by brace matching and
the repaint required INSIDE it. The first structural attempt anchored on the
nearest '=>' and landed inside the callback body, because `for (const f of ...)`
has one; it anchors on the enclosing '=> {' now. Four arms mutation-tested,
including that decoy, and every mutated file restored byte-identical.
vue-i18n v9 pluralizes by an English-shaped INDEX and applies it to every
locale. Measured on a real instance, authoring 'a | b | c':
ru n=0 -> the ONE form, n=2 -> the MANY form CLDR: 0=many, 2=few
ar n=3, 11, 100 -> ALL the dual form CLDR: few, many, other
That is confidently wrong grammar that looks translated, which is worse than
the single fixed form the regions summary shipped until now. upg100 proposed a
plural key and was correctly refused on this exact ground
(upg-100-list.vue:3006) — "real plural support needs a pluralRules map at
createI18n". The refusal's premise was the missing map, so this adds it.
THE PRECONDITION SWEEP (dx-shepherd), before installing anything: every
catalogue value containing " | " across all 11 locales, and every call site
passing a plural choice.
pipe keys: wall.prayedCount, wall.havePrayed — and ONLY in en. Every other
locale ships a pipe-free label form ("Помолились: {n}").
call sites: exactly two, both in wall-of-people-groups
(CommentsDialog.vue:36, wallCanvas.js:198).
BEFORE/AFTER, 2 keys x 11 locales x n = 0,1,2,3,11,100:
132 renders compared, 0 CHANGED.
Zero changed because the map is SCOPED TO EXCLUDE en, which is the one locale
holding pipe strings today. en is also the case dx-shepherd's assertion (3) is
about: vue-i18n's 3-form default is zero|one|many while CLDR for en is
one|other, so an en string written "none | one | {n}" would change meaning at
n=0. Leaving en out means it cannot.
★ THE BUG I SHIPPED INTO MY OWN FIRST DRAFT, caught by reading the rendered
Russian rather than by any assertion: the index was taken against the fixed
zero|one|two|few|many|other list, which is only correct for a locale that HAS
all six. ru has {one, few, many, other} — no zero, no two — so 'few' sat at
global index 3 while the authored string holds it at 1, and n=2/n=3 rendered
"2 стран" instead of "2 страны". ARABIC HID IT COMPLETELY, being the one locale
that does have all six, so its two indexes coincide. The index is now computed
against each locale's OWN category subset, and ru/ro are authored in that order.
ar 0 لا دول · 1 دولة واحدة · 2 دولتان · 3 دول · 11 دولة · 100 دولة
ru 0 стран · 1 страна · 2 страны · 3 страны · 11 стран · 100 стран
(regionCount likewise; all six categories distinct in ar, all three in ru)
check:plural-forms carries studio's three assertions, each mutation-tested to
fail: FORM COUNT (a 2-form ru string fails against its 4 categories), the
tStatic PATH (reading a pipe key through tStatic fails — tStatic does no
selection at all, so the page would show the literal "a | b | c"), and ZERO (a
5-form ar string fails because ar has an explicit zero category). Every mutated
file restored byte-identical afterwards.
The regions shim gains tc(key, n). t() resolves through tm()+rt(), and rt()
takes a message only — there is nowhere to pass the choice — so a pipe string
would come back whole. tc goes through vue-i18n's own t(key, named, choice).
Registered in all three places and VERIFIED by re-reading the parsed
package.json, not by trusting the replace: my last gate registration silently
no-opped on two of three anchors and studio caught it at the landing.
Two interop defects on production plugins, found by reading the links rather
than the code around them, and corrected against DX-life's own component once
dx-shepherd pointed me at it.
THE COUNTRY LINK POINTED AT THE PEOPLE GROUP. On the regions dashboard's card,
the country name under the title linked to that people group's own profile —
the same destination as the "Full Profile" button directly beneath it. Two
links on one card, one labelled with a country, both landing in the same place.
Theirs, in BOTH card variants (app/components/public/UupgsList.vue:610, :638):
localePath(`/regions/${countrySlug(country_code.value, country_code.label)}`)
A page on the HOST site — not a research profile, and not the ../countries/
path my first pass reached for. So the base is a PROP, not a constant: only the
host knows where its country pages live, and DX-life's are under /regions/.
Prop, then profile-config, then the site base, so an embedder can point country
names anywhere without touching the research base.
people-groups-grid is ported from that same component and had the same defect
in a different disguise: `${researchUrl}../countries/<slug>`, climbing out of
the research base to a section DX-life does not use for this. Both now build
the same URL from the same countrySlug.
Our countrySlug is theirs: identical body, identical slugify (every transform,
in order), and the tables behind it match exactly — 242 codes each, zero
missing, zero differing labels. My first comparison of those tables parsed
their file into ZERO entries and reported "no differences", which is the shape
of a check that cannot fail; the number above is from the run that actually
parsed 242.
FOUR LINKS DEPENDED ON A TRAILING SLASH NOBODY WAS TOLD ABOUT. researchUrl and
prayBaseUrl are set by whoever embeds the element, in someone else's HTML. The
joins were bare concatenation, and RegionCountryMap's own prop comment records
the contract without ever telling the embedder:
Pray -> prayBaseUrl/<slug>, Full Profile -> researchUrl<slug>
One needs a trailing slash, the other must not have one. An embedder writing
research-url="https://doxa.life/research" — the spelling a human would choose —
got "https://doxa.life/researchbantu-tswana" on every profile link the plugin
draws: a 404, with nothing logged anywhere.
We cannot test what an embedder passes, so joinBase() encodes the rule instead
of documenting it: exactly one slash, whatever we are handed. Eight shapes
checked, including a doubled slash, a leading slash on the path, an empty path
and a null base.
Driver test round 2, #2 — the half the @media print rules could not do. The
complaint was "no loading indicator on a click": the reader presses Print, the
dialog takes a moment to rise, and nothing says anything is happening. It is
worse on this page than most, because what the dialog waits for is a MAP —
press it a moment early and the sheet carries a grey rectangle.
printInPlace.js gains printWhenReady({ waitFor, onBusy, timeoutMs }). This
pattern already exists five times in the repo (upg-100-list c54a11d5a, four of
exp's 786a83461, PeopleGroupsWall.printWall); they agree on the easy half —
set a flag, print, clear it — and each invents the hard half, which is what
"ready" means and what happens when ready never comes. The hard half is the one
that strands a reader, so it is the half that graduates:
· THE TIMEOUT PRINTS. It does not reject and does not cancel. A reader who
clicked Print and waited has asked for paper; a slow tile server is a reason
to print a plainer map, not a reason to produce nothing.
· allSettled, not all — one waiter that rejects must not cost the print.
· onBusy clears in a finally, on every path: a rejected wait, a throwing
print, a host with no window.print. A busy state that outlives its own
operation makes the page look broken rather than slow.
RegionCountryMap answers when it is done, and map.loaded() is not that answer —
it reports true while tiles still stream, which is how gospel's charts printed
blank twice today. The condition is: style applied, camera stopped, tiles for
this camera in. It checks synchronously FIRST and only then subscribes to
'idle', because this component's own header already records that once('load')
measured unreliable in both directions here — an event that fired before you
listened never arrives. Always resolves, always removes its listener.
The button is quiet, sits under the title, and is hidden on paper. Busy state
carries aria-busy and :disabled, not just a changed label: the screen reader is
told, and disabled is what actually stops a second print. Both strings are in
all 11 locales; "Print" was already translated 23 times elsewhere in the tree
under other keys, so this adds the key the dashboard's own catalogue lacked.
Mutation-tested, since the failure this guards is the path nobody takes: a
waiter that never settles returns "timeout" after the ceiling and prints anyway
(201ms measured on a 200ms ceiling), a rejecting waiter still prints, and the
busy flag returns to false in all three.
Seven changes to two plugins, re-applied onto current main as ONE source patch
rather than rebased, because the rebase conflicted in the generated bundles —
where a conflict is never resolvable by hand, since neither side matches what
either source builds. Source re-applied clean, everything rebuilt from it.
people-groups-grid
· the saved column count is restored before first paint, not after, so a
returning reader no longer watches the grid snap from auto to their choice.
It re-reads if the profile-config key arrives late, and never over a choice
the reader has since made by hand.
· the cards-per-row control is inset from the card edge it was sitting level
with (Driver round 2, #5).
· the window vocabulary handle is released on unmount, identity-checked so the
first of two grids on a page cannot strip the second's live handle.
regions-dashboard
· the four stat cards no longer let Population stretch across a tablet row.
auto-fit collapsed the empty tracks; auto-fill with the track minimum capped
at a quarter of the row gives at most four columns and never a stretched
orphan. Measured at nine widths.
· the dashboard is printable at all. It had no @media print rule, so the app
shell's absolute root and inner scroller meant the browser printed one
screenful; the legend stayed on top of the map; interactive chrome printed.
· both window globals are released on unmount, not just the remembered one,
each identity-checked and on its own line.
shared asset
· the worldmap watermark is 242 KB rather than 311 KB — same 394 paths, same
drawing, coordinate precision trimmed to what the render resolves.
Found by the bug-class sweep. RegionCountryMap is passed :research-url; the
UupgsList beside it is not — zero research mentions in its entire tag. So a host
setting profile-config.researchUrl got it honoured on the map popup's Full
Profile button and silently ignored by every profile link in the country list,
which fell back to a hardcoded doxa.life default.
The shape is worth more than the line: one config, two consumers, only one
bound, and the unbound one carrying a plausible default. A missing binding that
fell back to an empty string would have rendered a visibly broken link; this one
renders a working link to somebody else's site, which is why it survived. It is
invisible in either file alone — the parent's tag list has to be read against
the children's props.
One line. Bundle rebuilt; the binding is in the artifact.
My scripted migration inserted the import behind a guard that silently did not fire:
`if 'defineMapElement' not in s.split('function')[0]` became false as soon as the
inserted CALL appeared before the first `function` keyword — so the files that most
needed the import were exactly the ones that did not get it. 11 of 16.
Every one of those elements would have thrown "defineMapElement is not defined" on
load. A bundler does not error on a free identifier; only loading does. studio caught it
in L64 via landing-preconditions, which imports each profile, and fixed it there
(37f75601) plus added an arm to check:element-bootstrap.
This repairs the same 11 on this branch so it is not a landmine if it lands after L64,
and it is byte-equivalent to studio's fix in effect. Verified by PARSING each entry and
resolving the identifier against its import bindings — 16 of 16 bound — rather than by
re-running the kind of regex that wrote the bug.
The element-bootstrap migration called defineMapElement in eleven entry files that never
imported it. A build does not fail on an unbound name; the element does, the moment it loads,
with 'defineMapElement is not defined'. The landing precondition caught it by importing each
profile. check:element-bootstrap now fails when an entry calls the helper without importing it.
The Driver asked for the arrow on the people-group grid and on the regions list.
Those needed different wiring, and the reason is worth recording because it is
not a preference.
The regions dashboard owns its scroller: .rd-scroll has overflow-y:auto, so the
plugin mounts the button itself and drives its own element. It deliberately does
not touch the page's scroll — this renders inside a shadow root in someone
else's page, and their scroll is not ours to move.
The people-group grid owns no scroller at all. .pgg-root sets no overflow and
.pgg-body has no rule; the grid simply grows and something outside it scrolls.
In this shell that something is .vtl-host, which lives in the layout, outside
every plugin's shadow root, so the grid could not reach it even if it should.
So the arrow for that surface belongs in the layout, beside the circle the
layout already owns, driving the pane it already owns. The grid is untouched.
.vtl-host is per tab, so the arrow follows `active` rather than binding one ref,
or it would scroll whichever pane rendered last.
One component, two consumers, and each drives a scroller it actually owns.
Checked that this adds nothing to the hardcoded-English census: 256 before, 256
after — the three regions entries there are PosterWall prop literals and predate
this.
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.
ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".
SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.
All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.
★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.
check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.
THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.
ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".
SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.
All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.
★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.
check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.
THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
rega shot the hover dim at 1440 and called the non-hovered pins pale rings on
the pastel fills. That is what they were, and the cause is a property that was
never set: circle-opacity dims the FILL only, and circle-stroke-opacity is a
separate property defaulting to 1. So a pin dimmed to 0.25 kept a fully opaque
1px white stroke, and at the dimmed radius that ring is most of the pin.
Worth saying plainly, because the obvious fix was to keep the stroke opaque
while dimming the fill: that was already the behaviour, and it was the cause
rather than the cure. The stroke now dims with the fill, so a non-hovered pin
recedes as a dot instead of becoming an outline. The fill dims less as well,
0.25 to 0.45, and the dimmed radius goes 2.5 to 3 at low zoom for the same
reason the resting state was raised earlier: a stroke is a fixed px cost, so at
radius 2.5 the coloured core carrying the region's identity is about 1.5px.
dx-life's two modes keep an opaque stroke; only the chooser's dim is ours.
The property is set in both places. A paint property added at layer creation but
not re-applied where the mode is re-applied never updates on hover, which is the
bug this file already carries a warning about.
The map below the fold, measured at 1440 with the container at 1200: the map's
top sits at about 583px and the map is 780px tall, because min-height beats what
16/9 asks for at that width. A reader sees roughly its top 190px. Tightening the
three gaps above it and dropping the outsized title one step recovers about
99px, so about 286px shows on load. It does not put the whole map above the fold
and nothing leaving the map 780px tall can; that needs the map bounded by
viewport height, which changes its rendered size and is on the Driver's sheet.
Scoped to the all-regions page and to 1200px and up, because the region and
country pages are the clone and their rhythm is measured against dx-life.
Rebuilt on current main rather than merged, so the shared manifests carry every
other bundle's current hash and only this bundle's moved: four lines, one
integrity plus the three script tags that embed it.
The new hardcoded-English gate found this in its first run over the production
plugins: an aria-label built by concatenation, so a screen-reader user on a
non-English locale heard "Open the" and "dashboard" in English around a region
name in their own language.
It uses the key the regions list already uses for the same sentence, which is
translated in all eleven locales, so the label now reads as one sentence rather
than two languages spliced together.
Found by the gate in the plugin that wrote it, which is the argument for making
it shared rather than private.
Regions: the country and region counts under the dashboard title were English
template literals; they are now label keys ('Countries: %count%') in all eleven
locales, which also sidesteps plural forms.
Gospel: the Zume chip joined 'in' + a language name, which reads backwards in
Hindi; it is now one phrase key with a {language} placeholder.
UPG-100: the Batch Select button title comes from legend.title.batchSelect.
`tiles` built four stat objects that nothing rendered. The commit which cloned
DX-life's region dashboard replaced its template block with `stats` and the
shared StatCard, and removed the .rd-tile styles with it, but left the computed
behind.
It was not merely unused: it carried four bare English labels, so a localisation
scan reports it as hardcoded English every time. It cost this audit the time to
prove it harmless, and would have cost the next one the same. `stats` renders
the same four figures and puts every label through t().
Verified superseded rather than unfinished before deleting: the template usage
and the computed were added by one commit and the usage removed by the next, the
styles are already gone, and nothing in the plugin refers to it. Behaviour is
unchanged.
A regions audit across 60 keys and 10 locales found the catalogue itself sound
-- every key present everywhere, and the few strings identical to English are
cognates their sibling languages spell differently. The gaps were in the plugin,
in the places a value is absent and a fallback renders instead.
Eight `|| 'Other'` fallbacks rendered the English word in every locale, though
'Other' is translated in all eleven and the regions list already called t() for
exactly this. A country subtitle concatenated ' people groups ' as a literal.
Both now go through t().
The map popup aligned its stat values with text-align:right, which does not flip
for Arabic, so the values sat on the wrong side of a right-to-left popup. It
uses the logical end instead, as nine other declarations in these files already
do.
Two strings are left deliberately: the region and all-regions subtitles build
"N countries" and "N regions" from template literals, and no key exists for
either in any locale. Word order and plural agreement differ per language, so
inventing a construction across eleven of them would be worse than the English
it replaced. Those need catalogue keys and are reported rather than guessed.
The Driver reported the pins as hard to see. On the chooser this is the state
that answers it: what the page shows on load, before any hover.
Two things make a pin harder to read here than on a cloned country page, and
neither is a matter of taste. The chooser renders the globe, so every pin sits
at the lowest useful zoom instead of a framed close-up; and the 1px white stroke
is a fixed cost, so at radius 3 it consumes a third of the pin's width and the
coloured core carrying the region's identity is only about 2px. Raising the
resting radius from 3 to 4 at zoom 2 grows that core from 2px to 3px -- a ~50%
gain in the part a reader actually distinguishes, for ~33% more footprint.
Opacity goes to 1 for the same reason: 10% transparency over a basemap costs
contrast and buys nothing while nothing is being de-emphasised.
Only the chooser changes. The cloned country and region pages keep DX-life's
exact geometry, and the hover behaviour is untouched -- dimming the other
regions is the design saying "these pins are one group", which is a taste call
rather than a legibility defect.
Whether this is now easy enough to see needs eyes; the argument here is only
that the readable part grows faster than the footprint.
check:public-hygiene reports role nouns in doxa-maps-spa/manifest.json.
studio guessed a meta.json description; it is neither that nor the SPA's.
The manifest generator extracts each element's @description from its SFC
header, and both leaking entries are my files: region-dashboards.vue and
regions-mfe.vue.
Rewritten to describe the element rather than who asked for it. Verified by
regenerating the tree: doxa-maps-spa/manifest.json goes 2 occurrences to 0.
Found the same way as the last two: match the shipped text back to source
rather than reason forward from the bundle name.
package.json and check-universal-props.mjs both conflicted only because
main already carries the work: studio landed check:square-map-controls and
my LOC-ENUS refinement, so main's copies are the right ones and were taken
whole.
I broke package.json first, trying to dedupe programmatically: my rule
matched on indentation + key name, which removed a key that legitimately
appears in BOTH the scripts object and the //scripts-adjacent docs object.
Restored from origin/main rather than hand-repaired. 286 scripts, no
duplicate keys, and every entry of mine present -- they all went out with
commits that have landed.
The Driver reported the list page being slow and guessed the posters. The
posters are behind a v-if and never load until a region is opened. The cost
was the mount fetch: it requested the full PEOPLE_GROUP_LIST_FIELDS for every
people group, and this page renders none of it -- a region's label, group
count, country count and country names all come from country_code and
wagf_region alone. Verified by running summarizeRegions on two-field rows:
the four values this page binds all come out.
SIZE OF THE SAVING: NOT MEASURED, and I will not put a number on it. Six of
eight fields are dropped per row across the whole dataset, including two
coordinates, a population integer and three status/count fields, so the
reduction is real -- but sizing it needs the live endpoint, which I cannot
call from here. My first attempt quoted a proxy off
public/assets/people-groups/people-groups-slim.json; that file is the IMB
dataset in PascalCase and carries NONE of the eight field names, so both
halves of that comparison were measuring empty objects. The figure was
meaningless and is removed rather than rounded.
PEOPLE_GROUP_LIST_FIELDS is untouched: the dashboard tab needs all eight for
its map pins and stat cards.