Commit graph

27 commits

Author SHA1 Message Date
doxa-maps-88655c70
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.
2026-09-25 07:47:06 +00:00
doxa-maps-88655c70
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.
2026-09-25 07:30:14 +00:00
doxa-maps-88655c70
b197055bf0 Print a UPG-100 list straight from the map legend
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-25 07:10:23 +00:00
doxa-maps-88655c70
89bd9c5d79 Poster wall: the banner gets stickers, top and bottom
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-25 06:41:56 +00:00
doxa-maps-88655c70
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.
2026-09-25 05:15:58 +00:00
doxa-maps-88655c70
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 6603dc17.

★ THIS IS PREVENTIVE, NOT A FIX FOR AN OBSERVED BREAK, and the distinction
matters. MEASURED: no bare bundle currently consumes either component, so the
blank page cannot be reproduced today — which matches the shepherd's framing
("LIVE today: none; latent debt"). What is fixed is the embedding hazard, and
the gift policy asks for plugins that work "standalone AND embedded".

⚠ THE FALLBACK IS DERIVED, NOT A TABLE. Hardcoding English for these eleven
keys would put a second copy of the `en` catalog inside the component, and two
copies of one fact drift — the exact trap behind the embedBase asset-path bug
this week. Humanizing the key's last segment gives "buttons.prayerList" ->
"Prayer List", "buttons.cardDeck" -> "Card Deck", "legend.columns.name" ->
"Name": readable English that CANNOT go stale, rather than a raw key shown to a
reader. te() answers false so every caller falls through to its own default.

VERIFIED: the fallback produces the correct English for every real key these
components use; with i18n present nothing changes —
check:legend-info-localized PASS (es/ar/zh still translate, 0 blank),
check:poster-card-fits PASS across 5 widths x 3 locales,
check:a11y-button-names PASS, SRI consistent.

I also confirmed 6603dc17 is an ancestor of my HEAD before pushing, per the
shepherd's warning: the FilterDropdown fix is intact in my tree, not reverted.
2026-09-25 04:02:56 +00:00
doxa-maps-88655c70
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.
2026-09-24 23:17:42 +00:00
doxa-maps-88655c70
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
2026-09-24 22:05:31 +00:00
doxa-maps-88655c70
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.
2026-09-24 22:04:28 +00:00
doxa-maps-88655c70
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.
2026-09-24 21:59:34 +00:00
doxa-maps-88655c70
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.
2026-09-24 21:26:49 +00:00
doxa-maps-88655c70
c57b51044f Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
# Conflicts:
#	public/doxa-maps/index.html
2026-09-24 19:56:38 +00:00
doxa-maps-88655c70
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.
2026-09-24 19:54:43 +00:00
doxa-maps-88655c70
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.
2026-09-24 19:46:36 +00:00
doxa-maps-88655c70
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.
2026-09-24 19:19:02 +00:00
doxa-maps-88655c70
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.
2026-09-24 10:02:22 +00:00
doxa-maps-88655c70
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.
2026-09-24 09:54:40 +00:00
doxa-maps-88655c70
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.
2026-09-24 09:40:31 +00:00
doxa-maps-88655c70
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.
2026-09-24 09:24:29 +00:00
doxa-maps-88655c70
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.
2026-09-24 05:44:06 +00:00
doxa-maps-88655c70
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.
2026-09-24 04:56:31 +00:00
doxa-maps-88655c70
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.
2026-09-24 04:26:29 +00:00
doxa-maps-88655c70
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.
2026-09-24 03:05:17 +00:00
doxa-maps-88655c70
ad6da54674 Checkpoint the swarm's in-flight work
Some checks are pending
build-gate / build (push) Waiting to run
Five lanes had 465 insertions across 18 files sitting uncommitted, some for
15 minutes. That is the state that silently lost the shell-scroll fix earlier
today: a verified fix existed only in the working tree, a second agent's edit
to the same file overwrote it, and the reverted version got committed and
described as the fix.

Nothing here is finished and no card is being closed on it. This is a
snapshot so the work survives an overwrite, taken at a moment when every file
had been quiet 15s or more, so no agent was mid-write.

Verified coherent before staging rather than committed blind: template-literal,
mjs-parse, sfc-parse, no-orphan-gates, kpi-unit, composite-reg, script-flags,
quoted-scripts and backlog-status all PASS. check:synced-css fails on
PeopleGroupDetail.vue and doxa-simple-map.vue only -- the same two files and
the same counts as before the swarm started, both untouched by any agent, both
already carded.

Two untracked files included because both are load-bearing: internal/
check-facet-one-field.mjs is wired into package.json with a self-test, and
library/utils/viewportScrim.js is imported by DataTable, SemanticTreeLegend
and PosterDialog -- a shared helper for the modal-scrim fix rather than six
copies of the same workaround.
2026-09-22 21:42:18 +00:00
doxa-maps-88655c70
9dc4dda8cf consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.

The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.

Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
  - join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
  - tauri frontendDist '../public' -> '../../public', since src-tauri is now one
    level deeper; check:tauri caught this exactly as the card predicted
  - the WordPress contract test's dirname(__DIR__,2) -> 3, same reason

Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00
doxa-maps
35ac1c5607 docs: simplify all folder READMEs (what's here + graphic + link); migrate complex reference to docs/architecture/{data-sources,i18n,components,app-wrappers,poster-generation}.md 2026-09-11 18:50:31 +00:00
doxa-maps
d5ba21fa76 doxa-maps — portable bundler for embeddable JAMstack maps
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.

History reset to a single clean commit; no PII, IPs, or internal terms.
2026-09-10 01:53:07 +00:00