Commit graph

17 commits

Author SHA1 Message Date
Doxa Maps
57009b7e28 Big Picture and Simple Research mount again: import the three locale names they use
08f89f55 gave both composites a text-direction computed that calls
getRuntimeLocale(), detectLocale() and RTL_LOCALES, but imported none of them.
The build does not check free names, so the bundles on main reference three
undefined globals. watch(textDir, reflectDir) evaluates the computed during
setup, so setup throws a ReferenceError in every locale, and Vue renders
nothing for the whole composite: the map, the dashboard and the grid.

Reproduced headless with the same computed + watch shape under
@vue/runtime-core: lang '' fails with "getRuntimeLocale is not defined", and
lang 'ar' with "RTL_LOCALES is not defined"; neither renders.

After this import, both bundles build and neither contains a free reference
to the three names.
2026-09-26 03:02:46 +00:00
Doxa Maps
08f89f55b3 Big Picture's header speaks the reader's language, and both composites turn round for it
TWO DEFECTS FROM THE UNIVERSAL-PROPS MATRIX, and they are the same defect at two
levels: a component can be perfectly localized and still show English, and a
page can be perfectly translated and still be laid out backwards.

★ THE HEADER HAD NO i18n AT ALL. Every label at the top of Big Picture was an
English literal passed as a PROP — FilterDropdown translates nothing, it renders
what the caller hands it — so Region, Bloc, People Group and Religion sat in
English above an Arabic page. They now read the catalogue, grid namespace first
because this header drives the cap-2 grid and its sibling reads the same keys.
Reset uses controls.reset_all, so its English becomes "Reset all": an existing
entry rather than invented wording, and arguably more accurate, since it clears
the search too.

Its search placeholder and two aria-labels have no usable entry — the nearest,
grid.search, drops "or countries", which this search actually covers — so they
are on the DECIDE card rather than quietly reworded. They are invisible to
check:composite-locale-leaks by design: its criterion is that the catalogue
already holds the string, and it does not.

★ RTL IS A DIRECTION, NOT A TRANSLATION. createAppI18n reflects an RTL locale
onto document.documentElement, which is right for a leaf page it owns and
useless for an element dropped into somebody else's page — the case these
composites exist for. A host asking for Arabic got Arabic WORDS in a
left-to-right layout: readable glyphs in the wrong place, which reads as a
styling quirk rather than the bug it is.

Both composites now set `dir` on their custom-element HOST rather than on the
layout inside it, and that choice is the fix: `dir` takes effect through the
inherited CSS `direction`, and inheritance crosses shadow boundaries, so one
attribute covers the composite's own chrome AND every nested element beneath it
— including the grid half the audit found bare. Only ever SET, never cleared: a
host page that declares its own dir owns that decision.

VERIFIED HEADLESS: three bundles build; setAttribute("dir","rtl") is present in
both composites and controls.reset_all in big-picture; freshness and SRI clean,
nothing else stale; gates:pre 6/6. NEEDS EYES: Big Picture and Simple Research
at ?lang=ar — header labels in Arabic, and the layout actually mirrored.
2026-09-26 00:13:46 +00:00
Doxa Maps
b5d849f746 The map follows the list: filter the grid and the camera goes there
The link ran one way. A choice on the map narrowed the grid — region, religion
family and language, verified on both engines — and a choice in the grid moved
nothing. A reader who filtered the list down to 83 peoples had no idea where in
the world they were.

★ IT IS THE GRID'S FILTERED SET, NOT A ROW SELECTION, AND THAT IS A FINDING.
The card says "selecting N rows". people-groups-grid has no row selection — no
checkboxes, no selection state — and the desktop pair's fly-to-selection is
driven by the DASHBOARD's own data table through its selection bus
(useSelectionCamera.js), not by this grid. So building "select rows" here would
have meant inventing a control nobody asked for. The grid already reports the
slugs it is showing on every `groups-filtered`, so that existing signal is the
wire: no new UI, and a grid mounted alone still emits into a room with no
listener.

The composite sends slugs rather than a bbox because it has no coordinates —
this map is the half that knows where anything is, so it frames them itself.

CAPPED AT 400, and the cap is the point rather than a performance worry: a
larger set is not a selection, it is "almost everything", and framing it would
swing the camera to a world view that tells the reader nothing while moving a
lot.

CAMERA POLICY COPIED, DELIBERATELY, from research-dashboard's
useSelectionCamera.js, which worked all of this out for the desktop pair: a
single row has a zero-area bbox and would fit at street level for what is really
a country-scale centroid; several peoples in one place are the same problem by
another route; a set spanning most of the globe animates the wrong way around
the antimeridian if framed literally. One shared composable in library/ is the
right end state — cited rather than imported because a library move cannot be
landed from this lane today, and a later move should delete this block rather
than leave a third copy.

⚠ MOTION SAFETY. The Driver is photosensitive, and a camera that re-animates on
every keystroke is the strobe class this repo has already paid for. An identical
slug set never re-flies, and a reduced-motion preference jumps rather than eases
— same destination, no traversal.

VERIFIED HEADLESS, per the browser policy: both bundles build, `fitSlugs`
appears in the sender and the receiver, SRI and composite-freshness are clean
and no other bundle goes stale. The behaviour itself NEEDS EYES.
2026-09-25 23:28:53 +00:00
Doxa Maps
29fc335b00 Prove the grid-under-map link on both engines, not just the one we happened to run
BUILD-INTEROP-CONTRACT-GRID-UNDER-MAP-BOTH-PROVIDERS asks for a provider matrix:
the same assertions, once per engine, with identical counts. Interoperability
that holds on one provider and not the other is not interoperability.

It is not a formality here. The map half of this link resolves a religion family
through its own legend, and the legend is drawn by the map engine — MapLibre
drops an entire layer in silence for one misplaced paint property, which would
surface as a link delivering nothing while the Mapbox run stayed green.

So the gate now runs the whole measurement under ?provider=mapbox and
?provider=maplibre, and then asserts the two agree: the same starting total, the
same rows for the same family, and the same rows over the composite's wire. A
per-engine pass with different numbers would mean the same choice narrows to
different peoples depending on which engine a partner embedded.

MEASURED, identical to the row on both:

    mapbox     2,107 groups · Buddhism 83 direct · 83 linked · mutant 0
    maplibre   2,107 groups · Buddhism 83 direct · 83 linked · mutant 0

The mutant still runs first on each engine, so a green here still means the gate
can tell a right mapping from a wrong one — twice over now.
2026-09-25 22:35:48 +00:00
doxa-maps-88655c70
412602cdef Five more gate scripts read the tree's name from the build config
The research verify-* scripts and research-map-plus-grid's two gates hardcoded 'doxa-maps' (11
literals); they now import TREE_NAME from internal/_tree-name.mjs like the rest of the tooling.
2026-09-25 16:25:22 +00:00
doxa-maps-88655c70
5639dd1ccf Let a host set the language of a whole page, not half of it
Embedded with profile-config {"locale":"ar"}, these dashboards came back half
translated: the map's six tabs in Arabic and the grid's four dropdown labels in
English, on the same screen. That reads as "some strings were never translated",
and it sends whoever investigates to the catalogues — where every one of those
strings is already present and correct.

Two readers, one locale, and only one of them was being told.

  t()      — vue-i18n, follows the plugin's locale, which ProfileLoader already
             drove from the host's config.
  tStatic  — the plugin-free reader that components mounted as their own custom
             element must use (useI18n() throws at a custom-element root and
             takes the subtree with it). It resolved the locale from the URL and
             the document, which are the only two things a STATIC page can say.

So a locale set at RUNTIME moved every t() string and no tStatic string. Now the
i18n instance publishes its locale whenever it changes — one choke point, so
ProfileLoader, the SPA's picker and the studio's picker all work without any of
them remembering to tell the static reader as well.

★ ON THE WINDOW, NOT IN THIS MODULE, AND THE FIRST VERSION GOT THIS WRONG.
A composite loads SEVERAL bundles — research-map-plus-grid loads its own,
doxa-research-map's and people-groups-grid's — and each carries its own copy of
library/i18n. A module-level variable set inside the map's copy is invisible to
the grid's copy, so the first attempt translated the map and left the grid in
English: the very defect it was written to remove, reproduced one layer down.
Measured, then moved to a window-level store, the way the shared map token has
always crossed bundles. Module state is per-BUNDLE; a locale is per-PAGE.

Both composites also publish the locale themselves, because a composite sits
ABOVE every ProfileLoader on its page rather than under one.

MEASURED with check:host-locale-contract, on c7c2050d and on this commit:

    dashboard-plus-grid/dashboard-plus-grid      32 of 2121  ->  2121 of 2121
    research-dashboard/research-dashboard        75 of 1891  ->  1891 of 1891
    research-map-plus-grid                      489 of 1201  ->  1201 of 1201
    whole fleet                                  18 failing  ->     1 failing

The one left is big-picture/big-picture, which installs no i18n and mounts no
ProfileLoader at all — a different shape of gap, and not this lane's bundle.

Every bundle was rebuilt; composite-freshness and SRI are clean.
2026-09-25 14:26:02 +00:00
doxa-maps-88655c70
471bd05cea Stop showing English where the catalogue already holds the translation
A reader on an Arabic page saw a handful of English words among the Arabic:
the table's "People Groups" title, its count line, the Engagement column, the
Gospel Access chart title, and the side drawer's close button. Every one of them
had a correct Arabic entry sitting in the catalogue; nothing looked it up.

THE CHECK CAME FIRST, AND IT REPLACES A SWEEP THAT PASSED A BROKEN TREE.
The old method sorted every rendered string into third-party / data / chrome and
counted only chrome. That split is a guess, and it guessed wrong exactly where
the defect lived: single words like "Region", "Prayer" and "Country" are as
plausible as values as they are as labels, so they scored as data and the sweep
reported zero on a page that was visibly leaking.

check:composite-locale-leaks asks a question with an answer instead:

    the English catalogue holds this exact string as a value, AND
    this locale holds a DIFFERENT value for that key, AND
    the page still renders the English one.

No judgement about what kind of word it is — if the translation exists and the
reader is not getting it, that is the defect. The two surfaces a lookup cannot
judge (vendor map chrome, and table/facet cells carrying API values) are muted
by NAME with a stated reason, never by word, so muting one can never hide the
next real leak that happens to reuse it. The gate failed on this tree naming 9
strings before any of them were fixed.

Also threaded here: both composites now accept `lang` / `locale` in
profile-config and pass it to the children they mount, alongside `tk` and
`provider`. A partner writing {"lang":"ar"} reached nothing before — the knob
was documented and read by nobody.

The prayer facet's label in DashboardResearchFilters is now a catalogue getter
too. Measured before touching it: dashboard.config.js does define prayer_level
and FACETS spreads the config last, so that literal never rendered for this
config — it is a default for a config that omits the facet, and it stays, with
the merge order written down so the next reader need not re-derive it.

Four strings remain English with no catalogue entry at all — "Top countries",
"Select all {n}", the table dock's title, and the singular "group". They need
wording in ten languages that cannot be composed from existing entries, so they
are a DECIDE card for the Driver rather than translations invented here.
2026-09-25 12:16:59 +00:00
doxa-maps-88655c70
16629f4906 Link religion and language between the map and the grid, by the one table they already share
A reader who picks a religion family on the map now sees the grid narrow to the
same peoples, and a language selection travels the same way. Region already
linked; these two did not, because the two halves do not name a religion the
same way and nothing yet proved what the honest translation was.

The map filters religion through its legend's flat tab, keyed by the FIRST
LETTER of the religion code ('B'); the grid filters through a derived axis that
runs library/colors/religion.js forward per row ('BLA' -> 'Buddhism'). One
shared table, read in opposite directions, copied by neither half — so the wire
carries the family NAME and each side resolves it against its own data.

Two measurements changed the design on the way:

  - mapStore.filters.religions, which the map's filter panel writes, is read by
    NOTHING: the mapLayers.applyFilters() it was meant to reach is called and
    never implemented anywhere in the tree. A link routed through it would have
    set state, passed a smoke test and dimmed no pin.
  - the map's legend selections are mutually exclusive by design, so it can
    light exactly one of region/religion/language. The composite mirrors that
    rather than letting the grid hold three facets the map cannot show.

Religion links at the FAMILY and not at the code, deliberately: a map cannot
distinguish 'BLA' from 'BMA' — both dim as Buddhism — so linking at the code
would promise a precision one half cannot keep.

check:linked-vocabulary proves it by measurement rather than by reading: it
compares the total reached over the composite's wire against the total the grid
gives for the same facet on its own (83 of 2,107 both ways), and runs the MUTANT
first — the legend key 'B' instead of 'Buddhism' — failing as NO-TEETH if the
wrong mapping scores the same as the right one. Measured: mutant 0, real 83.

The CLI doc's apk:debug quote was realigned in passing; package.json had gained
app:start-page and the doc still described the older chain.
2026-09-25 10:20:27 +00:00
doxa-maps-88655c70
7f700935c5 Simple Research + Big Picture: the host's style stops at the box, and the last English goes
TWO FIXES THAT BOTH SHOW UP ONLY WHEN THESE PLUGINS ARE SOMEBODY ELSE'S GUEST.

1. ATTRIBUTE FALLTHROUGH. These SFCs mount AS custom elements, so a style the
   host puts on the tag lands on the component's own root element. MEASURED with
   a host style of "display:flex; position:relative; height:100%": that exact
   string appeared verbatim on the inner root div of people-groups-grid,
   stack-demo, research-map-plus-grid and dashboard-plus-grid. The gallery sets
   one on every tab, and the same fallthrough collapsed the Prayer Walk Planner
   to 76px. inheritAttrs:false on each root — a host styling the TAG is styling
   the box, not our internals.
   AFTER: the inner root carries no inline style on any of the four, and each
   still renders full height (2997-4010px in the harness), so the guard did not
   become the collapse it prevents.
   Worth knowing for the next probe: shadowRoot.firstElementChild is a <style>
   node — Vue injects styles first — so a probe that trusts it measures nothing
   and reports "no fallthrough, nothing to fix". Take the first non-style child.

2. THE RESIDUAL CHROME the in-gallery re-measure found: the dashboard title,
   Align Dashboard and its tooltip, World, Vertical Legend, groups in scope, the
   per-row aria templates (Filter by / Remove filter / groups), and the map
   drawer's aria label. Catalogued in all 11 locales and read through tStatic,
   reusing existing wording where the catalogue already had the word.

The aria TEMPLATES matter more than their size suggests: they repeat per row, so
one untranslated template is dozens of English strings in a screen-reader's ear.
2026-09-25 08:19:56 +00:00
doxa-maps-88655c70
03b838e16d Simple Research: one filter state, so the two halves move together
The composite showed a map and a grid that ignored each other: filtering the grid
to Africa moved its own count 2,107 → 315 while the map beside it kept every pin.
The composite now owns ONE filter state; the children report theirs upward as DOM
events and accept ours downward as attributes, so each stays independently
embeddable — a child mounted alone hears nothing and is handed nothing.

MEASURED on a served built tree, region facet:
  grid → map   2,107 → 315, and the map's own state follows (externalRegion and
               selectedRegion both 'africa'); clearing returns both
  map → grid   the map's map-filtered contract → the grid's list 2,107 → 315;
               clearing returns it
  standalone   grid alone 24 rows / Total 2,107 / 0 errors; map alone canvas 1 /
               0 errors — unchanged
  MUTATION     disabling the composite's push leaves the grid filtering itself
               (315) while the map stays empty, and kills the reverse direction:
               the check fails when the link is broken

Four things this cost, each of which first looked like it worked:

1. StackLayout applies `attrs` ONCE at create — its documented contract. The
   first version recomputed `sections`; the composite adopted the filter and the
   map's attribute in the DOM never changed. A computed nobody re-reads is not a
   channel, so the state is now PUSHED onto the mounted children.
2. The echo cancelled itself. Composite pushes → the grid re-filters → it emits
   groups-filtered with activeFilters still EMPTY → the composite adopts that as
   "the reader cleared it" and wipes what it just applied. Both handlers were
   correct; the round trip undid itself in under a second. A push is not a user
   action, so the push window ignores children.
3. The grid ignored host filters BY DESIGN when its own dropdowns are visible:
   activeFilters picks local OR props, never both. Right for the desktop
   composite, which hides them; silently wrong here. A host facet now seeds the
   LOCAL state, so the map's selection appears as a chip in the grid's own bar
   and the reader can clear it there.
4. That seeding called lbl(), which is local to another computed — "lbl is not
   defined", watcher dead, attribute set, list unchanged. Visible only after the
   probe was widened to console.error. A silent watcher failure looks exactly
   like a feature that was never wired.

SCOPE, DELIBERATE: region only. The map filters religion and language by the
canonical strings its own data carries; the grid filters them by slugs from a
different table. Linking those means inventing a mapping, and a wrong link is
worse than an honest gap.

The map also gains a verification handle (window.__researchMapFilterState), the
same contract as __rdashMap and __grMap: a linked filter is a STATE claim, and
every DOM proxy for it was either tab-specific or a selector guess.
2026-09-25 06:10:43 +00:00
doxa-maps-88655c70
7609cd36f0 Composites: pass the provider prop down, so keyless embeds are keyless
The token fix was half the contract. resolveMapProvider's ladder starts at
profile-config.provider, and a child reads the config its PARENT writes — so a
partner asking for

    <research-map-plus-grid profile-config='{"provider":"maplibre"}'>

got a child still on mapbox, which then demanded a token. MEASURED on a
third-party origin: "No Mapbox access token provided", 0 tiles, no canvas — for
the exact configuration whose entire purpose is needing no token. Keyless at
volume is why MapLibre support exists; a composite that drops the prop cancels
it, and the failure reads as a token problem rather than a plumbing one.

Both composites now thread `provider` beside `tk`, validated to the two known
values so a typo cannot silently select a third engine.

MEASURED AFTER, partner page, NO token anywhere in the config:
  research-map-plus-grid  canvas 1280x906, ZERO requests to api.mapbox.com,
                          basemap painted, 12,379 pins, legend, grid
  dashboard-plus-grid     canvas present, ZERO requests to api.mapbox.com
Screenshot read: the keyless map also shows the deferred search-bar rule holding
on a partner page — no search bar, and the legend risen into its place with no
gap.
2026-09-24 19:05:19 +00:00
doxa-maps-88655c70
dc252bb304 Composites: give a partner's embed the token it passes in
A JS embed of either composite mounted a map that could not draw. MEASURED the
way a partner actually embeds — one origin serving the tree with CORS, a
SEPARATE origin serving the page:

  research-map-plus-grid   "No Mapbox access token provided", style 0, tiles 0, no canvas
  dashboard-plus-grid      the same
  (iframe embeds were fine throughout: style 2, tiles 9, 0 failures)

Both composites declared NO PROPS, so the host's profile-config — token included
— was read by nobody. Our own leaves hide this completely: a leaf carries a
BAKED token and assigns window.MAP_TOKEN, which the shared-token bridge hands to
the nested map. The failure therefore exists ONLY where there is no leaf, which
is precisely the partner page the embed snippet is written for. It is also the
snippet we publish: profile-config='{"tk":"YOUR_MAPBOX_TOKEN"}' is the documented
way to supply a key, and it did nothing.

Both now parse their host config, publish the token page-wide through the shared
composable, and hand it to their children. The two differ in how children are
mounted, so the last step differs:

  research-map-plus-grid composes by TAG + BUNDLE URL, so the token is threaded
  into the child element's profile-config, where its own ProfileLoader reads it.

  dashboard-plus-grid bakes its children in and bypasses ProfileLoader entirely
  — and ProfileLoader is the only provider of `mapboxToken` in the repo, so
  DashboardMapPanel's inject fell through to its empty default. It now provides
  `mapboxToken` itself, from the host's tk with the shared page token behind it.

Spread last and only when present, so a host that supplies none leaves the key
absent and the child asks for exactly what its standalone leaf asks for. A
malformed attribute is caught and ignored: the map half is more useful
token-less than the page is with a thrown setup().

This went in the PLUGINS, not the library. A shared change reaches every map —
that is the lesson SHOW_EXPORT_BUTTON taught this week, and nothing here needs
to be shared to work.

MEASURED AFTER, same harness: research-map-plus-grid canvas 1280x906, style 2,
tiles 9, 0 failures; dashboard-plus-grid canvas present, style 2, tiles 10, 0
failures — basemap, pins, legend, table and charts all drawn. Screenshots read.
Both LEAVES re-checked for regression: canvas 1, 0 failed requests, each still
painting its own map.
2026-09-24 19:00:24 +00:00
doxa-maps-88655c70
e7bc86d4f2 Map engine is one prop: MapLibre drops the search bar, legend rises
Simple Research showed a Fly + Clusters toolbar where the search bar used
to be, and the map offered no way to pick an engine without writing a
second map.

FLY + CLUSTERS ARE NOW LEAF-ONLY. LegendTools renders at top:14px centred
on the legend column — the search bar's exact slot — so on the Regions and
Languages tabs (the legendTypes its predicate allows) it reads as having
REPLACED the search bar. MEASURED on the leaf: fly toolbar at y=58 against
the search bar at y=56, the two on top of each other. In a stack whose
bottom half is a grid the reader is scanning, a fly-tour that moves the
camera underneath fights the other half, so the composite passes
legendTools:false. The standalone leaf keeps both.

THE ENGINE IS ONE PROP. resolveMapProvider already read profileConfig
.provider, so research-map.instances.json publishes the maplibre instance
and a partner running thousands of embeds changes one value instead of
maintaining a second map. Mapbox stays the default: it carries the richer
feature set. MapLibre is the keyless option for volume.

MAPLIBRE SHIPS NO SEARCH BAR, ON PURPOSE. GeocoderComponent's maplibre path
still calls makeMapboxForwardGeocode(accessToken) (:526) — the geocoding
REQUEST goes to Mapbox whichever engine draws the tiles. Shipping the bar
under maplibre would keep billing the exact key the option exists to avoid
while LOOKING free. Under mapbox it stays, unchanged.

AND THE LEGEND RISES INTO THE VACATED SPACE. SemanticTreeLegend's
--stl-top defaults to 60px that exists only to reserve room for a search
bar; leaving it when the bar is gone is a dead band. MapToolbar's
has-geocoder collapses its own reserved gap the same way.

MEASURED against the built tree:
  leaf mapbox      search@56  legend@104   fly@NONE (Prayer) / @58 (Regions)
  leaf maplibre    search@NONE legend@58   — legend rose 46px
  composite        search@NONE legend@104  fly@NONE on Regions

7/7 pre, 18/18 post.

KNOWN GAP, pre-existing and NOT introduced here: the mapbox composite still
shows no search bar. mapboxToken is injected per element and the composite
passes neither tk nor tokenUrl to its child, so the child's geocoder gets
an empty token and bails while the tiles render from another path. Isolated
but not fixed.
2026-09-24 00:43:44 +00:00
doxa-maps-88655c70
3685b196cb Show the cap-4 composite in the studio's Simple Research slot
The cap-5 shell's "Simple Research" tab rendered an error panel instead of
the map, in both trees:

  Could not load <doxa-research-map> from ../../doxa-research-map/
  doxa-research-map.js

StackLayout already anchored sibling-bundle srcs to the tree root, but it
anchored on the /doxa-maps/ segment of window.location.pathname. The studio
mounts at the SERVER ROOT (/index.html), which has no such segment, so the
lookup missed and the literal '../../' was returned untouched. The browser
resolved it against '/', where '../' cannot climb, and requested
/doxa-research-map/doxa-research-map.js — 404, both children, empty stack.

That is why it survived: every gate and every screenshot used a LEAF url,
and the studio is the one host that is not one. The same composite at its
own leaf renders perfectly.

resolveBundleSrc now falls back to bundleTreeRoot() — the bundle's OWN
script URL, which rollup rewrites to document.currentScript.src in the IIFE
build. That is a fact about where the code came from, not a guess about
where the page is, which is the lesson stack-demo.vue wrote down when this
defect appeared for the third time. It returns null in dev ESM and for any
non-<name>/<name>.js layout, so it never guesses. The pathname rule stays
FIRST and the untouched-string fallback stays LAST, so every path that
worked before is byte-identical: this can still only fix a path, never
break one.

research-map-plus-grid also adopts stack-demo's bundleSrc() dev/prod
helper. In dev, vite serves entries at /plugin-designs/<bundle>/index.js and
there is no <bundle>/<bundle>.js to reach, so the dev surface 404'd too.
import.meta.env.DEV is statically replaced, so the built output is
byte-identical to the literal it replaces.

Verified in a browser against the built tree, reading the screenshots:
studio root -> Simple Research paints the world map with its prayer legend,
on the dev server too. Negative-tested: disabling the new fallback and
rebuilding restores the exact 404 and the empty canvas.

No regression across every StackLayout consumer — rmpg shallow leaf, rmpg
deep default/ leaf (the case the pathname rule was written for),
spa -> Simple Research, dashboard-plus-grid, stack-demo — and all seven
studio dashboards. 7/7 pre, 22/22 bundles, 18/18 post.
2026-09-23 23:26:40 +00:00
doxa-maps-88655c70
4c8f6b4a6d Share the mobile composite too, and make the url absolute so the embed works
research-map-plus-grid had the same defect fixed yesterday on its desktop
sibling: sharing the composite emitted a link to doxa-research-map, the child it
nests. The gate said it exactly — "share URL reopened <doxa-research-map>, not
<research-map-plus-grid>" — and a recipient got the map alone, losing the grid.

Checked all four cap-4 composites rather than assuming the fix generalised.
my-upg-100-list owns its Share button directly and is fine. big-picture composes
a header plus the grid, owns no map and ships no Share button at all; the gate
reports "window.mapboxgl UNDEFINED" against it, which is its map precondition,
not a share failure — that bundle is out of this gate's scope and I put it in
the probe by mistake. So one real instance, now fixed.

Tier 1 again, for the same reason: this child also hardcodes a prop
(research-map.vue:2149 `:embed-path="'doxa-research-map/research-map/index.html'"`)
and the ladder is `embedPath || cfg?.shareEmbedPath`, so a prop outranks host
config and tier 2 is unreachable. Delivered through the profile-config attribute
rather than Vue provide, because this composite mounts its children as real
custom elements via src — there is no provide chain to reach them.

The url had to be ABSOLUTE, and the first version was not. resolveShareUrl
returns a tier-1 shareUrl verbatim (embedBase.js:95, `return whole.trim()`, no
resolution), and ShareButton feeds the same string into its embed snippet:
<iframe src="{shareUrl}">. A relative '../../…' would have resolved correctly in
the share LINK, because the browser resolves it against this page, and then
404'd in the EMBED, because it resolves against the partner's page instead. That
would have passed the gate — the gate navigates the link — while breaking the
deployment method the link exists for. Tier 1 is documented as "a WHOLE url" for
precisely this reason. Now computed with resolveEmbedBase() so it stays correct
on doxa.life, on nuxtbase.app and in a dropped tree.

Verified the desktop sibling did not have the same latent flaw: it produces a
fully absolute url, confirmed in the live gate output.

smoke:share-all now carries eleven targets. Full suite 11/11 pass · 0 fail.
Eleven gates pass, including sri, composite, composite-srcs and synced-css.
2026-09-23 08:32:50 +00:00
doxa-maps
236fef5682 Give nested ProfileLoader children the profile-config they require
Some checks are pending
build-gate / build (push) Waiting to run
Two composites nested a child by tag and URL without a profile-config. Those
children do not render a component directly — their bundle entry mounts
ProfileLoader, which reads profile-config.profile to choose which view to load.
With none, the element still mounts and still reports its tag, so every
structural check said both children were present, while the loader rendered
"missing or invalid profile-config prop" where the map should be. The grid half
of each page worked, which is how it survived.

Found by reading a regenerated README screenshot, not by any gate. The mobile
screenshot showed an error card and an empty pane; the fixed one shows the real
research map with its data, controls and progress figures.

research-map-plus-grid was mine, shipped that way. stack-demo predates it and had
the same defect against research-dashboard. Both now pass the shape their own
generated leaf pages use, so nested and standalone mounts ask for the same thing.

check:composite-srcs now requires a profile-config on any child whose bundle
mounts ProfileLoader, derived from which entries import it rather than a list.
The first version of that check tested for the word and passed on its own
explanatory comment; it now matches the quoted attribute key, and a negative test
that mutates only the key fails as it should.

Also regenerates three derived artifacts that had drifted behind this flight's
changes: the library index, the capability exemplars table, and all twelve README
screenshots.
2026-09-22 09:37:35 +00:00
doxa-maps
b57aef457c Add the mobile capability-4 composite: research map above the people-groups grid
Some checks are pending
build-gate / build (push) Waiting to run
The desktop composite pairs the research dashboard with the grid. This is its
mobile sibling: the monolithic 7-tab research map on top, the same grid beneath,
stacked to scroll. Which one a shell shows is an aspect-ratio choice made at the
SPA level, so neither plugin knows about the other and both stay embeddable.

The grid's own search and filters are ON here. The desktop composite turns them
off because the dashboard header owns those controls and drives the grid through
props; the research map has no such header, so switching them off would leave the
grid with no way to be searched at all.

Composes its children by tag and bundle URL rather than importing their .vue
files. Importing the research map directly would bypass its bundle entry, which
is where its three bundle-private colour strategies, its profile registry, its
Pinia and i18n pair and the RTL text plugin are installed — and losing those is a
runtime defect in a green build, because a thrown setup() removes the subtree
with an empty error.

Add check:composite-srcs, which resolves every nested src against the built tree
and asserts every nested tag is registered. A wrong relative depth is a 404, and
the stack layout swallows a failed section on purpose so one broken child cannot
take the whole stack down — which means the cost of that resilience is a silently
empty band. Negative-tested both ways: breaking the depth and renaming a tag each
fail with the file and path named; restoring returns to pass.
2026-09-22 07:34:02 +00:00