At rest on a phone the search row sits where the gallery's 44px ☰ circle lands (bottom-left, or
bottom-right in Arabic), so the circle covered the start of the search box. The row now starts at
--map-fab-beside, which the layout publishes only while the circle shows. Everywhere else the
value is 0, so the change does nothing until the circle comes back.
The filters toggle turned every svg in the button over when the filters were open. So the filter
icon beside the label flipped as well, with its longest line at the bottom, which read as a broken
icon. That shows on a phone, where the grid opens with its filters expanded. Only the chevron turns
over now.
The ⓘ is now the column's top control, and its one-time pointer runs sideways from it across the
map's top band. On a phone that band is these maps' legend (title and fold button), and MapToolbar's
phone rule put the column at top:72px inside it, so the pointer lay over the legend's title and
buttons. Below 700px the column now starts 8px under the FOLDED legend's measured bottom, whatever its
height in each language. It is measured only while folded and before the toolbar exists, so opening
the legend never moves the controls. Desktop is unchanged.
Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
The research map draws the ⓘ as the first button of its own control column (MapInfoButton), and the
gallery's Simple Research row switches to trigger: 'control' in the same commit, so the tab never
shows two ⓘ, and never shows none. At mount the map tells the gallery that its control is coming, so
the floating ⓘ does not flash while a slow phone loads. A failure or an unmount withdraws that
promise, and the floating ⓘ comes back at once. On a page with no gallery nothing answers, and the
map is unchanged.
Their control column renders only once the map exists, after the data, which can take tens of
seconds on a slow phone. At mount each map now tells the gallery's MapInfo that a control is on its
way (Prayer Progress's announcement), so MapInfo waits for the map instead of flashing its floating
ⓘ first. A failure or an unmount before the map exists withdraws the promise, so the floating ⓘ
shows at once. Gospel's single-card page builds no map and promises nothing.
Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
The ⓘ becomes the first round control in each map's own column (MapInfoButton, as Prayer Progress
has it), and each tab's gallery row says trigger: 'control', so there is exactly one ⓘ: the control.
MapInfo keeps its fail-open, so a page where the control never arrives still gets the floating one.
Rebuilds cities-internet, gospel-resources and doxa-maps-spa, with their pages and the two manifests.
Every map whose ⓘ is its own control (MapInfo trigger 'control') now gets the same pointer:
it shows the first time that control is on screen in a page load, and goes for the rest of the
load once the reader opens the popup or leaves the tab. A full reload shows it again. This is
page-load memory, never storage, so the popup offers no checkbox in control mode: the "Keep
tooltip turned on" box and its string (all 11 locales) are removed, along with the pointerKeep
prop that selected this model per tab. Floating-mode maps keep their stored model and their
"don't point again" box, unchanged.
Leaving the tab is read from the pane (hidden or inert), on the control's intersection change
and the existing 1.5 s measurement tick.
Rebuilt doxa-simple-map (it ships the mapInfo catalogue) and doxa-maps-spa, then tree + SRI.
Prayer Progress draws its control column only once the style and the people-group data are in,
which on a slow phone can take longer than any fixed wait, and then the gallery's floating ⓘ
appeared and was swapped for the control. Now the map announces at mount that a control is on its
way ('dxmaps:mapinfo-pending'), and MapInfo waits for the map instead of a clock.
- A known failure (the map reports an error) or an unmount before the map is ready withdraws the
announcement, and the floating ⓘ shows at once.
- While a control is pending, the 30 s wait becomes a 90 s backstop (time on screen), so an
unreported failure still gets a floating ⓘ.
- A map that never announces anything keeps the 30 s wait, and pages with no MapInfo are unchanged.
Rebuilt doxa-simple-map and doxa-maps-spa, then tree + SRI.
The dashboard header gains an info button ("What can this map do for you?") to
the left of Share. Both are rounded squares at the vertical tabs' radius
(--vtl-radius), matching the app's own roundedness; map controls stay round.
Built as a reusable, parameterized header-button form so other dashboards adopt
it by prop, not by copy:
- MapControlButton: new shape="rounded-square" (border-radius: var(--vtl-radius,8px)).
- ShareButton / MapInfoButton: a shape prop forwarded to MapControlButton;
ShareButton also squares its rotating ring so a round ring never wraps a square.
Defaults stay 'round' — every existing consumer is unchanged in behaviour.
- research-dashboard DashboardTopBar mounts the ⓘ and Share as one header-button
group, sized to the bar with the 44px tap target kept for both. DOM order
[ⓘ, Share] puts ⓘ at the logical start: left in English, right of Share in
Arabic (the flex row reverses under rtl).
- doxa-maps-spa dashboard-plus-grid tab -> trigger:'control', so the tab's
MapInfo is driven by the header ⓘ and the gallery's floating ⓘ is suppressed:
exactly one ⓘ, fail-open. No new strings (reuses mapInfo.open, 11 locales).
The control column goes back to the library's round, spaced buttons at every width; the square
grouped card read too compact, on phones most of all. The ⓘ stays the top control.
In control mode, MapInfo no longer shows its floating button before the map's control has
announced itself. The map draws its column only once it is ready (style and people-group data),
so the floating ⓘ appeared for a moment and was then replaced by the control. The wait is bounded
at 30 s of time on screen, counted only while the pane is visible and not inert: measured column
arrival times run from 7 to 19 s on a local serve, so a shorter wait expired inside a normal load
and flashed late. If no control arrives, the floating button shows, so there is still always a way
to open the popup. Once a control has announced itself, measurement alone decides, as before.
MapInfo's plain <script> block (the page-load pointer memory) now sits above <script setup>, so
check:setup-order no longer reads it as used before its definition; its explanation moves into
<script setup> so the library index keeps the component's own description. Compiled output is
unchanged by the move.
Rebuilt doxa-simple-map and doxa-maps-spa (the only bundle that compiles MapInfo.vue), then
tree + SRI.
Three rules for work that spans plugins, in section 3 (Reuse library/ first):
- A component that could serve more than one plugin is built once in library/,
driven by props, and landed through the build owner, so other plugins adopt it
instead of copying it. This replaces "Graduate late".
- A new pattern starts on one plugin, built by its owner. A maintainer approves it on
the dev server, and only then do the other owners adopt it in their own plugins.
- Map controls are round and spaced: MapToolbar's default column of round
MapControlButtons, not its grouped square card. Dashboard header-bar buttons are
rounded squares with the vertical tabs' radius, var(--vtl-radius, 8px).
- pointerKeep maps (Prayer Progress only) point ONCE PER PAGE LOAD: the pointer shows the first time
the map is on screen in a load. Opening the popup hides it for the rest of that load, and the next
full load shows it again. The memory is a module-scope Set (pointerDoneThisLoad), because the gallery
remounts a heavy tab's MapInfo on every switch back. It is marked when the pointer is on screen, not
on mount. Nothing about it is persisted any more (no seen write in show() or hide()).
- The keep box (still stored, per map) keeps the pointer after the popup closes and on every
appearance. A page-load pointer ignores the every-map off switch, which is not offered beside it.
- The label is "Keep tooltip turned on" (en), with de retuned to match; the other nine already say
"keep ... on".
- Rebuilt doxa-simple-map and doxa-maps-spa, then tree and fix:sri.
posterSheets.js and poster-render.js are compiled into every bundle that prints
a poster; landing-preconditions step 4 named regions-dashboard, my-upg-100-list
and country-posters as stale. Each built with BUNDLE, then tree, then fix:sri.
- Prayer Progress's column becomes the engine's square grouped card (controls="square"). Its first
control is MapInfoButton, which opens the gallery's MapInfo for the tab. The gallery's tabInfo
'prayer-map' entry now uses trigger 'control', so the floating button hides while that control is
on screen, and shows again (fail-open) whenever it is not.
- MapInfo, control mode: the first-visit pointer now points at the control. Its place is measured
against the pane (which scrolls) and goes on the control's physical inner side, so an Arabic page,
whose column mirrors to the left, gets the pointer on the right with the arrow reversed. It shows at
every width: the under-1200px rule was measured for the floating pointer crossing each tab's top
strip, and does not apply here.
- MapInfo, pointerKeep (prayer-map only): the popup offers "Keep tooltip activated" instead of the
every-map "don't point again" box. It is off by default, and opening the popup retires the pointer.
Checked, the pointer keeps showing on every load. It is stored per map.
- mapInfo.keepPointer in all 11 locales.
- Rebuilt doxa-simple-map and doxa-maps-spa, then tree and fix:sri.
The country poster previews were the dense layout -- small facts table, both
vision columns, a smaller map -- while every UPG-100 thumbnail is SPARSE. Now
that the click-time print is SPARSE too, the stored country previews have to be,
or the card and the print disagree.
Generated with SPARSE=1 and checked BEFORE any browser time:
· the generator's own layout guards: 173 of 173 ok, octilinear, 0 diagonals.
· SPARSE on all 173: 0 sheets draw a dense-table element; all 173 carry the
bold "N UPG's" count.
· real values: 0 sheets show 0, NaN or blank population. Worst cases by UPG
count: Sudan 105 / ~20.7M, Brazil 103 / ~63K, Laos 95 / ~968K.
Shot on the fleet's shared Chromium (no private browser), one page at a time:
173 of 173, 0 failed, all 3000x3000, none blank. Smoke-tested and looked at
first: Sudan shows the bold facts row, no vision columns, "1 of ~170 maps".
ALL 173 CHANGE: 173 source thumbnails, 173 full-size and 173 served previews
differ from development, 0 strays, nothing written to a stray library/ path.
China is deliberately NOT here: its thumbnail stays the dense "map 174 of 174"
it has tonight. Its print becomes SPARSE with "map 1 of 1" via the small-total
guard. The remaining numbering difference is R-CHN-SERIES-TIER, a product call
after the demo.
A v-show-hidden button is still a child, so it took the square toolbar's
:last-child corners (MapToolbar controls="square") from the last button a
reader can see, on every page where no host answers (a standalone leaf, a
partner page). Found by exp reading the bridge.
The control now asks from the toolbar's own element (MapToolbar provides it
as mapToolbar.el), or from its placeholder's parent, and renders its button
only once a host has answered (v-if). The rendered button then announces
itself, so the host measures and focuses the real element.
Cities with Internet's and Gospel Resources' fold buttons translated "key" literally: "Clave" and
"المفتاح" name a key for a lock. They now use each language's word for a map legend, the one the
maps already use elsewhere: "Leyenda" and "وسيلة الإيضاح".
Rebuilds cities-internet and gospel-resources, with their pages and the two manifests.
The "what can this map do for you?" button can now live IN a map's control
column instead of floating over it. This is the bridge only: nothing changes
until a map adds the control and its gallery tab sets trigger: 'control'.
- map-controls/MapInfoButton.vue (new): a MapControlButton with the info
icon and the localized mapInfo.open label. Clicking it sends a bubbling,
composed 'dxmaps:mapinfo-open' event out of the plugin's shadow root to
the gallery pane. On mount it asks 'dxmaps:mapinfo-control' and shows
ONLY if a MapInfo on the same pane answers (ack), so a standalone leaf or
a partner page never shows a button that opens nothing. A MapInfo that
mounts later broadcasts 'dxmaps:mapinfo-host' and the button asks again.
- info/MapInfo.vue: new prop trigger, 'floating' (default, unchanged) |
'control'. In control mode it listens on its own pane: it acts only on
events whose composedPath contains the pane, and stops them there. Its own
floating button and pointer hide ONLY while the announced control is
MEASURED usable: connected, a non-zero on-screen rect, and in no
inert/aria-hidden subtree. It is re-measured by IntersectionObserver and
ResizeObserver plus a 1.5 s fallback timer, never inferred from an unmount
event. Escape and close return focus to the control when it is on screen,
else to the floating button. aria-expanded is kept on the control.
Proved on a UPG-100 pilot served locally (gallery page, 1440, MapLibre and
Mapbox, en and ar), exactly one info button on screen at every step:
wall 1 floating; Map 1 control (it wins its hit test and sits inside the
toolbar column); opened from the control, Escape closes it with focus back
on the control; toolbar hidden 1 floating, shown again 1 control; tab away
and back 1; back to the wall 1 floating. 0 page errors. That pilot's wiring
is not in this commit.
The SPARSE series line rounds its total to tens -- 173 reads as ~170, 28 as ~30
-- because a series that large is read as a size, not counted. Below ten the
rounding breaks: China is the only 3x4 country sheet, so its tier has one member,
and Math.round(1 / 10) * 10 = 0 printed "1 of ~0 maps".
Under ten the line now states the exact count, "map 1 of 1" -- the same text
the dense layout already prints for China, so the move to SPARSE is not a
regression for it on the print.
PROVED: China rendered SPARSE reads "— map 1 of 1" with 0 occurrences of "~0";
and Sudan rendered after this change is BYTE-IDENTICAL to Sudan rendered before
it, so no series of ten or more changes -- the 173 country sheets and the
UPG-100 zones are unaffected.
Not decided here: China's thumbnail says 174 of 174 and its print 1 of 1. That
is per-tier against whole-series numbering, the pre-existing R-CHN-SERIES-TIER,
left for a product decision after the demo.
poster-render.js is sha384-pinned; re-pinned so check:poster-render-pinned
accepts it.
The bold header on a poster -- the UPG count and population set large, a
bigger map, and no "Jesus' Strategy to Feed the 5000" / "What is a Prayer List"
columns -- is the SPARSE layout, the "thumbnail-worthy" design. The `posters`
script generates with SPARSE=1, so every stored thumbnail has it. But the env
handed to the generator in the browser carried only FORMAT and POSTER_DATE, so
EVERY sheet drawn at the click -- UPG-100 and country alike -- fell back to the
dense layout.
MEASURED on FINAL (two UPG-100 zones, Northwest and South-Central India): the
thumbnail shows the SPARSE header; the print the reader actually gets shows the
small facts table, both vision columns and a smaller map. That is the reported
"the country poster looks worse than the UPG-100 poster" -- the comparison was a
SPARSE thumbnail against a dense sheet.
(a) posterSheets.js passes SPARSE for every POSTER format and NOT for 'packet'.
That is exactly the split the two build scripts make: `posters` sets
SPARSE=1, `packets` does not. The renderer's formats are 3x4, 3x3, letter and
packet, so 'packet' alone keeps the full layout its pages were built around.
(b) check:poster-browser-equiv's poster case now runs the Node side with
SPARSE=1 as well. It used to compare dense with dense, so it passed while no
reader ever saw that layout on a card. The packet case stays dense on both
sides. PASS: country-npl 909026 bytes on each; asia-india-gujarat (packet)
835047 on each.
The stored UPG-100 thumbnails are untouched by this. What changes is the PRINT,
which now matches them.
Fully open, the legend took 297px of a 712px phone map, 86% of its width, with the India-by-state
block and both caveats always showing, and there was no way to shut it. On a phone (under 700px,
Gospel Resources' breakpoint) the title stays and the rest folds behind "Key & notes": folded by
default, remembered for the session, and a remembered choice wins over the width default.
Desktop is unchanged. Two new strings, in all eleven locales.
Rebuilds cities-internet only, with its three pages and the two manifests.
Reported from a phone: on the Regions tab, "2 posters per row" for the country
posters did not work -- a 390px phone showed ONE per row.
THE CAUSE is a deliberate clamp in PosterWall. The per-row setting is 2, but the
grid is Math.min(perRow, fits) with fits = floor((viewport - 36) / 210), and at
390 that is floor(354 / 210) = 1. So the setting said two and the phone silently
showed one.
★ WHY THE CLAMP EXISTED, AND WHY IT IS SAFE TO RELAX NOW. It was added when a
two-up card (~162-170px) CLIPPED its action labels -- 'Prayer List' and 'Poster
Map' both read 'Pra...', and Romanian's 'Pachet de cartonase' was measured at 54
of 81 controls clipped at ro/390. That was then fixed separately: the labels WRAP
(.pfs > .pbtn, "a label too long for its box wraps"). With wrapping in place the
clamp no longer protects anything; it only costs the phone a column.
★ IT IS OPT-IN, NOT A GLOBAL CHANGE. PosterWall is shared by twelve files,
several of them frozen experimental maps. A new minCardPx prop defaults to 0,
which means the existing 210px clamp, so every wall that does not set it lays out
byte-for-byte as before. Only regions-list sets 150, which gives two per row at
360, 390 and 414, still one at 320 (which genuinely cannot hold two), and no
change on desktop (already capped at the per-row setting of 2).
MEASURED on the regions-list leaf page, this branch served locally, against
FINAL on :4611 -- by hit test and by eye:
FINAL @390 en 1 per row, 367px cards
AFTER @390 en 2 per row, 169px cards, 0/123 labels clipped
AFTER @390 ro 2 per row, 0/123 labels clipped <- the old clipping corner
AFTER @390 ar 2 per row, RTL, 0/84 labels clipped
AFTER @1440 en 2 per row, unchanged
0 horizontal overflow and 0 console errors in every cell. The print buttons now
stack three high on a phone card instead of sharing a line.
KNOWN, COSMETIC, NOT CHANGED: at two-up the card's "53 UPG · Show on map" pill
wraps "Show on / map" onto a second line. Readable, not clipped.
On a phone under DX Life's header, the band ran 32px past the fold (y132 to y876 on an 844px
screen). The legend sheet's bottom and its collapsed strip began below the fold, so a reader who
collapsed the legend could not reopen it without scrolling. The band now takes the height that is
visible below its own top, capped at the old 88svh, which is also the fallback. It is measured once,
and again only on a width change, so the URL bar showing and hiding does not resize the map.
The word is a military term, and the Driver asked for "prayer movement(s)" / "mobilization movement"
instead. The five user-facing strings in all eleven locales (55; the translated word too: Kampagne,
campaña, حملة, अभियान, 宣传活动 …) now say movement, in each language's natural church usage: Russian
"движение" with "вдохновить" rather than "мобилизовать", Arabic "حركة" with "تحفيز" rather than "تعبئة",
Chinese "宣教动员" rather than 宣传 (propaganda) or 运动; the Portuguese lines move to Brazilian forms like
the rest of the catalogue. The instance title and the meta note follow, and the CSV exports' column
headers become bridge_language / bridge_language_code (a user sees them in every spreadsheet).
Identifiers stay: the "view":"campaigns" profile-config value (embeds pass it), the i18n key names,
file and function names, and the campaigns-server references.