127 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
471bd05cea |
Stop showing English where the catalogue already holds the translation
A reader on an Arabic page saw a handful of English words among the Arabic:
the table's "People Groups" title, its count line, the Engagement column, the
Gospel Access chart title, and the side drawer's close button. Every one of them
had a correct Arabic entry sitting in the catalogue; nothing looked it up.
THE CHECK CAME FIRST, AND IT REPLACES A SWEEP THAT PASSED A BROKEN TREE.
The old method sorted every rendered string into third-party / data / chrome and
counted only chrome. That split is a guess, and it guessed wrong exactly where
the defect lived: single words like "Region", "Prayer" and "Country" are as
plausible as values as they are as labels, so they scored as data and the sweep
reported zero on a page that was visibly leaking.
check:composite-locale-leaks asks a question with an answer instead:
the English catalogue holds this exact string as a value, AND
this locale holds a DIFFERENT value for that key, AND
the page still renders the English one.
No judgement about what kind of word it is — if the translation exists and the
reader is not getting it, that is the defect. The two surfaces a lookup cannot
judge (vendor map chrome, and table/facet cells carrying API values) are muted
by NAME with a stated reason, never by word, so muting one can never hide the
next real leak that happens to reuse it. The gate failed on this tree naming 9
strings before any of them were fixed.
Also threaded here: both composites now accept `lang` / `locale` in
profile-config and pass it to the children they mount, alongside `tk` and
`provider`. A partner writing {"lang":"ar"} reached nothing before — the knob
was documented and read by nobody.
The prayer facet's label in DashboardResearchFilters is now a catalogue getter
too. Measured before touching it: dashboard.config.js does define prayer_level
and FACETS spreads the config last, so that literal never rendered for this
config — it is a default for a config that omits the facet, and it stays, with
the merge order written down so the next reader need not re-derive it.
Four strings remain English with no catalogue entry at all — "Top countries",
"Select all {n}", the table dock's title, and the singular "group". They need
wording in ten languages that cannot be composed from existing entries, so they
are a DECIDE card for the Driver rather than translations invented here.
|
||
|
|
d400861d79 |
Merge main into bundler-2/work
# Conflicts: # public/doxa-maps/big-picture/big-picture.js # public/doxa-maps/big-picture/big-picture/big-picture/index.html # public/doxa-maps/big-picture/big-picture/frontier/index.html # public/doxa-maps/big-picture/big-picture/index.html # public/doxa-maps/country-posters/countries-map/index.html # public/doxa-maps/country-posters/country-poster-wall/index.html # public/doxa-maps/country-posters/country-posters-mfe/index.html # public/doxa-maps/country-posters/country-posters.js # public/doxa-maps/doxa-research-map/doxa-research-map.js # public/doxa-maps/doxa-research-map/research-map/index.html # public/doxa-maps/doxa-research-map/research-map/maplibre/index.html # public/doxa-maps/index.html # public/doxa-maps/manifest.json # public/doxa-maps/people-groups-grid/people-groups-grid.js # public/doxa-maps/people-groups-grid/people-groups-grid/driven/index.html # public/doxa-maps/people-groups-grid/people-groups-grid/index.html # public/doxa-maps/people-groups-grid/people-groups-grid/people-groups-grid/index.html # public/doxa-maps/research-dashboard/research-dashboard.js # public/doxa-maps/research-dashboard/research-dashboard/index.html |
||
|
|
53e2c6623e |
The last English in the shared header, panel and facet rail — and a 24px touch target
E RESIDUALS, from rega's in-gallery re-sweep. All three lived in SHARED
components, which is why a grep of my own plugin found nothing:
SearchFilterHeader its label map was a literal object of English defaults —
the fallbacks every host gets, since no host passes
`labels`. Filters, Selected, World and the chip remove
button now come from the catalogue; a host's own labels
still win, which is what the prop is for.
ResearchMapFilterPanel the two "Any" options and the "{n} active" pill.
DashboardFacetRail the Expand/Collapse aria template, which repeats per row:
one untranslated template is dozens of English strings in
a screen reader.
dashboard.config.js the facet SECTION titles (People, Place, …). The earlier
pass converted `label:` and missed `title:`.
★ AND A LOOKUP BUG THAT LOOKED EXACTLY LIKE A MISSING TRANSLATION. 'Any' and
'0 active' stayed English after being catalogued, because the panel asks for
`controls.<key>` while I had authored them under `dashboard.<key>`. The
catalogue had the words; the component was looking one namespace over. Keys now
live where the panel reads, and its lookup falls back across both, so a string
authored in either place renders.
MEASURED after, ru, on the built tree: 0 English chrome strings on Simple
Research and 0 on Big Picture.
One probe correction worth keeping: a substring match on 'People'/'World'
flagged 'South Asian Peoples' and 'Arab World' inside aria labels whose frame
was already translated — data, not chrome. Whole-string matching separates them.
TOUCH TARGET (regions' phone sweep): .rd-proj "🌐 Globe" measured 74x23 at
390x844, one pixel under WCAG 2.5.8 AA. min-height:24px under
@media (pointer:coarse) — stating the 24 rather than growing padding, because
the height is padding plus a line box the renderer derives from the font and the
arithmetic lands at 23.7. Mouse sizing untouched. MEASURED after: 73.5x34.
Every bundle embedding the changed shared components is rebuilt in this commit —
a shared fix is not landed until its consumers are.
|
||
|
|
79dd00a12c |
Merge main: no source conflicts, artifacts rebuilt rather than merged
Every conflict was a build artifact — another agent had pushed a full rebuild of all 23 bundles. Minified .js and baked-SRI leaf pages are generated files: merging their text is meaningless and can produce a bundle that matches neither side, so they are resolved by taking one side and then rebuilding from the merged source. The incoming diff carries 68 source files, among them library/composables/ useMapInstance.js and the dashboard/gospel catalogues in 10 locales, so the rebuild is all 23 bundles and not only the ones I touched — regenerating the tree without rebuilding has shipped a stale .js here before. |
||
|
|
7f700935c5 |
Simple Research + Big Picture: the host's style stops at the box, and the last English goes
TWO FIXES THAT BOTH SHOW UP ONLY WHEN THESE PLUGINS ARE SOMEBODY ELSE'S GUEST. 1. ATTRIBUTE FALLTHROUGH. These SFCs mount AS custom elements, so a style the host puts on the tag lands on the component's own root element. MEASURED with a host style of "display:flex; position:relative; height:100%": that exact string appeared verbatim on the inner root div of people-groups-grid, stack-demo, research-map-plus-grid and dashboard-plus-grid. The gallery sets one on every tab, and the same fallthrough collapsed the Prayer Walk Planner to 76px. inheritAttrs:false on each root — a host styling the TAG is styling the box, not our internals. AFTER: the inner root carries no inline style on any of the four, and each still renders full height (2997-4010px in the harness), so the guard did not become the collapse it prevents. Worth knowing for the next probe: shadowRoot.firstElementChild is a <style> node — Vue injects styles first — so a probe that trusts it measures nothing and reports "no fallthrough, nothing to fix". Take the first non-style child. 2. THE RESIDUAL CHROME the in-gallery re-measure found: the dashboard title, Align Dashboard and its tooltip, World, Vertical Legend, groups in scope, the per-row aria templates (Filter by / Remove filter / groups), and the map drawer's aria label. Catalogued in all 11 locales and read through tStatic, reusing existing wording where the catalogue already had the word. The aria TEMPLATES matter more than their size suggests: they repeat per row, so one untranslated template is dozens of English strings in a screen-reader's ear. |
||
|
|
3aaa53d501 |
Merge main: take the poster tape's overlap rule, keep the touch minimum
Their tape system supersedes mine and is the better invariant, so it wins whole. I had made both stickers self-centre on the card's edge; they pin both by --card-pad-top and --tape-overlap, which makes the OVERLAP onto the banner the constant instead of the centre. That is what the Driver's rule actually asks for -- extra height then grows into empty space rather than walking over the artwork -- and one variable now governs both stickers so neither can drift from the other. Kept only the orthogonal part of mine: the (pointer:coarse) block that lifts .pbtn, .pstock and .tape-h to the 24px WCAG 2.5.8 minimum. Dropped my padding override on .tape-h -- their rule sizes and places that sticker, and re-stating its padding here would fight a geometry the Driver has already approved. Also took their new .pstock.is-on state. Fixed a comment of mine that the merge made false: it claimed both tapes self-centre, which stopped being true in this very commit. Rebuilt all 23 bundles, not just the ones I touched. The incoming diff brought library/i18n/index.js and three shared library components, which reach nearly every bundle -- and regenerating the tree without rebuilding has shipped a stale .js here once before. tree + fix:sri after; check:sri PASS across 54 script references on 55 leaf pages. |
||
|
|
fab96ce5db |
Phone: every shared control clears the 24px touch minimum
Measured all seven production maps at 390x844 with touch emulation. 195 controls sat below WCAG 2.5.8's AA minimum of 24x24 CSS px, spread across five maps -- but they were not five owners' bugs. Grouped by class, 195 failures were ten classes, and 119 of them lived in library/: the same PosterWall, PinHint, LegendMobile, SemanticTreeLegend and ResearchMapFilterPanel controls repeated down a list and across four maps. Routing the per-map counts would have filed hundreds of defects against owners who do not own the geometry. Raised the shared controls, scoped to (pointer:coarse). 2.5.8 is a touch ergonomics rule, and these components sit on maps this change does not own: a finger gets the bigger target, the mouse keeps the density those owners designed. The poster row is the reason this matters -- its height was tuned by eye against the card's blue outline, and growing it for a pointer that needs no help would have spent the artwork clearance to fix nothing. min-height states the 24 instead of deriving it. Growing the padding first MEASURED 23.7px and 23.0px -- under by a third of a pixel and by one, because the height is padding plus a line box the renderer computes from a 10.5px font. The declared minimum also survives a font swap or a locale with taller glyphs. The header tape now self-centres like the footer. It hung on top:-11px, half of its then-fixed 22px height, which silently becomes wrong the moment the height changes -- as it just did. top:0 with translate(-50%,-50%) sits on the border at any height, the mechanism the footer already used, and one less number to keep in sync. Two exceptions in 2.5.8 are now encoded in the probe rather than remembered. Inline: 48 .uupg__country-link "failures" are an <a> inside a paragraph, computed display:inline, sized by the line-height of the text around it -- the spec exempts them and the probe now skips display:inline, counting them separately. Equivalent: a .lpb-seg progress segment is 10px tall inside a track that clips overflow, so no hit-area extender can reach 24px; it resolves to a tree node and calls the same selectNode a full-width legend row calls. 195 failures on five maps -> 4 on two. UPG-100 alone went 137 -> 0. The poster tape gate and per-row persistence still pass. |
||
|
|
5516c157bb |
Merge main into bundler-2/work
# Conflicts: # public/doxa-maps/country-posters/countries-map/index.html # public/doxa-maps/country-posters/country-poster-wall/index.html # public/doxa-maps/country-posters/country-posters-mfe/index.html # public/doxa-maps/country-posters/country-posters.js # public/doxa-maps/index.html # public/doxa-maps/manifest.json |
||
|
|
b197055bf0 |
Print a UPG-100 list straight from the map legend
The Driver, as a first-time user: "I select one and it gives me print
options at the top right of the legend ... that's the only way that map
would be usable."
The affordance already existed - SemanticTreeLegend renders an export
menu gated on a selection, the plugin passes :export-enabled, and
@export was wired. What the handler did with it was the problem: the
packet branch delegated to the shared composable, and the poster branch
hand-rolled an <a download> of posters/<id>.webp. That is wrong twice.
It downloads a file silently, which was ruled out on 2026-09-23 ("no
silent file download as the default action"), and it has no fallback, so
for the lists whose .webp is not shipped it saved a 404.
The wall had a correct poster print the whole time, private to
PosterWall.vue, so this surface could not reach it. It moves into
usePosterPrints.printPoster and both surfaces now call the one copy -
what the card asked for: "reuse the existing three print actions rather
than growing a fourth implementation." Two guards moved with it, each
learned the hard way: a THROWN fetch is a miss rather than an error (or
the rasterise fallback is skipped entirely), and a blank raster fails
loudly (an SVG in an <img> is an isolated document, so a missing
stylesheet paints black with no error).
Also in this commit, both from measurement rather than review:
THE STICKER OVERLAP IS NOW A CONSTANT, NOT HALF A VARIABLE. Centring the
print row on the banner edge put half its height on the banner - and
that height is content-dependent: 68px in en, 54px in zh, taller when ro
wraps at 390px. Measured ro/390px: 27 of 27 stickers on the map by 4px
while en and zh were clean at the same padding. No fixed padding can
clear half of a variable. The row's top is now pinned --tape-overlap
above the edge and extra height grows downward into the row gap, so the
amount on the banner is identical in all 11 locales at every width.
15 width x locale combinations now pass with map-covered 0.
THE PER-ROW PREFERENCE IS CLAMPED TO WHAT THE VIEWPORT HOLDS. It renders
as an inline grid-template-columns, and inline beats the stylesheet, so
the Driver's default of 2 overrode minmax(210px,1fr) at every width -
including 390px, where two columns give a ~162px card. Reported from a
390x844 phone shot. The preference is for desktop density, not an
instruction to overflow a phone; it survives, clamped by the same 210px
minimum the stylesheet already uses.
THE ROW GAP WAS CLEARING A DISTANCE NOTHING OCCUPIES. 88px was tuned
when the print row hung fully below its card. It now straddles the
banner edge, so the same shot showed a ~180px empty band between rows.
56px still clears a two-line row in the long-label locales.
And the composite shell re-dispatches doxa:select-zone, so a list chosen
in the poster wall is ISOLATED on the map rather than merely scrolled
to. The map half was already built and listening on its own element; the
shell owed it one re-dispatch and nothing here re-implements selection.
|
||
|
|
89bd9c5d79 |
Poster wall: the banner gets stickers, top and bottom
The card is a BANNER that displays a map, the way a label displays a design on a bottle. A banner taped up gets a sticker at each end, and each sticker straddles the edge it tapes: part on the banner's own white space, part overhanging. Neither one touches the map. MEASURED at 1440px before this change: banner 240-426, map 241-425 -> 1px of white space at each end header sticker 35px above the banner's top edge, -1px below footer sticker 1px above the bottom edge, 67px below So the map filled the banner edge to edge and there was no banner surface for a sticker to sit on. The footer row could only hang off into space, which is what shipped, and every assertion in the gate was green through all of it. The banner now reserves white space at both ends (--banner-pad-top / --banner-pad-bottom on .main), and each sticker centres on its edge: header sticker 17px on the banner / 17px above it footer sticker 35px on the banner / 33px overhanging map covered by either: 0px The print row also moved INSIDE .main. It was a sibling, so top:100% resolved against the CARD, 27px past the banner - the re-parenting and 07d3341c's centring transform are one fix, and neither works alone. That commit's per-row localStorage preference is preserved. Card Deck is no longer a peer button with a 162px <select> beside it. The stock picker is a disclosure: press Card Deck, choose a size, and that choice prints. A native dropdown in the row collides with the host page's own dropdowns, which is why it is buttons rather than a <select>. Print-button labels WRAP instead of clipping. With the per-row default now 2, a 390px viewport halves the card to ~170px, where Romanian's "Pachet de cartonașe" cannot share a line: measured 54 of 81 controls clipped at ro/390px, en and zh clean. The ruling on this wall is that a long label wraps; nowrap was forcing a clip. Also fixes the transparent strip above a pinned section header. Sticky pins to the scroll container's PADDING box, so with .wall's 22px padding-top the header rested 22px down and posters scrolled visibly through the gap. A ::before extends the header's opaque fill upward by exactly that padding, read from the same var so the two cannot drift. Measured by sampling the strip's pixels: 1 colour, 100% header background, where before it was 1338-2831 colours of poster artwork. The MFE shell's own chrome is translated, in all 11 locales. Measured with ?lang=ar: "Select View", "Views", "Prayer Resources" and "Map" rendered English while the poster wall's buttons inside the panes translated correctly, because each pane installs i18n and the wrapper around them never did - the chrome a reader meets FIRST was the untranslated part. The wall's own heading, tag and intro go through t() for the same reason, with the <strong> inside the translated string so each language emphasises its own words. The gate grew four assertions, each mutation-tested to fail on the defect it was written for: off-frame (row floated off the banner), head-off (header sticker not straddling), map-covered (a sticker on the poster), and selects (a native dropdown back in the row). Includes a rebuild of the four PosterWall consumers, which also picks up studio's shared useMapInstance unmount guard and exp-shepherd's translated LegendMobile labels. |
||
|
|
9a18dee1c2 |
Merge main into bundler-2/work
# Conflicts: # package.json # public/doxa-maps/country-posters/countries-map/index.html # public/doxa-maps/country-posters/country-poster-wall/index.html # public/doxa-maps/country-posters/country-posters-mfe/index.html # public/doxa-maps/country-posters/country-posters.js # public/doxa-maps/index.html # public/doxa-maps/manifest.json |
||
|
|
b4a0387495 |
Translate the chrome that was English in all eleven locales
Measured under ?lang=ru on both composites: every Latin-script string left on the
page was ours. The map above was fully translated; underneath it the grid, the
map's own side menu and filter panel, and the whole research dashboard were
English in Arabic, Chinese, Hindi, Russian and everything else.
Three surfaces, three reasons, none of them a missing translation:
the grid read labels from a host-supplied `t` object that nothing ever
filled, with an English literal at each call site as fallback
side menu +
filter panel plain string literals
dashboard labels declared as literals in dashboard.config.js — facet
rail, chart titles, KPI strip, table headers, segmented
control, search placeholder
New catalogues for all 11 locales (grid, controls, dashboard), REUSING the
catalogue's existing wording wherever the word already existed — Регион, Блок
родства, Население, Вовлечён, Графики — so two panels never name one thing two
ways, and nothing is coined that the project already has a word for.
Read through a new plugin-free tStatic() rather than useI18n, because these
components are also mounted by entries that install no i18n plugin. An unguarded
useI18n() throws and a thrown setup() removes the subtree with no build error —
the defect that had just blanked people-groups-grid everywhere (
|
||
|
|
502a4186dc |
Cities with Internet and the phone legend speak the page's language
Cities with Internet was English end to end: the legend title, the four bands,
the World Bank caveat, the new people-group row and the whole country sheet. Its
chrome now comes from a connectivity catalog area in all eleven locales, following
gospel-resources: i18n is optional, English from the shipped catalog is the
fallback, and carrier and platform names stay as data. The legend and the sheets
are anchored with logical insets, so in Arabic the legend sits opposite the control
rail instead of under the zoom buttons.
LegendMobile hard-coded its column labels ("Prayer Status", "Engagement Status",
…) and titles ("Legend", "Languages", …) in English. They now read
legend.colLabel.* and legend.titleBar.*, which every catalog already translates.
English is byte-identical, so other consumers are unchanged until they rebuild.
Measured in the Research Gallery tab on a served built tree, at 1280 and 390, in
en/ar/zh/ru/hi: Cities shows no English UI string (only the Mapbox and
geoBoundaries attribution); ar is right-to-left with 0 clipped and the legend
clear of the control rail.
|
||
|
|
4cfe8ec5e2 |
Merge commit 'd989d598f18227e677f762c15c21df27013a57ad' into bundler-1/work
# Conflicts: # public/doxa-maps/index.html |
||
|
|
07d3341c83 |
Poster footer becomes tape on the border, and per-row persists (Driver)
Driver: "see how the header for show on map is inline on the top of the poster
box? but the footer print options are not inline on the border or outline of the
box... we want the footer to imitate the header, by being inline like a piece of
tape, but not overlapping the poster itself".
The header manages that with .tape-h{top:-11px} -- a fixed offset that works only
because its height is fixed. The footer's height is not: it carries two to four
controls plus a select and grows with translated labels, so the old
margin-top:-14px left it hanging BELOW the edge rather than sitting on it, and
any height change moved it again. translate(-50%,-50%) at top:100% centres it on
the bottom border at ANY height, which is the same piece of tape the header
achieves and is self-correcting when a locale makes it taller.
MEASURED on a rendered card: the header's centre sits 1.0px from the card's top
edge (height 22.0) and the footer's centre is now -1.0px from the bottom edge
(height 30.0) -- tape on the border, mirroring the header. It also stays well
clear of the artwork, footer top 800.1 against a preview bottom of 557.5, which
is the rest of the Driver's rule: half the tape overlaps the card and half hangs
below, so the overlap grows only half as fast as the tape does.
Per row, also Driver: minimum 2, maximum 5, default 2, "and it needs to honour
local storage so if you change the default number it is saved in local storage
so you can reload and your default you set is persistent". Choices are now
2..5, the default is 2, and the chosen value is stored under one key for the
whole wall rather than per instance -- a preference that only survives on the
page you set it on is not a preference. A stored value outranks the `columns`
prop, because the prop is only what the page suggests before the reader has
said otherwise. Every localStorage access is wrapped: it throws outright in a
private window or with site data blocked, and a preference must never be able
to break the wall.
|
||
|
|
4d773d6ee8 |
Move internal files out of the repo; fix five dead links; a fuller delegation diagram
Moved out (kept outside the repo, in the team's internal tracker): - AGENT-BRIEF.md: operating rules for the team's own agents - documentation/slide-deck-outline.md, documentation/README-longform.md: internal pitch material; the README and why.md cover the same ground - plugin-designs/gospel-resources/SOURCES-CHECKED-AND-REJECTED.md: research log - .claude/ (an editor status line), .forgejo/ (a CI workflow that never ran), _archive/ (a retired staging page; already gitignored but still tracked) Seven code comments that named these files now describe the rule instead. Fixed five dead links left from the docs/ -> documentation/ move, including the one in template-bundle that every new plugin inherits. The delegation diagram now shows the whole model: the Plugin Studio with the shared library and the merge queue into main, seven production plugins with capability badges and the Experimental shelf, one agent per plugin (a roaming agent for the shelf), the shepherd's oversight ring, a legend, and the five ways a build ships. |
||
|
|
6494b97993 |
library: PosterWall and SemanticTreeLegend no longer require i18n to render
Some checks are pending
build-gate / build (push) Waiting to run
Both called useI18n() UNGUARDED. That function THROWS ("Need to install with
`app.use` function") on any host that never installed vue-i18n, and seven
bundles here do not — big-picture, doxa-maps-spa, doxa-vision-card,
golden-path-check, people-groups-grid, plugin-studio, stack-demo. In a library
component this widely consumed, an unguarded call is a BLANK PAGE waiting for
the first bare host that renders it: exactly how library FilterDropdown blanked
people-groups-grid, a production plugin, fixed by studio in
|
||
|
|
90ceac36ba |
PinHint must not require vue-i18n either, and a gate so the next one is caught
The shepherd reported a P0 I caused: |
||
|
|
6603dc176b |
FilterDropdown: translate when the host has i18n, never require it
Some checks are pending
build-gate / build (push) Waiting to run
|
||
|
|
a7f57c117d |
poster cards: a blue outline on hover, and a gate that can see it
Some checks are pending
build-gate / build (push) Waiting to run
The card asks for the Pinterest/Instagram behaviour: hovering a poster outlines it in blue so a reader knows which one their click is about to act on. ★ THIS IS NOT A RESTORED BORDER. Several reports asked to bring back "the blue outline around the poster", and MEASURED 2026-09-24 there has never been one: all 136 elements painting a frame on this wall compute outline:none, and every border is grey (.card #e3e6ea, .main #dfe3e7). Nothing regressed -- the affordance was never built. The reports kept recurring because a hover state is invisible to a screenshot, so each check confirmed "no outline" and concluded the sighting was mistaken. outline, NOT border, and that is load-bearing. An outline paints OUTSIDE the box and takes no space, so it cannot move anything. The rest of this wall's gate asserts where the print row sits at five widths; a hover that grew a border would re-lay-out the card and push that row, so a cosmetic state could break a gate about print controls. :focus-within carries it to the keyboard, and @media (hover:none) drops the whole state on touch -- with no pointer it would either never appear or stick after a tap, leaving a card looking permanently selected. THE GATE NOW MOVES THE POINTER. Everything it measured before was of a loaded, untouched page, which is precisely why this affordance could go unbuilt while being repeatedly requested. ⚠ AND ONE OF MY OWN ASSERTIONS WAS DECORATION UNTIL I MUTATION-TESTED IT. I first asserted the card's OUTER width is unchanged by hover. It can't change: the card is a grid item, so the track fixes its width and a border grown on hover eats inward. MEASURED with border-width:4px on hover -- outer 224->224px, unfalsifiable, while the CONTENT box went 222->216px. It now asserts clientWidth, which is what actually repositions the print row. MUTATION-TESTED: remove the outline -> FAIL at all four pointer widths (exit 1); grow a border alongside it -> FAIL naming the content-box shift (exit 1); restore -> PASS (exit 0), reporting "hover 2px rgb(47, 111, 219)" at 768-1920 and "no outline (touch, correct)" at 390. The print/open controls the card also asks for are already there and were verified clickable in the same pass: Prayer List, Card Deck, the stock select and Poster Map, all four with full labels and real handlers, 0 clipped. |
||
|
|
77989089e0 |
Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts: # public/doxa-maps/index.html |
||
|
|
5a959d6b94 |
Two posters per row on the regions wall, with a control to change it
The Driver, verbatim: "i need two posters per row on the regiosn posters and witha setitn got icnreat eh number of poswers per row". The wall's own grid is repeat(auto-fill, minmax(210px, 1fr)), which lays five across a region card. It now takes a `columns` count and an optional `columnsControl`, both opt-in and both defaulting to the behaviour that shipped — so country-posters and my-upg-100-list, which did not ask for this, keep the auto-fill they have always had. Measured after the change: they still lay out five across and neither grew a control. The control is a <select>, not a stepper or a pair of chevrons. It is what every reader already knows for "choose one of a few", it is keyboard- and screen-reader-operable with no work, and it shows its current value without a second element. Imitating the solved pattern rather than inventing one. Its label is catalogued in eleven locales like everything else on this surface. The probe measures GEOMETRY, not CSS. Asserting grid-template-columns would pass on a rule that says repeat(2, …) while the cards actually wrapped three across because one had a min-width the track could not honour — and a computed style is exactly the kind of proxy that has fooled this suite five times today. It counts how many cards share a rendered top coordinate, which is what a reader sees: [2,2,2,2,2]. And it proves the control by USING it — set it to 3 and the rows become [3,3,3,3,3]. A default that happens to be right would pass a one-shot check with an inert knob. One thing worth recording for the next person: the first attempt put the explanatory comment inside the PosterWall tag's attribute list, and the Vue compiler rejected it. An HTML comment belongs above the tag. This file had already learned that once. |
||
|
|
88a566f49d |
legend: the height cap now derives from the offset instead of repeating it
Some checks are pending
build-gate / build (push) Waiting to run
THE LEGEND OVERFLOWED THE BOTTOM OF THE MAP because two numbers that had to
agree were written out twice.
.stl-panel is capped at calc(100% - var(--stl-top) - 12px), but this profile
set `top` DIRECTLY with !important instead of setting --stl-top. So the panel
moved down to 58px while the cap kept subtracting 8px, and the two silently
disagreed by exactly the difference.
MEASURED at 390px in a 420px-tall host: top 58px, max-height calc(100% - 20px),
panel bottom 458 in a 420px map -- 38px PAST the map. Same 38px at 560px,
because the error is the mismatch, not the map's size. After: -12px, the
intended bottom margin, at 390/420, 390/560 and 1440/500 alike.
Two more copies of the same mistake went with it. The mobile rule paired
top:64px with max-height:calc(100vh - 72px) -- a constant unrelated to the
offset, AND the wrong box: 100vh measures the VIEWPORT while the panel is
positioned inside the MAP. Those agree only when the map fills the screen,
which is true of every standalone leaf and false the moment a host puts a
header above it. .fit-content had the same fixed 72px. Both now derive from
--stl-top, so they cannot drift apart again.
★ THIS IS INVISIBLE ON EVERY PUBLISHED LEAF. A standalone leaf gives the map
the whole viewport, and a panel that overflows a full-height map is still on
screen. It only appears when the host makes the map SHORTER than the viewport
-- which is how the element is actually embedded. Verify in a constrained
host, never on the bare leaf.
The a11y half of this work is NOT here. I fixed the same two nameless legend
controls locally while the remote was unreachable, then found on rebase that
they were already fixed on main -- better, with the row's own label
interpolated ("Expand Africa" rather than a generic string), and in one
key namespace instead of the second one I had introduced. Took main's version
whole and dropped my duplicate locale keys; only the CSS above is mine.
|
||
|
|
608ef3dd56 |
poster cards: print options wrap to full words instead of an ellipsis
The Driver rejected the ellipsis by name: the print options must LINE WRAP so
every label stays readable, because a button that no longer says what it does
has not been fixed.
Three measurements drove the CSS:
- a <fieldset> takes min-width:min-content from the UA stylesheet, which
OVERRIDES width and max-width. .pfs read width:82% while computing 431px
inside a 212px card. min-width:0 goes on the fieldset ITSELF, not only on
its children.
- the row is anchored at its TOP (top:100%;margin-top:-14px), not its bottom.
At bottom:-14px every extra line grew UPWARD over the preview image, which
is how the first overflow fix broke the card it was meant to repair.
- wrapping took the row from 35px to 78px, so the grid's row gap moves 64px
to 88px. The old gap was tuned when the row was a single line.
No text-overflow anywhere in .pfs. min-width:0 alone had let three 76-79px
labels share 172px, showing 40px each.
The gate gains the assertion that would have caught the ellipsis: a control is
clipped when scrollWidth exceeds clientWidth. Mutation-tested -- restoring
text-overflow:ellipsis reproduces 81 of 108 clipped and exits 1. Without that
assertion the gate passed a wall whose buttons all read "Pra...".
Measured at 390/768/1024/1440/1920px: 0 clipped, 0 overflow, 0 over-preview,
0 neighbour intrusions, 108 controls.
|
||
|
|
db36397b93 |
Translate the last three English literals in components the clone renders
A dropdown every localised surface renders had no t() at all. Its two visible
strings — the clear button and the empty state — were English literals, and one
of them, "No options found", was ALREADY catalogued in eleven locales elsewhere
in this repo: the translation existed and the component was ignoring it. The
poster wall's export modal had the same shape, reassuring the reader that
nothing is uploaded in English only.
Three keys in buttons.json across eleven locales, and the literals replaced.
Both bundles that render these components — regions and country-posters,
whose cards are routed to me — are rebuilt. check:i18n-parity passes with no
locale regressed.
AND A BUILD HAZARD IN MY OWN COMMAND SHAPE, worth writing down because it is
exactly the failure the brief warns about. I ran
BUNDLE=x bun x vite build 2>&1 | tail -2 && bun run tree && bun run fix:sri
The vite build FAILED on a syntax error — I had put a const inside
defineProps — and tree and fix:sri ran anyway, because the exit status of a
pipeline is the LAST command's, and `tail` succeeded. So the chain regenerated
the tree and re-hashed against a bundle that had not been rebuilt: the "tree
alone ships a stale .js" trap, opened by the pipe I added to keep the output
short. Caught because the next build's byte count moved, not because the chain
complained. Any agent using `| tail` after a build needs pipefail or a separate
status check.
|
||
|
|
9ba6ad7a7a |
Name the legend's carets, and translate a hint that had never been translated
TWO SHARED-COMPONENT DEFECTS, both routed to me, both a step away from where the routing pointed. THE 14 NAMELESS BUTTONS ARE ONE COMPONENT. check:a11y-button-names reported 14 visible buttons with no accessible name across two country-posters leaves. The routing named countries-map.vue, which contains exactly one <button> and it has visible text — the gate prints the LEAF, so the file was an inference. Running the gate and then a probe that prints what a developer can grep for — class, innerHTML, box size, the custom-element host chain — named the real one: library/components/legends/SemanticTreeLegend.vue:1228, seven <button class="stl-caret">, 14x14, a 9x9 chevron and nothing else. The 14 is those 7 counted twice, because country-posters-mfe composes countries-map; worth saying, or the next reader hunts a second defect that does not exist. "(no title either)" in the gate's output is the diagnostic half: it rules out the title-only control this gate was built for, and it rules out Mapbox's own controls, which ship aria-labels. These were ours and they were empty — assistive technology said "button" once per expandable branch. Fixed at the source, which is why it lives in library/: an aria-label naming the branch the caret discloses, and aria-expanded, because a name tells a screen-reader user what the control is for and still not whether the branch is open. Three keys across eleven locales rather than a hardcoded label. And the same defect one element over, latent: .stl-x-chip carried only a `title` — precisely what this gate exists to reject — and hardcoded English besides. It escaped the sweep because it renders only while a row is selected. Seven surfaces render that legend, so one fix reaches all of them. The artifacts are per-bundle though: country-posters is rebuilt here because its cards are routed to me, and my own bundle with it. The other five stay stale until their owners rebuild, and I have not touched them. AND THE PIN HINT HAD NEVER BEEN TRANSLATED. The ask was to audit eleven locales for a stale "red dot". There is nothing stale, because there is nothing: no locale file contains "Tap a", "pinHint" or "coloured dot"; PinHint.vue imports no useI18n; the sentence is a JavaScript string literal. So every map that renders the hint has been showing an English sentence to readers in Arabic, Russian and Chinese. Which is worth noticing about the fix that came before it. The colour half was found and fixed carefully — the comment above it records the Driver reporting it five or more times, and why a shared component must not name a colour it cannot know. All correct. Nobody asked whether the sentence was in the catalogue. Same shape as the Russian headings next door: wrong only for readers who do not file bug reports in English, and so invisible to everyone who could. Eleven locales, every one saying coloured and never red. The pinHintLabel override still comes first, so a profile that does want to name its colour can. And the key falls back to the English sentence when a locale lacks it, so a missing translation degrades to today's behaviour rather than rendering the literal "messages.pinHint.tapDot" — the failure mode of a naive t() swap. Flagged and not taken, because they are shared components with owners: FilterDropdown renders a hardcoded English "Clear selection" and "No options found" (the latter already exists translated in the regions catalogue, so the component is ignoring a string that is right there), and PosterWall's export modal renders its reassurance line untranslated. |
||
|
|
15b9c94399 |
Phone: the pin hint stops sitting on top of the legend sheet
MEASURED at 390x844 on doxa-research-map: the hint pill sat at y=700 and the open
legend sheet begins at y=603, so "Tap a coloured dot — someone still needs
prayer" printed straight across the Progress rows. 1,418 Needs Prayer and the
Full Prayer Coverage line were unreadable underneath it, in the phone screenshot
the README showcases.
Nothing was clipped, which is why the existing below-the-fold clamp had nothing
to say: the pill was fully visible and fully in the way. So the sheet becomes a
FLOOR, exactly as the attribution strip already is — this component has always
treated the credit line as something to clear rather than cover, and the sheet is
the same kind of obstacle. If the pill would cross the sheet's top edge, it lifts
by the overlap; only when the two really intersect horizontally, and only ever
upward, so a surface with no sheet is unchanged.
TWO THINGS THE FIRST ATTEMPT GOT WRONG, both found by re-measuring rather than by
reading the diff:
· The sheet does not exist when the pill mounts — LegendMobile renders after the
map's first canvas. A mount-time querySelector found nothing, so no observer
was bound and the clamp never ran again: the pill moved 0px and the "fix" was
a no-op that looked plausible. The observer is now bound lazily by the first
clamp that SEES a sheet, with three settle re-measures after mount to make
sure one of them does.
· The sheet also changes height as the reader drags or collapses it, and each
height is a different floor. The existing host observer watches the MAP, which
a sheet drag does not resize, so the sheet needed its own.
MEASURED AFTER — 390, 360 and 414px wide: no overlap between the pill and any
painting part of the sheet; screenshot read at 390, where Progress now shows 689
/ 136.7M, 1,418 / 69.6M and Full Prayer Coverage in full. Desktop 1280x900
unchanged: pill still at the map's bottom edge, no lift applied. The two other
hint consumers (country-posters-mfe, regions-mfe) show no overlap either.
One probe note worth keeping: .legend-sheet-root spans the whole map with
pointer-events:none and paints nothing, so an overlap check that counts it reports
a collision no reader can see. The obstacle is the sheet, not the box it lives in.
|
||
|
|
c57b51044f |
Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
# Conflicts: # public/doxa-maps/index.html |
||
|
|
b026c3ef83 |
poster cards: the print row belongs below the preview, and a fieldset ignores width
Some checks are pending
build-gate / build (push) Waiting to run
The Driver caught my own fix breaking the poster section, in the same session I landed it. flex-wrap wrapped the print row onto three lines; the row is anchored at the card BOTTOM, so growing it 35px to 78px pushed its TOP up from 216px to 173px in a 238px card — straight over the preview image and the outline. I traded a horizontal overflow for a vertical one. My gate measured only left and right edges, so it reported PASS on a visibly broken card. Same instruments-lie family the flock banked today: a green that measures the wrong thing. The real cause of the ORIGINAL overflow, which both the old CSS and my first fix missed: a <fieldset> gets min-width:min-content from the UA stylesheet, and that OVERRIDES width and max-width. The rule said width:82% while the element computed 431px inside a 212px card. That is why "it has a width, looks fine" survived two rounds of this bug — the declaration was there and inert. Fixed with min-width:0 on the fieldset itself, flex-wrap:nowrap, and flex:1 1 0 on the stock <select>, which sizes to its widest option and was taking 188px of a 198px row. Measured through all three states: 431x35 at top 216 overflowing, then 78px tall at top 173 covering the preview, now 172x35 at top 216 — one line, below the 184x184 preview, inside the 212px card. Clean at 390/768/1024/1440/1920px with all 108 controls full size. The gate now measures the vertical axis and sweeps five widths. Mutation-tested against my own regression: restoring flex-wrap fails with "27 print rows COVER the preview image (37px)" at four of the five widths. 390px stays clean, which is exactly why single-width testing would have missed it a second time. |
||
|
|
f22609fb3c |
Clone the six global rules the component's own stylesheet never showed
The Driver looked at the two sides and named the components: "the tbabel
compoent is not the same", and of Prayer Coverage / Adoption Progress /
Engagement Progress, "thes three are not th esame css of the dx lfie site, it
properl spces and lcolros thigns while ours doe snot". He was right about both,
and for one reason: I ported RegionDetail.vue and its <style scoped>, and the
rules that differ are not in either. They are in the global sheet every page on
their site loads.
base/_globals.scss:99-105 every <table> is a 25px-rounded, #666-ringed WHITE
card; :205-207 gives every <td> a 1px #666
gridline. Ours was bare and borderless.
composition/_stack.scss .stack spaces children with margin-top, not gap —
and .region-goal{margin-top:auto} OVERRIDES it.
Ported as gap, which no margin can override, so
both applied and every goal card carried an extra
23.526px. That is his "spacing is not the same".
base/_globals.scss:19-23 every <section> is padded
clamp(--spacing-2xl,6vw,--spacing-4xl) top and
--spacing-4xl bottom: 73.614px on each side of the
countries table, where we had none.
base/_globals.scss:132-141 every <a> is --color-brand-light at weight 500
with a 2px/4px underline. Their country cell is a
NuxtLink; ours is a <button>, which inherits none
of it and read as plain body text.
base/_globals.scss:200-202 <strong> is 500, not the UA's bold. The "Goal:"
label wears it three times.
All five ported, and the remaining .stack ports converted from gap to their
margin-top so the next margin-overriding child behaves as it does on their
page. Measured against live doxa.life/regions/africa: 107 properties
identical, 0 CSS differences, 0 structural-text differences.
The measurement gap matters more than the fix. _probe-region-vs-live.mjs
reported 79/79 IDENTICAL through every one of these and could not have failed:
goalCard's spec was padding/radius/background/colour/align with no gap, so the
property that spaces the children was never read; table's was minWidth and
width; td had no border; and the country link had no entry in the selector map
at all. The spec now also reads gap and flexDirection on the card, marginTop on
its children, the td border, the table's radius/background/box-shadow/overflow,
section padding, strong weight, rowGap on all five stacks, and a new tdLink
pair — 107 properties.
Also, the Driver's other report on that screen: "poster not generated yet ...
it is not supposed to say not generated yet it is supposed to show the
previews". Ethiopia's country-eth.webp exists — 174 of the 175 records have
theirs, India being the one that legitimately does not and carrying its own
reason. Measured on the built tree and on the dev server: every preview request
200, zero cards in that branch, and the lazy contract holds (nothing fetched
until the dropdown is opened). So the file is not missing; a host layout is.
PosterWall now tries the self-contained copy generate-tree stages inside the
tree before giving up, and when it does give up it no longer claims the poster
was never generated — it says the preview is unavailable, which is the only
thing a failed image request establishes.
|
||
|
|
7ae1572c94 |
poster cards: the print-options row stays inside its own card
Some checks are pending
build-gate / build (push) Waiting to run
The repo-hygiene agent found this in a fresh 1440px screenshot, which is the whole lesson: .pfs already had width:82%, so reading the CSS said it was fine. Flex items default to min-width:auto and refuse to shrink below their content width, so four controls — Prayer List, Card Deck, the stock select, Poster Map — pushed the row open regardless of what the container declared. A constrained box does not constrain flex children, and only geometry shows that. Measured at 1440px on cap4-upg100-mfe: 27 of 27 cards overflowing, worst 110px past a 212px card, 36 intrusions into a neighbouring card's box. After flex-wrap:wrap plus min-width:0 on the children: 0 and 0, with all 108 print controls still a usable size. That second number is asserted deliberately. "Nothing overflows" is trivially satisfied by a row whose buttons collapsed to zero width, which would be a worse product than the overflow it replaced. check:poster-card-fits holds both, and is mutation-tested: reverting the fix reproduces the reporter's exact figures. Also adds check:backlog-refs — a card may not depend on a card that does not exist, because a dangling depends_on can never be done, so the card can never become ready and nothing says so. Its first run disproved the finding that motivated it. I had censused three dangling references, including the PRINCIPLE card the loop prompt lists as open work. The gate came back PASS, and chasing the discrepancy rather than assuming the gate was broken showed my census was wrong: it read only the builder backlog, and all three cards exist in 01-ideation-research. Cross-phase references are legitimate, and a single-backlog census structurally cannot see them. Reporting three dangling refs would have sent someone hunting for cards that were never lost. It fails only for depends_on on an OPEN card; a dangling parent is reported and passes, because failing on filing debt teaches people to delete the reference rather than restore the card. Both severities mutation-tested. |
||
|
|
6e958bc22f |
legend: open on the UPG-100 list, which is what the Driver asked for first
Some checks are pending
build-gate / build (push) Waiting to run
Measured at the rendered layer: the legend opened on REGION at desktop and mobile. That is the Driver's first complaint about this legend word for word — "it shouldn't have me sitting there on Regions, I don't know what to do, it's confusing" — and the card recorded that part as shipped. Every source signal said it worked. LEGEND_DEFAULT_TAB = 1 existed, defaultTab was a declared prop, it seeded a local ref, and it was named in a watch fallback. The root cause is one layer down. SemanticTreeLegend resolves its tab as `instance ? instance.activeTab.value : localActiveTab.value`, and usePplrInstance initialises activeTab to ref(0). Zero is indistinguishable from "the user chose tab 0", so for every profile sharing an instance store the defaultTab prop was dead — read on every render, never able to win. An activeTabTouched sentinel fixes it: the store only wins once something actually chose a tab, meaning a tab click or a node selection that jumps to that node's generation. A sentinel rather than a new default, so no other profile's landing tab moves as a side effect. And a regression of my own, from the previous commit. There was only one :default-tab left in the profile and it was on <LegendMobile>, where defaultTab is not a prop. Last tick I "dropped the inert attribute" with a string replace that matched the DESKTOP block first, deleting the working one and leaving the inert copy. A dropped attribute is silent, so the build stayed green. Restored to SemanticTreeLegend, and LegendMobile now has a real defaultTab prop so the phone honours it too. Both widths open on "UPG-100 Lists", and on ?lang=es both open on "Listas UPG-100" — locale-independent. A user's own choice still sticks: clicking Region holds at 0, and selecting a row does not snap back, so the sentinel is not a trap. check:legend-export-reachable still passes and now selects a real zone rather than a region, because the legend opens on the Lists tab. The two fixes compound: a phone user lands on the lists and can export the one they pick. |
||
|
|
cc6a5ea901 |
legend: a phone can now export the packet — it was a prop, not a design call
Some checks are pending
build-gate / build (push) Waiting to run
Last tick I found the export unreachable on mobile and recorded it as Driver-gated: "WHERE the control goes on a phone is a design call, the sheet has no titlebar". Both halves of that were wrong, and checking them was this tick. LegendMobile RENDERS SemanticTreeLegend itself — titlebar and all — and the 0x0 elements my measurement found belonged to the OTHER, hidden desktop instance mounted below the breakpoint. That geometry made it look as though the sheet had nowhere to put the control. A Driver decision was never needed. Two gaps, not one. The sheet passed neither :export-enabled nor @export, and exportEnabled defaults to false, so the control's v-if was never satisfied. Fixed via provide/inject, matching onSemanticTreeSelect immediately above it, because the sheet renders several levels down where an emit would not reach. Opt-in by construction: enabled only when a profile provides a handler. The other three LegendMobile consumers provide none and are verified untouched. Even then the export did nothing, and that second gap is the one that mattered. The profile passed :nodes but no :tabs, so hideTabs was true and the sheet showed a single flat Region tree with no tab strip — no UPG-100 Lists tab, no selectable zone, and onLegendExport bails on `if (!zoneId) return`. Passing :tabs fixed it. A :default-tab attribute was added and then removed: defaultTab is not a prop on LegendMobile, so it would have been inert. Verified end to end at 390x844: the tab strip appears, a real zone selects, Export opens, Packet clicks, and one print frame is produced. A packet on a phone. The gate written last tick — which failed on mobile by design — now passes at both widths. The newly visible mobile tabs are fully localized in en/es/zh, so catalog work from earlier ticks now reaches a surface that never showed tabs at all. Four of my own probe errors were caught by measuring rather than concluding. Clicking a REGION row left selectedKey empty, so the handler correctly bailed and it read as "export does nothing" — the same zero on DESKTOP is what proved it was the probe and not a regression. country-posters shows the same mobile gap on its own surface. Not taken; its owner's call. |
||
|
|
a206fe4deb |
Composed maps: stop the geocoder deleting every keystroke
Typing into the research-map's search bar inside a composite (the Simple
Research dashboard) wiped the box instantly and never queried. Standalone, the
same map searched fine — which is the shape of the bug, not a coincidence.
mapbox-gl-geocoder unwraps exactly ONE shadow level to find the input:
var e = t.target && t.target.shadowRoot ? t.target.shadowRoot.activeElement : t.target
if (!(e ? e.value : "")) { this.fresh = !0, 9 !== t.keyCode && this.clear(t), ... }
Its handler is debounced, so it reads event.target after dispatch has ended, by
which time the target is the OUTERMOST host. One hop is therefore exactly right
for a map one custom element deep and exactly wrong for a map inside another
one. Measured, same build, same keystroke:
doxa-research-map (1 deep) target=DOXA-RESEARCH-MAP -> unwrap = the input -> _geocode("Bhu")
research-map-plus-grid (2) target=RESEARCH-MAP-PLUS-GRID -> unwrap = <doxa-research-map> -> clear()
An element with no .value reads as an empty box, so the vendor cleared what you
had just typed. Walk the activeElement chain to the bottom instead of one step
and hand the vendor an event whose target is that element; nesting deeper cannot
break it again, and at one level it resolves to the same input as before.
Two things worth recording. The receiver is passed explicitly: the Mapbox build
binds this method in its constructor and the MapLibre build does not, and an
unbound call landed on `this === undefined`, which swallowed the keystroke AND
the query ("Cannot read properties of undefined (reading
'_hideGeolocateButton')") — green-looking text with no search behind it. And
neither suspect on the card was the cause: the clear came from inside the vendor
bundle, not from _geoBeingCleared, and SearchBar is not involved.
MEASURED on the built tree, both engines' mapbox mode, typing "Bhutan":
fix off leaf ok (1 geocoding request) / composite value="" and 0 requests
fix on both keep "Bhutan", both fire the query, both list 19 results
including People Cluster, People Group + Country, Country and Place
Screenshot read, not just the log. gates:pre 7/7, gates:post 34/34.
|
||
|
|
445131cf94 |
Merge main into bundler-3/work — settle the 21-file conflict, keep both campaigns
Only 2 files conflicted in source: package.json and its CLI doc, both taken to main's refactored gate pipeline (gates:pre/gates:post/build:bundles) after verifying every check the old inline chain named still runs there. The other 19 were generated: public/assets/tables mirrors keep main's deliberate deletion, and public/doxa-maps is regenerated by the build that follows. |
||
|
|
a177f98aff |
legend: the controls no longer read their own i18n key aloud
Some checks are pending
build-gate / build (push) Waiting to run
The legend's toggle button announced the literal string "aria.toggleLegend" to screen readers. Not in translated locales only — in English too. vue-i18n echoes the key when a lookup misses, and `toggleLegend` was absent from every catalog while its two siblings in the same file, backToLegend and closeDetailView, were present. Six call sites across LegendDesktop.vue and LegendMobile.vue, desktop and mobile alike. aria-label is the one string a sighted tester never sees. Every other untranslated string in this tree got caught because somebody looked at the screen; this one is only ever spoken aloud, so it survived every visual QA pass. A second instance turned up in the same sweep with the opposite shape: SemanticTreeLegend hardcoded 'Collapse legend' / 'Expand legend' into both title and aria-label with no t() at all — correct English, and untranslatable. One key missing FROM the catalog, one string never routed TO it. Adds toggleLegend, collapseLegend and expandLegend to all 11 aria.json catalogs and routes the hardcoded pair through t(). check:i18n-parity already guards them. New gate smoke:aria-labels asserts BOTH shapes, because either assertion alone is insufficient: "no raw keys" passes a hardcoded English label forever, and "labels differ across locales" passes a raw key, since a key is identical in every locale. It detects a raw key by SHAPE — dotted, camelCase, no spaces — which is locale-independent and works on the Arabic and Chinese pages where no English word list could. Mutation-tested twice. Removing the catalog key fails with the raw key named, on all four viewport/locale combinations. Hardcoding one label back to English initially PASSED: the differ count dropped 6 to 5 and nothing asserted on it — the same "a number printed but not asserted on is decoration" shape this tree keeps rediscovering — so the gate gained a LOCALIZED_FLOOR ratchet and now fails that too. Verified the fix generalizes beyond the profile it was found on: country-posters reports 182 of 190 accessible names localized, 0 raw keys. Rebuilds the six bundles that embed a legend; check:sri confirms every leaf matches. Wave 34/34. |
||
|
|
6af75895c7 |
legend: the info popovers explain the map in the reader's own language
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for the per-tab info buttons to stay — "the tab itself gives
you a little information button that will explain what a UPG-100 list is."
They existed, and they were English-only in every locale.
SemanticTreeLegend rendered `tab.info` RAW in all three places it appears (the
button's title=, and the popover body) while the tab LABEL beside it went
through the catalog. So a Spanish reader saw a translated chip next to an
English explanation of what that chip means — and the explanation is the part a
reader who does not read English actually needs. The three UPG-100 tab labels
were absent from tabs.json entirely, so they fell back to hardcoded English too.
Measured on the served leaf before changing anything: ?lang=es changed 2 of 76
rendered strings ("POPULATION" -> "POBLACIÓN") and left all three tabs and all
three info popovers in English. That positive control is what proves the locale
switch WORKS and the strings were simply never catalogued — without it this
reads identically to a broken ?lang=.
tabInfo() mirrors tabLabel exactly: `legend.tabInfo.<i18nKey|id>`, te() guard,
falling back to the hardcoded English so a tab with no catalog entry renders
unchanged rather than blank. The v-if stays keyed on the raw `tab.info`:
whether the affordance EXISTS is a property of the tab definition, while its
TEXT comes from the catalog.
Catalogued in all 11 locales, labels and info. check:i18n-parity already guards
them — deleting one zh key fails the build by name.
New gate smoke:legend-info renders the REAL served page and clicks each info
button. check:tabs-localized could not see any of this: it tests 7 hardcoded
research-map tabs and REIMPLEMENTS tabLabel inside page.evaluate, so it proves
the mechanism works rather than that a given profile is localized — and it never
looked at `info` at all. Mutation-tested: reverting the popover to the raw prop
fails with "the catalog is present and unreached".
Partial-translation contract verified by deleting 2 of 3 fr info keys and 1 of 3
fr labels: translated keys render French, untranslated fall back to English,
nothing blank, and labels and info resolve INDEPENDENTLY.
Rebuilds the six bundles that embed the legend so the tree stays internally
consistent; check:sri confirms all 55 script references match. Full wave 34/34.
|
||
|
|
87bd98d610 |
Restore the clickable poster preview — the last capability the migration dropped
Some checks are pending
build-gate / build (push) Waiting to run
Migrating my-upg-100-list onto the shared wall left one behind, and it was not
decoration: clicking the poster preview opened it for printing, a Driver-named
behaviour ("show the poster in the browser to help print it"). I removed it, so
this is a regression I introduced and am closing.
previewAction: 'none' | 'print'. Default 'none' keeps the image inert, which is
exactly what country-posters and key-cities render today.
★ IT IS A KEYBOARD CONTROL, NOT JUST A CLICK TARGET. The original carried
role="button", tabindex="0" and Enter/Space handlers alongside @click. Re-adding
the click alone would have been a half-restoration that LOOKS complete — an <img>
with only @click is unreachable without a mouse, and the focus-visible outline is
how a keyboard user sees where they are on a control that is otherwise a picture.
The alt and title change with the mode too: "prayer poster" and "click to preview
and print" are different promises to a screen reader.
buttons.previewPrintPoster added to all eleven locales in the same change;
check:i18n-parity still reports 0 keys of debt.
VERIFIED AT THE RENDERED LAYER, and exercised BY KEYBOARD rather than by mouse:
100-list (preview-action="print") — 27 previews, is-clickable, role=button,
tabindex=0, cursor zoom-in, translated title, descriptive alt. Focused the image
and dispatched ENTER: 1 print frame opened, 0 downloads, 0 errors, with every
.webp ABORTED so it ran through the rasterise fallback.
country-posters (default) — 174 previews, is-clickable false, role null,
tabindex null, original alt. Inert exactly as before.
That completes the consolidation: my-upg-100-list 890 -> 179 lines, and all four
walls now render from one 633-line component with nothing left unported.
|
||
|
|
649b0872eb |
my-upg-100-list renders from the shared PosterWall — 890 lines to 174
Some checks are pending
build-gate / build (push) Waiting to run
The original wall is gone: 716 lines of duplicated markup, CSS, export modal and print wiring deleted. This profile now supplies only what cannot be shared — REGION_OF/regionFor (its taxonomy, keyed by list id because a selector can be a wagf_block, a country_codes array or pg_slugs and none of those implies a continent) and fetchListRecords (the people-groups API, the selector evaluator and the pg_slugs special case). It joins country-posters (94 lines) and key-cities (75) on one component, which is what the Driver asked for: "don't recreate the wheel but imitate upg 100 list". It took three capability additions across three ticks, because migrating without them would have REGRESSED this wall rather than consolidated it: cardStocks, then posterAction="print" and hidePrayerListWhenSoon — the last two a Driver ruling, not a preference. ★ TWO REAL BUGS, BOTH INVISIBLE AT THE SOURCE LAYER, BOTH BUILDING CLEANLY. 1. groupBy SIGNATURE MISMATCH. regionFor takes an ID; the shared wall's groupBy takes the whole ITEM. `:group-by="regionFor"` compiled, the page loaded, and then threw `c.startsWith is not a function` at first render — the whole wall blank, zero cards, nothing wrong in the source. Adapted in the template rather than changing regionFor, since the id is what the taxonomy is keyed by. 2. A THROWN FETCH IS A MISS, NOT AN ERROR — and this one is a robustness gap in the SHARED component, affecting every consumer. fetch REJECTS rather than returning !ok when a request is blocked or offline, and exportPoster let that reject escape to the outer catch, skipping the rasterise fallback and showing "Failed to fetch" on a poster the browser could have generated from posters/<id>.html. The wall this component was EXTRACTED FROM already guarded it; the extraction did not carry the guard across. check:poster-print-fallback went PASS -> FAIL with 0 .html requests, and is green again. VERIFIED AT THE RENDERED LAYER, all three print actions on the migrated wall: 27 cards, 27 stock pickers, 4 buttons per card — the original's three prints plus show-on-map. CARD DECK 116 cards / 116 QR codes / @page 2.5in x 3.5in / 0 errors, identical to pre-migration. POSTER PRINT with every .webp aborted: 1 .html fetched, "generating the poster", clean finish. PRAYER LIST: 1 people-groups fetch, 0 errors — positive evidence the provider ran, since that print cannot work without it. REGRESSION CHECK: country-posters still downloads (0 print frames, 1 download), so the default is unchanged for the walls already on this component. |
||
|
|
9f076ffbc8 |
PosterWall can print the poster instead of downloading it — the Driver-rejected UX it still encoded
★ THE BLOCKER ON MIGRATING THE 100-LIST WAS NEVER ONLY A MISSING CAPABILITY. It was a Driver-rejected UX baked into the shared component, and migrating as planned would have shipped it. my-upg-100-list/poster-wall.vue:280 records the ruling verbatim (2026-09-23): "three print buttons, each opening an in-browser print dialog, no silent file download as the default action. A raw 'Download' button and a 'Prayer List: coming soon' placeholder were exactly the two buttons he objected to." exportPoster(), downloadPacket() and download() were REMOVED from that wall for that reason. PosterWall still had both: exportPoster() downloads a .webp unconditionally, at two call sites, and downloadPacket() renders the "coming soon" placeholder whenever there is no records provider. Consuming it would have reintroduced the exact two buttons the Driver named. Two props close that, both defaulting to today's behaviour so country-posters and key-cities are untouched: posterAction: 'download' | 'print' — 'print' opens the OS print dialog on the poster. BOTH delivery paths (the shipped .webp and the browser-rasterised fallback) now route through ONE deliverPoster(), so the modes cannot drift or be applied to only one of them. Printed blob urls are held until unmount rather than revoked at print time: the preview <img> is still reading from them, and revoking early prints a blank sheet. hidePrayerListWhenSoon — suppress the Prayer List button rather than show it as a placeholder. The placeholder stays honest where the feature is genuinely unbuilt; it is only wrong on a wall that DOES print the list. VERIFIED AT THE RENDERED LAYER, both modes, by counting the two outcomes that actually distinguish them: poster-action="print" -> 1 print frame opened, 0 downloads default (prop absent) -> 0 print frames, 1 download Counted at appendChild, where openPrintFrame attaches the iframe, rather than by polling for iframes — the frame SELF-REMOVES on afterprint, so polling finds none and reads as a failure. ⚠ Two probe mistakes worth recording, because both looked like product bugs: a HUD selector that matched <script>/<style> text and returned the page's whole token ladder, and a first click that landed on INDIA — which country-posters marks `unsupported` (855 groups do not fit a sheet), so it short-circuited with zero poster requests. Skipping cards showing .ph.is-soon fixed it; the next click fetched country-chn.webp and completed. |
||
|
|
a501f98e2c |
PosterWall can offer a card-stock picker — the capability keeping the 100-list on its own copy
Some checks are pending
build-gate / build (push) Waiting to run
The card's first step was a gap analysis: what does the 888-line my-upg-100-list wall render that the shared component's props cannot express? Diffed both templates. They are STRUCTURALLY THE SAME — wall/tag/sub, sections with head and count, a card with "N UPG · Show on map", main with image-or-placeholder, a Print Options fieldset, the same export modal. Exactly TWO real deltas: 1. the CARD-STOCK PICKER — closed here; 2. a clickable preview image (.img.is-clickable) — still open, and small. Everything else that wall "owns" is data and taxonomy: REGION_OF, sections, fetchListRecords. That is precisely what country-posters (94 lines) and key-cities (75) already supply to this same component. `cardStocks` takes true for every stock card-render.js supports, or an array for a subset; `cardStock` remains the DEFAULT selection. Default false is today's behaviour byte-for-byte — neither migrated consumer mentions the prop and both still build. ★ LABELS ARE DERIVED FROM card-render.js STOCK, never hand-written. The 100-list carries its own DECK_STOCKS array duplicating all five labels, beneath a comment saying they "come from the renderer's own STOCK table so this list cannot drift". That was the intent, not the implementation. Deriving them makes it true, and the migration will delete the duplicate. ⚠ The import is LAZY on purpose. usePosterPrints imports card-render.js dynamically to keep a 10KB renderer out of every wall's initial bundle; importing it statically for five labels would undo that for every consumer, including the ones that show no picker at all. Options fill in when the module resolves and the <select> is not rendered until then, so there is no flash of unlabelled ids. If the configured cardStock is not among the offered stocks, the selection falls back to the first that is, rather than printing at a size the reader was never shown. buttons.cardStockAria added to ALL ELEVEN locales in the same change: check:i18n-parity still reports 0 keys of debt. An English-only key would have broken the ratchet emptied two ticks ago. VERIFIED: the derivation unit-tested against the real STOCK table — all five stocks, a subset, a subset EXCLUDING the configured default (falls back correctly), and an unknown id (dropped, not rendered blank). Not yet exercised in a browser because the picker is gated on a `records` provider and NO wall passes :records to the shared component yet — the 100-list still holds its provider in its own file. Opting country-posters in temporarily gave 0 pickers and 0 errors, which is that gate working as designed. The migration will exercise it end to end. |
||
|
|
e390454d31 |
Map controls: restore the keyboard focus ring that outline:none removed
The brief says of the no-selection chooser: "Keep the dropdowns as the
keyboard/screen-reader path." Pin clicking is mouse-only by
construction, so that path is load-bearing — and nothing had ever
pressed Tab. There was zero keyboard coverage across 42 probes.
The path itself is sound, and is now asserted: 30 distinct focus stops;
the chooser's region keys are real focusable buttons and Enter on one
selects it ("All Regions" to "Africa") with no mouse involved; the
SELECT VIEW strip carries role=tab on both buttons with exactly one
aria-selected.
The defect that fell out: of those 30 stops, 29 showed a 2px ring and
one showed outline:none — button.mcb, the shared MapControlButton. Its
line 38 sets outline:none deliberately, to kill the webkit focus box on
click/tap alongside -webkit-tap-highlight-color, but it had no
:focus-visible replacement. So a sighted keyboard user tabbing onto ANY
map control in ANY bundle saw nothing at all. The accessible name was
already correct (aria-label mirrors title, fixed 2026-09-21), so screen
readers were never affected — only the people who navigate by eye and
keyboard, which is the very group outline:none was written for.
The fix is a :focus-visible rule plus a .dark variant. :focus-visible
is the correct half of the pair: it never fires for a mouse click, so
the original reason for outline:none still holds. Consumers re-checked
per RULE 2 — ShareButton, ZoomIn, ZoomOut, Fullscreen, Help,
ThemeToggle, Location, map-controls/index.js, my-upg-100-list and
research-dashboard's DashboardDataTable — all render the button
icon-only and none styled focus themselves, so the rule only adds a
ring where there was none. All five affected bundles rebuilt.
Proven both ways: Tab gives solid 2px rgb(26,115,232) with
:focus-visible true and the "Share" label intact; a real mouse click on
a fresh page gives outline:none with :focus-visible false.
Three of my own errors on the way, each caught before it shipped. I
first read a region key's name as "Africa315" and nearly fixed a
non-bug — the button already carries aria-label="Open the Africa
dashboard", which wins over textContent, and the label and count have a
real 7.5px flex gap. I put backticks in a CSS comment inside a template
literal, which ended the string and broke four of five builds;
check:sfc-parse caught it at once. And my mouse assertion failed twice
on correct behaviour: synthetic PointerEvents with a manual focus() do
not update the browser's pointer-modality heuristic, and clicking the
button that had just held keyboard focus leaves the page in keyboard
modality. Only a fresh page with no key ever pressed is a clean test.
Regression: 22 suites green at the time of commit, including new
keyboard and focus-ring suites.
|
||
|
|
7fd1c6bcd1 |
feat(i18n): the poster wall's own labels now translate
Last tick made the 11 locales selectable. That immediately exposed what was sitting behind the door: the wall's labels — Poster Map, Prayer List, Card Deck, Print Options, Show on map, Close — were hardcoded English, so a reader who chose Spanish got Spanish page furniture around English buttons. All six are now keys in buttons.json, translated across all 11 locales, plus the aria-label which interpolates the item name rather than concatenating it. ★ AND IT CLOSED TWO OLDER ORPHANS. buttons.json had 21 keys in English and 19 everywhere else: `share` and `shareSteps` — the Share button's title and its five-step explainer, rendered on EVERY map. Ten locales had been shipping an English Share button since those keys were added, invisibly, because vue-i18n falls back to English rather than erroring. Both are translated now. Verified in a real browser across four languages: the wall's buttons read "Lista de oración | Mapa del cartel" under ?lang=es, "Liste de prière | Carte de l'affiche" under ?lang=fr, and the Arabic text renders with dir=rtl — zero page errors in each. The Share button's title follows too: Compartir, Поделиться. The wall's `t` and `locale` come from the SAME useI18n instance, so a label and a printed card deck can never disagree about which language the reader chose. |
||
|
|
04f165efd3 |
feat(poster): share the print actions, keep the record fetching per bundle
The poster wall was generalised into library/components/poster/PosterWall.vue and two bundles migrated onto it — but the ORIGINAL, my-upg-100-list's 903-line wall, was left on its own copy. So the one wall carrying the real print actions is the one wall nothing else benefits from, and the shared component's packet path was still a "coming soon" stub. Every print fix made in the 100-list this flight (the card deck, the stock picker, the image_url fetch) was invisible to the three bundles on the shared component, and vice versa. ★ THE SEAM IS WHAT MADE THIS SHAREABLE AT ALL. The PRINT ACTIONS are common; the RECORD FETCHING is not. fetchListRecords in the 100-list knows the people-groups API, the selector evaluator and the pg_slugs special case; Key Cities and Country Posters have entirely different data shapes. usePosterPrints.js therefore takes a `records(item)` PROVIDER and owns everything after it — the HUD state machine, the in-place print frame, and the documents. That also keeps the bundles honest. A wall that CAN supply rows passes the prop and gets the list + card-deck prints; one that cannot omits it, the Card Deck button is not rendered, and Prayer List keeps its truthful "coming soon" instead of a button that fails at click time. Country Posters shows exactly that today: 175 cards, Prayer List and Poster Map, no Card Deck — because it has no provider yet, not because something broke. ⚠ ONE exportState, not two. The composable owns a ref by default, and a wall rendering its OWN state while these actions wrote to a private one would show no progress and swallow every error — the mechanism working and the feedback not, which is the failure mode this flight keeps finding. PosterWall hands the composable its own ref so both write to the surface the reader is looking at. Prints IN PLACE via the shared openPrintFrame, never window.open() — the mechanism the Driver asked for by name: "not supposed to open up a new browser, but should open up the print dialog directly in the browser". NOT DONE, and deliberately: the 100-list wall is NOT yet migrated onto the shared component. Its template is 107 lines against the shared 72 with a different structure, and it is a Driver-reviewed surface — that is a rewrite whose risk does not fit in one tick alongside this. The shared half lands first and is verified; the migration stays open on the card with this composable now waiting for it. Verified: all three bundles on the shared wall build; country-poster-wall renders 175 cards with zero page errors; check:poster-prints drives the composable in a browser and asserts BOTH directions. |
||
|
|
61dcd9b28e |
Merge main into bundler-1/work (regenerate the built tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The nine conflicts were all in generated artifacts under public/. Built files are not hand-merged: every bundle whose source either side touched was rebuilt, the tree regenerated against main's new generate-tree, and the hashes re-baked. Source merged clean — main never touched PosterWall.vue or en/regions.json, they only appeared in a two-dot diff because this branch changed them. |
||
|
|
ddde518a85 |
Regions: expand a region to its country posters, reusing the shared wall
Driver request: "a toggle or carrot added to the bottom of the region section that says view country posters, which will let you view country posters for each of the countries in the regions" — with View-on-map and Print, "the same reusable code from upg 100 list". This is a third sanctioned addition alongside the tab strip and the no-selection chooser. Almost nothing was built. library/components/poster/PosterWall.vue was already the shared, fully parameterised grid, and it already owned the card layout, the "Show on map" header button, the PRINT OPTIONS footer and the client-side raster. regions-list.vue supplies only data and a toggle — the same division country-poster-wall.vue states in its own header. - A "View country posters" toggle at the bottom of every region section, carrying aria-expanded and aria-controls, using the same caret SVG the header and footer links already use, rotated when open. - Opening a region renders that region's countries. Measured: 41 cards for Africa and all 41 previews LOAD (naturalWidth>0 — a 404 still yields an <img>, so the load is the real check). No assets were copied: public/assets/previews already holds 174 country-*.webp and generate-tree already stages them. - "Show Chad on the map" reaches the Region Dashboards tab with title "Chad" and hash "#chad", by mapping PosterWall's CustomEvent listId back through the country-selected the tabs already exchange. - India prints its own reason with the exact number from the data; China renders a real poster. Only India is unsupported — 855 groups and no sheet, against China's 154 on a 3x4. Two defects the screenshot caught and no assertion did: - PosterWall's root is position:absolute;inset:0, because in its own bundles it IS a pane. Embedded in a region card it painted 1440x1100 over the region's country list. Fixed by parameterising the shared component rather than overriding its CSS locally: new `embedded` and `sectionHeads` props, both defaulting to today's behaviour, so country-posters and key-cities are unchanged by construction. Both re-checked. Re-measured: root static, 1200px wide, panel below the grid, no overlap. - With one unnamed group the wall printed a section head reading the literal "Other" plus a count duplicating the region card's own summary. Suppressed by sectionHeads=false. The dataset moved to library/data/country-posters.json now that it has a second consumer, one import line changed in country-posters, and emit-countries.mjs was retargeted so the next generator run cannot recreate the duplicate. The three new strings do not exist on DX-life, so there was nothing to copy and nothing was invented in another language: en only, with the other seven locales falling back to the key. Regression: 15 suites green, including region-vs-live 73/73, country-vs-live 28/28, mobile 44/44 and 26/26, and a new _probe-region-posters.mjs. check:library-index drifted for real this time (3 new library files) and is regenerated here. |
||
|
|
ed27b40e66 |
Put the tables in the house palette instead of Tabulator's stock theme
Every table in the product was shipping Tabulator's DEFAULT theme — #333 header, #666 rows, #444 filter boxes — against a house style that is Material LIGHT and has no dark mode. Measured identically in both served consumers before the change: row rgb(102,102,102), header rgb(51,51,51). OVERRIDDEN BY SPECIFICITY, NOT BY ORDER, and that is the load-bearing part. The vendor sheet is fetched and appended AFTER these rules, so any tie goes to the vendor. Each override is written one class deeper than the rule it replaces: four for the column header, five for the filter inputs, four for the paginator. Re-ordering the injection would have been the fragile fix — it breaks the first time injectCss is made sync. Getting that wrong is visible in stages, which is why it took three passes: at three classes the columns stayed dark while the header went light; at four the filter inputs were still #444 and the "Page Size" label was rendering white on a now-light footer, invisible. The computed-style probe reported the header and row fixed and said nothing about either — both were caught by looking at the screenshot. Dark mode is left alone. .dt.is-dark is three classes and still beats these, so callers using it keep it; this repo ships light only, but silently removing another consumer's dark mode is not this change's business. Blast radius named and checked: three consumers import DataTable — gospel-resources, cities-internet and key-cities. The first two are the ones with a served table page and both were re-checked, by computed style (row #fff, header #f3f3f1 in both) and by reading both screenshots. key-cities ships no data-tables page, so it could not be exercised live. The gospel-resources link and deep-link probes both still pass, so the restyle did not disturb the anchors the table exists for. |
||
|
|
874186660e |
Merge main into bundler-2/work
# Conflicts: # public/doxa-maps/index.html # public/doxa-maps/manifest.json # public/doxa-maps/my-upg-100-list/my-upg-100-list.js # public/doxa-maps/my-upg-100-list/poster-wall/index.html # public/doxa-maps/my-upg-100-list/upg-100-list/index.html # public/doxa-maps/my-upg-100-list/upg-100-mfe/index.html # public/doxa-maps/regions-dashboard/region-dashboards/index.html # public/doxa-maps/regions-dashboard/regions-dashboard.js # public/doxa-maps/regions-dashboard/regions-list/index.html # public/doxa-maps/regions-dashboard/regions-mfe/index.html |
||
|
|
d154a76f64 |
feat(posters): generate the poster in the browser when none is shipped
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "it would be a big win to not have to have 80mb of images in a public folder and let the posters get generated by the website." Downloading a poster used to end at `poster not found (404)` whenever the pre-rendered .webp was absent. It now falls back to rasterising the poster's own HTML in the browser — fetch posters/<id>.html, inline its stylesheet into the <svg>, draw to a canvas, hand back a blob. Same move the prayer packet already makes: generate on click, ship nothing. This unblocks dropping the 41 MB of committed .webp posters without degrading a working feature, which is the part that had to come first. It also covers the KEY CITY poster wall for free — both walls share this component. ⚠ AN SVG LOADED THROUGH <img> IS AN ISOLATED DOCUMENT: it cannot see the host page's CSS, so every var() falls back to its initial value and `fill`'s initial value is BLACK. The packet shipped exactly that bug once (660 of 661 map elements black, no console error). The stylesheet is therefore inlined INTO the svg, and the result is checked by measuring pixels — a blank render throws rather than handing over a black rectangle. Verified by check:poster-raster: laos rasterises to 3000x4000 with luminance 229 and 54 distinct colours; strip the <style> block and it drops to 13 and 2 colours and the gate fires. WHAT I MEASURED AND REJECTED ALONG THE WAY, so nobody repeats it: shipping the poster HTML *instead of* the webp looked like a clean win on the first sample (laos: 115 KB gzipped vs 213 KB webp) and does NOT generalise — poster HTML scales with geometry complexity while webp is a fixed-resolution raster, so asia-india-rajasthan is 414 KB gzipped vs 239 KB webp. Generalising from one sample is how that idea nearly shipped. Also measured: 351 of the 378 committed posters (35.4 MB) are city-* posters belonging to the Key Cities feature, not to any of the 29 UPG-100 lists. They are a live feature, not dead weight — which is why this lands as a fallback in the shared component rather than a deletion. encodeURIComponent, not btoa, for the data URL: the posters carry non-Latin-1 people-group and place names and btoa throws on those. |
||
|
|
305fd93c97 |
Merge main into bundler-2/work
# Conflicts: # public/doxa-maps/index.html # public/doxa-maps/manifest.json # public/doxa-maps/my-upg-100-list/my-upg-100-list.js # public/doxa-maps/my-upg-100-list/poster-wall/index.html # public/doxa-maps/my-upg-100-list/upg-100-list/index.html # public/doxa-maps/my-upg-100-list/upg-100-mfe/index.html # public/doxa-maps/regions-dashboard/region-dashboards/index.html # public/doxa-maps/regions-dashboard/regions-dashboard.js # public/doxa-maps/regions-dashboard/regions-list/index.html # public/doxa-maps/regions-dashboard/regions-mfe/index.html |