27 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 | ||
|
|
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. |