doxa-maps/docs/reference.md
doxa-maps-88655c70 15ce17f907 fix the impossible 27/26 zone badge and replace a faked page-2 shot with the real third profile
The badge read "27/26 zones" — a count exceeding its own total — in two shipped README
screenshots. 26 was the leaf count before slice 09b-6 added the Frontier-Asia uncovered
rows; the literal was never updated, so it drifted the moment the data grew. The
denominator is now derived from perZone.size, which tracks the zone list automatically
and cannot disagree again. Measured from clusters.json: 13 regional + 12 asia + 2
uncovered + 1 deaf = 28. The corrected badge also surfaces 1 genuinely unresolved zone
that the stale denominator was masking.

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

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

check:shot-distinctness gates this, blocking in CI. It keys on url+viewport, so the
deploy2 mobile shot of the same SPA stays legitimate rather than a false positive.
2026-09-21 11:29:25 +00:00

362 lines
28 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*,
**[why.md](./why.md)**; for the short "what it is", **[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** | `plugin-designs/<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
`plugin-designs/`; 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`.
**Two words first.** An **element** is a custom HTML tag this repo invents, such as `<doxa-map>`: the browser
learns it once, then you can paste it into any page as often as you like; each pasted tag is one live copy, an
*instance* (people also say "web component"). An **app profile** (this repo also says *mounting file*) is one
flat `.vue` in `plugin-designs/<bundle>/` that defines a whole map app — its main component; the settings on the
tag (`profile-config`) name which app profile to load and the shared `ProfileLoader` picks it. In class-and-object
terms: the element class is the class, each tag is an instance, the app profile is the screen that instance loads.
Most repos need one such file per app; doxa-maps builds hundreds of maps from one shared `library/`, so every map
gets its own app profile — and those files come in five shapes:
**Profile types — the ladder**, smallest first (head words are established terms; the `-element` compounds are
this repo's own; the coined names stay as a.k.a.):
`single-element` → `preset-element` → `multi-view-element` → `composite-element` → `app-shell`.
| Type (a.k.a.) | Machine key | What it is | How the build recognises it |
|---|---|---|---|
| **single-element** (single) | `single` | one app profile, one map — paste its tag anywhere, set it up in `profile-config`; the default | a flat `.vue` with no `<profile>.meta.json` and no `*.instances.json` in the folder |
| **preset-element** (parameterized) | `parameterized` | one app profile + a small table of named presets; each row is a ready-made map with its settings baked in, with its **own embed page** and sidebar entry — same code, same tag | the bundle ships `<name>.instances.json` — `{ "instances": [{ "id", "title", "props" }] }` or a bare array; the build writes one embed page per row at `<bundle>/<profile>/<id>/index.html` |
| **multi-view-element** (monolithic) | `monolithic` | one app profile holding several screens you switch between — tabs or steps — in one `.vue`, shown one screen at a time | declared: `<profile>.meta.json` → `{ "type": "monolithic" }` (or `"multi-view-element"`) |
| **composite-element** (nested-elements-mfe) | `nested-elements-mfe` | an app profile whose template places other app profiles' maps together (stacked or side by side, e.g. a map + a poster wall) and ships the set as one new element — one tag embeds it all | declared: `<profile>.meta.json` → `{ "type": "nested-elements-mfe", "composes": [ … ] }` (or `"composite-element"`) |
| **app-shell** (nested-mfe-spa) | `nested-mfe-spa` | the outer frame — sidebar, header, navigation — that loads any of the other kinds and switches between them without a reload; the top rung, the full doxa-maps app | a compiled Vue custom element **today** — `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue`, built via `defineCustomElement` like every other rung (its shell layout is `library/layouts/app-wrapper-material/`). `app-wrapper-material/index.html` is now only the dev-time plugin-gallery scaffold, not the shipped shell |
Composite kinds are always **declared** in `<profile>.meta.json` (`{ "type": …, "label": … }`, either spelling —
see `TYPE_ALIASES` in `internal/plugin-gallery/discover.mjs`) — the build never guesses them from source (tag-scanning
produced false positives). Discovery is one pure module,
`internal/plugin-gallery/discover.mjs`; `bun run dev` renders its result as the **plugin gallery** (the same
generated page the build writes to `public/index.html` — see "Plugin gallery" under Build scripts: a list grouped by
bundle, every map deep-linkable at `#/<bundle>/<profile>[/<instance>]`), and `bun run smoke:gallery` proves every
element actually boots inside it.
---
## 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)
│
├── plugin-designs/ ← 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
├── 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 plugin-designs/<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",
"provider": "mapbox", // map engine: "mapbox" (default) | "maplibre" (preview);
// also flippable per page via ?provider= or window.MAP_PROVIDER
"assetsBase": "/assets", // where tables/, boundaries/, previews/ are served (see below)
"shareUrl": "https://…", // optional; the WHOLE url the Share button links/embeds (beats everything)
"embedBaseUrl": "https://…/doxa-maps", // optional; served tree root the Share button joins embed pages to
"tabs": [ /* parameterized screens read this */ ]
}'></doxa-map>
```
- `assetsBase` — data-driven profiles (connectivity-map, resources-map, cities-map, the poster walls
and data tables) fetch `${assetsBase}/tables/*.json` and `${assetsBase}/boundaries/*.geojson`
**before** they create their map. Unset, it defaults to `./assets` **relative to the host page**;
if the host doesn't serve that path the fetch gets an HTML fallback, `JSON.parse` throws, and the
map is never created (no console error, only the profile's status line). The generated pages and
the shell set it for you (`../../../assets` or the absolute `assetsBaseUrl` from the build config);
any other host must point it at a copy of `public/assets/`.
- **Share ladder** (`library/utils/embedBase.js`) — what the Share button links and iframes:
`shareUrl` (a whole url, e.g. the plugin gallery's own deep link) > the profile's **tree-relative**
embed page (`<bundle>/<profile>/index.html`, the `embedPath` prop or `shareEmbedPath`) joined to an
embed base — `embedBaseUrl` > `window.MAP_EMBED_BASE_URL` > the root the built bundle derives from
its **own script url** (two segments above `<bundle>/<bundle>.js`, so a dropped tree works on any
host/CDN with no config) > the page's own location. Never hardcode `/js/doxa-maps/…` in a profile.
- `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`, or the next free port — read the terminal). `/` is the **plugin gallery** of every element — `internal/generate-gallery.mjs` in mode `dev` over `internal/plugin-gallery/{discover,dev-plugin}.mjs`; every bundle is also served at its own html path. Dev-only routes: `/__plugin-gallery/manifest.json` (the manifest-shaped model the page fetches), `/__plugin-gallery/registry.json` (the discovery registry the smoke tools read), `/assets/*` (served from `public/assets/`, else `plugin-designs/*/data/`). **Writes nothing to `public/`** — no HMR across that boundary. |
| `bun run smoke:gallery` | Opens every element of the plugin gallery by deep link in Firefox and proves it boots: element defined, shadow DOM populated, no error card, a WebGL canvas for map profiles (checked through nested shadow roots), no HTML-for-JSON data responses. `--json <file>` writes the evidence; exit 1 on any failure. The pre-handoff gate. |
| `bun run smoke:share` | The share round-trip gate (Firefox): opens a host page (`--plugin-gallery` = every canvas element of the dev plugin gallery; `--url <page> --tag <element>` = the website or any CDN page), clicks Share, reads the URL, fetches it, reopens it in a fresh page and asserts the SAME custom element mounts with a WebGL canvas. `--json <file>`; exit 1 on any failure — proves REQ-1 ("links work wherever the tree is hosted"). |
| `bun run check:public-hygiene [<dir>] [--strict]` | Proves a served folder is fit to publish: scans for local paths, internal folder names, dev-server URLs, JWT-shaped tokens, `.env`/token files, source maps, documentation and staging/preview pages, and prints an inventory (bundles, leaf pages, assets by folder). Under `--strict` any root entry other than `index.html`, `assets/` and `doxa-maps/` is a finding ("outside the drop-in contract"). Exit 1 on a finding. Run it on the website's drop-in before any public push. |
| `bun run sync:dropin <dest> [--from-config \| --only a,b] [--prune]` / `bun run deploy:dev` | Syncs the built `public/` into a host checkout (e.g. the website's `public/embed/doxa-maps-build/`). **The drop-in contract** lives in ONE place — `vite.doxa-maps-build-config.json` → `dropin.include`, exactly three root entries: `index.html` (the built plugin gallery — the root page generated by `internal/generate-gallery.mjs`, listing every bundle/profile/instance and mounting the chosen one), `assets/` (data) and `doxa-maps/` (the bundle folders + `manifest.json`). `--from-config` copies only those, `--prune` removes everything else in the destination, so a host carries exactly the served web-root and nothing else at its root. `dropin.exclude` (same file) lists built files inside the allowlist that no page fetches — the full people-groups CSV, the tables only internal scripts read, the table index; they stay in `public/` for the scripts, are skipped by `--from-config` (counted as "excluded (not shipped)") and removed from the destination if a copy is there. Without an allowlist it is a plain full mirror. Never copies credentials, docs or staging pages; `--protect a,b` keeps host-owned files; `--dry-run` first; prints a copied/unchanged/deleted/pruned/excluded summary and the resulting top-level tree. `deploy:dev` = `build` → sync `--from-config --prune` into the website (`DOXA_DROPIN_DEST` overrides the path) — the only sanctioned way a map changes on the site; then commit in both repos. |
| `bun run build` | `clean` → one `vite build` per `plugin-designs/*/` folder → `tree` → `check:drop` → `check:parse` → `verify` → `gallery` (writes `public/index.html`, the built plugin gallery, from the manifest). |
| `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. |
**Plugin gallery.** One generator, two modes. `internal/generate-gallery.mjs` exports
`renderGallery(model, { mode })`, a pure function from a manifest-shaped model (bundles → profiles →
instances, with `scriptUrl` / `iframeUrl` / `embed` / `integrity`) to the whole page: the list, the stage,
`#/<bundle>/<profile>[/<instance>]` deep links, Open page, Copy embed, the token ladder, lazy engine load,
the phone layout and the baked fallback. Mode `build` (`bun run gallery`) renders
`public/doxa-maps/manifest.json` into `public/index.html` — the root page of the served web-root and,
through the drop-in, the website's `/doxa-maps` page. Mode `dev` is what `bun run dev` serves at `/`: the
same page over `galleryModel(discoverSpaRegistry())` (`internal/plugin-gallery/dev-plugin.mjs`), differing
only in the loader (it imports `/plugin-designs/<bundle>/index.js` instead of adding the built IIFE), the live
model route (`/__plugin-gallery/manifest.json`), the `/assets/*` dev route and the composability ladder's
"?" popovers in the header — kept out of the built page to keep it lean. Nothing about the gallery is
written twice.
**How a build works.** `vite.config.js` is multi-entry by discovery: at build time it
registers every `plugin-designs/` 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.
**Shadow-style comments are stripped from production bundles.** Component CSS is injected
from `useShadowStyles(\`…\`)` template literals that ship verbatim, so `internal/vite-plugin-strip-shadow-css-comments.mjs`
(build only, `enforce: 'pre'`) removes the `/* … */` blocks inside those literals and collapses the blank lines
they leave; a literal with `${…}` is left untouched with a warning, and dev mode keeps every comment.
**What a built bundle folder contains.** Beyond the IIFE, `bun run tree` generates an
`index.html` **per bundle** (staging/showcase), **per profile**, and **per instance** —
each profile/instance page is a standalone, directly linkable/iframe-able map — plus
`public/doxa-maps/manifest.json` with ready-made `embed.iframe`/`embed.script` snippets for
every profile. Full design: [architecture/embeddability.md](./architecture/embeddability.md).
**Preset instances.** A `<profile>.instances.json` beside a parameterized profile
(`{ profile, apiBaseUrl?, instances: [{ id, title?, props }] }`) makes the build emit one
standalone page per instance at `<bundle>/<id>/index.html`. Recipe:
[how-to.md](./how-to.md#ship-n-preset-variants-of-one-screen-instances).
**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.
### Standalone gates — the SILENT failure classes
These are **not** in `build:unlocked`, deliberately: each checks something a fresh build would mask,
so running it inside the build would make it unfalsifiable. Run them before publishing.
| Script | Catches | Why it cannot live in the build |
|---|---|---|
| `bun run check:composite` | a composite (cap-4/cap-5) still shipping an OLD copy of a child it nests — a one-bundle rebuild does NOT recompile its parents | a full build makes every composite fresh *by construction*, so it could never fail there |
| `bun run check:shots` | a README screenshot that predates the bundle it shows — **a stale screenshot looks exactly like a current one** | needs a browser to repair (`bun run shots`), which CI does not have |
| `bun run check:library-index` | `docs/reference-library-index.md` fallen behind `library/` — its header promises it "can't drift", which is only true if regenerated (`bun run library-index`) | a fresh checkout gives every file the same mtime, so mtime comparison always passes in CI (CI regenerates + diffs instead) |
| `bun run check:done-evidence` | a backlog card marked `done` with no bluf **and** no fields — a status word the next team cannot reconcile against the code | reads `_qa-cycle/`, which is not in any repo CI clones |
| `bun run verify:gate1` | the Gate-1 chain end-to-end against LIVE data: header filter → composite props → real grid re-filter → count back up | needs network; it is an acceptance test, not a build step |
### The screenshot gates — three different lies a picture can tell
A README image can fail in three ways that all look identical to a passing shot run. `bun run shots`
reports `OK` for every one of them, because it proves a page *loaded* — never that it looks right.
| Script | Catches | In CI? |
|---|---|---|
| `bun run check:shots` | a PNG older than the bundle it shows — **a stale screenshot looks exactly like a current one** | report-only (repair needs a browser) |
| `bun run check:shot-distinctness` | two slots capturing the same view — shipped as "List — page 1"/"page 2", the same picture under two captions | **blocking** (parses one file) |
| `bun run check:shot-fidelity` | this machine cannot *render* what the shots must *show* (no emoji font ⇒ every icon a tofu box) | not yet — reports locally |
The distinctness case is the instructive one: the shot chased a "Next" control and a scroll that
**do not exist** (the list has no pagination), every attempt sat inside `try/catch` so the miss was
silent, and the two PNGs differed by 0.16% render noise — so they were *not* byte-identical and no
checksum would have caught them. Meanwhile a real third profile (`poster-wall`) had no shot at all.
**A slot faking a view it cannot reach, while a genuine artifact goes unshot, is the recurring
shape** — the same one that captioned the product SPA as the plugin-studio.
**`bun run check:quoted-scripts` is the exception that proves the rule above** — it is the one
doc-integrity gate that DOES run in CI (blocking), because it compares two files already in the
checkout: it needs no browser, no network, and no mtime. It asserts that the `scripts` block
quoted verbatim in `docs/README-SECTION-3-cli-commands.md` still matches `package.json`. That doc
carries its own hand-written plea to re-transcribe after every edit, and drifted anyway — twice.
The second drift omitted `check:public-hygiene` (the secret scan) from the build chain it
documents, which is the dangerous shape: **an omission in a quoted list is invisible**, since
there is no wrong character to notice. `package.json` is what runs, so the fix is always to update
the doc.
`bun run verify:5-caps` proves all five capability tiers mount and render. It needs a token —
`MAPBOX_TOKEN=$(curl -s <token-endpoint>) bun run verify:5-caps` — and it sets **both** `?tk=`
(the top-level element) **and** `window.MAP_TOKEN` (a composite's nested children read the global,
not the query param). Without a token it reports the token outage rather than the capabilities.
---
## `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/` driven by `library/api/sources.json`. Three source types:
| Type | What | Points at |
|---|---|---|
| `rest-api` | versioned REST endpoint (default) | the live people-group API; base URL resolves at runtime (`window.MAP_APP_API_URL` → `VITE_API_BASE_URL` → same-origin) |
| `csv` | a local CSV parsed at load | the Joshua Project dataset (+ photo fallbacks) |
| `api` | a single JSON URL | any plain JSON endpoint |
Country/region polygons are **not** a `sources.json` connection — they render from Mapbox's built-in
`country-boundaries-v1` tileset plus a lazy-loaded WAGF-regions GeoJSON.
**Adding or changing a source?** Full contract + checklist:
[architecture/data-sources.md](./architecture/data-sources.md).
---
## Colors
**Reused by more than one map → shared `library/colors/`. Only your map uses it → your bundle's
`src/colors/`** (merges over the shared set, that bundle only). Filename derives the mode key
(`language-family.js` → `languageFamily`).
**Writing or overriding a strategy?** Full contract + checklist:
[architecture/color-strategies.md](./architecture/color-strategies.md) ·
two homes in depth: [../contributing/COLORS.md](../contributing/COLORS.md).
---
## 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 in [architecture/file-map.md](./architecture/file-map.md) 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`.
---
## Going deeper — the architecture folder
Simple lookups live here; the full contracts, catalogs, and wiring detail live in
[`architecture/`](./architecture/):
| Doc | What it holds |
|---|---|
| [file-map.md](./architecture/file-map.md) | every file, one line each, with the gotcha each carries |
| [data-sources.md](./architecture/data-sources.md) | the `api/` seam: connection contract + checklist |
| [color-strategies.md](./architecture/color-strategies.md) | the strategy contract + legend wiring |
| [components.md](./architecture/components.md) | shared component catalog + conventions |
| [i18n.md](./architecture/i18n.md) | the locale contract + add-a-language checklist |
| [embeddability.md](./architecture/embeddability.md) | generated per-map pages + manifest — embeddable by default |
| [app-wrappers.md](./architecture/app-wrappers.md) | the SPA shell: `nav.js` + `PROFILES` contracts |
| [poster-generation.md](./architecture/poster-generation.md) | the poster/preview generator pipeline |
| [patterns-and-provenance.md](./architecture/patterns-and-provenance.md) | the two multi-map patterns + provenance |
The **Semantic Tree Legend** component (a reusable hierarchical legend) is documented in
[file-map.md](./architecture/file-map.md#the-semantic-tree-legend-mapcomponentslegendssemantictreelegendvue).
---
## Quick reference — "I want to…"
| I want to… | Do this |
|---|---|
| Add a new map | copy `plugin-designs/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/plugin-designs/<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` |