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.
493 lines
57 KiB
Markdown
493 lines
57 KiB
Markdown
# Reference
|
||
|
||
Look-up tables and contracts for doxa-maps. For learning, see the
|
||
**[tutorial](./tutorial.md)**; for tasks, **[how-to.md](./how-to.md)**; for the *why*,
|
||
**[explanation.md](./explanation.md)**.
|
||
|
||
`doxa-maps` is a Vite bundler: **one shared library, N independent embeddable bundles.** Each
|
||
bundle is a single self-contained `.js` file that registers a custom HTML element
|
||
(`<doxa-map>`, `<doxa-research-map>`, …) you can drop on any web page.
|
||
|
||
---
|
||
|
||
## The three zones
|
||
|
||
| Zone | Path | Alias | License | You edit it… |
|
||
|---|---|---|---|---|
|
||
| **Shared library** | `library/` | `@map` | Apache-2.0 | …once; the fix reaches every bundle on next build |
|
||
| **Your app-profile** | `app-profiles/<name>/` | — | yours | …freely; nothing here leaks to other maps |
|
||
| **The build machine** | `internal/` | — | Apache-2.0 | …never — it's the bundler's own scripts |
|
||
|
||
**Terminology.** "Profile" is used at two levels: a **bundle / app-profile** is a folder under
|
||
`app-profiles/`; a **profile screen** is one `.vue` file sitting **flat** in that folder (no
|
||
`profiles/` subfolder — the entry globs `./*.vue`). A bundle can hold several screens; the host
|
||
picks which to render with the `profile` field of `profile-config`.
|
||
|
||
**Three profile patterns** (mix freely in one bundle): **Individual** (one `.vue` per bundle) ·
|
||
**Parameterized** (one screen driven by `profile-config` props at runtime) · **Nested** (one
|
||
screen that renders others via tabs, workbench-style).
|
||
|
||
---
|
||
|
||
## Directory tree
|
||
|
||
```
|
||
doxa-maps/
|
||
├── library/ ← SHARED LIBRARY (alias @map, Apache-2.0)
|
||
│ ├── index.js ← thin barrel; prefer subpath imports for tree-shaking
|
||
│ ├── ProfileLoader.vue ← the ONE component that reads profile-config & loads a screen
|
||
│ ├── components/ ← map UI: legends/, map-controls/, poster/, dashboards/, drawers
|
||
│ ├── composables/ ← useMapInstance, useMapData, useMapLayers, … (+ legends/, poster/)
|
||
│ ├── api/ ← DataSourceManager (csv | api | rest-api) + sources.json
|
||
│ ├── colors/ ← one color strategy per file, auto-registered by _registry.js
|
||
│ ├── constants/ ← never-change values: mapDefaults.js, zoom.js, poster*.js
|
||
│ ├── i18n/locales/ ← 11 base locales: ar de en es fr hi it pt ro ru zh
|
||
│ ├── stores/ ← Pinia: dataStore, mapStore, uiStore (per-instance)
|
||
│ └── utils/ ← pure helpers (geoUtils, clustering)
|
||
│
|
||
├── app-profiles/ ← ALL bundles live here (each subfolder = 1 bundle)
|
||
│ ├── <bundle>/
|
||
│ │ ├── index.js ← bundle ENTRY — globs ./*.vue, registers the web component
|
||
│ │ ├── index.html ← Vite html entry = the dev STAGING page
|
||
│ │ ├── <profile>.vue ← 1..N FLAT profile screens (no profiles/ subfolder)
|
||
│ │ └── src/ ← everything else the map needs (its own colors/, api/, …)
|
||
│ └── template-bundle/ ← minimal copy-me reference bundle
|
||
│ (folders starting with _ or . are SKIPPED by the build)
|
||
│
|
||
├── internal/ ← the bundler's own build scripts — never edited by map builders
|
||
├── layouts/ ← the app shell: mounts several bundles as one single-page app
|
||
├── capacitor/ ← native projects (android/, ios/)
|
||
├── public/ ← THE SERVED WEB-ROOT, committed:
|
||
│ ├── index.html the app shell
|
||
│ ├── doxa-maps/<bundle>/ built bundles + manifest.json + staging HTML
|
||
│ └── assets/ posters & data the maps fetch at runtime
|
||
├── vite.config.js ← multi-entry: discovers app-profiles/<name>/ folders
|
||
└── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings
|
||
```
|
||
|
||
---
|
||
|
||
## The `profile-config` runtime contract
|
||
|
||
A bundle exposes ONE attribute, `profile-config` — a JSON string parsed by
|
||
`ProfileLoader.vue` at runtime. It is the entire wiring between host page and map, so the same
|
||
built bundle re-points to different data/colors/language with **no rebuild**.
|
||
|
||
```html
|
||
<doxa-map profile-config='{
|
||
"profile": "doxa-simple-map", // which flat .vue screen to render (required)
|
||
"tk": "pk.eyJ...", // Mapbox token (required; host-supplied, never baked)
|
||
"instanceId": "my-map", // optional; isolates window events between embeds
|
||
"dataSource": "pray-tools", // or { "type":"rest-api","endpoint":"https://…" }
|
||
"colorSet": "default",
|
||
"locale": "en",
|
||
"tabs": [ /* parameterized screens read this */ ]
|
||
}'></doxa-map>
|
||
```
|
||
|
||
- `profile` **must** match a flat `.vue` filename in the bundle. Mismatch → an inline
|
||
"Profile not found" error box listing the available names.
|
||
- `tk` is supplied at runtime so the token is never compiled in (dev uses `.env`'s
|
||
`VITE_MAPBOX_TOKEN`).
|
||
- `instanceId` scopes cross-instance window events so multiple embeds never interfere.
|
||
- `dataSource` is handed to `DataSourceManager` (`csv` | `api` | `rest-api`).
|
||
- `ProfileLoader` `provide()`s to descendants: `mapboxToken`, `dataSource` (both **computed
|
||
refs** — `unref` before use), `colorSet`, `profileConfig`, `instanceId`, `lang`, and the
|
||
instance-scoped Pinia stores `uiStore` / `mapStore` / `dataStore`. Descendants **inject**;
|
||
never call `useXxxStore()` directly (it would cross-bleed between embeds).
|
||
|
||
Each bundle registers a **shadow-DOM** web component via `defineCustomElement`, with its own
|
||
ShadowRoot, styles, Pinia, and i18n. `customElements.define()` is one-shot per tag name, so
|
||
each bundle registers a **distinct** tag.
|
||
|
||
---
|
||
|
||
## Build scripts & output
|
||
|
||
| Script | What it does |
|
||
|---|---|
|
||
| `bun run dev` | Vite dev server + HMR (default `http://localhost:5173`). Serves every bundle at its html path. **Writes nothing to `public/`** — no HMR across that boundary. |
|
||
| `bun run build` | `clean` → one `vite build` per `app-profiles/*/` folder → `tree` → `check:drop` → `check:parse`. |
|
||
| `bun run build:bundle` | Builds ONE bundle (needs `BUNDLE=<name>`). |
|
||
| `bun run build:watch` | Rebuilds into `public/` on every change (integration loop). |
|
||
| `bun run clean` | Wipes + recreates the `public/<name>/` output tree. |
|
||
| `bun run tree` | Writes `manifest.json` + self-contained staging HTML, then copies the tree to every build-config `paths` destination. |
|
||
| `bun run verify` | Data-integrity + shipped-bundle + network-policy guards (fast, no browser). |
|
||
| `bun run verify:full` | + rendered posters and live apps in Firefox. |
|
||
|
||
**How a build works.** `vite.config.js` is multi-entry by discovery: at build time it
|
||
registers every `app-profiles/` subfolder that (a) doesn't start with `_`/`.` and (b) has
|
||
**both** `index.html` and `index.js`. One `vite build` builds **exactly one** bundle, selected
|
||
by `BUNDLE=<name>`, so the output is a self-contained **IIFE** (`inlineDynamicImports: true`).
|
||
`bun run build` loops every folder. Each bundle only bundles what its screens import, so sizes
|
||
differ (simple ≈ 420 KB, research ≈ 1.3 MB).
|
||
|
||
**Where output goes.** One bundle folder → one file: `public/doxa-maps/<bundle>/<bundle>.js`.
|
||
CSS is injected by JS (`cssCodeSplit: false`), so there is no separate `.css`. The tree root
|
||
name comes from `vite.doxa-maps-build-config.json` `name` (default `doxa-maps`).
|
||
`emptyOutDir: false` keeps sibling bundles intact when you rebuild one.
|
||
|
||
**Bun is required.** `bun.lock` is the committed lockfile; `bun x vite` runs project-local
|
||
Vite. `npm run …` resolves the same scripts but Bun must be on PATH.
|
||
|
||
---
|
||
|
||
## `vite.doxa-maps-build-config.json` fields
|
||
|
||
Edit this one file, then `bun run build`.
|
||
|
||
| Field | What it does |
|
||
|---|---|
|
||
| `name` | Output folder name (default `doxa-maps`). |
|
||
| `paths` | **Extra** folders to copy the whole built tree into. Point at a host repo's `public/` → that host serves the maps and becomes the CDN. Empty (the default) ⇒ written to `public/<name>/` only, since `public/` is already the committed web-root. |
|
||
| `embedBaseUrl` | The public URL the tree is served from, used to generate **absolute, paste-anywhere** embed snippets. Empty ⇒ relative URLs (position-independent; keeps the drop-portability gate green; required for Capacitor, which loads from `https://localhost`). |
|
||
| `tokenUrl` | Endpoint each generated page fetches its Mapbox token from — a URL, **not** a secret. Set absolute (`https://…/api/maps/token`) so a CDN folder and the mobile app both work; endpoint must send CORS for the embedding origins. `window.MAP_TOKEN_URL` overrides at runtime. |
|
||
| `token` | Opt-in literal token baked into every page. **Leave empty** — prefer `tokenUrl`. |
|
||
| `assetsBaseUrl` | Where the shared asset tree (previews, posters, boundaries) is served from. Empty ⇒ a relative path per page depth (self-contained under one web-root). Set absolute for partner embeds and mobile, where there is no shared web-root to climb to. |
|
||
|
||
**Token resolution ladder:** per-map `tk` prop > `?tk=` > baked `token` > `window.MAP_TOKEN_URL` > `tokenUrl` > same-origin `/api/maps/token`.
|
||
|
||
---
|
||
|
||
## Data sources
|
||
|
||
Maps read data through `@map/api/` (the engine) driven by `library/api/sources.json` (the
|
||
declaration). `sources.json` lists every connection by id, names one `activeSource`, and maps
|
||
each source's raw field names onto the system field names components expect (`fieldMappings`).
|
||
`DataSourceManager.js` does **fetch → normalize → cache**; the registry front door is
|
||
`api/_registry.js`. To add or change a source, edit `sources.json` — components stay
|
||
source-agnostic.
|
||
|
||
| Type | What | Points at |
|
||
|---|---|---|
|
||
| `rest-api` | Versioned REST endpoint (the default) | The live people-group prayer/engagement/adoption API. Base URL resolves at runtime: `window.MAP_APP_API_URL` → `VITE_API_BASE_URL` → `''` (same-origin), so a host repoints it with no rebuild. |
|
||
| `csv` | A local CSV parsed at load | The Joshua Project people-groups dataset (also the people-group photo fallback URLs). |
|
||
| `api` | A single JSON URL | Any plain JSON endpoint. |
|
||
|
||
**Country / region polygons are not a `sources.json` connection.** Boundaries render from
|
||
Mapbox's built-in `country-boundaries-v1` tileset (highlighted by `iso_3166_1_alpha_3`), plus a
|
||
local WAGF-regions GeoJSON lazy-loaded on the Regions tab.
|
||
|
||
---
|
||
|
||
## Colors — two homes, one rule
|
||
|
||
**Reused by more than one map → shared library. Only your map uses it → your profile.**
|
||
|
||
- **Shared taxonomies** live in `library/colors/` (`religion`, `adoption`, `engagement`,
|
||
`language-family`, `prayer-progress`). The registry auto-discovers every `<name>.js` there.
|
||
- **Single-map strategies** live in the bundle's `src/colors/`. The bundle's `index.js` runs
|
||
`registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true }))`, merging local
|
||
files **over** the shared set for that bundle only. (`affinity-block`, `doxa-region`,
|
||
`resource` are research-only and live in the research bundle.)
|
||
|
||
The **filename** derives the mode key (`language-family.js` → `languageFamily`). Same key as a
|
||
shared strategy → **override** (your map only); new key → **add** (your map only). A `colors.json`
|
||
of plain values can sit beside the `.js` files — the `*.js` glob ignores it, so values and
|
||
strategies coexist. Strategy contract (`export default { name, propertyKey, palette, getColor,
|
||
applyColor, buildColorExpression }`) lives in `library/colors/README.md`; `religion.js` is the
|
||
canonical example.
|
||
|
||
---
|
||
|
||
## Importing from `@map`
|
||
|
||
Everything under `library/` is importable via the `@map/…` alias (`vite.config.js` maps `@map`
|
||
→ `library/`). The import path mirrors the file path: a file at `library/<sub>/<file>` imports
|
||
as `@map/<sub>/<file>`. Examples:
|
||
|
||
```js
|
||
import SemanticTreeLegend from '@map/components/legends/SemanticTreeLegend.vue'
|
||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||
import { useMapData } from '@map/composables/useMapData.js'
|
||
import langFamilyByIso from '@map/data/langFamilyByIso.json'
|
||
```
|
||
|
||
Prefer subpath imports over the thin `@map` barrel (which exports `MAP_CORE_VERSION` only) so
|
||
tree-shaking works. The per-file catalog below is the authoritative menu — every `library/`
|
||
file listed is importable this way. A generated one-line import index can also be produced with
|
||
`node internal/generate-library-index.mjs`.
|
||
|
||
---
|
||
|
||
## The Semantic Tree Legend
|
||
|
||
`@map/components/legends/SemanticTreeLegend.vue` is a reusable hierarchical legend for any
|
||
one-to-many semantic tree (Affinity Block → Cluster → People Group, or Language Family →
|
||
Language). You feed it a tree of nodes; it renders one tab per generation plus rows.
|
||
|
||
Node shape: `{ id, label, color?, count?, pop?, filter?, info?, children?: [ … ] }`
|
||
|
||
| Prop | Meaning |
|
||
|---|---|
|
||
| `nodes` | The tree (array of root nodes). |
|
||
| `tabs` | Optional labels per generation; auto-generated from depth if omitted. |
|
||
| `columns` | Numeric columns to show (default `['count','pop']`). |
|
||
| `title` | Panel heading. |
|
||
| `hideTabs` | Render a single flat level. |
|
||
| `exportEnabled` | Opt-in flag revealing the (scaffold) Export affordance. |
|
||
|
||
Selecting a node walks the tree to highlight it with its ancestors/descendants and emits a
|
||
`select` event the map listens to for filtering. If a per-instance store is provided (via
|
||
`usePplrInstance`) it syncs `activeTab`/`selection` through that shared store; otherwise it
|
||
falls back to local state and still works standalone. It never imports the map or geocoder —
|
||
the profile screen wires its `select` emission to map filtering.
|
||
|
||
---
|
||
|
||
## File map — every file, one line each
|
||
|
||
Generated by a per-file agent scan. The `@map/…` import path mirrors each `library/` file path
|
||
(see *Importing from `@map`* above).
|
||
|
||
## library/ — shared core (`@map` alias)
|
||
|
||
### Root files
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/ProfileLoader.vue` | Root component of every custom element: parses the `profile-config` JSON attribute, async-loads the profile .vue, provides shared context (mapboxToken, dataSource, colorSet, profileConfig, instanceId, lang, uiStore/mapStore/dataStore). | Only reader of `profile-config`; uses injected `appI18n` (useI18n() throws at custom-element root); bundle entry MUST `app.provide('profileModules', import.meta.glob('./*.vue'))` or setup throws. Descendants inject stores, never call `useXxxStore()`. |
|
||
| `library/index.js` | The `@map` barrel entry — intentionally thin; exports `MAP_CORE_VERSION` only. | Keep the barrel thin: subpath imports (`@map/components/...`, `@map/stores/...`, `@map/colors/_registry.js`) are the preferred pattern; stores/JSON deliberately not re-exported (tree-shaking). |
|
||
|
||
### api/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/api/DataSourceManager.js` | Data-loading class: fetches people-group data (csv / json api / rest-api per `sources.json`), normalizes rows to system field names, caches per source. | Exports `DataSourceManager` class + legacy `dataSourceManager` singleton — prefer `new DataSourceManager()` per map. `sources.json` is inlined at build time; 0,0-coord rows dropped; `{value,label}` objects split into field + fieldLabel. |
|
||
| `library/api/_registry.js` | Front door to the data-source seam (parallel of `colors/_registry.js` for data): `SOURCE_TYPES`, `SOURCES`, `getActiveSourceId`, `getSourceConfig`, `listSources`, `getApiBaseUrl`, `DataSourceManager`. | Connections are pure data in `sources.json` (activeSource + sources by id, each with type + fieldMappings) — add/change sources there, not in components. |
|
||
| `library/api/apiBaseUrl.js` | Runtime API base URL resolver: `getApiBaseUrl()` = window override > Vite env var > `''` (same-origin). | Hosts set `window.MAP_APP_API_URL` before the IIFE loads to repoint the API without a rebuild. |
|
||
| `library/api/sources.json` | Data-source registry: pray-tools REST API + doxa-csv fallback, with endpoints, fieldMappings, poll settings, systemFields, `_meta.mapboxTokenUrl`. | Single source of truth for ALL bundles' data config; fieldMappings keys ARE the canonical camelCase field names; base/token URLs resolve at runtime via `window.MAP_APP_API_URL` / `window.MAP_TOKEN_URL`. |
|
||
|
||
### colors/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/colors/_registry.js` | Color-strategy engine: eager-globs sibling strategy files; exports `COLOR_MODES`, `registerStrategies`, `getColorStrategy`, `buildColorExpression`, `getFeatureColor`, `listStrategies`. | Dropping `<name>.js` beside it (or in a bundle's `src/colors/` via `registerStrategies`) IS registration — filename derives the mode (kebab→camelCase); `_`/`.`-prefixed or no-default files skipped; bundle-local modes override shared ones per-bundle; unknown mode falls back to LANGUAGE_FAMILY. |
|
||
| `library/colors/adoption.js` | Binary red/green strategy on `adoptionStatus`. | Exports PROPERTY_KEY, PALETTE, LABELS, getColor, applyColor + default strategy object; getColor falls back to `_raw.adopted_by_churches`; LABELS keys (notAdopted/hasAdopted) differ from PALETTE keys (notAdopted/hasAdoption) — easy to trip on. |
|
||
| `library/colors/engagement.js` | Binary red-unengaged/green-engaged strategy. | getColor tolerates boolean/1/number>0/{value} shapes with `_raw.people_committed` fallback, but the Mapbox expression only matches true/1 — normalize before styling. |
|
||
| `library/colors/language-family.js` | Maps language-family names to hex colors for pins/legend/expressions; exports PALETTE, `getLanguageFamilyColor`, `canonicalFamilyName`, applyColor + compat aliases. | Exactly one PALETTE key per family — `canonicalFamilyName()` folds variants onto it (dupes break legend rows). JS fallback is black #000000; the Mapbox match fallback is grey #95a5a6 — intentional divergence. |
|
||
| `library/colors/prayer-progress.js` | 3-tier prayer-coverage strategy (red/orange/green) keyed on `peoplePraying`; `FULL_PRAYER_THRESHOLD` = 144 (hardcoded). | applyColor supports colorSource 'properties' vs 'feature-state'; JS getters fall back to `_raw.people_praying` but Mapbox expressions read only `peoplePraying`. |
|
||
| `library/colors/religion.js` | Maps Doxa religion codes (first letter = family) to pin colors; exports RELIGION_FAMILIES, PALETTE, `getReligionFamily`, applyColor/buildColorExpression. | Family = FIRST letter of religion code (unknown → 'U'); blue is reserved exclusively for Christianity — never add another blue. `buildColorExpression` alias is required by registry callers. |
|
||
|
||
### components/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/components/PeopleGroupDetail.vue` | Detail panel for a selected people group: CTAs, PPLR stats + live prayer count, image, description. Props: peopleGroup (required), hideHeader, dark, action. | Injects 'uiStore' — parent must provide. Fetches `/api/people-groups/detail/{slug}` with deliberately NO cache-buster (CF 5-min cache) — never re-add one or stub the count. Styles duplicated in useShadowStyles string AND scoped block — edit both. |
|
||
| `library/components/ResearchMapFilterPanel.vue` | Multi-field filter panel (population range, engagement/adoption selects, language/religion chips) for the research-map. | Injects mapStore/dataStore; only writes `mapStore.filters` — the profile deep-watches and applies; options come from `dataStore.facets`. |
|
||
| `library/components/ResearchMapSideMenu.vue` | Research-map side drawer: cluster-mode toggle, pin-dim slider, filter-panel slot, poster/reload buttons. Props: isOpen; emits close/open-poster/reload-map. | Writes `mapStore.clusteringMode` + `clusterPinOpacity` (map reacts via useMapClustering). Keep box-shadow on `.open` only. |
|
||
| `library/components/SideMenuDrawer.vue` | Shared slide-in left drawer (75% width, max 420px) over the map area, slot content + reload button. Props: isOpen, isDark. | Absolute positioning — parent must be a positioned container inside the custom element; needs i18n keys buttons.closeMenu/reloadMap, messages.menuComingSoon. |
|
||
|
||
### components/legends/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/components/legends/LegendDesktop.vue` | Desktop legend card: collapsible pill rendering LegendRows tabs, swaps to PeopleGroupDetail on pin select, defers to LegendMobile on mobile. Props: legendType, initiallyCollapsed, isDark, popupAction, disableCollapse. | Requires injected mapStore/dataStore/uiStore; styles live in useShadowStyles only. Language-family tab intentionally empty here (SemanticTreeLegend is a sibling). Geometry (top:60px/left:8px/380px) must stay in sync with SemanticTreeLegend's `.stl-panel`. Listens for `doxa:openDesktopLegend` scoped by mapId. |
|
||
| `library/components/legends/LegendMobile.vue` | Mobile bottom-sheet legend chrome (drag/collapse/detail) wrapping the same LegendRows/SemanticTreeLegend/PeopleGroupDetail. | All CSS lives in a useShadowStyles template literal — a stray backtick in its comments silently blanks the whole mobile stylesheet. |
|
||
| `library/components/legends/LegendRows.vue` | Flat-grid legend table: colored clickable filter rows, count/population columns, collapsible children, sticky title + totals footer. Emits filter-click. | ONE flat CSS grid with subgrid rows (spec: docs/legend-spec.md); `.lrg-item` wrappers are display:contents on desktop only for LegendMobile compat — don't add layout to them. Never use scrollIntoView (host-page scroll regression 554bf10). |
|
||
| `library/components/legends/LegendTools.vue` | Floating pill toolbar (auto-fly tour + clusters toggle) above the desktop legend card. Emits legend-row-fly. | Only renders for doxa-regions / language-family legend types; must stay a SIBLING of LegendDesktop (own `.lt-` CSS namespace); flight/cluster state lives in injected mapStore; hidden below 768px. |
|
||
| `library/components/legends/SemanticTreeLegend.vue` | Hierarchical tabbed legend panel (collapsible, sortable, drill-up/down tree). Props: nodes, tabs, columns, open, exportEnabled; emits select/update:open/export. | Syncs selection via the per-instance mediator (usePplrInstance) when present, else local state; styles via useShadowStyles. Hidden kill-switch `SOW_EXPORT_BUTTON=false` hides the fully-wired Export menu across ALL bundles regardless of `:export-enabled`. |
|
||
|
||
### components/map-controls/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/components/map-controls/index.js` | Barrel re-exporting all map-control components. | New controls need an export line here or they're invisible to consumers. |
|
||
| `library/components/map-controls/MapControlButton.vue` | Round base button all toolbar controls wrap; props isDark, title, active, disabled; emits click. | CSS via useShadowStyles key 'map-control-button'; keep changes parameterized (shared by all bundles); tight box-shadow is deliberate. |
|
||
| `library/components/map-controls/MapToolbar.vue` | Absolute column-flex layout shell for control buttons; prop hasGeocoder; slot for profile-chosen buttons. | Pure layout; force-slims slotted `.mcb`/`.hbg-btn` controls to 32px; hasGeocoder=false collapses the mobile top offset. |
|
||
| `library/components/map-controls/GeocoderComponent.vue` | Mapbox Geocoder search bar with DOXA local-search results and dual mount modes (map control vs standalone header). Emits result, people-group-result, aggregate-result, clear, error. | Depends on CDN globals MapboxGeocoder/mapboxgl + injected stores; on select it mutates store filter state directly; standalone mode uses `onAdd()` with a stub map because `addTo()` breaks in shadow roots. |
|
||
| `library/components/map-controls/HamburgerButton.vue` | Renderless hamburger/X toggle mounted as a Mapbox custom control (top-left). Props map, isOpen, isDark; emits toggle. | DOM built imperatively via map.addControl; parent owns open/close state; silently no-ops if `map` isn't set at mount. |
|
||
| `library/components/map-controls/FullscreenButton.vue` | Fullscreen toggle on the shadow host, with iOS pseudo-fullscreen CSS fallback. Props mapContainer, isDark; emits change. | iOS path mutates host/body/html inline styles and snapshots/restores them — don't set inline styles on the host while fullscreen is active; listens on the ShadowRoot's own fullscreenchange. |
|
||
| `library/components/map-controls/HelpButton.vue` | "?" button that emits `open-help`. | Presentational only; i18n key buttons.aboutThisMap. |
|
||
| `library/components/map-controls/LocationButton.vue` | Flies to user geolocation, falls back to default center/zoom on denial/timeout. Props map, center, zoom, userZoom. | Errors silently swallowed into flyToDefault (5s timeout, 60s maximumAge); i18n key buttons.resetView. |
|
||
| `library/components/map-controls/ShareButton.vue` | Share button + popover (copy link / copy iframe snippet with size presets). Props isDark, embedPath. | Embed URL order: embedPath prop > profileConfig.shareEmbedPath > fallback `/maps/doxa-research-map.html`; keep the execCommand copy fallback for file:// contexts. |
|
||
| `library/components/map-controls/ThemeToggleButton.vue` | Sun/moon button emitting `toggle`. | Dumb emitter — parent handles uiStore.toggleTheme() + style/theme work; i18n keys buttons.switchToLightMode/switchToDarkMode. |
|
||
| `library/components/map-controls/ZoomInButton.vue` | Zoom-in one step. Props map, isDark. | map prop nullable (optional-chained); i18n key buttons.zoomIn. |
|
||
| `library/components/map-controls/ZoomOutButton.vue` | Zoom-out one step. Props map, isDark. | map prop nullable; i18n key buttons.zoomOut. |
|
||
|
||
### components/poster/ (+ slots/)
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/components/poster/PosterDialog.vue` | Modal for configuring (size/DPI/orientation/format/title/slots) and triggering poster export, with live preview + blob download. Props: modelValue, getMap (required fn), legendRows, metadata, initialSize. | Pass a getMap accessor FUNCTION, not a map instance; footer attribution forced-on from constants/posterDefaults; size change auto-overrides DPI/orientation via posterSizes presets. |
|
||
| `library/components/poster/PosterPreview.vue` | Scaled live poster preview: snapshots the map canvas to PNG and overlays slot components at layout positions. Props: spec, getMap, legendRows, metadata. | Snapshot re-captures only on mount and spec dims/dpi change (not pan/zoom); legendRows prop is declared but unused (rows come from spec.slots.legend.rows). |
|
||
| `library/components/poster/slots/TitleSlot.vue` | Poster title strip (title/subtitle/align). | Preview-only — the headless export path uses separate SVG stubs in useMapPoster, so visual changes here don't affect exported posters. |
|
||
| `library/components/poster/slots/FooterSlot.vue` | Stub footer slot (display:none). | Intentional stub; keep the `spec` prop and `data-slot="FooterSlot"` contract expected by PosterPreview. |
|
||
| `library/components/poster/slots/LegendSlot.vue` | Stub legend slot (display:none). | Implement here when poster export is wired; keep the spec/data-slot contract. |
|
||
| `library/components/poster/slots/NorthArrowSlot.vue` | Stub north-arrow slot (display:none). | Knowing stub wired into PosterPreview; pending per build-friction notes. |
|
||
| `library/components/poster/slots/ScaleBarSlot.vue` | Stub scale-bar slot (display:none). | Implement here rather than creating a new slot file. |
|
||
|
||
### composables/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/composables/useAvatarGenerator.js` | Single-letter avatar generator (letter + 8-color palette by charCode). Exports generateAvatar, generateAvatarHTML, getAvatarStyle. | Pure functions, no reactivity; AVATAR_COLORS is hardcoded here, not in colors/_registry; generateAvatarHTML interpolates into raw HTML. |
|
||
| `library/composables/useCountryOutline.js` | Instant outline-only country highlight using Mapbox country-boundaries-v1 vector tiles. Returns showCountry(iso3)/clearCountry(). | Singleton source/layer per map (repeats swap the filter); worldview filter required or outlines draw 2-3x; camera framing is the caller's job; ops are try/catch no-ops pre-style-ready. |
|
||
| `library/composables/useDoxaSearch.js` | Local geocoder (localGeocoder for MapboxGeocoder) over 11 categories with tab-context ordering and Carmen-GeoJSON output. Returns search, searchGrouped, index, aggregates. | Call during setup (uses useI18n). Result shape is contract-bound to GeocoderComponent + research-map's onGeocoderAggregateResult; religions collapse to the legend's 10 families; ALLOWED_BY_CONTEXT keys must match TAB_SEARCH_CONTEXT strings in profiles. |
|
||
| `library/composables/useGeocoderSearch.js` | The single copy of geocoder-result → map-selection logic (kind-switch, per-kind filter builder, legend-tab routing, clear handling). Returns onPeopleGroupResult, onAggregateResult, onClear, clearGeocoderProgrammatic. | Kind→property map + filter builder live ONLY here — never re-implement in a profile; legendRouters injects divergent per-profile logic; busAdapter reroutes handlers to a non-map sink. Always clear via clearGeocoderProgrammatic (feedback-loop guard). |
|
||
| `library/composables/useMapClustering.js` | Three clustering modes (mapbox bubbles, MST lines, network lines) behind one API. Returns enabled, mode, setMode, applyClustering, clearClustering, cleanup, … | inject('mapStore') — never import stores directly; clear/toggle hides layers + setData(EMPTY_FC) instead of removing (GL heap-leak avoidance); only cleanup() removes sources/layers — call from host onBeforeUnmount. |
|
||
| `library/composables/useMapData.js` | Loads people-group data via injected DataSourceManager, canonicalizes languageFamily/region keys, builds derived legend datasets. Returns loadData, peopleGroups, normalizedPeopleGroups, regionsData, stats, …. | Pass dataSourceManager + markRaw in options. Language family MUST derive from ISO 639-3 code (langFamilyByIso), never localized labels; doxa-region registry reads are lazy and guarded on propertyKey==='doxaRegion'; regions map ISO alpha-3 → stable slug. |
|
||
| `library/composables/useMapEvents.js` | Mapbox layer event handlers: pin/region/cluster clicks, popups, cursor hover, cleanup. | Call during setup (injects 'uiStore', useI18n). If uiStore has legendMode, pin clicks route to the legend panel instead of a popup. Hardcoded layer IDs ('language-family-pins', 'regions-fill', 'clusters'); attach after layers exist, detach on unmount. |
|
||
| `library/composables/useMapFly.js` | Camera navigation: fly/fitBounds helpers, panel-offset centering, antimeridian-safe legend-row zoom. | zoomToLegendRow reads getSource()._data and re-implements Mapbox filter evaluation in JS — extend _matchesFilter/_resolveExpr for new expression ops; wide spans ease to a world overview instead of fitBounds. |
|
||
| `library/composables/useMapInstance.js` | One map's lifecycle: shadow-DOM CSS injection, aspect-aware dynamic minZoom floor, edge scroll strips, teardown. Returns map, isMapReady, initializeMap, destroy. | initializeMap() in onMounted with a real DOM element ref (string IDs break in shadow DOM); minZoom is a lower bound raised to a desktop-only no-duplicate-world floor; expects sibling `.map-scroll-edge` strips and global mapboxgl; always call destroy(). |
|
||
| `library/composables/useMapLayers.js` | Creates/manages all Mapbox layers (pin GeoJSON with family derivation + co-location fan-out, regions fill/border, connection lines, prayer-glow rAF ripple) and theme-aware pin ring widths. | Do NOT add a style.load listener here — the host profile owns post-setStyle reconciliation (flash race). addPrayerGlowLayers is idempotent; keep zoom 'case' inside interpolate OUTPUTS; dark-ring exceptions are config-only, keyed by strategy name. |
|
||
| `library/composables/useMapPacket.js` | Multi-page "prayer packet" PDF: page 1 = poster map raster, then one jsPDF vector card per people group. Returns renderPacket, progress, isRendering. | Page 1 reuses useMapPoster.render — keep that the single source of truth; jspdf lazy-imported; footer attribution on every page is a ToS requirement; field reads deliberately defensive across 3 object shapes. |
|
||
| `library/composables/useMapSelection.js` | Applies region/family selection to layer paint props (highlight/fade opacity, connection lines). | Region matching is on iso_3166_1_alpha_3 (tileset has no `region` prop); labels fuzzy-normalized to slugs; selection state lives in the Pinia mapStore. |
|
||
| `library/composables/useMapTheme.js` | Seamless light/dark toggle by flipping Mapbox Standard's lightPreset/theme config props. Returns bootStyle, applyTheme, swapTheme, isDarkTheme. | Never setStyle to switch themes (reloads map, wipes custom layers); applyTheme must run after style.load and re-asserts once on 'idle' (fast-toggle grey-film race); profiles should watch(uiStore.theme) → applyTheme. |
|
||
| `library/composables/usePplrInstance.js` | Per-map mediator "instance store" (provide/inject refs) so map, geocoder, legend in one cluster communicate. Exports createPplrInstance, provideInstance, useInstance, selectNode, clearAll. | Not Pinia — plain refs under key 'pplrInstance'; useInstance() returns null when standalone, so consumers must null-guard; profiles bridge instance.selection to their own store. |
|
||
| `library/composables/usePrayerStatistics.js` | Fetches the server-side prayer-coverage aggregate (`/api/people-groups/statistics`) — single source of truth for all legends. | Module-level singleton state (one shared fetch); never recompute prayer counts from loaded pins (client scans diverge from parent site); load() is idempotent; bucket getters are exclusive and sum to total. |
|
||
| `library/composables/useSelectedPin.js` | Highlights the selected people-group dot and floats a black SVG "GO" pin over it. Returns initialize, cleanup, updateSelectedPin. | Pass getActivePinColor via options — inject() never resolves here, and omission silently renders black. Marker pin is deliberately always black; requires injected 'uiStore' + a 'language-families' source; circle-emissive-strength re-asserted per selection (Standard lighting bug). |
|
||
| `library/composables/useShadowStyles.js` | Injects a component's CSS string into the containing Shadow DOM root on mount (defineCustomElement drops async-child styles). | Call at setup() time; no-ops in light DOM; idempotent via label/CSS-hash key — same label across components suppresses injection. |
|
||
|
||
### composables/legends/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/composables/legends/useLegendData.js` | Builds the unified LegendItem tree (rows, columns, title, filter wiring, totals) for every legend type from injected stores. | Research-only strategies (resource, doxa-region, affinity-block) are read LAZILY from _registry at compute time and GUARDED on propertyKey — getColorStrategy silently falls back to language-family. Prayer counts come from usePrayerStatistics, not client pin scans. |
|
||
| `library/composables/legends/researchLegendOptions.js` | Research-map options object (aliases, religion-family exclusions, custom row builders) fed into useLegendData. Exports RESEARCH_LEGEND_OPTIONS. | doxa-region/resource strategies are registered by the research bundle's src/colors glob — all registry reads MUST stay lazy (inside builders/getters, not module scope), guarded on propertyKey. |
|
||
| `library/composables/legends/useAffinityBlockLegendData.js` | Aggregates pins into a 4-tier Affinity Block → Cluster → People Group → PG-in-country tree for SemanticTreeLegend. | Mirrors useLanguageFamilyLegendData's node shape; colors read lazily + guarded on propertyKey==='affinityBlock'; all tier filters use uniqueId member lists, not code matching. |
|
||
| `library/composables/legends/useLanguageFamilyLegendData.js` | Aggregates features into a family → language → dialect legend tree (rows, sorting, expand state, highlight events, filter tree). | API's language_family is null — family is derived client-side from comma-inverted primary_language labels via langFamilyByLanguage.json + sign-language rule; langTree filters match pins by `language` string. Only EMITS `legend:highlight` window events; the map listener (research-map.vue) dims pins. |
|
||
| `library/composables/legends/useWagfRegionsLegendData.js` | Aggregates items into a 3-tier WAGF Region → Block → Country tree with filter expressions and region colors. | REGION_COLORS is keyed on the stable wagf_region slug (never localized labels) and must stay mirrored with SLUG_PALETTE in the research bundle's src/colors/doxa-region.js; highlight() uses the `legend:highlight` window CustomEvent. |
|
||
|
||
### composables/poster/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/composables/poster/useMapPoster.js` | Renders a live map to a poster-grade PNG/PDF blob via a hidden off-screen map (single-canvas ≤4096px or tile-stitch) plus SVG overlay slots. Returns render, progress, isRendering, cancel; also posterPixels. | jspdf is lazy-imported for PDF and throws if missing; mutates window.devicePixelRatio during hidden-map spawn; requires window.mapboxgl; Mapbox attribution footer always composited (ToS); headless slots use stub SVGs, not the Vue slot components. |
|
||
| `library/composables/poster/usePosterLayout.js` | Pure layout math: PosterSpec + pixel dims → absolute {x,y,w,h} boxes per slot. Exports computeLayout, inchToPx, usePosterLayout. | Pure/no reactivity despite the use* name; absent slots return zero boxes; footer is ALWAYS present (attribution per ToS); defaults from constants/posterDefaults.js. |
|
||
|
||
### constants/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/constants/mapDefaults.js` | Default Mapbox init settings + canonical LAYER_ORDER, overridable per app-profile. | Keep style = Mapbox Standard (theme is a lightPreset flip, not setStyle); keep renderWorldCopies:true with no maxBounds (zoom-out capped by computeWorldMinZoom); never hardcode a token here. |
|
||
| `library/constants/posterDefaults.js` | Default poster-print layout constants (margins, bleed, slot sizes, typography) + `defaultPosterSpec(size)` factory. | All measurements in inches (px via spec DPI); footer attribution mandatory; override via the PosterSpec passed to useMapPoster, don't edit defaults per-map. |
|
||
| `library/constants/posterSizes.js` | Preset poster dimensions (US + ISO) with inch/mm/300-DPI px. Exports POSTER_SIZES, POSTER_ORDER, getPosterSize, listPosterSizes, listByCategory. | Pure data; new sizes go in BOTH POSTER_SIZES and POSTER_ORDER; getPosterSize returns null on unknown id; orientation flips via PosterSpec, not here. |
|
||
| `library/constants/zoom.js` | Centralized zoom constants: region fly-to configs, min/max constraints, interpolation curves, clustering thresholds, fly durations + getters. | CLUSTERING_ZOOM is NOT the runtime source of truth — ClusterHelpers.DEFAULT_MAPBOX_CLUSTER_OPTIONS is; keep them in lockstep and clusterMaxZoom ~2 below the map zoom cap or clusters never break apart. |
|
||
|
||
### data/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/data/iso-numeric-to-alpha2.json` | ISO 3166 numeric country code → alpha-2 lookup. | Currently has NO code consumers (docs mention only). Importable via `@map/data/*.json`. |
|
||
| `library/data/langFamilyByIso.json` | ISO 639-3 language code → language-family name lookup. | Generated from the EN API + langFamilyByLanguage.json; the canonical family-derivation source (labels are locale-fragile). |
|
||
| `library/data/langFamilyByLanguage.json` | English language name → family name lookup. | Its lookup logic (reversed-name fallback, stripped variants) is duplicated in 4 composables (useMapData, useMapLayers, useDoxaSearch, useLanguageFamilyLegendData) — keep them consistent. |
|
||
|
||
### i18n/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/i18n/index.js` | Vue I18n setup: eagerly inlines all locale JSON catalogs via glob, builds a per-map i18n instance with document-lang detection. Exports createAppI18n (default), detectLocale, SUPPORTED_LOCALES, RTL_LOCALES. | Folder name IS the locale, filename IS the $t() namespace; new locales must also be added to SUPPORTED_LOCALES (+ RTL_LOCALES if RTL). Do NOT convert to lazy imports (IIFE grew ~22 KB when tried). |
|
||
| `library/i18n/locales/*` (97 catalogs) | 11 locales (ar, de, en, es, fr, hi, it, pt, ro, ru, zh), one JSON per UI area (buttons, legend, detail, options, search, messages, aria, prayerLaps, tabs, popup). | Add a language by copying `en/`, translating values without renaming/dropping keys. `en/` is the fallback source of truth; missing keys/files silently fall back to English. File sets are uneven: en/fr have 10 areas, ar/de/es/pt/ru 9, hi/it/ro/zh only 8 (no tabs.json — their tab strings render in English). |
|
||
|
||
### stores/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/stores/dataStore.js` | Pinia store caching fetched datasets per sourceId with loading/error state. Exports useDataStore. | All fetching routes through this store (no fetch() in components); markRaw() is mandatory on cached payloads. |
|
||
| `library/stores/mapStore.js` | Pinia store: registry of live map instances + cross-map state (selections, filters, auto-fly, clustering, fullscreen, persisted prefs). Exports useMapStore. | Selections are mutually exclusive — each select* action nulls the others; single store shared across ALL maps on a page (activeMapId tracks focus); prefs persist to localStorage 'mapUserPreferences'. |
|
||
| `library/stores/uiStore.js` | Pinia 'ui' store: theme, tabs, mobile legend sheet, search/hamburger, pin filters, selected people group, toasts, modals, loading, breakpoints, a11y. Exports useUIStore. | `toggleHamburgerMenu` is defined TWICE in actions (second silently wins); actions validate and silently no-op on invalid values; call init() once; selectPeopleGroup() hijacks legendState/legendMode. |
|
||
|
||
### utils/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `library/utils/ClusterHelpers.js` | Pure geometric/GeoJSON helpers + default Mapbox cluster options/ramps shared by all clustering modes. | Distances are degree-space Euclidean (viz only); convexHull mutates its input and returns an open ring; DEFAULT_MAPBOX_CLUSTER_OPTIONS.clusterMaxZoom (6) must stay below the map zoom cap or clusters never break apart. |
|
||
| `library/utils/MSTClusteringUtil.js` | Kruskal MST + Union-Find turning grouped lat/lng points into GeoJSON LineStrings per group with no orphans. | Distances in raw degrees (maxConnectionDistance is degrees); O(n²) edge building per group; maxConnectionDistance can silently split groups (console.warn only). |
|
||
| `library/utils/NetworkClusteringUtil.js` | Nearest-neighbor "network/shard" cluster-line generator (denser alternative to MST). | Naive Euclidean degrees, NOT a true MST; points need parseFloat-able latitude/longitude; groups with <2 valid points return no lines. |
|
||
| `library/utils/geoUtils.js` | Pure geo/GeoJSON helpers (bounds, centroid, zoom, coordinate/feature validation, color helpers) shared across bundles. | Pure/no-deps by contract (no Vue/Mapbox). darkenColor has hardcoded per-region hex overrides bypassing the percent math; isValidCoord takes an array, isValidCoordinate takes (lon, lat) numbers. |
|
||
| `library/utils/DataSourceManager.js` | Back-compat shim re-exporting the real module from `library/api/DataSourceManager.js`. | Shim only — new code imports from `@map/api/`; don't add logic here. |
|
||
| `library/utils/apiBaseUrl.js` | Back-compat shim re-exporting getApiBaseUrl from `library/api/apiBaseUrl.js`. | Deprecated alias; import from `@map/api/apiBaseUrl.js` directly in new code. |
|
||
|
||
---
|
||
|
||
## app-profiles/ — one folder per bundle
|
||
|
||
### demo-map/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `app-profiles/demo-map/index.js` | Bundle entry: defines/registers the `<demo-map>` custom element wrapping @map/ProfileLoader. | import.meta.glob calls must live HERE (build-time, relative): `./*.vue` → 'profileModules', `./src/colors/*.js` → registerStrategies. i18n provided as 'appI18n' (custom-element root can't useI18n()). |
|
||
| `app-profiles/demo-map/index.html` | Dev/staging page: loads Mapbox GL peer, mounts `<demo-map>` with a profile-config attribute, pulls ./index.js as Vite entry. | %VITE_MAPBOX_TOKEN% replaced by Vite from gitignored .env; sets window.DOXA_TOKEN + MAP_APP_BASE_URL; Mapbox GL must load before the bundle script. |
|
||
| `app-profiles/demo-map/demo-map.vue` | Demo profile: two-tab map (religion coloring vs all-purple pins) composed from @map composables + a local single-color strategy. | Relies on ProfileLoader injections; tab colorStrategy IDs must exist in the registry; layer name 'language-family-pins' is hardcoded for tab repaints; legend duck-types the strategy shape. |
|
||
| `app-profiles/demo-map/src/colors/colors.json` | Static hex palette (pins, polygons, fallback) — code-free edit surface. | Edit hex strings only; per-value logic goes in a strategy .js in the same folder. |
|
||
| `app-profiles/demo-map/src/colors/single-color.js` | Local strategy painting every pin one constant purple. | Auto-registered via the src/colors glob — filename becomes the mode key (single-color → singleColor); keep the buildColorExpression alias and default `name`; applyColor must return a Mapbox expression array. |
|
||
|
||
### doxa-research-map/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `app-profiles/doxa-research-map/index.js` | Bundle entry: globs profiles + colors, registers strategies, sets up the RTL plugin, defines `<doxa-research-map>`. | Globs are relative to THIS file — profile .vue at bundle root, strategies in src/colors/, or they silently don't register. Deliberately does NOT register legacy `<doxa-map>` (owned by doxa-simple-map; one-shot customElements.define). |
|
||
| `app-profiles/doxa-research-map/index.html` | Staging showcase: gem-framed instances from a flat DOXA_SLOTS array, profile tabs, overview grid. | Slot `profile` must match a .vue filename at bundle root; profile + tk (+ ?locale=) auto-injected into config; ./index.js script tag rewritten to the IIFE at build. |
|
||
| `app-profiles/doxa-research-map/research-map.vue` | The research-map profile (~2100 lines): 7-tab map (prayer/adoption/engagement/people-groups/regions/languages/religions) with SemanticTreeLegend, geocoder, filters, detail panel, lazy poster/packet export. | Tabs are hardcoded in ALL_TABS/FEATURES (NOT profile-config); config arrives only via injections; maxZoom pinned to 22 deliberately; heavy deps (poster, packet, clustering, detail panel, mobile legend) are lazy-loaded — keep them off the boot path. |
|
||
| `app-profiles/doxa-research-map/src/colors/affinity-block.js` | Strategy mapping ROP1 affinity-block codes (A001–A017) to names and pin colors. | ROP1 code is the primary key (name lookups are lossy legacy); reads properties.affinityBlock falling back to `_raw.ROP1`; keep NAMES and PALETTE keys in sync; A016 intentionally absent. |
|
||
| `app-profiles/doxa-research-map/src/colors/doxa-region.js` | Strategy coloring pins by DOXA/WAGF region; SLUG_PALETTE is authoritative. | Color by slug, never localized display labels (break in non-EN locales); uses @map/utils/geoUtils generateColorFromString as fallback. |
|
||
| `app-profiles/doxa-research-map/src/colors/resource.js` | Binary green/black coloring by gospel-resource availability + per-resource palette for the legend. | Default coloring is intentionally binary despite the per-resource PALETTE; resource values must literally equal 'Available' (properties or _raw CSV columns via getCSVColumn); legacy aliases kept for compat. |
|
||
|
||
### doxa-simple-map/
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `app-profiles/doxa-simple-map/index.js` | Bundle entry registering `<doxa-map>` (legacy) AND `<doxa-simple-map>` mounting ProfileLoader with per-instance Pinia/i18n. | Q3 contract: the bundle runs import.meta.glob('./*.vue') itself and provides 'profileModules' (ProfileLoader can't glob across @map). defineCustomElement called twice on purpose (one class per tag); RTL plugin registered once. |
|
||
| `app-profiles/doxa-simple-map/index.html` | Staging showcase: gem-framed `<doxa-map>` slots (Engagement/Prayer/Adoption) with profile tabs and overview grid. | Edit only DOXA_SLOTS; `profile` must match a bundle-root .vue; ?locale= forwarded into every config; build rewrites the module script to ../../app/doxa-simple-map.iife.js. |
|
||
| `app-profiles/doxa-simple-map/doxa-simple-map.vue` | Tabbed (prayer/engagement/adoption) single-map profile wiring library composables, legend, geocoder, and strategies into one Shadow-DOM-safe embed. | Behavior gated by in-file FEATURES flags (geocoder/hamburger/overlapDensity/activeOverlayEmphasis OFF here); tabs come from injected profileConfig; SHARE_EMBED_BY_ID hardcodes instance-page URLs — renaming instances in simple-map.instances.json breaks share links. |
|
||
| `app-profiles/doxa-simple-map/simple-map.instances.json` | Instance manifest: build emits N preset standalone embed pages (prayer/engagement/adoption) from the one parameterized profile. | Build-consumed data, not runtime code: each instance becomes `doxa-simple-map/<id>/index.html`; instances SPLIT one profile into many render targets; colorStrategy/legend/popup values must match registered strategy names. |
|
||
|
||
### template-bundle/ (copy-me starter)
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `app-profiles/template-bundle/index.js` | Copy-me entry: globs flat .vue profiles, registers private strategies, defines `<template-bundle>`. | Globs are position-bound: `./*.vue` flat (filename = profile name) and `./src/colors/*.js` (colors.json ignored by the glob); tag name = bundle folder name. |
|
||
| `app-profiles/template-bundle/index.html` | Staging page rendering both template profiles side-by-side. | The profile-config `"profile"` field selects which bundle-root .vue renders; copying this bundle means updating tag names and profile strings. |
|
||
| `app-profiles/template-bundle/template-map-a.vue` | Minimal example profile "A" — colored placeholder, no Mapbox/data, demonstrates profile-config injection + sandbox color JSON. | Filename minus .vue IS the profile name; must sit at bundle root; sandbox imports are relative (./src/...) — @map alias is library-only. |
|
||
| `app-profiles/template-bundle/template-map-b.vue` | Minimal example profile "B" — shows multi-profile bundles; both profiles compile into one bundle JS. | Host selects it via profile-config `{"profile":"template-map-b"}`. |
|
||
| `app-profiles/template-bundle/src/colors/colors.json` | Plain-values hex palette a non-programmer edits. | Values-only; logic goes in a strategy .js beside it; imported relatively from the bundle-root .vue. |
|
||
| `app-profiles/template-bundle/src/colors/example-mode.js` | Copy-me template for a bundle-private strategy (two-bucket example on peoplePraying). | Filename IS the mode key (kebab → camelCase); matching a library mode overrides it for this bundle only; `name` in the default export is required or the tab/legend label is blank. |
|
||
|
||
---
|
||
|
||
## internal/ — build machinery and staging (do not edit for per-map styling)
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `internal/clean.mjs` | Pre-build script that deletes/recreates the output tree `public/<name>/`. | Reads the tree name from vite.doxa-maps-build-config.json (fallback "doxa-maps"); must run BEFORE per-bundle vite builds. |
|
||
| `internal/generate-tree.mjs` | Post-vite step that scans app-profiles/ and writes the self-contained doxa-maps/ output tree (manifest.json, staging pages, standalone profile/instance pages) and copies to configured destinations. | Runs AFTER per-bundle builds (expects the IIFE at `public/<name>/<bundle>/<bundle>.js`); discovery is convention-driven (bundle = non-underscore dir with .vue at ROOT — profiles moved into src/ silently vanish); all URLs stay relative unless embedBaseUrl is set; inlines staging-forge.{css,js}. |
|
||
| `internal/generate-library-index.mjs` | Scans live library/ code and regenerates docs/reference-library-index.md (one-line import catalog). | Output is DERIVED — never hand-edit the index; rerun after any library/ change; descriptions come from each file's first header-comment line. |
|
||
| `internal/drop-portability-check.mjs` | CLI gate (`check:drop`): scans built .html/.json for root-relative refs and fails the build if any would break subpath/Drop hosting. | Defaults treeDir to `public/<name>` from the build config — build first; only checks .html/.json and a fixed manifest key list (iframeUrl\|scriptUrl\|url\|embedUrl\|output) — new URL keys need adding here. |
|
||
| `internal/inline-parse-check.mjs` | Post-build verifier (`check:parse`): syntax-checks every inline classic `<script>` in the built HTML via `node --check`. | Only matches bare `<script>` (no attributes), checked as classic scripts — import.meta or a literal `</script>` in snippets is exactly the bug class it catches. |
|
||
| `internal/staging.js` | Dev-only "Embeddable JAMstack Maps" showcase script: auto-discovers bundles via import.meta.glob, renders bundle tabs + live map tiles with lazy WebGL-guarded mounting and a locale switcher. | A bundle appears only if index.js exists and its tag is regex-scraped from customElements.define; map slots come from parsing window.*SLOTS / profile-config attrs. Never iframe the maps (cross-realm instanceof fails); MAX_LIVE=4 with forced WEBGL_lose_context eviction. |
|
||
| `internal/staging-forge.js` | Client script for the generated staging pages: semantic bundle/profile/instance tree sidebar + live map iframe stage + token paste-once UX. | Must stay file://-safe (no import/export; manifest inlined as window.FORGE.manifest); the DEV-HMR block is stripped by generate-tree.mjs — keep the DEV-HMR-START/END markers intact. |
|
||
| `internal/staging-forge.css` | Dark "Embeddable JAMstack Maps" theme for the generated staging pages (tree sidebar, token bar, gem-framed stage, mobile layout). | Selectors are a contract with the staging HTML/JS generator (which sets --gem/--gem-rgb per node); pure presentation for internal pages. |
|
||
| `internal/staging-reference-v2.html` | Hand-authored reference mockup of the staging-root tree page (nested details, lazy iframes) that generate-tree.mjs's template replicates. | Not served or built — spec-by-example; rules encoded: inline manifest (file:// safe), lazy iframe src on toggle, tk never baked, normal-flow header. |
|
||
| `library/scripts/research/` | Table + resource builders shared by cities-internet, gospel-resources and key-cities. In `library/` because more than one profile consumes the output. |
|
||
| `library/scripts/posters/` | Poster and preview generator shared by country-posters and my-upg-100-list. Its `out/` tree is render output and is gitignored; what ships is the exported subset in `public/assets/`. |
|
||
| `app-profiles/gospel-resources/scripts/` | Link and click-sheet verifiers. Single-profile, so they live with the profile rather than in the library. |
|
||
| `internal/write-token.mjs` | Writes `public/api/maps/token.json` from the `MAPBOX_TOKEN` environment variable. Refuses an `sk.*`; writes nothing when unset. |
|
||
|
||
---
|
||
|
||
## Root config files
|
||
|
||
| File | What it is | Agents should know |
|
||
|---|---|---|
|
||
| `vite.config.js` | Vite config building one IIFE bundle per invocation (BUNDLE env var) from auto-discovered app-profiles/; serves all bundles in dev. | Build requires `BUNDLE=<name>` (`npm run build` loops all); bundle discovery needs BOTH index.html AND index.js at profile root (`_`/`.` dirs skipped); `@map` alias → library/; jspdf externalized to an esm.sh CDN URL in builds; output name from vite.doxa-maps-build-config.json. |
|
||
| `vite.doxa-maps-build-config.json` | The single committed build-config: output tree name ("doxa-maps"), extra copy destinations (paths), embedBaseUrl, Mapbox tokenUrl/token. | Edit this ONE file then `bun run build`; empty embedBaseUrl keeps manifest URLs tree-relative (drop-portability gate); token ladder: per-map prop > ?tk= > baked token > window.MAP_TOKEN_URL > tokenUrl > same-origin /api/maps/token. tokenUrl is a URL, not a secret. |
|
||
| `package.json` | Root npm/bun manifest: scripts (dev, build, build:bundle, clean, tree, check:drop, check:parse) + deps (vue 3, pinia, vue-i18n, topojson-client, world-atlas, jspdf; vite 5 toolchain). | `build` iterates app-profiles/*/ setting BUNDLE per invocation — adding a folder auto-builds; internal/ scripts run as part of build (check:drop currently invoked twice, likely unintentional). Bun-first toolchain. |
|
||
| `index.html` | Root staging page ("Embeddable JAMstack Maps") listing every bundle as tabs and lazy-mounting each bundle's maps in live frames; logic in /internal/staging.js. | Loads mapbox-gl v3.24.0 + geocoder globally ONCE so custom elements share window.mapboxgl in the same realm; pinned CDN versions must match library expectations. |
|
||
|
||
---
|
||
|
||
## Quick reference — "I want to…"
|
||
|
||
| I want to… | Do this |
|
||
|---|---|
|
||
| Add a new map | copy `app-profiles/template-bundle/` → rename folder + tag in `index.js` |
|
||
| Add a screen to an existing bundle | drop a `.vue` flat in that bundle's folder, request it via `profile-config.profile` |
|
||
| Fix a legend / control for ALL maps | edit it once in `library/` (`@map`); rebuild bundles |
|
||
| Add a color strategy | new file in `@map/colors/` (shared) or the bundle's `src/colors/` (map-private) |
|
||
| Build everything | `bun run build` → `public/doxa-maps/<bundle>/<bundle>.js` |
|
||
| Build just one | `BUNDLE=<name> bun run build:bundle` |
|
||
| Preview in isolation | `bun run dev` → `http://localhost:5173/app-profiles/<name>/index.html` |
|
||
| See a change on the served site | `bun run build`, then refresh (NOT `dev`) |
|
||
| Repoint the API without a rebuild | set `window.MAP_APP_API_URL` before the bundle loads |
|
||
| Repoint the Mapbox token | pass `tk` in `profile-config`, or set `window.MAP_TOKEN_URL` / build-config `tokenUrl` |
|