doxa-maps/README.md
doxa-maps-88655c70 cba9de3adb
Some checks are pending
build-gate / build (push) Waiting to run
Tell agents to use the dev server, and to push
TWO RULES THAT WERE COSTING REAL TIME.

1. A FULL BUILD IS NOT NEEDED TO SEE A CHANGE. Agents were running the minutes-
long `bun run build` for edits the dev server hot-reloads. Documented `bun run
dev`, plus the parallel-safe form.

   The trap that makes the parallel form mandatory: Vite's default behaviour on
   a busy port is to silently move to the next one, and FIVE gates have 5173
   written into them (check-facet-changes-map, plugin-gallery-smoke,
   smoke-studio, generate-gallery, check-scrim-covers-viewport). A second dev
   server without --strictPort therefore leaves those gates talking to somebody
   else's server, or to none, reporting a confident wrong answer with no error.
   Verified: `--port 5299 --strictPort` serves 200 on root and on the registry,
   and a duplicate on the same port exits 1 instead of drifting.

2. A LOCAL COMMIT IS INVISIBLE. The Driver runs a laptop watcher against the
   Forgejo repo and sees changes in the browser from THAT repo, so work that
   stops at `git commit` has not been delivered however well it was verified.
   Documented the push, with the credential kept out of .git/config and filtered
   from output.

   Also documented the follow-up nobody expects: after pushing to an inline URL,
   `git log @{u}..` still reports the commits as unpushed, because git only moves
   a remote-tracking ref when it pushes to a NAMED remote. That read as "63
   unpushed commits" on a branch that was fully pushed.

check:doc-scripts passes, so every command quoted here exists in package.json.
2026-09-23 15:45:24 +00:00

351 lines
18 KiB
Markdown

# doxa-maps — one build, maps that embed everywhere
**A Vite bundler that turns each map into a self-contained web component.** Write a map in
`plugin-designs/<your-map>/`, run one build, and get a folder you can drop on any host — iframe it,
script-embed it, wrap it as a mobile app, or open every map at once as one SPA.
This README has three parts, in the order you need them:
1. **[The 5 plugin capabilities](#1-the-5-plugin-capabilities)** — what you can build, simplest to richest.
2. **[The 5 deployment methods](#2-the-5-deployment-methods)** — where a plugin can run.
3. **[The Plugin Studio](#3-the-plugin-studio)** — ★ where you *work*. Start here if you are new.
---
## 1. The 5 plugin capabilities
A "capability" is **how much a plugin nests** — not how hard it is. Every screenshot below is a real
bundle from this repo's build output.
| # | Capability | What it is | Example bundle |
|---|------------|------------|----------------|
| 1 | **Single element** | One folder → one bundle → one custom element. | `all-people-groups` |
| 2 | **Prop-table element** | One `root.vue` + a preset table → many maps from one plugin. | `doxa-simple-map` |
| 3 | **Monolithic element** | One `root.vue` nests many views internally → one element. | `doxa-research-map` |
| 4 | **Nested-element SPA** | A composite that nests *other plugins* and wires them with props/events. | `big-picture` |
| 5 | **Nested-SPA / app shell** | Many SPAs nested into one deep-linkable app. | `doxa-maps-spa` |
> The tier is decided by the files, not by how simple a folder looks: an `*.instances.json` makes a
> bundle **cap-2** however light it is, and a `meta.json` `type` declares tiers 3/4/5. The authoritative,
> generated answer lives in
> **[documentation/reference-capability-exemplars.md](./documentation/reference-capability-exemplars.md)**
> — cite that file rather than remembering.
### Capability 1 — single element
One folder, one `.vue`, one embeddable element. `plugin-designs/all-people-groups/` shows every people
group on one map.
![Capability 1 — a single-element plugin (all-people-groups)](documentation/readme-images/cap1-single-element.png)
### Capability 2 — prop-table element
One `root.vue`, many maps. `plugin-designs/doxa-simple-map/` ships a preset prop-table
(`simple-map.instances.json`) that renders it three ways — each a standalone embeddable page from the
**same** code.
| Prayer | Engagement | Adoption |
|--------|------------|----------|
| ![Capability 2 — prayer preset (doxa-simple-map)](documentation/readme-images/cap2-prayer.png) | ![Capability 2 — engagement preset (doxa-simple-map)](documentation/readme-images/cap2-engagement.png) | ![Capability 2 — adoption preset (doxa-simple-map)](documentation/readme-images/cap2-adoption.png) |
### Capability 3 — monolithic element
A whole multi-view map from one `root.vue`. The trade-off: you cannot embed just one of its tabs.
![Capability 3 — the monolithic research map (doxa-research-map)](documentation/readme-images/cap3-monolithic-research.png)
### Capability 4 — nested-element SPA
A composite that nests **lower-tier plugins** and wires them with props and events. Each inner plugin
stays independently embeddable, and the composite is embeddable too. `plugin-designs/my-upg-100-list/`
ships three leaves:
| Composite (`upg-100-mfe`) | List (`upg-100-list`) | Poster wall (`poster-wall`) |
|---------------------------|-----------------------|-----------------------------|
| ![Capability 4 — the UPG-100 composite (my-upg-100-list)](documentation/readme-images/cap4-upg100-mfe.png) | ![UPG-100 list leaf](documentation/readme-images/cap4-upg100-list.png) | ![UPG-100 poster-wall leaf](documentation/readme-images/cap4-upg100-poster-wall.png) |
### Capability 5 — nested-SPA / app shell
Every plugin in one deep-linkable page. Two cap-5 apps ship, and the difference matters when you embed
one: **`doxa-maps-spa` is what an end user gets; `plugin-studio` is what a developer works in.**
*`doxa-maps-spa` — the curated product SPA*, with tabs grouped by audience.
![Capability 5 — the curated product SPA (doxa-maps-spa)](documentation/readme-images/cap5-doxa-maps-spa.png)
*`plugin-studio` — the developer studio*, grouping every built plugin by capability (5 → 1). It is
auto-generated: the build discovers every folder in `plugin-designs/`, so a new plugin appears in the
studio without any wiring. This is [section 3](#3-the-plugin-studio).
![Capability 5 — the developer studio (plugin-studio)](documentation/readme-images/cap5-plugin-studio.png)
---
## 2. The 5 deployment methods
Ordered easiest to hardest. **Status is stated honestly** — "proven" means it has been run and seen, and
nothing below is upgraded past what the files support.
| # | Method | Status | Command / install |
|---|--------|--------|-------------------|
| 1 | **iframe** on any site | ✅ **proven** | `bun run build`, then paste the `embed.iframe` snippet from `manifest.json` |
| 2 | **Mobile app** (Capacitor, Android) | 🛠️ built, not shot | `bun run check:mobile` (no toolchain) · `bun run phone` needs Android SDK + JDK 17 + `adb` |
| 3 | **JavaScript embed** (`<script src>` + SRI) | ✅ **proven** | `bun run build`, then paste the `embed.script` + `integrity` snippet |
| 4 | **WordPress plugin** (shortcode + block) | 🛠️ built, contract-proven, not shot | `bun run check:wp-embed`, then install `deployment/wordpress/doxa-maps-embed/` |
| 5 | **Desktop app** (Tauri) | 🛠️ built, not shot | `bun run check:tauri` (no toolchain) · `cargo tauri build` needs Rust + webview libs |
**What "not shot" means:** the code exists and its contract test passes, but nobody has captured it
running on a real device or install. Each is blocked by a missing local toolchain, not by missing code —
Android SDK for method 2, a live WordPress for method 4, and a GUI (this box is headless) for method 5.
Methods **1 and 3 are the two proven-live paths.** If you need a map on a site today, use one of those.
The full per-method command matrix, with every prerequisite, is in
**[documentation/README-SECTION-3-cli-commands.md](./documentation/README-SECTION-3-cli-commands.md)**.
### The Mapbox token (read this before you file a bug)
The maps use Mapbox, which needs a `pk.` token. **The token works differently in dev than in the built
tree**, and diagnosing one from the other is the most common way to lose an hour.
| Where | How it gets a token |
|---|---|
| **Dev server** (`bun run dev`) | Baked into the page for you — from `.env` (`VITE_MAPBOX_TOKEN`) if present, otherwise from `token` in `vite.doxa-maps-build-config.json`. |
| **Built pages** (`bun run serve`, or hosted) | **Not baked.** Each page resolves a token at runtime: `?tk=<TOKEN>` on the URL, a token you paste into the page's own input box (kept in `localStorage`), or a token endpoint (`tokenUrl`). |
So on a built leaf page you typically need:
```
http://localhost:4173/doxa-maps/demo-map/demo-map/index.html?tk=pk.your_token_here
```
To use your own token in dev, create a `.env` — it is **gitignored** (`.gitignore` lines 21-24), so it
never reaches the repo:
```bash
printf 'VITE_MAPBOX_TOKEN=%s\n' 'pk.your_token_here' > .env
```
**If a basemap is an empty grey void while the pins still draw, that is the token failing** — not a
broken app.
**Why this is genuinely awkward:** the five deployment methods each handle the token differently, and a
JAMstack embed has nowhere safe to keep a secret. The answer is not to hide the token but to make hiding
it unnecessary: a public `pk.` token is restricted to specific URLs in the Mapbox dashboard. **MapLibre
is being pursued to remove the per-embed token requirement entirely — that work is IN PROGRESS, not
done** (`maplibre-gl` is a dependency and the page generator has a MapLibre path, but no plugin-design
runs on it yet).
---
## 3. The Plugin Studio
★ **This is where you work — and the keyword is INTEROPERABILITY.**
The Plugin Studio is one place to **view all of your plugins at once** and **edit all of them in one
place using your agent**. Every plugin appears in a single view, sorted **highest capability down to
lowest**, so you can see the whole catalogue instead of hunting through folders.
That sorting is the point. Once you can see every plugin side by side, you can tell your agent **which
ones to group together** — any number of them — and compose them into **higher-capability plugins**
using **props, events and layouts**. Group cap-1 and cap-2 elements and you get a cap-4 nested-element
SPA; group those and you get a cap-5 app shell. That is what makes the studio the right place to build
map plugins, rather than a gallery you merely look at.
**Why this matters.** Without a working studio, a developer *sees the fruit but not the deeper
strategy* — they get the maps, but they cannot reproduce or extend them. The studio is what turns a set
of finished maps into something you can build on.
### Get it running
Four steps from nothing to every map in one place.
```bash
# 1. Clone (the repo root IS the studio — there is no subfolder to cd into).
# This host is PRIVATE: it will prompt for your Forgejo username + token.
git clone --branch doxa-maps-plugin-studio https://git.nuxtbase.app/doxa-life/doxa-maps.git
cd doxa-maps
# 2. Install dependencies (installs 206 packages; needs Node >= 18)
bun install --frozen-lockfile
# 3. Optional — your own Mapbox token for dev (see section 2)
printf 'VITE_MAPBOX_TOKEN=%s\n' 'pk.your_token_here' > .env
# 4. Open the Plugin Studio
bun run dev
```
Then open **http://localhost:5173/** — that root URL *is* the Plugin Studio, with every plugin in one
spot. (If port 5173 is busy Vite picks another one and prints it; read the line it logs.)
> **You do not need to build first.** The build output in `public/` is committed, so a fresh clone has
> plugins to show the moment the dev server starts.
> **If step 1 fails with `remote: Unauthorized`,** the clone is not broken — `git.nuxtbase.app` is a
> private self-hosted Forgejo and anonymous cloning is refused. Ask for an account, then use your
> username plus a Forgejo **access token** (not your password) when git prompts.
Two things worth knowing:
- **`/` is the studio; `/__gallery` is the old staging gallery**, kept but demoted to an explicit URL.
- **Edits hot-reload.** The studio mounts the real `plugin-studio` custom element through its own
`index.js` — the same path the built bundle takes — so what you see is the real element, not a preview.
### Work with your agent
The studio's companion is your coding agent. The loop:
1. Open the studio (`bun run dev`) and look at the catalogue, sorted cap-5 → cap-1.
2. Tell your agent which plugins to **group** — e.g. *"combine the research map and the people-groups
grid into one composite."*
3. The agent writes a new folder in `plugin-designs/<name>/`, wiring the children with props, events and
a layout from `library/layouts/`.
4. Refresh the studio. The new, higher-capability plugin is there.
**The whole contract is: write your app inside `plugin-designs/<your-map>/`.** There is no registration
step and no per-plugin wiring — the build discovers every folder under `plugin-designs/`. Copy
`plugin-designs/template-bundle/` to start a new one.
Two shared layouts do the composing, both in `library/layouts/`:
- **`VerticalTabsLayout`** — one element visible at a time, lazy-mounted on first open.
- **`StackLayout`** — the opposite: elements stacked top-to-bottom at natural height, so the page
scrolls through them.
### Working fast: use the dev server, not the build
A full `bun run build` rebuilds every bundle and takes minutes. **You do not need it to see a
change.** The dev server hot-reloads:
```bash
bun run dev # http://localhost:5173
bun x vite --port 5299 --strictPort # any other port, when 5173 is taken
```
★ **If more than one of you is working in this repo at once, EVERY dev server must name its own
port, and must pass `--strictPort`.** Vite's default behaviour on a busy port is to silently move to
the next one — 5173 becomes 5174 — and five gates (`check-facet-changes-map`,
`plugin-gallery-smoke`, `smoke-studio`, `generate-gallery`, `check-scrim-covers-viewport`) have
`5173` written into them. A second server without `--strictPort` therefore leaves those gates
talking to somebody else's server, or to none, and reporting a confident wrong answer with no error.
With `--strictPort` the duplicate exits non-zero instead of drifting — verified: port taken → exit 1.
Reach for the full build only when you need the deployable tree, an SRI-consistent leaf page, or a
manifest. To check one bundle's compiled output without touching anyone else's:
```bash
BUNDLE=<name> bun x vite build # this bundle only; does NOT clean the tree
```
### Everything must reach Forgejo
**A local commit is invisible.** The Driver runs a laptop watcher against
`git.nuxtbase.app/doxa-life/doxa-maps` and sees changes in the browser from THAT repo — so work that
stops at `git commit` has not been delivered, no matter how well it was verified locally.
Push every increment:
```bash
TOKEN="$(pass show vivarium/forgejo-certified-sandbox | head -1)"
git -c http.postBuffer=524288000 \
push "https://oauth2:${TOKEN}@git.nuxtbase.app/doxa-life/doxa-maps.git" HEAD:doxa-maps-plugin-studio \
2>&1 | sed "s/${TOKEN}/[REDACTED]/g"
unset TOKEN
```
Never write the credential into `.git/config` (see the clone note above), always filter output
through `sed`, and prefer many small pushed commits over one large local one.
★ After pushing to an INLINE url, `git log @{u}..` still reports the commits as unpushed, because git
only moves a remote-tracking ref when it pushes to a NAMED remote. Sync it so the next reader is not
misled:
```bash
git fetch "https://oauth2:${TOKEN}@git.nuxtbase.app/doxa-life/doxa-maps.git" \
'refs/heads/doxa-maps-plugin-studio:refs/remotes/origin/doxa-maps-plugin-studio'
```
### The production build
The dev server is for working. When you want the deployable tree:
```bash
bun run build # ~4 minutes; writes public/doxa-maps/
bun run serve # serve the BUILT tree at http://localhost:4173
```
`bun run build` takes a **lock**: if another build is already running it fails fast rather than
corrupting the shared output. Set `BUILD_LOCK_WAIT=600` to queue behind it instead.
### Before you MEASURE the built tree — `bun run check:settled`
```bash
bun run check:settled # exit 0 = safe to measure/publish; exit 1 = do not
```
**The lock protects a second WRITER. It does not protect a READER.** Every build begins with
`bun run clean`, which `rm -rf`s `public/doxa-maps/`, so while a build runs the tree is transiently
near-empty — and anything reading it then gets a confident, wrong answer: "that bundle is missing",
"the dashboard tab does not exist", "461 files were deleted". Those are not hypotheticals; each has
happened here, and one nearly produced a bug report against a correct change.
So run this gate before any browser check, any publish, and any claim about what the tree contains.
It reports `builders`, `waiters`, `churn(45s)`, `dirs`, `leaves` and `quietFor`, and **grade it by
exit code** — like every gate here, it prints teaching prose after its verdict, so `| tail -1` will
mislead you.
★ **Do not substitute a `pgrep` check for this.** A process list samples an INSTANT; the lock knows
the QUEUE. Between one queued build finishing and the next acquiring the lock there is a real window
where zero build processes exist and the tree is nonetheless about to be wiped — and a long quiet
period makes that window look *more* convincing, not less, because the queue is idle precisely
because it is between runs. Measured 2026-09-23: `pgrep` reported 0 builders while the gate reported
`builders=1 waiters=1`, and `ps` confirmed a build had started 17 seconds earlier.
If you are one of several agents working this repo at once, the stronger rule is simply: **do not run
`bun run build` at all.** Edit source, and let one owner run the single authoritative build. To
sanity-check one bundle without touching anyone else's output, use
`BUNDLE=<name> bun x vite build` — it does not clean the whole tree.
---
## Repository layout
Two directories matter to a plugin author:
```
plugin-designs/ ← YOUR FREE ZONE. One folder per plugin. Copy template-bundle/ to start.
library/ ← the SHARED toolkit (layouts, components, composables, colors, stores, i18n).
```
Everything else is supporting cast: `internal/` (build + gates), `public/` (committed build output),
`deployment/` (Capacitor, WordPress, Tauri), `documentation/`.
**`plugin-designs/` is a free zone.** Your plugin is its own application in its own folder, building to
its own output. Nothing you do there can break another author's map.
**`library/` is a HIGH BAR.** It is multi-tenant — reused by many maps and authors — so every edit is a
breaking-change risk to everyone. Only contribute here when a component is genuinely reusable *and*
parameterized, so other maps can use it. Never widen or casually add. Keep map-specific,
non-parameterized code in your own plugin-design.
---
## More docs
| Read this | When |
|---|---|
| **[documentation/develop-a-plugin-and-see-it-live.md](./documentation/develop-a-plugin-and-see-it-live.md)** | ★ the golden path — zero to a plugin live, in 6 steps |
| **[documentation/README-SECTION-3-cli-commands.md](./documentation/README-SECTION-3-cli-commands.md)** | the full per-method CLI + install matrix |
| **[documentation/reference-capability-exemplars.md](./documentation/reference-capability-exemplars.md)** | which bundle is the capability-N example (generated) |
| **[documentation/reference-library-index.md](./documentation/reference-library-index.md)** | the menu of reusable `library/` pieces you can import |
| **[documentation/why.md](./documentation/why.md)** | 2 min — the problem, the idea, one diagram |
| **[documentation/tutorial.md](./documentation/tutorial.md)** | build your first map, step by step |
| **[documentation/how-to.md](./documentation/how-to.md)** | task recipes (embed, data, mobile) |
| **[documentation/architecture/](./documentation/architecture/)** | the deep end — embeddability, app-wrappers, file map |
---
> **Bottom line: maps should be open source, run everywhere, and multiply.** This bundler makes each map
> a self-contained file that embeds on your site, your partners' sites, WordPress, and mobile — from one
> build. The Plugin Studio is where you compose them into something bigger.