Commit graph

47 commits

Author SHA1 Message Date
doxa-maps-88655c70
5e6bed7d7a Hand the wheel back when a scrollable ANCESTOR owns the scroll
Some checks are pending
build-gate / build (push) Waiting to run
Mapbox calls preventDefault on every wheel over its canvas. The panel already
knew that and disabled scroll-zoom when the page could scroll -- but it asked
whether the DOCUMENT scrolls:

    canPageScroll = document.documentElement.scrollHeight > innerHeight + 4

Inside the cap-5 shell the document is pinned to one screen and an ancestor
ELEMENT owns the scroll. Measured on the dashboard-plus-grid tab at 1600x900,
walking the composed tree from .pgg-root upward:

    div.stl          oy=visible  ch=769  sh=2838  over=2069
    section.ps-pane  oy=auto     ch=769  sh=2838  over=2069   <- real scrollport
    div.ps-stage     oy=hidden   ch=769  sh=769
    document         sh=900      ch=900                       <- "nothing scrolls"

So the condition computed false, scroll-zoom stayed enabled, and the map ate
every wheel with 2069px of grid mounted below the fold.

Proven at the event level rather than inferred: the wheel at (800,600) lands
on canvas.mapboxgl-canvas, and .ps-pane.scrollTop stays 0 after a real
mouse.wheel at both y=600 and y=860, while a programmatic scrollTop = 800
moves it to 800. The scroller was never broken; the gesture was consumed
before it arrived.

That is why three earlier fixes to the containers were each correct and
changed nothing -- they made the page scrollable, and the wheel still never
reached it. The honest question is "is there a scrollable ancestor", not "does
the document scroll"; the document is one possible answer and in an embedded
host it is the wrong one.

The ancestor walk hops shadow boundaries via parentNode.host, because
parentElement stops at one and every plugin here is a compiled custom element.

Browser verification still pending: the build queue is draining several
agent-requested rebuilds, so check:shell-scroll has not yet run against a
settled tree carrying this change. The fix is confirmed present in the
minified bundle.
2026-09-22 18:18:22 +00:00
doxa-maps-88655c70
e1d27cb476 Record which kind of verification closed each card
Some checks are pending
build-gate / build (push) Waiting to run
A card was closed on an agent measurement while the bug was still visible to
the Driver, and the card had no field in which that disagreement could live.
The board said done and the product said broken, and nothing in the data knew.

Adds fields.verification (agent|human|both) plus verification_kind
(visual|state) and a conflict flag, back-filled across 179 closed cards, and
a gate that refuses an anonymous close.

The gate does not require human sign-off. Gating the board on human review was
tried and it stalled. What it forbids is losing the distinction, so the
question "which of these did a human actually look at?" stays answerable.

The visual/state split encodes the asymmetry the Driver named: human
verification is more reliable on visual bugs, agent verification on non-visual
ones. A headless probe reads geometry, not perception -- it can measure a menu
at dTop=4 and still be blind to it feeling wrong. So a visual card closed on
agent evidence is a hypothesis; a state card closed on agent evidence is a
result. The counts are reported separately.

Negative-tested three ways: stripping a tag, claiming human verification with
no quote, and marking a conflict card done each exit 1 by name.
2026-09-22 17:00:26 +00:00
doxa-maps-88655c70
9f1f657209 Expand a branch under its own parent, even when a filter is active
Some checks are pending
build-gate / build (push) Waiting to run
Expanding a node that had just been selected pushed its children to the bottom of the
tree instead of under the node. The Driver isolated it precisely: clicking only the
arrow always worked, and clicking the row first and then the arrow did not — which
pointed at the filtered path rather than at the expand logic.

The re-admission of filtered-out rows walked the unfiltered baseline and appended
anything the baseline did not recognise. A child revealed by expanding a selected node
is exactly that: it never existed in the unfiltered snapshot, so it had nowhere to go
but the end. With no filter active that path is never taken, which is why the arrow
alone behaved.

It now walks the current rows and splices the missing ones back at their anchors, so
live order is preserved and the baseline is only consulted for what is absent.

Verified three ways: expanding alone puts the first child at index one; selecting then
expanding also puts it at index one, one level below its parent; and a filtered rail
still keeps its shape, going from sixty-six rows to sixty-seven with thirty-five dimmed
in place and none lost. A ten-step random walk left one hundred and fifteen selectable
rows and no page errors, and Clear All returned the rail to sixty-five.
2026-09-22 16:45:03 +00:00
doxa-maps-88655c70
d79f01590f Put the CSV header on the first line so the export opens as a table
Some checks are pending
build-gate / build (push) Waiting to run
The export carried its provenance comment above the header, and every spreadsheet
importer treats line one as the header — so the scope sentence became the column names
and the real header became the first data row. A single-column line above a sixty-eight
column table is what "glitched" looks like on screen.

Three other explanations were plausible and all three are wrong, measured before
anything changed: the file is comma-separated, not tab-separated, so the tabs in the
report were terminal rendering rather than bytes; the escaper is genuinely RFC-4180, and
embedded commas, quotes and newlines all survive a strict parser round trip; and
injection-prefixed cells were already neutralised. Only the position was wrong, and
chasing the other three would have produced churn and no fix.

The provenance line moves to the end rather than away. It records which filters produced
the file, which is the difference between a dataset and an anecdote, and a trailing
single-column row lands as one populated cell instead of destroying the column mapping.
The field counter that read line two moved with it — left alone it would have counted
the first data row, usually the same number, so it would have looked right and been
wrong.

A new check parses the export the way a spreadsheet would, which no existing gate did:
forty-six checks and not one read the file. It extracts the shipped escaper rather than
restating the rule, so it cannot agree with itself while the product disagrees.
Negative-tested by putting the comment back on top.
2026-09-22 16:30:36 +00:00
doxa-maps-88655c70
6b29e146df Let the deepest two tree tiers reach the grid
Some checks are pending
build-gate / build (push) Waiting to run
Cluster and People Group changed the dashboard correctly and could never move the grid,
because the fields behind them were not in the grid's fetch. They are real fields, unlike
the two family axes, which are derived and have no row field to filter against at all —
those stay unmapped rather than being aimed at a lookalike and quietly showing the wrong
rows.

Verified against the live data before mapping, since a wrong mapping is worse than no
mapping: both fields are present on 2,106 of 2,107 rows in the same value-and-label shape
as the axes already wired, with 194 and 1,736 distinct values, no collisions once
normalised, and no case where one entry's label matches another's code. Codes and names
never overlap, so either vocabulary can be accepted safely.

Measured end to end: selecting the Telugu cluster narrows the grid to 93, which is
exactly the number of rows the API holds for it.
2026-09-22 16:22:21 +00:00
doxa-maps-88655c70
56dbb6770d Let the wheel reach the page when a map is only part of it
Some checks are pending
build-gate / build (push) Waiting to run
The people-groups grid under the dashboard could not be reached by scrolling. Every
earlier investigation looked at section heights and overflow, because that is what the
documented fix addressed — but the cause was the map itself: Mapbox binds the wheel for
zoom, so the document never saw the event. Measured before: fourteen wheel ticks left
scrollY at zero and the first grid card exactly where it started.

Scroll-zoom is now disabled only while the page has somewhere to scroll, so a map that
is the whole page still zooms on wheel and a map stacked above a grid gets out of the
way. Measured after: scrollY 1549 and the first card moved from 897 to -652.
2026-09-22 16:03:33 +00:00
doxa-maps-88655c70
ec012f45b6 White rings on the pins, one place for selections, and two gates that told the truth
Some checks are pending
build-gate / build (push) Waiting to run
Pins keep their navy but gain a white ring and a radius that shrinks as you zoom out, so
neighbouring dots separate instead of merging into one blue mass. The Driver asked for
rings rather than clustering, explicitly to avoid more logic.

The top header no longer carries selection state. That belongs under the filter toggles,
in the second header, and nowhere else.

The filter menu is anchored to its trigger again. It had been moved to fixed positioning
to escape a scroller that was clipping it out of existence, and viewport coordinates go
stale in ways that put the menu somewhere unrelated to the box that opened it.

A gallery row now carries its identity as an attribute. A Vue key is a reconciliation
hint and is never rendered, so the smoke check read undefined and reported a broken deep
link on a page that works — measured zero occurrences of that attribute in both the
generator and the built page. Asserting on the visible label instead would have coupled
a machine check to display text.

That repaired gate immediately earned its keep: it caught the gallery asking for assets
one directory too deep. The relative asset base was correct while the gallery was the
root page and silently wrong once it moved aside for the studio, and the failure surfaced
as a JSON parse error rather than a 404, because an unknown path answers with HTML.
2026-09-22 16:01:16 +00:00
doxa-maps-88655c70
5725f5b9d9 Let a stacked composite find its siblings in either tree
Some checks are pending
build-gate / build (push) Waiting to run
stack-demo rendered two empty sections in dev. Its sibling bundles were addressed with
a tree-relative path, which the browser resolves against the document URL, so no host
and no attribute can redirect it — correct in the built tree, a 404 in dev. Measured:
the dev entry answers 200 while the path the relative form resolves to answers 404. The
sources now resolve at runtime, so the same file is right in both trees, and the
production branch compiles away to the string it replaces.

That was the third place this defect appeared. Both capability-5 shells carried the same
hardcoded paths and were cured the same way, and each instance was found separately
after the previous one was fixed — because a literal parent-directory path reads as
correct in review and is invisible to a gate that only ever checks the built tree.

Removing the 404 was not enough: the sections were still empty with zero failed
requests. The shared layout injected each bundle with a classic script, and a classic
script pointed at a dev ES module fires its load handler successfully and only then
throws, so the promise resolved while the tag never registered and the wait for
definition quietly timed out. A load handler cannot detect that.

The module rule now lives in the shared layout rather than in a fourth private copy of
the same loader, so the two other composites that inject through it are covered too.
Verified after: both elements define, sixty-six facet rows and twenty-four cards on
stack-demo and on dashboard-plus-grid, zero failed requests on each.
2026-09-22 15:37:41 +00:00
doxa-maps-88655c70
8b1e3d3f5d Stop the capability-5 shells claiming a native element property
Some checks are pending
build-gate / build (push) Waiting to run
Both shells declared a `title` prop. defineCustomElement maps every prop to an
attribute, and `title` already exists on every HTMLElement as the tooltip, so Vue warned
on every single mount that it would overwrite the native property. Harmless in effect —
nothing here wants a tooltip — but a warning repeated on every mount is where a real
error goes to hide, and this repo has already paid for that once when a thrown setup()
silently removed three buttons behind noise that looked ignorable.

Renaming the prop was safe because nothing outside ever set it: no host passes title=,
no profile-config reads it, and the value is only forwarded to the shared layout, which
is an ordinary Vue component rather than a custom element and so has no collision of its
own. Both shells carried the identical declaration — the same duplicate-implementation
pattern that earlier turned one resolver assumption into two separate outages — so both
were fixed together.

The studio smoke check now samples several tabs instead of only the first, and measures
node depth inside the mounted element rather than counting the slot's children. The old
check read one child for every pane whether the element rendered a full dashboard or
nothing at all, which is the "mounts is not works" trap its own header warns about. It
polls for content rather than sleeping a fixed interval, because a fixed wait either runs
too slow to use on every edit or marks a slow tab broken, and a gate that cries wolf gets
switched off.

Measured after: zero Vue warnings in the browser console.
2026-09-22 15:03:52 +00:00
doxa-maps-88655c70
38a7dbb939 Land the swarm's work: no iframes anywhere, a tree that keeps its shape, deeper facets
Some checks are pending
build-gate / build (push) Waiting to run
Checkpointing thirteen files of agent work that were sitting uncommitted while three
agents wrote to a live tree the Driver was watching through hot reload. Uncommitted work
is work at risk, and a tree being edited under an observer oscillates between states that
look like regressions and are really just partial saves.

The second capability-5 shell now mounts its tabs as real elements too, so no iframe
remains in either shell. It was the last holdout: eight tabs, all iframe mode, each
pointing at a hardcoded built-tree leaf that does not exist under the dev server.

The facet rail keeps its shape when a filter narrows it. Selecting one place took the
rail from sixty-six rows to twenty-one — forty-six rows deleted, none dimmed — so the row
a user was about to click vanished under the cursor, which is what "the buttons went
dead" actually was. Rows whose count falls to zero are now re-admitted at their original
slot and depth, dimmed and inert, which also restores the component's own promise to lose
nothing against the tree it replaced.

Expanding a branch and filtering by it are now separate, visible affordances: the whole
row filters, and expand is a real button with its own hit area and label. Rows that were
never clickable are unmistakably inert rather than silently ignored.

Three more facet axes reach the grid — affinity bloc, engagement status and primary
language — each verified to share a field with an existing prop rather than being mapped
on name resemblance. Two derived facets remain unmapped on purpose: they have no matching
row field, and mapping them would filter confidently to the wrong rows.
2026-09-22 14:51:52 +00:00
doxa-maps-88655c70
9424b86d99 Hand the tree base to nested shells so they stop guessing it
Some checks are pending
build-gate / build (push) Waiting to run
A capability-5 shell mounted inside the studio carries its own tab list and resolves
it against its own tree base, which it derives from its script URL when nobody supplies
one. Nested here that guess was empty, so the resolver took its parent-directory
fallback and requested paths above the leaf at the server root — three bundles 404'd
that way. The requests were non-fatal, because each tab still rendered from its
fallback list, which is exactly why they survived: a silent 404 behind a working
screen. The studio already knows the correct base, since it is the one it resolved
against, so it now passes it down through the existing profile-config path rather than
letting the child re-derive it.
2026-09-22 14:13:49 +00:00
doxa-maps-88655c70
50937cf766 Plugin studio embeds real elements, and a rooted URL is no longer double-prefixed
Some checks are pending
build-gate / build (push) Waiting to run
EVERY TAB FAILED TO LOAD. Introducing tree-base for the dev server prefixed it onto
URLs that were already rooted, producing /__plugin-gallery//plugin-designs/<b>/index.js
and a 404 on all 29 tabs. Both capability-5 shells had byte-identical resolve()
implementations, so both carried the same defect; both now return an already-absolute
ref untouched and join through exactly one slash. The built tree hands relative refs
and keeps its base, so this is one rule for both trees rather than a dev-only branch.

THE STUDIO NO LONGER FRAMES ANYTHING. Map elements were nested in iframes pointing at
their standalone leaf pages, to dodge a one-shot customElements.define collision. That
collision cannot occur here: 19 bundles claim 19 distinct tags, none shared, and two map
elements were proven to coexist in one document with the first unaffected by the second.
Every pane now mounts the plugin's real custom element, which is what makes a row both
reviewable and nestable.

Panes keep two lifetimes, and the difference is the WebGL context budget rather than
taste. Light rows stay mounted so switching back is instant and state survives. Map rows
are destroyed on deactivate, because a hidden map holds its GPU context: the browser
caps live contexts, then silently reclaims the OLDEST, so a map visited ten tabs ago
would go blank with no error anywhere. getContext never returns null and contextlost
never fires on the new canvas, which is what makes that failure so hard to attribute.

The dev server serves a leaf route for one element with no chrome, and passes the tree
base to nested shells so they discover the live tree instead of a baked fallback.
2026-09-22 14:02:20 +00:00
doxa-maps-88655c70
012c79328a Free the filter menu from its clipping ancestor; square the people-group images
FILTER DROPDOWNS WERE INVISIBLE. Opening a filter rendered its options underneath
the surrounding content, making the dashboard's primary control unusable.

The cause was CLIPPING, not stacking order, and that distinction is why an earlier
attempt failed. The filter band is a one-row horizontal scroller authored as
overflow-x:auto with overflow-y:visible — but CSS Overflow 3 coerces a visible axis
to auto when the other axis is not visible, so it computes to auto/auto and clips
vertically. Clipping happens during painting, before stacking order is consulted, so
raising the band to z-index 55 changed nothing: it won a stacking fight that was never
the problem. You cannot re-order pixels that were never generated.

The menu is now position:fixed, anchored to the trigger's viewport rect, which takes it
out of the clip chain entirely. Fixed coordinates go stale whenever anything scrolls or
resizes, so they are recomputed on open and while open, listening in the capture phase
so the dashboard's inner scroll regions are seen too.

PEOPLE-GROUP IMAGES WERE LETTERBOXED. A fixed 130px height against a 220px column
cropped each person to a band. The image is now a square via aspect-ratio, and the
column floor rises to 240px, roughly doubling the visible area. object-fit:cover is
kept deliberately — stretching to a square would distort faces, which is worse than
the letterbox it replaces.
2026-09-22 13:31:11 +00:00
doxa-maps-88655c70
44dd2d01f3 Make dashboard selections actually filter the grid, and fit the camera to the data
Some checks are pending
build-gate / build (push) Waiting to run
Four fixes to the research dashboard and the cap-4 composite, all Driver-reported.

GRID SHOWED ZERO ON EVERY SELECTION. Picking a region showed "1,309" in the chart
above an empty grid below. The two halves spell the same value differently: the
dashboard emits the human LABEL ("Asia", "North America & non-Spanish Caribbean")
while the API stores the machine VALUE ("asia", "north_america_&_non-spanish_caribbean"),
and the comparison was a strict ===, so it never matched. Measured over the 2,107
live records. Adds a shared valueMatches() normaliser used by BOTH applyDropdownFilters
and applyWith, so facet counts and shown rows cannot disagree. Compares value OR label,
case- and separator-insensitively, chosen over a label-to-value lookup table that would
need maintaining per axis and would rot when the API adds a region.

The grid's own dropdowns were never affected, because they build their options from the
data itself; only the host-prop path could inject a label into the comparison. That is
why the standalone grid is fine and the gate passes 13/13 on the same records.

COUNTRY NOW REACHES THE GRID. Clicking India in Top Countries did nothing because
country_code had no matching prop and was deliberately dropped rather than guessed at.
Adds a real countryCode prop and maps it. Country is prop-only, not a fifth dropdown.

LONG FACET LABELS HID THE COUNTS. white-space:nowrap let a long label eat the row, and
the rail scrolls vertically only, so there was no way to recover the number. Labels now
wrap; the bar yields; the count never shrinks.

CAMERA FLEW TO THE NORTH POLE on globally dispersed selections. Fits now trim outliers,
reject invalid and null-island coordinates, and unwrap longitudes across the antimeridian,
so the view lands on the data instead of a pole.
2026-09-22 12:50:37 +00:00
doxa-maps
d085a541ca Let a host reach the dashboard's own settings, so the provider is really swappable
Some checks are pending
build-gate / build (push) Waiting to run
The provider option landed last change but nothing a host passed could reach it:
the dashboard's config is a static import, so the setting existed in source and
was unreachable by an embedder. Real and unusable is the same trap as a nav
control that exists off-canvas. Hot-swappable has to mean a partner can choose
without a rebuild.

profileConfig is the channel every other plugin already uses for this, so this
adds no new mechanism — it stops the dashboard being the one plugin that ignores
it. The merge is shallow and the host wins. Shallow is deliberate: the static
config's nested objects are a coherent set that a partial override would corrupt,
and a deep merge would invite half-replaced facet maps that fail far from the
edit. What a host legitimately sets is top-level scalars.

With no host config the behaviour is byte-for-byte what it was, verified against
the baseline captured before any of this work: same node count, same canvas, same
row count, same engine, same ten tile responses.
2026-09-22 11:53:26 +00:00
doxa-maps
6a239201c0 Let the research dashboard choose its map provider, Mapbox still the default
Some checks are pending
build-gate / build (push) Waiting to run
The Driver wants MapLibre available without breaking Mapbox: keep every Mapbox
component working, add the option beside it, start with this dashboard.

The change is deliberately additive. provider defaults to mapbox, so with no
config the behaviour is what it was and nothing Mapbox-shaped can regress. A host
opts in through dashboardConfig, which is how every other dashboard setting
already arrives, so the cap-4 composites can pass it down with no new mechanism.

The plumbing already existed and was simply unreachable from here: useMapInstance
takes a provider and loads the matching engine, and the style registry maps every
named style to both engines with the MapLibre side pointing at keyless URLs. This
call passed neither provider nor style, so it silently took the default.

The token stays wired when MapLibre is selected. MapLibre ignores it, and keeping
it means switching back is one word with nothing else to restore, which is what
hot-swappable has to mean to be worth anything.
2026-09-22 11:39:10 +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
94dc3c64e8 Correct the shell's account of how it mounts its tabs
Some checks are pending
build-gate / build (push) Waiting to run
The file header said tab 1 was bp-big-picture, mounted directly via its real tag.
Both halves are false: tab 1 is dashboard-plus-grid, and every tab is an iframe —
there is no direct-tag mount left in this shell. A reader trusting that text would
look for a mount path that does not exist and maintain the wrong bundle believing
it was the one users see. The big-picture bundle is still a valid element; it is
simply one of the parts already composed into the tab-1 composite.

The locale switcher also poked slot zero by hand, writing a profile-config named
big-picture onto what is now an iframe, which ignores it. Harmless, and that is
why it survived: the effectiveLocale watcher was doing the real work all along,
branching on the tab's mode and re-pointing the iframe src that carries the
locale. Removed rather than repaired, since a second hardcoded locale path is one
more thing to keep in sync for nothing. Verified by driving the switcher: the
active frame moves from locale=en to locale=es.

The direct-tag branch in the template is kept. It is generic, keyed on the tab's
mode rather than a bundle name, and remains the right way to nest a future
token-free element. Only the comment claiming tab 1 uses it was wrong.
2026-09-22 08:33:24 +00:00
doxa-maps
684b209704 Show the research-map composite instead of Big Picture on a phone
Some checks are pending
build-gate / build (push) Waiting to run
The Driver's ordering: desktop opens on the Big Picture dashboard; mobile opens
on the research map and does not show Big Picture at all. Both capability-4
composites already existed, so this is the shell choosing between them. A row in
the nav model can now declare onlyWhen desktop or mobile; everything below the
first rung is identical on both, which is why only the first two rows carry it.

One nav model rather than two, so the teaching order lives in one place. Two
parallel models would be two things to keep in sync and the failure would be
silent: a row added to one and forgotten in the other simply never appears.

Crossing the breakpoint can remove the row that is currently active, which would
leave the user on a mounted but unlisted tab with no highlight. The shell now
snaps to the first surviving row, and establishes the viewport class before
opening the first tab so a phone never opens Big Picture for a frame.

Also fixes a pre-existing bug this exposed: with no header there was no way to
open the rail on mobile. The drawer sits off-canvas, the only toggle lives inside
the header, and the collapse control is inside the drawer itself — measured
off-screen at x:-244. Every tab except the one that happened to open was
unreachable. A floating button now appears only when narrow, headerless and shut,
so no existing consumer can see a change.
2026-09-22 08:13:41 +00:00
doxa-maps
914b0f0ca5 Fix the poster URL helper's wrong extension, and correct a stale header
Some checks are pending
build-gate / build (push) Waiting to run
posterUrl() said .png. There are 378 poster .webp files and no matching .png, so
the helper could only ever have produced a 404 — but it was never called, because
the export and print paths each built the same URL inline. A dead helper carrying
a wrong constant is worse than no helper: it is primed to break whoever tidies up
by routing a call through it. Point it at .webp and route both callers through
it, so the format lives in one place.

The file header still said posters "DO NOT EXIST YET. Nothing has been generated."
378 previews are generated, shipped and served live, and the wall paints from
them. A reader following that line would go build a generator that already ran.
Replace it with what actually ships: previews by default, full-resolution posters
excluded because bundling them pushed the tree past what the catalog would accept.
2026-09-22 07:47:29 +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
doxa-maps
e7eeb4d02a golden-path-check: walk the develop-a-plugin-and-see-it-live path end to end
Some checks are pending
build-gate / build (push) Waiting to run
The north star is that an admin agent can author a plugin, build it, publish
it and see it live. Every step was documented and individually verified, but
never walked in one pass by following the doc as written. This plugin is that
walk, authored from the documented three-file minimum.

It stays useful afterwards: it reads the manifest.json it was published
beside and reports what the catalog actually contains -- bundle and profile
counts, how many profiles describe themselves, and whether it is listed. If
you can see it answering, the path worked. Deliberately dependency-free: no
map engine, no token, no data API, so it can mount when everything else is
broken, which is when someone reaches for it.

The walk found a real defect in my own plugin, which is the point of doing
it. The first version wrapped the element in ProfileLoader (copied from a MAP
bundle) without createPinia. ProfileLoader builds three stores in setup(), so
setup threw -- and a thrown setup removes the subtree silently. Build green,
manifest listed it, nothing complained. Only smoke:embeds caught it: "shadow
DOM nearly empty (1 nodes)". That is exactly what Step 3b exists for and why
the doc puts it before publish.

The fix is the cap-1 pattern doxa-vision-card already uses: mount the root
SFC directly. ProfileLoader earns its cost for a map plugin; it is the wrong
wrapper for an element that draws no tiles and takes no token.

Verified: 17 -> 18 bundles, 26 -> 27 profiles, auto-discovered with no config
edit; renders 19 shadow nodes reporting live catalog state; embed suite 41/41.
2026-09-22 03:55:18 +00:00
doxa-maps
344c4dc831 Describe every plugin: 15 of 26 profiles shipped with no description
Some checks are pending
build-gate / build (push) Waiting to run
More than half the catalog had an empty description in manifest.json, so the
plugin-studio — the surface a Doxa inheritor browses to learn what exists —
showed flagship plugins as a bare "bundle - <tag>" with no explanation, while
a demo bundle explained itself fully.

Cause: the generator reads a JSDoc @description TAG, and these files carried
only prose. Every description added here is a summary of what that file's own
header already documented, not new invention. Claims were checked against the
artifacts: doxa-simple-map's "three presets" was verified against
simple-map.instances.json (prayer, engagement, adoption).

The two cap-5 shells needed a second pass. Their headers are HTML comments
with no leading asterisk, and the real TAG_RE requires `* @tag value`, so the
first attempt extracted nothing.

Result: 26/26 profiles described, 0 empty; the built gallery went from 15
empty / 11 populated to 0 / 26. Verified in a browser, not just in the
manifest. Embed suite 40/40.
2026-09-22 03:11:07 +00:00
doxa-maps
fc1159151f Give country-posters a real description so the SPA stops calling it "experimental bundle"
Some checks are pending
build-gate / build (push) Waiting to run
Both country-posters profiles had a prose header but no @description tag, and
the generator reads the TAG, not the prose. It emitted an empty description
into manifest.json, so the cap-5 SPA fell back to its generic label and the
Experimental rail showed a nameless tile — the only bundle in the app that
did. A Doxa inheritor browsing the SPA met "experimental bundle".

Added @description (and the @element tag both files were also missing, which
validates against the build's choke-point rule) to countries-map.vue and
country-poster-wall.vue, with text grounded in what each profile does.

Verified at the symptom, not the intermediate: the rendered rail now shows
the real description and the placeholder count is 0. Embed suite 40/40.
2026-09-22 02:45:07 +00:00
doxa-maps
7f08d09edc Remove the tile-403 on-screen banner; connect dashboard selections to the grid
Some checks are pending
build-gate / build (push) Waiting to run
Two Driver-reported defects.

1. No more "Basemap tiles blocked" popup on the map. The banner reported an
   operator problem (one Mapbox account setting on one deployment origin) to
   the end user, who can do nothing about it, on every map and every load. The
   console diagnosis stays, because it reaches the person who can act.

2. Selections in the dashboard now filter the grid beneath it. The two halves
   were never connected: research-dashboard had no defineEmits and its bus is a
   per-instance factory that is never provided, while dashboard-plus-grid passed
   the grid no filter props. The grid's receiving end already worked. Added the
   emit, plus a translation layer between the dashboard's snake_case facet keys
   and the grid's camelCase props, mirroring big-picture.

   Proven live: typing "Turk" takes the grid from 24 cards to 22 with wholly
   different rows. Only 4 of 12 facets map; the rest are dropped rather than
   guessed, because religion_family carries a family name while the grid's
   religion prop matches a code, and mapping them would filter to zero rows.
2026-09-22 01:43:56 +00:00
doxa-maps-88655c70
9dc4dda8cf consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

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

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

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

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

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00
doxa-maps-88655c70
f1772fda6f correct the dot-visibility comment to match the two-part fix actually shipped
The comment claimed a three-part fix including 'a thin white inner ring for
contrast'. No such property was ever added, and a mapbox circle layer has a
single stroke, so no inner ring is possible on this layer. A reader trusting
the comment would look for a second layer that does not exist.

Verified live against the running map rather than the source: the upg-pins
layer reports stroke-color rgba(15,20,25,0.9), opacity 1, and stroke-width
interpolating 1.2 -> 2 across zoom. The fix is real; only its description
was wrong.
2026-09-21 17:34:37 +00:00
doxa-maps-88655c70
5ddabe26e2 light the remaining unconditional loading overlays and the copy-me starter
A previous sweep found six dark matches, correctly ruled five of them unreachable, and
fixed the one real violation in doxa-simple-map. Re-running it with a luminance test
rather than a hex prefix found more, because the earlier pattern also matched the green
accent #10b981 and so buried the genuine surfaces in noise.

Three further unconditional violations, all the same shape as the one already fixed: the
loading overlays in all-people-groups, country-posters and doxa-research-map each render
on every load via v-if="!appReady", so a near-black flash was the first thing a visitor
saw. All four now use the palette surface with muted text.

The template-bundle starter was also dark in both profiles. That one matters more than a
leaf: the documented recipe is to copy template-bundle and rename it, so every future
plugin would have inherited a surface the house style forbids. The two profiles stay
visibly distinct — a tinted surface with an accent border, b white with a neutral one.

Verified by reading the shipped rule out of each built bundle: 4 of 4 found, all
#f3f3f1 at luminance 243. The found-count is reported so a run that matches nothing
cannot read as a pass — an earlier version of that check found zero rules and printed
ALL LIGHT.
2026-09-21 16:40:53 +00:00
doxa-maps-88655c70
4d92e69377 count only zones that are meant to paint, so the badge reads full health
Deriving the denominator from perZone.size fixed the impossible 27/26 but left a
permanent 27/28, which reads as "one zone is broken". I carded it as a bug on exactly
that reading. It is not a bug.

resolveAllZones deliberately excludes the deaf zone from the region-fill tapestry and
records zero for it, because its A017 affinity selector double-stamps land already owned
by the 13 geographic zones — 16 of oceania-indonesia's 18 Pacific ISO3 codes alone. That
double-stamp showed deaf twice and underlapped every other region. Deaf stays complete in
the list, the legend and the pins; only the polygon fill is withheld.

So a by-design exclusion was being displayed as a shortfall. Counting only zones that are
meant to have geometry makes the badge report 27/27 when everything is healthy, and fall
below its total only when a zone genuinely fails to resolve.

Verified: 28 leaves minus 1 deaf is 27 expected, 27 resolve, and the leaf now shows
195 polys · 27/27 zones · 2107 pins. The composite does not display this badge at all,
which a first probe misread as a failure.
2026-09-21 16:03:54 +00:00
doxa-maps-88655c70
2b72fb6a55 round the top-header search bar to a full pill
The Driver asked for the top-header search bar to be more rounded. It is now 16px, which
is exactly half its measured 32px height, so the ends are true semicircles. Any larger
value renders identically because the browser clamps to height/2, making this the actual
maximum rather than a number picked by eye.

Scoped to the top header only. The card's scope field named the shared SearchBar
component, but that component's own 10px is not what renders here: measuring the computed
style showed 6px, because DashboardTopBar overrides it. Editing the library would also
have changed people-groups-grid and big-picture, which was not asked for.

Verified 6px to 16px at two viewports, confirmed as a full pill, with the magnifier icon
still clear of the curve at a 10px inset.
2026-09-21 15:51:10 +00:00
doxa-maps-88655c70
b08ed8b484 centre the grid explainer in the existing table header
The Driver rejected v1, which added the hint as its own paragraph above the grid: "you
dont have to add a SECOND HEADER to make this show up... it could be CENTRED on the same
header where it says total 2107." So it now rides the bar that is already there, beside
the live count.

Centred by absolute positioning against the bar, not by flex spacers. The first attempt
here used a spacer on each side and measured 55px off centre at both 1440px and 1280px,
because spacers split the leftover space rather than the bar, and the left cluster
(dock, title, count, selection controls) and right cluster (toast, Export CSV) differ in
width. Absolute at left:50% with translateX(-50%) is relative to the bar itself, so the
clusters cannot drag it, and being out of flow it adds no height by construction.

The bar gains position:relative to be that containing block. pointer-events:none keeps
the hint from intercepting a click meant for a control beneath it, and it hides below
1100px where the clusters leave no free middle.

Measured against a pre-change baseline of 32px: 32px at all three viewports, one header
bar, and centre offset 0 at 1440px and 1280px.
2026-09-21 13:42:55 +00:00
doxa-maps-88655c70
15ce17f907 fix the impossible 27/26 zone badge and replace a faked page-2 shot with the real third profile
The badge read "27/26 zones" — a count exceeding its own total — in two shipped README
screenshots. 26 was the leaf count before slice 09b-6 added the Frontier-Asia uncovered
rows; the literal was never updated, so it drifted the moment the data grew. The
denominator is now derived from perZone.size, which tracks the zone list automatically
and cannot disagree again. Measured from clusters.json: 13 regional + 12 asia + 2
uncovered + 1 deaf = 28. The corrected badge also surfaces 1 genuinely unresolved zone
that the stale denominator was masking.

legend-tree.js asserted "33 leaves" and "7 Frontier-Asia uncovered" in a comment; the
shipped data carries 28 and 2. Corrected to point at the arrays instead of a count.

README showed "List — page 1" and "List — page 2" as the same picture. The shot loaded
one URL twice and tried to reach a page 2 by scrolling and clicking Next; neither exists,
the list has no pagination, and every attempt sat in try/catch so the miss was silent.
The PNGs differed by 0.16% render noise, so no checksum would have caught them. Mean-
while poster-wall — a real manifest-listed profile — had no screenshot at all. Shoot the
real third thing rather than fake a fourth.

check:shot-distinctness gates this, blocking in CI. It keys on url+viewport, so the
deploy2 mobile shot of the same SPA stays legitimate rather than a false positive.
2026-09-21 11:29:25 +00:00
doxa-maps-88655c70
4c9aca1321 show the grid filters by default and make the toggle a real control 2026-09-21 05:50:19 +00:00
doxa-maps-88655c70
702333d6de light the simple-map loading overlay; both synced copies 2026-09-21 01:11:05 +00:00
doxa-maps-88655c70
b309140d02 declare research-dashboard as monolithic (cap-3); it was inferred as single (cap-1) 2026-09-20 07:34:50 +00:00
doxa-maps-88655c70
bb5fd9aed4 fix big-picture @element to a bare tag and stop embed-smoke reporting every error as a mount timeout 2026-09-20 07:07:48 +00:00
doxa-maps-88655c70
981cc40633 Shared layout and legend tools on the DX palette; keep dark-surface ink ramps 2026-09-20 06:15:37 +00:00
doxa-maps-88655c70
37b9a550f7 Add show-total prop so an embedded grid defers its row count to the host 2026-09-20 05:57:47 +00:00
doxa-maps-88655c70
9fd520577b Trim table chrome to the 4px grid; recovers 64px of column width at narrow viewports 2026-09-20 05:47:38 +00:00
doxa-maps-88655c70
4a3eb5e445 Size table columns from measured need; derive the summary row from the column set 2026-09-20 05:39:41 +00:00
doxa-maps-88655c70
12ef4a529b Primary button uses a sage shade that meets WCAG AA for text at its rendered size 2026-09-20 05:24:44 +00:00
doxa-maps-88655c70
eae4e2be4c Restore cap-4 i18n context (configureApp) and re-theme shared search components to the DX palette 2026-09-20 05:15:02 +00:00
doxa-maps-88655c70
85014aad11 Session work: strobe fix, grid layout, stacked tree legend, selection camera, chip bounding, asset inheritance, gates 2026-09-20 04:49:25 +00:00
doxa-maps-88655c70
ea0c47b277 Brand-neutral shared VerticalTabsLayout with lazy nested-element hosting; fix cap-5 prayer tab leaf path; neutralize library identifiers; add Forgejo CI build-gate workflow
Some checks failed
build-gate / build (push) Has been cancelled
2026-09-19 17:24:30 +00:00
84fe1da1ab Bake profile type into manifest; plugin-studio cap-5 element; UPG-100 print+dots 2026-09-19 06:56:27 +00:00
doxa-maps-88655c70
2d64e21fbf Complete app-profiles->plugin-designs sweep across comments/docs; build green; layouts kept at root (load-bearing) 2026-09-17 18:18:19 +00:00
doxa-maps-88655c70
1a3925c6a6 Rename app-profiles -> plugin-designs; build green under new naming (load-bearing paths updated, guards pass) 2026-09-17 18:18:18 +00:00