doxa-maps/documentation/architecture/file-map.md
Doxa Maps e19697673d
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Doxa Maps — the map bundler, 7 production plugins and the Experimental shelf
2026-09-25 20:07:59 +00:00

44 KiB
Raw Blame History

Architecture — the file map (every file, one line each)

The lean lookup (zones, directory tree, config, quick-ref) is in ../reference.md. This is the exhaustive per-file catalog with the gotchas each file carries.

Generated by a per-file scan. The @map/… import path mirrors each library/ file path (see Importing from @map in the reference).

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: documentation/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 (tree-relative <bundle>/<profile>/index.html). Share URL via utils/embedBase.js: profileConfig.shareUrl > embedPath prop / profileConfig.shareEmbedPath joined to the embed base (profileConfig.embedBaseUrl > window.MAP_EMBED_BASE_URL > the bundle's own script url two segments up) > the page itself; 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.

plugin-designs/ — one folder per bundle

demo-map/

File What it is Agents should know
plugin-designs/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()).
plugin-designs/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.
plugin-designs/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.
plugin-designs/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.
plugin-designs/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
plugin-designs/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).
plugin-designs/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.
plugin-designs/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.
plugin-designs/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.
plugin-designs/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.
plugin-designs/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
plugin-designs/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.
plugin-designs/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.
plugin-designs/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.
plugin-designs/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
plugin-designs/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.
plugin-designs/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.
plugin-designs/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.
plugin-designs/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"}.
plugin-designs/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.
plugin-designs/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 plugin-designs/ 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 documentation/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/.
plugin-designs/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 plugin-designs/; 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 plugin-designs/*/ 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.

The Semantic Tree Legend (@map/components/legends/SemanticTreeLegend.vue)

@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.