Commit graph

30 commits

Author SHA1 Message Date
Doxa Maps
4a12b009aa Every runtime CDN script and stylesheet is an exact version with an integrity hash
maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).

- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
  @5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
  Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
  static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
  --online re-fetches each and compares the hash (all 9 match today).

MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
2026-09-26 05:50:24 +00:00
Doxa Maps
05ab8b9da1 Place search makes no Mapbox call without a real token, and says so
(cherry picked from commit cd03a2c6d0)
2026-09-26 03:34:31 +00:00
doxa-maps-88655c70
a206fe4deb Composed maps: stop the geocoder deleting every keystroke
Typing into the research-map's search bar inside a composite (the Simple
Research dashboard) wiped the box instantly and never queried. Standalone, the
same map searched fine — which is the shape of the bug, not a coincidence.

mapbox-gl-geocoder unwraps exactly ONE shadow level to find the input:

    var e = t.target && t.target.shadowRoot ? t.target.shadowRoot.activeElement : t.target
    if (!(e ? e.value : "")) { this.fresh = !0, 9 !== t.keyCode && this.clear(t), ... }

Its handler is debounced, so it reads event.target after dispatch has ended, by
which time the target is the OUTERMOST host. One hop is therefore exactly right
for a map one custom element deep and exactly wrong for a map inside another
one. Measured, same build, same keystroke:

  doxa-research-map (1 deep)   target=DOXA-RESEARCH-MAP        -> unwrap = the input          -> _geocode("Bhu")
  research-map-plus-grid (2)   target=RESEARCH-MAP-PLUS-GRID   -> unwrap = <doxa-research-map> -> clear()

An element with no .value reads as an empty box, so the vendor cleared what you
had just typed. Walk the activeElement chain to the bottom instead of one step
and hand the vendor an event whose target is that element; nesting deeper cannot
break it again, and at one level it resolves to the same input as before.

Two things worth recording. The receiver is passed explicitly: the Mapbox build
binds this method in its constructor and the MapLibre build does not, and an
unbound call landed on `this === undefined`, which swallowed the keystroke AND
the query ("Cannot read properties of undefined (reading
'_hideGeolocateButton')") — green-looking text with no search behind it. And
neither suspect on the card was the cause: the clear came from inside the vendor
bundle, not from _geoBeingCleared, and SearchBar is not involved.

MEASURED on the built tree, both engines' mapbox mode, typing "Bhutan":
  fix off  leaf ok (1 geocoding request) / composite value="" and 0 requests
  fix on   both keep "Bhutan", both fire the query, both list 19 results
           including People Cluster, People Group + Country, Country and Place
Screenshot read, not just the log. gates:pre 7/7, gates:post 34/34.
2026-09-24 16:16:36 +00:00
doxa-maps-88655c70
e390454d31 Map controls: restore the keyboard focus ring that outline:none removed
The brief says of the no-selection chooser: "Keep the dropdowns as the
keyboard/screen-reader path." Pin clicking is mouse-only by
construction, so that path is load-bearing — and nothing had ever
pressed Tab. There was zero keyboard coverage across 42 probes.

The path itself is sound, and is now asserted: 30 distinct focus stops;
the chooser's region keys are real focusable buttons and Enter on one
selects it ("All Regions" to "Africa") with no mouse involved; the
SELECT VIEW strip carries role=tab on both buttons with exactly one
aria-selected.

The defect that fell out: of those 30 stops, 29 showed a 2px ring and
one showed outline:none — button.mcb, the shared MapControlButton. Its
line 38 sets outline:none deliberately, to kill the webkit focus box on
click/tap alongside -webkit-tap-highlight-color, but it had no
:focus-visible replacement. So a sighted keyboard user tabbing onto ANY
map control in ANY bundle saw nothing at all. The accessible name was
already correct (aria-label mirrors title, fixed 2026-09-21), so screen
readers were never affected — only the people who navigate by eye and
keyboard, which is the very group outline:none was written for.

The fix is a :focus-visible rule plus a .dark variant. :focus-visible
is the correct half of the pair: it never fires for a mouse click, so
the original reason for outline:none still holds. Consumers re-checked
per RULE 2 — ShareButton, ZoomIn, ZoomOut, Fullscreen, Help,
ThemeToggle, Location, map-controls/index.js, my-upg-100-list and
research-dashboard's DashboardDataTable — all render the button
icon-only and none styled focus themselves, so the rule only adds a
ring where there was none. All five affected bundles rebuilt.

Proven both ways: Tab gives solid 2px rgb(26,115,232) with
:focus-visible true and the "Share" label intact; a real mouse click on
a fresh page gives outline:none with :focus-visible false.

Three of my own errors on the way, each caught before it shipped. I
first read a region key's name as "Africa315" and nearly fixed a
non-bug — the button already carries aria-label="Open the Africa
dashboard", which wins over textContent, and the label and count have a
real 7.5px flex gap. I put backticks in a CSS comment inside a template
literal, which ended the string and broke four of five builds;
check:sfc-parse caught it at once. And my mouse assertion failed twice
on correct behaviour: synthetic PointerEvents with a manual focus() do
not update the browser's pointer-modality heuristic, and clicking the
button that had just held keyboard focus leaves the page in keyboard
modality. Only a fresh page with no key ever pressed is a clean test.

Regression: 22 suites green at the time of commit, including new
keyboard and focus-ring suites.
2026-09-24 08:14:01 +00:00
doxa-maps-88655c70
79353ca66e One page, one map token — and the nested search bar comes back
"the element gets the tk to the element itself and does not need to only
gatekeep that to one component only — all components in a plugin can share
that tk" (Driver, 2026-09-23).

Inside ONE element that was already true: ProfileLoader provides
'mapboxToken' and eleven components inject it. The gate is the ELEMENT
BOUNDARY — every custom element mounts its own app with its own
ProfileLoader, which re-derives the token from its own profile-config, so a
composite that nests a real map hands the child nothing.

THE EXISTING FALLBACK COULD NOT FIRE. The ladder already read
window.MAP_TOKEN. MEASURED: that global IS set on the composite leaf and
the child still had no search bar — because the ladder lives in a
computed() and window.MAP_TOKEN is a plain global, not a reactive source.
The composite leaf FETCHES its token, so the computed resolved '' first,
cached, and never re-ran. useSharedMapToken makes it a ref that updates on
publish, and assignments to window.MAP_TOKEN are adopted through an
accessor, so already-generated pages keep working untouched. Storage lives
on window and changes travel as an event: each bundle is its own IIFE, so a
module-level ref is NOT shared between a composite and the bundle it nests.

THAT WAS NECESSARY AND NOT SUFFICIENT, which only measuring showed. With
the token shared and reactive the bar was STILL absent. Real cause:
GeocoderComponent's onMounted hit `typeof MapboxGeocoder === 'undefined'`
and RETURNED SILENTLY — no throw, no console error, no DOM. MapboxGeocoder
is "preloaded via index.html", true for a LEAF and false for a COMPOSITE
leaf, which generate-tree classifies as token-free while it goes on to nest
a real map. Measured, mapbox-gl-geocoder references per page: research-map
leaf 2, research-map-plus-grid leaf 0.

So the script is now fetched on demand when a host did not preload it,
reusing the exact shape the maplibre path already had (one loader, two
callers) and pinned to the same v5.0.0 the leaves preload. It resolves on
error as well as load, so a blocked CDN leaves the map usable and the bar
absent rather than hanging. This fixes every already-generated page without
regenerating any of them.

MEASURED on the built tree:
  leaf mapbox     search@56    legend@104
  leaf maplibre   search@NONE  legend@58    (unchanged: maplibre stays bar-free)
  composite       search@56    legend@104   (was NONE)

library/ reaches every bundle, so all 22 rebuilt: 22/22, 7/7 pre, 18/18
post, and a browser sweep of five StackLayout consumers plus all seven
studio dashboards.
2026-09-24 01:14:37 +00:00
doxa-maps-88655c70
61bd1cf8c5 Share menu opens at natural height in a page header (no scrollbar)
Some checks are pending
build-gate / build (push) Waiting to run
MapControlMenu's 'fit inside the map' height clamp is right for a map
host but wrong for ShareButton's header fallback: the fallback tags
the ~56px header band as the area (data-map-area='share-fallback'),
and the menu clamped INSIDE it -- measured on upg-100-mfe, a 291px
share menu rendered with a 69px scrolling body. A menu must never
outgrow a MAP's walls, but a header band is not a wall (nothing clips
below it), so a share-fallback area now bounds against the VIEWPORT.

Measured: header host body 291/291 no scroll, menu 342px tall below
the band; map host (doxa-simple-map) unchanged, 287/287 within the
map. Real map areas keep the original walls -- only the self-tagged
fallback takes the new branch.
2026-09-23 23:47:28 +00:00
doxa-maps-88655c70
988b1cef87 Share menu shows the whole embed snippet, with no scrollbar
Some checks are pending
build-gate / build (push) Waiting to run
The code box was max-height:90px + overflow-y:auto, which clipped a ~4-line
embed snippet to about two and a half lines and put a scrollport inside a
popover. Wrong trade for this content: the box exists to be read and copied
in one glance, and user-select:all means a single click selects the whole
snippet -- a scrollport fights that gesture, because the selection extends
into content the reader never saw.

height:auto lets the snippet size itself. The popover is short-lived and
anchored, so the extra lines cost nothing.

Measured on the demo-map leaf after opening the "Add to Your Website" tab:
max-height none, scrollHeight 110 == clientHeight 110, scrolls false.

Library change, so it reaches every bundle that mounts the share control on
the next full build.
2026-09-23 19:01:14 +00:00
doxa-maps-88655c70
ad6da54674 Checkpoint the swarm's in-flight work
Some checks are pending
build-gate / build (push) Waiting to run
Five lanes had 465 insertions across 18 files sitting uncommitted, some for
15 minutes. That is the state that silently lost the shell-scroll fix earlier
today: a verified fix existed only in the working tree, a second agent's edit
to the same file overwrote it, and the reverted version got committed and
described as the fix.

Nothing here is finished and no card is being closed on it. This is a
snapshot so the work survives an overwrite, taken at a moment when every file
had been quiet 15s or more, so no agent was mid-write.

Verified coherent before staging rather than committed blind: template-literal,
mjs-parse, sfc-parse, no-orphan-gates, kpi-unit, composite-reg, script-flags,
quoted-scripts and backlog-status all PASS. check:synced-css fails on
PeopleGroupDetail.vue and doxa-simple-map.vue only -- the same two files and
the same counts as before the swarm started, both untouched by any agent, both
already carded.

Two untracked files included because both are load-bearing: internal/
check-facet-one-field.mjs is wired into package.json with a self-test, and
library/utils/viewportScrim.js is imported by DataTable, SemanticTreeLegend
and PosterDialog -- a shared helper for the modal-scrim fix rather than six
copies of the same workaround.
2026-09-22 21:42:18 +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
a5c5cf1910 give every map control an accessible name
The zoom buttons render an icon through a slot and carried only `title`, so their
accessible name was empty: a screen reader announced "button" with no indication of what
it does. title is a tooltip, not an accessible name — unreliably announced, and never
surfaced to touch users.

Found while proving the research-dashboard zoom controls actually work. They do: a click
triggered six tile requests at zoom level 2, so the open-work item is function-verified,
not just present. The probe that matched aria-label found nothing while the probe that
fell back to title found both buttons, and that discrepancy was the defect.

One binding in the shared MapControlButton reaches all eight control buttons — zoom in
and out, fullscreen, location, help, share, hamburger, theme — across seven map bundles,
because every one composes this base. No caller changes: the component does not set
inheritAttrs:false, so a caller passing its own aria-label still wins through normal
fallthrough, and undefined omits the attribute when there is no title to mirror.

Verified across all seven bundles: 34 control buttons, 0 without an accessible name.
2026-09-21 13:02:56 +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
swift-river542
f6c86be7a9 Share menu: the multiplication steps read at 14px
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-17 01:59:02 -05:00
swift-river542
a025b69e14 Phones: the search bar sits in its own header band; the share menu is titled Share and shows its content without scrolling 2026-09-16 23:02:09 -05:00
swift-river542
c229edc66e MapControlMenu: one menu for every map control — inside the map, top and height of the toolbar column, on its inner side, RTL-aware, phone and desktop; the share button is its first consumer 2026-09-16 22:46:21 -05:00
swift-river542
3f99dd8c99 Share menu anchors next to its button on every width 2026-09-16 22:33:53 -05:00
swift-river542
c6b6dce11f Share menu is part of the map: rendered in the map area, placed from its button, top layer on its own; toolbar stays under the legend 2026-09-16 22:30:35 -05:00
swift-river542
9203b2fc87 Share menu is a compact 340px, never wider than the viewport; right-aligned under the toolbar on phones 2026-09-16 22:16:46 -05:00
swift-river542
a391cfb65e Share menu paints above every map layer: the toolbar lifts while a menu is open; the menu stays inside the map's bottom edge; phone band spans the map 2026-09-16 22:14:27 -05:00
swift-river542
b024d83d83 Share menu: first tab is "Embed link"; the rainbow ring rotates in every browser 2026-09-16 20:56:35 -05:00
swift-river542
d61aa9674e Share menu opens with the multiplication path: embed, list your city's sites, train them, train trainers, multiply maps everywhere for Jesus 2026-09-16 20:23:44 -05:00
swift-river542
d5d6c9231f Share button: a thin, slowly turning rainbow ring (reduced-motion safe) 2026-09-16 20:20:36 -05:00
swift-river542
9cefd6faa4 Plugin gallery: one generator for the dev server and the build
- internal/playground → internal/plugin-gallery; playground-smoke → plugin-gallery-smoke;
  generate-spa → generate-gallery (scripts: gallery, smoke:gallery; smoke:playground stays as alias)
- generate-gallery exports renderGallery(model, { mode }) — the dev plugin serves the same page
  at / from the discovered registry (dev mode imports the dev entries, ladder popovers on);
  the build writes public/index.html from the manifest (byte-identical to before)
- Docs and probes use the gallery vocabulary
2026-09-16 20:04:18 -05:00
swift-river542
12d1fee133 Public build folder is three entries: index.html, assets/, doxa-maps/
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- index.html is generated by internal/generate-spa.mjs from doxa-maps/manifest.json: one
  self-contained page that lists every bundle, profile and instance, mounts the chosen one,
  and offers Open page / Copy embed; build ends with `bun run spa` (the material shell is no
  longer installed; nav.js, icons.js, languages.js, pane-wrapper/, external/ and _headers
  leave public/)
- The website drop-in follows the same contract: dropin.include in the build config,
  sync-dropin --from-config --prune, public-hygiene --strict fails on any other root entry
- Production bundles no longer carry CSS comments: a build-only Vite plugin strips them from
  every shadow-style block (about 140 per map bundle, 5% smaller); remaining comments reworded
- Share pages and the pin renderer drop their note-style comments
2026-09-16 18:23:14 -05:00
swift-river542
7997f3b092 Maps stop running Mapbox's render loop through Vue's reactive proxy; prayer glow throttled
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- The Mapbox Map instance is unwrapped (markRaw) at the profile boundary and the layers
  composable works on the raw map, so Mapbox internals no longer pay Vue's get/set traps on
  every frame; the geocoder control instance is a shallowRef for the same reason
- Prayer glow rings animate at 12 steps/s with paint transitions disabled, constants moved to
  addLayer, and the loop pauses while the map is off-screen — the map can idle again
- New gate: internal/long-frames-probe.mjs measures long animation frames, blocking time and
  DevTools rAF violations per phase (boot / idle / interaction); idle violations on the prayer
  and research maps dropped from ~100 to the no-animation baseline
2026-09-16 14:11:55 -05:00
swift-river542
a876e5e97b Host-agnostic share links, token-endpoint chain, dev playground as reusable modules, share/asset gates, validated profile-type vocabulary
- ShareButton resolves its link from where the bundle is hosted (profile-config.shareUrl ->
  embedBaseUrl -> window.MAP_EMBED_BASE_URL -> the bundle's own script URL); profiles pass
  tree-relative embed pages, so links work in the playground, on doxa.life and on any CDN
- Generated leaf pages try token endpoints in order (override -> configured absolute URL ->
  same-origin) and only then offer the paste-once box; build config points at the canonical
  https://doxa.life/api/maps/token
- Dev playground extracted from vite.config.js into internal/playground/{discover,render,dev-plugin}.mjs;
  serves /assets/* like the built site so data-driven maps initialise; passes assetsBase +
  shareUrl to every element; header ladder with a Start-here primer and the five kinds of
  mounting files (single-element, preset-element, multi-view-element, composite-element, app-shell)
- New gates: smoke:playground (every element boots, with status text + data diagnostics),
  smoke:share (Share URL resolves and reopens the same map), sync:dropin (mirror public/ into a
  host checkout, never credentials), deploy:dev (build + sync)
- MapToolbar + ShareButton on every map profile; UPG-100 composite (map + prayer lists) on the
  pane layout; shared --map-radius token consumed by the legend components
- docs/reference.md: profile types table, profile-config assetsBase/shareUrl/embedBaseUrl, gates
2026-09-16 12:10:38 -05:00
doxa-maps
de054ca078 feat: origin-derived share URL — defaults to the map page's own location so embeds are correct on any CDN/host (GitHub Pages, host public/, external), explicit path still wins
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-11 22:04:51 +00:00
doxa-maps
c7260faaa3 feat: map provider swap — mapbox|maplibre engine + hybrid geocoder + provider badge; staging title doxa-maps 2026-09-11 21:47:25 +00:00
doxa-maps
35ac1c5607 docs: simplify all folder READMEs (what's here + graphic + link); migrate complex reference to docs/architecture/{data-sources,i18n,components,app-wrappers,poster-generation}.md 2026-09-11 18:50:31 +00:00
doxa-maps
d5ba21fa76 doxa-maps — portable bundler for embeddable JAMstack maps
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.

History reset to a single clean commit; no PII, IPs, or internal terms.
2026-09-10 01:53:07 +00:00