- library/components/WARNING.md, library/dashboards/README.md and
documentation/contributing/README.md taught "graduate into the library at the second
consumer". They now teach the plugin-designer skill's rule (section 3): a component that
could serve more than one map is built once in library/, driven by props, and landed through
the build owner; one-off or experimental pieces stay in a map's folder; a new pattern is a
pilot, then approved, then rolled out. The contributing guide's "full reasoning" link pointed
to an index without it; it now points to the skill.
- The app shell doxa-maps-spa is labelled Deep Research. Ten docs still called it the Research
Gallery (15 mentions): DEPLOY.md, the desktop how-to, embeddability, develop-a-plugin,
reference-universal-props, reference.md, the shell's EMBED.md, and the plugin-designer skill
with two of its references. Identifiers (doxa-maps-spa, branch and route names) are unchanged;
README.md is not touched.
Prayer Mobilization now reads Prayer Progress, UPG 100 List Posters, Country Posters, then Poster Design.
The row mounts the Styles + Sizes composite (bundle poster-design, profile poster-design-mfe), uses the same
tab id as the Posterverse shell, and carries the same label and subtitle in all 11 languages. As a curated
row it never appears on the Experimental shelf.
The tab layout takes an optional homeHref and homeLabel from its host. When a URL is given it draws a
link directly above the rail's Collapse button, in the same box, and a phone shows it at the top of the
drawer. It mirrors in Arabic and slims with the rail. With no URL nothing changes, so the Plugin Studio
and every other host look exactly as before. A javascript:, data: or vbscript: URL is ignored.
The gallery accepts home-href and home-label (or profile-config homeHref and homeLabel) and passes them
on; its own "Back to homepage" is the fallback label in all 11 languages.
Prayer Mobilization now reads Prayer Progress, UPG 100 List Posters (renamed in all 11 languages), then
Country Posters, which mounts its composite view and leaves the Experimental shelf. Posterverse, the
second shell, is kept off this shelf so a shell never opens inside a shell.
The embed guide opened with prose and a code block. It now leads with the
existing screenshot of the gallery embedded by script on a partner page,
then a two-column table (script vs iframe): what to paste, sizing,
language, deep link, keyless maps, and what the server must send. Every
row is behaviour measured this session. Then it names the seven
production maps in their sections.
Arabic did not flip the shell (reported by studio for matrix cell E). The
layout is written in physical terms: rail at left:0, content at left:256px,
translateX(-…) slides. So an Arabic gallery kept its rail on the left with
LTR geometry.
The layout now carries an additive :dir(rtl) mirror block, in BOTH copies
of its CSS (VTL_SHADOW_CSS and the style block). The rail anchors right,
content right:256px, the collapse and drawer slides go positive, the
collapsed paddings, auto-margins and text-align mirror, and the arrow SVGs
flip. translateX has no logical form, so this is a mirror block rather than
logical properties. LTR consumers match none of it.
The gallery sets dir (and lang) on the layout from its own language: rtl
for Arabic, ltr otherwise. So an English gallery on some other RTL page
keeps a Latin rail unmirrored.
Measured on the served built tree:
1400px ar: rail [944,1200], content [0,944], text right; collapsed =
a 72px rail on the right
390px ar: drawer parked off-canvas right, slides in to [944,1200]
LTR: studio, stack-demo and the gallery at 1400px geometry
identical before and after. The 390px differences seen once
were transition timing and did not reproduce.
The shared layout is compiled into doxa-maps-spa and plugin-studio, so
both bundles are rebuilt here. stack-demo's bundle came out byte-identical:
it does not compile the layout.
Language. The gallery fell back to the browser's language and forced it
onto every nested map through profile-config.locale, while its own rail
stayed English. Measured: a Spanish page viewed in a German browser showed
German maps under an English rail. It now follows the ladder every nested
map already uses: the `language` attribute, then ?lang=, then <html lang>
(watched live, so a host that switches language switches the gallery), then
English. The browser only counts once a visitor has used the gallery's own
switcher.
Its own words (rail, sections, hints, badges, notice) now ship in all 11
locales, in a private src/strings.js that reuses the library's terms.
Rail sweep, 11 locales at 1400px and 390px: 21 of 22 clean. The one clip,
Italian 'Avanzamento della preghiera' (191px in a 176px slot), now uses the
rail form 'Avanzamento preghiera'. A live <html lang> switch from en to
pt-BR re-renders the rail.
Interop (matrix cell D). check:gallery-embed gains a `linked` phase that
drives each composite's real control INSIDE a gallery tab:
- Big Picture: a facet shows "Showing 1,309" in the grid.
- Simple Research: a grid search narrows 2107 → 3. Its halves are
independent by design.
- Regions: clicking "Africa" in the list retitles the dashboard "Africa".
- UPG-100: "Show on map" brings the map.
UPG-100's missing zone selection is an owned, printed LINKED_KNOWN entry.
Full gate: js 13, iframe 13, neighbour 13, linked 4.
Embedded below the fold on a phone-width host page, the gallery disturbed
the page it was a guest on:
- The host's inherited typography flowed into the gallery. Shadow DOM
isolates selectors, not inheritance, so a host with body{font:20px/3;
letter-spacing:2px} re-flowed every rail row and legend (root measured at
20px / 60px / 2px). :host now resets the inherited text properties to the
gallery's own defaults (16px / normal / normal). Direction still inherits,
so RTL hosts stay RTL.
- Nested maps wrote the host's URL and title. The gallery now tells every
child urlSync:false. Regions needs to honour it (routed).
check:gallery-embed gains a good-neighbour phase: phone width, gallery
below the fold, hostile host CSS, the host's own #anchor and <title>. After
load and after every tab it asserts no host scroll, an unchanged URL, an
unchanged title, and no inherited typography. Two nested-plugin violations
are listed, owned and printed in NEIGHBOUR_KNOWN until their owners fix
them (same contract as MAPBOX_KNOWN):
- research-map.vue:647's scrollIntoView jumps the host to y=640 on load on
a phone.
- Regions' unconditional URL/title sync.
A boot timeout is now a named FAIL instead of an uncaught crash. The gate
caught a real one on the way: a rebuilt bundle whose leaf page still
pinned the old SRI hash.
The shell forwarded tk, tokenUrl, locale and treeBase to each nested map,
but not the engine. So <doxa-maps-spa provider="maplibre"> still loaded
mapbox-gl and Mapbox styles. On a keyless host, that is the embed failing.
The shell now forwards `provider` in each child's profile-config, validated
to the two known engines. That fixed the simple map tabs, but not the
composites: they mount their inner maps without passing the provider on.
Measured with one tab per clean page: Big Picture made 19 api.mapbox.com
requests, Regions 17, UPG-100 15, Simple Research 14. So when a host asks
for an engine, the shell also sets window.MAP_PROVIDER (only if the page
hasn't). That is rung 3 of every map's own ladder, so it reaches maps
nested at any depth. After it, all four composites make 0 Mapbox requests.
check:gallery-embed:maplibre runs the cross-origin gate with a keyless
provider and fails on any api.mapbox.com request, attributed to the tab
that was open. Before this change, js mode failed with 20 URLs. It has one
named, printed exemption: upg-100-list.vue pins a token-free mapbox-gl.css.
That belongs to the upg100 plugin and is routed there.
The gallery is going onto the DX-life research page, and nothing had tried
it on a page it didn't generate. From a second origin, a bare
<script> + <doxa-maps-spa> drew over the host's header, and 8 of 13 tabs
showed an error card.
Three causes, each fixed in the shell:
- No :host box. The layout root is position:absolute, so it anchored to the
viewport instead of the element. :host is now a positioned block with a
default height the host can override.
- A foreign page sets none of the globals a leaf page bakes. Every data call
hit the host's own origin and got HTML back. The manifest now publishes a
`runtime` block (apiBaseUrl + the public token every leaf already carries).
The shell fills window.MAP_APP_API_URL / window.MAP_TOKEN only when the page
hasn't set them, and forwards tk in profile-config.
- Nested elements resolved assets to <tree>/../assets, a sibling pool the
publisher doesn't ship (404 live). The shell now forwards an absolute
<tree>/assets. This also fixes the iframe mode, where Cities with Internet
and Gospel Resources were failing the same way.
Also adds a `tab` attribute (plus ?tab=) to deep-link into one map, and a
`tab-change` event for the host.
check:gallery-embed drives every tab in both modes from a second origin. It
passes here and fails on the previous tree (7 findings).
EMBED.md documents the contract. The Traefik template now sends CORS for the
whole tree: live manifest.json answers without Access-Control-Allow-Origin.