docs: fix cold-read gaps — embeddability design doc (generated pages + manifest), SPA recipe + PROFILES contract, copy-rename checklist

This commit is contained in:
doxa-maps 2026-09-11 18:37:45 +00:00
parent dbf31018a3
commit f0ceefa6db
7 changed files with 150 additions and 5 deletions

View file

@ -17,8 +17,14 @@ needs it — an expert move, not a first move. To see everything `library/` offe
## Build a new map — the steps
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.
1. **Copy this folder** → `app-profiles/<your-map>/`, then change **exactly four things**:
- the **folder name** (= bundle name = output file `<your-map>.js`),
- the **tag** in `index.js` (`customElements.define('<your-map>', …)`),
- the **tag + `profile` strings** in `index.html` (the staging page must mount your tag),
- the **`.vue` filenames** — rename `template-map-a.vue` to your screen's name and delete
`template-map-b.vue` if you only want one screen (filename = the `profile` name).
Nothing else. Check it: `bun run dev` → your map renders at
`http://localhost:5173/app-profiles/<your-map>/index.html`.
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.

View file

@ -10,7 +10,8 @@ when a simple doc says *"full contract in architecture/…"*, this is where it p
| [color-strategies.md](./color-strategies.md) | the color-strategy contract + legend wiring |
| [components.md](./components.md) | shared component catalog + shadow-DOM conventions |
| [i18n.md](./i18n.md) | the locale contract + add-a-language checklist |
| [app-wrappers.md](./app-wrappers.md) | the SPA shell / sidebar `nav.js` contract |
| [embeddability.md](./embeddability.md) | the generated per-map pages + manifest that make every map embeddable by default |
| [app-wrappers.md](./app-wrappers.md) | the SPA shell: `nav.js` + `PROFILES` contracts, `bun run shell` pipeline |
| [poster-generation.md](./poster-generation.md) | the poster/preview generator pipeline |
Simple docs: [../why.md](../why.md) · [../tutorial.md](../tutorial.md) · [../how-to.md](../how-to.md) ·

View file

@ -63,3 +63,31 @@ python3 -m http.server 4173 --bind 0.0.0.0
Then open it and narrow the window below 1024px (or use the browser device toolbar) to check the
mobile behaviour.
## The `PROFILES` registry (the wrapper's map catalog)
Beside `nav.js`, the wrapper's `index.html` declares `PROFILES` — one entry per mountable map:
```js
const PROFILES = {
'prayer-progress': {
tag: 'doxa-map', // the element to mount (a bundle may register several)
src: './doxa-maps/doxa-simple-map/doxa-simple-map.js', // the bundle IIFE, tree-relative
views: ['map'], // which sub-views (map/list/tree) this profile offers
// + the profile-config the wrapper passes (copied from the generated instance page's BASE_CONFIG)
},
}
```
`nav.js` ids route to `PROFILES` keys; the wrapper lazy-loads `src` on first visit and mounts
`tag`. Gotchas: an instance page's tag may differ from the bundle name (`doxa-simple-map`
registers `<doxa-map>` too), and `colorStrategy` values are registry keys derived from
FILENAMES (`prayer-progress.js` → `'prayerProgress'`).
## How a wrapper becomes the served SPA
`bun run shell` (`internal/install-shell.mjs`) **copies** `layouts/<wrapper>/` to the root of
`public/`, so `public/index.html` is the shell and `public/doxa-maps/…` are the bundles it
mounts — one served tree, two roles. Copy (not symlink) keeps Railway/Capacitor/file:// happy;
`--no-token` omits the local-only `token.local.js` (mandatory for release builds). Capacitor
also *requires* this root `index.html` (`cap copy` hard-fails without it).

View file

@ -0,0 +1,57 @@
# Architecture — the embeddability design (generated pages + manifest)
> The simple recipes are in [../how-to.md](../how-to.md). This is the design that makes
> **every map embeddable by default**: the build generates a self-contained page per map.
## What `bun run tree` generates
After the per-bundle vite builds, `internal/generate-tree.mjs` writes the served tree:
```
public/doxa-maps/
├── index.html ← staging root (semantic tree of every map)
├── manifest.json ← machine-readable catalog + ready-made embed snippets
└── <bundle>/
├── <bundle>.js ← the bundle IIFE (the island)
├── index.html ← per-bundle staging/showcase page
├── <profile>/index.html ← ★ standalone page per profile — link it or iframe it
└── <instance>/index.html ← ★ same, per parameterized instance (e.g. prayer/)
```
The ★ pages are the **embeddable-by-default surface**: every `.vue` profile (and every
instance in a `*.instances.json`) automatically gets a directly linkable, iframe-able URL —
no per-map work.
## Anatomy of a generated profile page
Each `<profile>/index.html` is a **bare full-page map** — no chrome, so the URL *is* the map
("Share shares THE MAP"). It loads: the Mapbox GL peer (once, same realm), the bundle IIFE via
a relative `../<bundle>.js`, and mounts the element with that profile's config. The token
resolves by the ladder (prop → `?tk=` → baked → `window.MAP_TOKEN_URL` → `tokenUrl` →
same-origin `/api/maps/token`), fetched at page load — tokens are treated as data, never eval'd.
All URLs stay **relative** unless `embedBaseUrl` is set, so the whole tree can be dropped under
any web-root or subpath and keeps working (`check:drop` gates this).
## The manifest — embed snippets as data
`manifest.json` (`doxa-maps-manifest@1`) lists every bundle → profiles, each with `element`,
`scriptUrl`, `iframeUrl`, and a ready-made `embed` object:
- `embed.iframe` — a paste-ready `<iframe src=".../<profile>/index.html" …>` snippet
- `embed.script` — the `<script src>` + `<element profile-config='…'>` pair
Hosts, staging pages, and the share button all read this one catalog — snippets are generated
once, never hand-written.
## Two embed modes, one rule about iframes
| Mode | What | When |
|---|---|---|
| **script + element** | host page loads the IIFE, drops the tag | full integration: host styles/sizes it, passes config |
| **iframe / direct link** | point at the generated `<profile>/index.html` | zero-effort embeds, CMSs that only allow iframes, shared links |
Both are supported on **built** pages. The one "never iframe" rule applies only to the **dev
staging** script (`internal/staging.js`), which mounts elements directly because a dev-server
iframe crosses realms (`instanceof` breaks). Built pages have no such problem — the generated
staging (`staging-forge.js`) itself uses an iframe stage.

View file

@ -34,6 +34,29 @@ colors, or language with **no rebuild** — just change the JSON on the page. Wi
---
## Link or iframe a map directly (zero-effort embed)
You don't have to use the script tag. **Every profile also builds its own standalone page** —
a bare, full-screen map at:
```
https://<your-cdn>/doxa-maps/<bundle>/<profile>/index.html
```
Link it, share it, or iframe it (perfect for CMSs and tools that only accept iframes, like
Start.me or Notion):
```html
<iframe src="https://<your-cdn>/doxa-maps/doxa-research-map/research-map/index.html"
width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>
```
You never write these snippets by hand — `public/doxa-maps/manifest.json` lists a ready-made
`embed.iframe` and `embed.script` snippet for every profile, and the staging pages show them
copy-paste ready. Design details: [architecture/embeddability.md](./architecture/embeddability.md).
---
## Add a new map profile
The fastest path is **copy `template-bundle` and rename it.** A bundle is one folder under
@ -175,6 +198,27 @@ snippets (leave it empty for relative URLs that work wherever the folder is drop
---
## Compose maps into a SPA (the app shell)
Several maps + one layout = a single-page app, served from the same `public/` tree.
1. **Pick a wrapper** in `layouts/` (`app-wrapper-material` is the full shell: sidebar,
lazy-loading, i18n).
2. **Register your map** in two places inside the wrapper:
- `nav.js` — add a sidebar entry (`{ kind, id, label, icon, target }`; `target` controls
desktop/mobile packaging).
- the `PROFILES` object in its `index.html` — tell the shell what to mount:
`{ tag: '<your-element>', src: './doxa-maps/<bundle>/<bundle>.js', views: ['map'] }`.
3. **Install the shell**: `bun run shell` — copies the wrapper to `public/index.html`, so the
site root IS the SPA and the bundles sit beneath it. (Also required for the mobile app.)
4. **Build & serve**: `bun run build`, then serve `public/` — the shell lazy-loads each bundle
the first time you open its nav entry.
Contracts and gotchas (PROFILES shape, tag-vs-bundle names, `--no-token`):
[architecture/app-wrappers.md](./architecture/app-wrappers.md).
---
## Build the Android / iOS app (optional)
**Mobile is separate from `bun run build`.** The default build produces **only** the `doxa-maps`

View file

@ -127,6 +127,12 @@ CSS is injected by JS (`cssCodeSplit: false`), so there is no separate `.css`. T
name comes from `vite.doxa-maps-build-config.json` `name` (default `doxa-maps`).
`emptyOutDir: false` keeps sibling bundles intact when you rebuild one.
**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).
**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.
@ -212,7 +218,8 @@ Simple lookups live here; the full contracts, catalogs, and wiring detail live i
| [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 |
| [app-wrappers.md](./architecture/app-wrappers.md) | the SPA shell / sidebar `nav.js` contract |
| [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 |
The **Semantic Tree Legend** component (a reusable hierarchical legend) is documented in

View file

@ -119,7 +119,9 @@ change. Conversation in, map out.
```bash
cp -r app-profiles/template-bundle app-profiles/harvest-map # 1. copy the starter
# 2. rename and edit the files inside to be your map (or let the agent do it while you describe)
# 2. change exactly four things: the folder name, the tag in index.js, the tag/profile
# strings in index.html, and the .vue filenames (delete the second one if you want one
# screen). Full checklist: app-profiles/template-bundle/README.md
# 3. it shows up in staging automatically — no list to touch
bun run build # 4. when ready, publish
```