docs: add performance section (one inlined bundle > vue-from-CDN); simplify app-profiles + template-bundle READMEs

This commit is contained in:
doxa-maps 2026-09-11 17:45:57 +00:00
parent 6785dd826b
commit 81db8dbebb
3 changed files with 95 additions and 337 deletions

View file

@ -38,6 +38,21 @@ standalone embeds *and* the building blocks of a full site.
is a runtime setting, not baked in. You can point a map at a different API — or swap the map engine
itself (Mapbox ↔ MapLibre) — by changing config, no rebuild. Portable and future-proof.
## Why it's fast (one self-contained file)
Everything a map needs is built into **one file** — the Vue runtime, the components, and the CSS
are all inlined. The browser makes **one request** and the map runs. There's no waterfall of extra
downloads and no dependency on a separate Vue CDN being online or version-matched.
- **One output file.** The JS carries its own CSS (`cssCodeSplit: false`) and its own Vue, built as
a single IIFE. Nothing to wire up, nothing that can 404 on its own.
- **Faster than loading Vue from a CDN.** A CDN copy of Vue is an extra request plus a version you
have to keep in sync; inlining removes both. Fewer moving parts, less technical debt.
- **Lean.** Tree-shaking drops unused code, so the single file stays small even though it's complete.
(The one deliberate exception is the heavy PDF-export library — loaded from a CDN only if someone
actually exports — so the everyday map load never pays for it.)
## Quickstart
Bun is required (`bun.lock` is the committed lockfile — install from <https://bun.sh>).

View file

@ -1,224 +1,72 @@
# app-profiles/ — where YOUR maps live
> **Zero-context summary:** the `doxa-maps` repo is a **shared Apache template**
> in `../library/` (aliased `@map`). You never edit it per-map. Instead, you make a
> folder **here**, in `app-profiles/<your-map>/`, and build *your* map by
> composing the template's building blocks. Your folder is **yours** — your
> color strategy, your API connections, your i18n. The template is **shared** —
> Apache-2.0, common to everyone, propagated to every map on the next build.
The shared template lives in `../library/` (aliased `@map`) — you never edit it per-map.
You build your map by making a folder **here**, `app-profiles/<your-map>/`, and composing the
template's building blocks. Your folder is yours (colors, API, i18n); the template is shared
(Apache-2.0, propagated to every map on the next build).
If you only remember one thing: **shared template → `app-profiles/<your-map>/` →
your custom color + API + i18n.** Read on for exactly where each piece goes.
---
## The bundle pattern (folder → bundle → profiles → script tags)
This is the whole convention in one picture:
## The pattern in one picture
```
app-profiles/<your-map>/ ← a FOLDER here = one JS BUNDLE named after the folder
├── index.js ← entry: registers the custom element, discovers profiles
├── overview.vue ← one .vue = one APP PROFILE (FLAT — directly in the folder)
└── detail.vue ← another profile in the SAME bundle
app-profiles/<your-map>/ ← one FOLDER = one BUNDLE, named after the folder
├── index.js ← entry: registers the custom element, finds the profiles
├── overview.vue ← one .vue = one PROFILE (flat in the folder — no profiles/ subfolder)
└── detail.vue ← another profile in the SAME bundle
builds to ─────────────► public/doxa-maps/<your-map>/<your-map>.js (one file)
builds to ─────────────► public/doxa-maps/<your-map>/<your-map>.js (one file)
```
- **Folder name = bundle name.** `app-profiles/<your-map>/` builds to exactly one
file, `public/doxa-maps/<your-map>/<your-map>.js`. (Folders starting with `_` or `.` are
skipped — handy for staging WIP.)
- **Each `.vue` in the folder = one app profile.** Profiles sit **flat, directly
in the bundle folder** — there is **no `profiles/` subfolder**. A profile is one
map screen; one bundle can hold many. (See `template-bundle/` for a minimal
two-profile example.)
- **Each profile is reachable from a single `<script src>` + custom element.** The
page loads the one bundle, then picks which profile to show with the
`profile` field in the element's `profile-config`. So every profile has a
paste-ready embed snippet that differs only in that one field.
- **Folder name = bundle name.** Folders starting with `_` or `.` are skipped (handy for WIP).
- **Each `.vue` = one profile** (one map screen). One bundle can hold many.
- **Pick the profile at embed time** with the `profile` field — every profile shares one bundle:
```html
<!-- same bundle, two profiles, chosen by `profile` -->
<script src="/doxa-maps/<your-map>/<your-map>.js"></script>
<doxa-map profile-config='{"profile":"overview","tk":"pk.eyJ..."}'></doxa-map>
<doxa-map profile-config='{"profile":"detail","tk":"pk.eyJ..."}'></doxa-map>
```
---
## The embed template (paste-ready — just swap the bundle name)
Copy this onto any page. Change **`<your-map>`** to your folder name and
**`profile`** to one of your `.vue` filenames (without `.vue`). Nothing else
changes between maps.
```html
<!-- 1. Mapbox peer (loaded once per page, before any bundle) -->
<link href="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js"></script>
<!-- 2. Your bundle — swap <your-map> for the app-profiles folder name -->
<script src="/doxa-maps/<your-map>/<your-map>.js"></script>
<!-- 3. The map — swap `profile` for one of your <name>.vue files (without .vue) -->
<doxa-map profile-config='{
"profile": "<your-map>",
"tk": "pk.eyJ...", // your Mapbox token
"dataSource": { "type": "rest-api",
"endpoint": "https://your-api.example/api/people-groups/list" }
}'></doxa-map>
```
- The `<script src>` URL is the only thing that changes between hosting modes:
a **relative** path (`/doxa-maps/<your-map>/<your-map>.js`) when the host serves the file, or an
**absolute CDN** URL (`https://cdn.example.org/.../<your-map>.js`) when an
external/partner site serves it. The bundle is identical either way. See
`../CONTRIBUTING.md` for the two CDN modes.
- The same bundle can point at different data with no rebuild — `profile-config`
is read at runtime, so swapping `dataSource` or `tk` on the page is enough.
---
## The three-step agent workflow
Adding a map is three steps, start to finish:
## Add a map — three steps
```bash
# ── Step 1: create the folder + at least one profile .vue ──────────────────
# 1. create the folder + one profile
mkdir -p app-profiles/<your-map>
# app-profiles/<your-map>/index.js ← copy doxa-simple-map/index.js, rename the tag
# app-profiles/<your-map>/<name>.vue ← your screen (FLAT in the folder), built from @map/* blocks
# index.js ← copy doxa-simple-map/index.js, rename the tag
# <name>.vue ← your screen, built from @map/* blocks (flat in the folder)
# ── Step 2: build ──────────────────────────────────────────────────────────
npm run build
# → public/doxa-maps/<your-map>/<your-map>.js appears
# (or, for just this one: BUNDLE=<your-map> npm run build:bundle)
# 2. build
npm run build # → public/doxa-maps/<your-map>/<your-map>.js
# (just this one: BUNDLE=<your-map> npm run build:bundle)
# ── Step 3: get the script tag and embed ──────────────────────────────────
# <script src="/doxa-maps/<your-map>/<your-map>.js"></script>
# <doxa-map profile-config='{"profile":"<name>","tk":"pk.eyJ..."}'></doxa-map>
# 3. embed the script tag + element (dev staging at `npm run dev` shows a paste-ready snippet)
```
The dev staging index (`npm run dev`, served at `/`) also **auto-detects** the new
folder and shows a copy-paste embed snippet for each profile — no edit to any
index needed. See `../CONTRIBUTING.md` for the build-vs-dev distinction.
## Your three things (everything else is shared)
---
- **Colors** — how pins are colored. Pick one the template ships (`getColorStrategy('language-family')`),
or drop your own in `app-profiles/<your-map>/src/colors/` (start from
`template-bundle/src/colors/example-mode.js`). Never edit `@map/colors/` to recolor your map.
- **API** — your map reads its data from the `profile-config` on the page, so the same bundle points
at different data with no rebuild. `DataSourceManager` accepts `csv` (bundled file), `api` (one JSON
URL), or `rest-api` (a live REST endpoint, the default). Prefer the real API over inlined JSON.
- **i18n** — the template ships 11 base locales in `@map/i18n/locales/`. Use `useI18n()` for strings
and `RTL_LOCALES` for right-to-left; add or override strings from your own profile.
## The model — three zones, never mixed
## The one hosting variable
```
doxa-maps/
├── library/ (alias @map) ← THE SHARED APACHE TEMPLATE — do not customize per-map
│ ├── components/ reusable map UI (legends, toolbar, buttons, drawers)
│ ├── composables/ useMapInstance, useMapData, useMapLayers, …
│ ├── colors/ the menu of ways to color pins (one strategy per file)
│ ├── config/ posterDefaults.js · posterSizes.js (map defaults: constants/mapDefaults.js)
│ ├── api/ DataSourceManager (csv | api | rest-api), sources.json, registry
│ ├── i18n/locales/ 11 base languages: ar de en es fr hi it pt ro ru zh
│ ├── utils/ shared helpers
│ └── stores/ Pinia stores (map / data / ui)
│
├── internal/ ← THE BUNDLER'S OWN BUILD SCRIPTS — never edited by map builders
│
└── app-profiles/<your-map>/ ← YOUR PRIVATE APP PROFILE — everything here is yours
├── index.js bundle entry (registers the web component)
├── *.vue your map screen(s) — FLAT in the folder, one per profile
└── data/*.json (optional) your own bundled data
```
The only thing that changes between hosting modes is the `<script src>`: a **relative** path when
your own site serves the file, or an **absolute CDN URL** when a partner site serves it. The bundle
is identical either way. See `../CONTRIBUTING.md` for the two CDN modes and partner-site embedding.
**Shared template (`library/`, `@map`)** is the Apache-2.0 base everyone shares. Fix a
bug or improve a legend *once* here and **every** map gets it on the next build.
Because it is shared, never bake one map's private choices into it.
## Profile patterns (mix freely)
**Your app profile (`app-profiles/<your-map>/`)** is the only place your map's
identity lives. Nothing here leaks to anyone else's map.
- **Individual** — one `.vue` per bundle (simplest).
- **Parameterized** — one profile, behavior driven by `profile-config` props (e.g. `tabs[]`).
- **Nested** — one profile that renders others (workbench-style, like `doxa-research-map`).
---
## See also
## Where YOUR three things go
### 1. Your color strategy
A *color strategy* decides how pins get colored (by language family, progress,
religion, …). The shared menu lives in `@map/colors/` — one
strategy per file, auto-registered by `_registry.js` (see that folder's README for the
contract).
- **Pick one** the template already ships — your profile selects it by color
mode (e.g. `getColorStrategy('language-family')`).
- **Add your own** — drop `my-strategy.js` into your own
`app-profiles/<your-map>/src/colors/` folder following the strategy
contract (start from `template-bundle/src/colors/example-mode.js`). It
merges over the shared set for **your map only** — never edit `@map/colors/`
to recolor your own map.
### 2. Your API connections
Your map gets its data through the **`profile-config`** attribute on the host
page — so the *same* bundle can point at different data without a rebuild (see the
embed template above). Inside your `.vue` profile you read it via
`inject('profileConfig')`, `inject('mapboxToken')`, `inject('dataSource')`, and
hand the source to `@map/api/DataSourceManager`, which supports three
shapes:
| `type` | What | Source |
|---|---|---|
| `csv` | a local CSV parsed at load | a file you bundle under your own `data/` |
| `api` | a single JSON URL | any plain JSON endpoint |
| `rest-api` | a versioned REST endpoint (the default) | a live REST API, base URL resolved at runtime |
Prefer the **real API** — do not inline JSON into the bundle. If you must ship
static data, put it in **your** `app-profiles/<your-map>/data/*.json`.
### 3. Your i18n
The template ships 11 base locales in `@map/i18n/locales/`. Your profile uses
`useI18n()` (from `vue-i18n`) for translated strings and `RTL_LOCALES`
(from `@map/i18n`) for right-to-left handling. Add or override strings for your
map from your own profile — the shared locales stay shared.
---
## Build conventions (how a folder becomes a bundle)
- **Folder name = bundle name** → builds to `public/doxa-maps/<folder-name>/<folder-name>.js`.
- Folders prefixed with `_` or `.` are **skipped** at build time (use for staging).
- Each folder MUST have an `index.js` entry that:
- evaluates `import.meta.glob('./*.vue')` locally (bundle-private registry —
profiles are **flat** in the folder, so the glob is `./*.vue`, not
`./profiles/*.vue`) and hands it to `ProfileLoader` via
`app.provide('profileModules', …)`,
- registers the web component(s) via `defineCustomElement`,
- lets `ProfileLoader` parse the host's `profile-config`.
The `profile` field in `profile-config` MUST match a `.vue` file sitting directly
in your bundle folder (the design is flat — no `profiles/` subfolder). See
`doxa-simple-map/index.js` for a complete, commented example, or
`template-bundle/` for a minimal two-profile skeleton.
## Three profile patterns (mix freely)
- **Individual** — one `.vue` profile per bundle (simplest).
- **Parameterized** — one profile, behavior driven by `profile-config` props at
runtime (e.g. `tabs[]`).
- **Nested** — one profile that internally renders others (workbench-style, like
`doxa-research-map`).
## License & provenance
The shared template (`library/` / `@map`, build tooling, this scaffold) is
**Apache-2.0** — a permissive base everyone shares. Your **app profiles** under
`app-profiles/<your-map>/` are **yours**: your color strategy, API connections,
and i18n. Building on the shared Apache template is exactly what lets you keep
your own maps private, custom, and uniquely yours.
## See also — the shared `library/` (`@map`) components you build from
- `../library/components/` — reusable map UI: legends, toolbars, buttons, drawers
- `../library/composables/` — `useMapInstance`, `useMapData`, `useMapLayers`, …
- `../library/colors/README.md` — the color-strategy contract
- `../library/api/` — `DataSourceManager` + the source registry
- `../library/i18n/` — base locales and RTL handling
- `../library/ProfileLoader.vue` — parses `profile-config`, mounts the right profile
- `../README.md` — repo overview & build pipeline
- `../CONTRIBUTING.md` — build vs dev, the two CDN modes, partner-site embedding
- `doxa-simple-map/index.js` — fully commented reference bundle entry
- `template-bundle/` — minimal two-profile skeleton to copy
- `../library/colors/README.md` — the color-strategy contract
- `../README.md` — repo overview & build pipeline · `../CONTRIBUTING.md` — build vs dev, CDN modes

View file

@ -1,176 +1,71 @@
# template-bundle — copy this to start a new map
You're a **programmer** (a *coder*, a *map-maker*) about to build a map. This folder
is your starting point. **Copy it, then build your map inside the copy.** It's a
tiny working bundle — two trivial profiles that render a colored box, no Mapbox, no
data — plus a `src/` sandbox laid out for the real code you'll add.
Your starting point. **Copy this folder, then build your map inside the copy** — don't start from
blank. It's a tiny working bundle (two trivial profiles that render a colored box, no Mapbox, no
data) plus a `src/` sandbox laid out for your real code.
Don't build from a blank folder. Start here.
## Two zones — know which one a file is in
---
## The two zones
Know which zone a file is in before you touch it.
| Zone | What it is | Can you edit it? |
| Zone | What | Edit it? |
|---|---|---|
| **`library/`** (imported as `@map/…`) | The shared, reusable, parameterized building blocks — map, legends, toolbars, composables, the color + API registries. | **No — reuse only.** Editing it breaks other maps. |
| **`app-profiles/<your-map>/src/`** | Your **safe sandbox**. Your screens, colors, API wiring, components, utils. | **Yes — build anything.** Nothing here leaks to any other map. |
| **`library/`** (imported as `@map/…`) | shared, reusable building blocks — map, legends, toolbars, composables, color + API registries | **No — reuse only.** A change here can break every other map. |
| **`app-profiles/<your-map>/src/`** | your private sandbox — screens, colors, API wiring, components, utils | **Yes — build anything.** Nothing here leaks to another map. |
To see everything `library/` gives you, read **`docs/reference.md`** at the bundler
root. That's the menu you pick from.
Rule of thumb: **local by default.** Code only moves into `library/` once a *second* map genuinely
needs it — an expert move, not a first move. To see everything `library/` offers, read
`../../docs/reference.md` (that's the menu you pick from).
## The three rules
## Build a new map — the steps
1. **Reference the template.** Start every new map by copying this folder.
2. **Build everything for your map inside your app-profile.** Colors, APIs,
components, utils, data — all of it goes in `app-profiles/<your-map>/`.
3. **Don't edit `library/`** (nor `internal/`) unless you're an expert and know
exactly why. `library/` is shared; a change there can break every other map.
Rule of thumb: **local by default.** Code only moves into `library/` once a
*second* map genuinely needs it — and that's an expert move, not a first move.
---
## To build a new map — the steps
1. **Copy this folder.** `app-profiles/template-bundle/` → `app-profiles/<your-map>/`.
The folder name becomes your bundle name (and its output file, `<your-map>.js`).
2. **List your requirements.** Write down what your map needs: which data, which
colors, which controls (legend, search, share, fullscreen…).
3. **Pick pieces from `docs/reference.md`.** Match your list to the reusable
`@map/…` components, composables, and utils. Import them **by reference** — never
1. **Copy this folder** → `app-profiles/<your-map>/`. The folder name becomes your bundle name (and
output file `<your-map>.js`). Rename the custom-element tag in `index.js` to your map's tag.
2. **List what your map needs** — data, colors, controls (legend, search, share, fullscreen…).
3. **Pick pieces from `../../docs/reference.md`** and import them by reference (`@map/…`) — never
copy library code into your folder.
4. **Put your custom code in `src/`.** Anything that's yours and specific to this
map — screens, components, hooks, helpers — goes in `src/` (see the map below).
Rename the custom-element tag in `index.js` to your map's tag.
5. **Edit your colors in `src/colors/colors.json`.** Change the hex values — no
code. For color that varies by data value, use a strategy in `src/colors/`
(start from `example-mode.js` there — a strategy is any `<mode>.js` in that folder).
6. **Run the build.** `bun run build` (or `npm run build`) → your `<your-map>.js`
appears. For just yours: `BUNDLE=<your-map> bun run build:bundle`.
---
4. **Put your own code in `src/`** — screens, components, hooks, helpers.
5. **Set colors in `src/colors/colors.json`** (plain hex, no code). For color that varies by data,
use a strategy `.js` in `src/colors/` (copy `example-mode.js`).
6. **Build** — `bun run build` (or just this one: `BUNDLE=<your-map> bun run build:bundle`).
## What's in this folder
```
app-profiles/template-bundle/
├── index.js ← bundle entry: registers <template-bundle>, globs ./*.vue
├── index.html ← dev staging page (required for the build to find the bundle)
├── template-map-a.vue ← profile "template-map-a" ← FLAT, at the bundle root
├── template-map-b.vue ← profile "template-map-b" ← FLAT, at the bundle root
└── src/ ← YOUR SANDBOX — build your map's own code here
├── api/ which data source this map reads
├── colors/ ALL your colors: colors.json (plain values) + strategy .js
│ files (data-driven — example-mode.js to copy)
├── components/ your map's own .vue pieces
├── composables/ your map's own logic hooks
└── utils/ your map's own helper functions
```
**The map-maker's rule:** the bundle **root** holds ONLY the application
profile(s) (`.vue`) plus the scanner-contract files (`index.js`, `index.html`,
`*.instances.json`, `README.md`). Everything else lives in `src/`.
Each `src/` subfolder has its own README explaining what belongs there. It builds
into **one** file: `public/doxa-maps/template-bundle/template-bundle.js`.
---
## The flat design (profiles at the bundle root)
The `.vue` profile files sit **directly in the bundle folder**, side by side with
`index.js`. There is **no nested `profiles/` subfolder** — because the entry
discovers profiles with a **flat glob**:
```js
// index.js
const profileModules = import.meta.glob('./*.vue') // ← './*.vue', not './profiles/*.vue'
```
```
template-bundle/
index.js
template-map-a.vue ✅ flat — discovered
template-map-b.vue ✅ flat — discovered
profiles/
template-map-c.vue ❌ would NOT be discovered
├── index.js ← bundle entry: registers <template-bundle>, globs ./*.vue
├── index.html ← dev staging page (the build needs it to find the bundle)
├── template-map-a.vue ← profile "template-map-a" ← FLAT at the bundle root
├── template-map-b.vue ← profile "template-map-b" ← FLAT at the bundle root
└── src/ ← YOUR SANDBOX (each subfolder has its own README)
├── api/ colors/ components/ composables/ utils/
```
Keep your **profile screens flat** at the root. The *pieces those screens are built
from* (small components, hooks, helpers) live in `src/`.
**The flat rule:** profile `.vue` files sit **directly in the bundle folder** — there is no
`profiles/` subfolder, because the entry discovers them with a flat glob
(`import.meta.glob('./*.vue')`). A `.vue` inside a `profiles/` subfolder would NOT be found. Keep
your profile screens flat; the small pieces they're built from live in `src/`.
## How the pieces map together
| Thing | Value here | Rule |
|---|---|---|
| Folder name | `template-bundle` | = the bundle name |
| Output file | `public/doxa-maps/template-bundle/template-bundle.js` | `<folder>.js` |
| Custom element tag | `<template-bundle>` | registered in `index.js` |
| Profile files | `template-map-a.vue`, `template-map-b.vue` | each `*.vue` = one profile |
| Profile name | `template-map-a` / `template-map-b` | the filename without `.vue` |
One bundle, two profiles. The page picks which profile to render with the
`profile` field of `profile-config` — both share the single `template-bundle.js`.
Everything builds into **one** file: `public/doxa-maps/template-bundle/template-bundle.js`.
## The two ways you import
- **Reuse from the library** → the **`@map/…` alias** (points at `library/`):
`import { useMapInstance } from '@map/composables/useMapInstance.js'`
- **Use your own sandbox code** → a **relative `./src/...` path**:
`import colors from './src/colors/colors.json'`
- **Reuse from the library** → the `@map/…` alias: `import { useMapInstance } from '@map/composables/useMapInstance.js'`
- **Your own sandbox code** → a relative path: `import colors from './src/colors/colors.json'`
There is no `@map/`-style alias for your sandbox — your own code is always a
relative path. That boundary is what keeps your sandbox private to your map.
---
There's no alias for your sandbox — your code is always relative. That boundary keeps it private.
## Embed it
```html
<script src="/doxa-maps/template-bundle/template-bundle.js"></script>
<!-- profile A -->
<template-bundle profile-config='{"profile":"template-map-a"}'></template-bundle>
<!-- profile B — same bundle, different profile -->
<template-bundle profile-config='{"profile":"template-map-b"}'></template-bundle>
```
(`profile` must match a `.vue` filename in this folder, without the `.vue`.)
## Build / preview
```bash
# build just this bundle → public/doxa-maps/template-bundle/template-bundle.js
BUNDLE=template-bundle npm run build:bundle
# or build every bundle
npm run build
# preview in isolation (HMR) — open the staging page for this bundle
npm run dev
```
## Add a third profile
Drop another flat `.vue` next to the others — no config to edit:
```bash
# app-profiles/template-bundle/template-map-c.vue
npm run build # → still one file, template-bundle.js, now with 3 profiles
```
Then request it: `<template-bundle profile-config='{"profile":"template-map-c"}'></template-bundle>`.
(`profile` must match a `.vue` filename in this folder, without `.vue`. Add a third profile by
dropping another flat `.vue` next to these and rebuilding — no config to edit.)
## See also
- `src/README.md` — the sandbox layout and the two import styles.
- `../../docs/tutorial.md` — the 3 zones + 3 rules, in one page.
- `../../docs/reference.md` — the menu of reusable `@map/…` pieces.
- `../README.md` — the app-profiles authoring guide (bundle pattern, embed template).
- `../../CONTRIBUTING.md` — build vs dev, the two CDN modes, partner-site embedding.
- `../doxa-simple-map/index.js` — a fuller, real-map reference bundle entry.
- `src/README.md` — sandbox layout · `../README.md` — the app-profiles authoring guide
- `../../docs/tutorial.md` — 3 zones + 3 rules in one page · `../../docs/reference.md` — the `@map/…` menu
- `../doxa-simple-map/index.js` — a fuller, real-map reference entry