Reference: the universal-props contract (engine, locale + dir, Mapbox token, token-service URL)

One page that says, for each prop the Plugin Studio header sets for every plugin, which channels
can carry it (header, profile-config, URL, window, attribute), which one wins, and the default;
how the studio hands it to a pane; and the rule a plugin follows to honour it. Ends with the open
points found while writing it down (tokenUrl has no reader, the token-clear bridge, raw maps on
window.DOXA_TOKEN, ladders that disagree), each routed as a defect. Scope is the four props the
header sets today; anything wider is DECIDE-UNIVERSAL-PROPS-SCOPE.
This commit is contained in:
Doxa Maps 2026-09-25 23:53:19 +00:00
parent 5f1db2350f
commit a2f0d90424

View file

@ -0,0 +1,154 @@
# Universal props: the contract every plugin honours
The Plugin Studio header sets a few values that apply to **every** plugin on the page. A plugin
honours them when it reads them through the library's resolvers, never through its own literals.
This page lists each one: where it can come from (its **channels**), which channel wins
(**precedence**), and what happens when nothing sets it (**default**).
The set here is the one the header sets today: **engine, locale (with direction), Mapbox token and
token-service URL**. Whether anything else counts as universal is an open decision
(DECIDE-UNIVERSAL-PROPS-SCOPE), not part of this contract.
`profile-config` is the element's JSON attribute (`<doxa-map profile-config='{"tk":"pk.…"}'>`).
File references are to `library/` unless they name another folder.
## How the studio hands them to a plugin
`paneConfig()` in `plugin-designs/plugin-studio/plugin-studio.vue` builds each pane's
`profile-config`, with these keys: `profile`, `instanceId`, `locale` (always), `tk` (when a token is
known), `tokenUrl` (when the host passed one), `provider` (when one is chosen) and `treeBase`. It also
publishes `window.MAP_TOKEN` and `window.MAP_PROVIDER` for maps nested inside composites.
A token or engine change re-mounts the panes. A locale change re-sets `profile-config` on the
**active** pane only.
The header keeps the reader's choices in this browser:
| Prop | localStorage key |
|---|---|
| engine | `plugin-studio:map-provider` |
| locale | `doxa-maps:locale` |
| token | `plugin-studio:mapbox-token` |
| token-service URL | `plugin-studio:mapbox-token-url` |
---
## 1. Engine: `'mapbox'` | `'maplibre'` (the header also offers "each map's own")
**The resolver:** `resolveMapProvider(profileConfig)` in `composables/useMapProvider.js`.
`useMapInstance` calls it when no `provider` argument is passed. Values are lowercased and
trimmed; anything unrecognised becomes the default.
| Rank | Channel | Name |
|---|---|---|
| 1 | profile-config | `provider` |
| 2 | URL | `?provider=` |
| 3 | window | `window.MAP_PROVIDER` |
| 4 | default | `'mapbox'` (`DEFAULT_PROVIDER`) |
- **The studio's own ladder:** the reader's choice (localStorage), then the studio's `provider` prop,
then its `profile-config.provider`. Only the exact strings `mapbox` and `maplibre` survive.
"Each map's own" sends no `provider`, so a pane falls to its own default: Mapbox for every map
except the ones below.
- **Plugins that do not follow the ladder, by design:**
- Prayer Walk Planner is keyless and always MapLibre.
- key-cities is Mapbox-only (gated, and it uses `mapbox://` styles).
- Research Dashboard's map panel (`DashboardMapPanel.vue`) accepts the page switch only when it
resolves to `maplibre`; otherwise its bundle config pins `mapbox`.
- **Rule for plugins:** create maps through `useMapInstance`. A raw `new gl.Map` must take its
engine from `resolveMapProvider`, its style from `styles/registry.js` (`resolveNamedStyle`), and
every Marker, Popup or control from `engineFor(map)`. Never use a `mapbox://` URL or a bare
`mapboxgl` global.
## 2. Locale (UI language) and its direction
**The resolvers:**
- `detectLocale` in `i18n/index.js`, used by `createAppI18n`.
- `ProfileLoader.vue`, which overrides it from profile-config, drives the app's i18n and
`setRuntimeLocale`, and publishes `window.__DOXA_PAGE_LOCALE`.
| Rank | Channel | Name |
|---|---|---|
| 1 | profile-config | `locale`, or `lang` (base code, must be in `SUPPORTED_LOCALES`) |
| 2 | URL | `?lang=`, or `?locale=` |
| 3 | host page | `<html lang>` |
| 4 | default | `'en'` |
- **Supported codes:** `SUPPORTED_LOCALES` in `i18n/index.js` (11 codes).
- **Direction:**
- `rtl` exactly when the locale is in `RTL_LOCALES` (today only `ar`).
- ProfileLoader sets `dir` on the plugin's own root, never on the host page.
- **Composites:** read `lang || locale` and forward `lang` to their children.
- **The studio's ladder** is richer than the library's: profile-config `locale|lang` → `?lang|locale`
→ saved `doxa-maps:locale` → `navigator.language` → `'en'`. It passes the result to each pane as
`profile-config.locale`.
- **Rule for plugins:**
- Take every user-facing string from `t()` or `tStatic()`.
- Format numbers with the active locale, never `toLocaleString('en-US')` or a bare `toLocaleString()`.
- Take direction from the resolved locale.
## 3. Mapbox token (a public `pk.` token)
**The resolver:** `ProfileLoader.vue`, which provides it to children as `mapboxToken`.
| Rank | Channel | Name |
|---|---|---|
| 1 | profile-config | `tk` |
| 2 | page-wide shared token | `window.__doxaMapToken`, fed by `window.MAP_TOKEN` through `useSharedMapToken.js` |
| 3 | default | none (`''`) |
- **Where it's used:**
- `useMapInstance` uses the token it is given, else `window.MAP_TOKEN`.
- Mapbox maps require a `pk.` token; MapLibre needs none.
- **Generated leaf pages** (`internal/generate-tree.mjs`) resolve the token before the element
mounts: `profile-config.tk` → `?tk=` → localStorage `MAPS_STAGING_TK` → the token baked at build
time → the token service (section 4).
- **The studio's ladder:** the reader's pasted token → a token fetched from the reader's URL → the
studio's `tk` prop → its `profile-config.tk` → the page's baked `window.MAP_TOKEN`. Only `pk.`
tokens are accepted. A secret `sk.` token is refused.
- **Rule for plugins:**
- Read the token only from the injected `mapboxToken`, or pass nothing to `useMapInstance`.
- Never use a private global or a literal.
## 4. Token-service URL
A URL that returns a public token as JSON (`{token|tk|accessToken}`) or as a bare string.
- **Generated leaf pages:** `window.MAP_TOKEN_URL` → the build config's `tokenUrl`, or
`sources.json` `mapboxTokenUrl` → `/api/maps/token`. This runs only when no direct token was
found; a token baked at build time wins.
- **The studio:** the reader's URL (localStorage). The studio fetches it itself, and the fetched
token then travels as `tk` / `window.MAP_TOKEN`.
- **Plugins:** none reads `profile-config.tokenUrl` today. The studio and the Research Gallery
forward it, but no element consumes it (see the open points below).
---
## Open points found while writing this down
Each is a defect or a decision, routed through dx-shepherd.
1. **`profile-config.tokenUrl` has no reader.** It is written by the studio and the gallery and
read by nothing. The reader's own URL is also never forwarded as `tokenUrl`, only as the token
it produced.
2. **A token fetched from a URL can arrive after the first pane has mounted.** The studio publishes
it without re-mounting, so the pane keeps no token until something re-mounts it.
3. **Clearing the token in the studio leaves the old one in use.** `delete window.MAP_TOKEN` removes
the accessor that `useSharedMapToken` installed and never reinstalls it. An empty value is also
never published, so `window.__doxaMapToken` keeps the old token.
4. **Three raw maps read `window.DOXA_TOKEN`** instead of the injected `mapboxToken`:
cities-internet `connectivity-map.vue`, gospel-resources `resources-map.vue` and key-cities
`cities-map.vue`. Only some dev pages set `DOXA_TOKEN`, so on a built page or in the studio
these maps ignore the page-wide token.
5. **`createAppI18n` still sets `<html dir="rtl">`** on the host page for an Arabic URL or page. It
is set-only by design, but it contradicts ProfileLoader's own-root rule.
6. **The locale ladders disagree:**
- The studio uses `navigator.language` and a saved locale; the library and the gallery do not.
- The locale lists differ: 11 codes in `SUPPORTED_LOCALES` against 8 in `languages.js`.
- The studio normalises `it`, `ro` and `zh` to `en`.
7. **A locale switch in the studio updates only the active pane.** Panes visited earlier keep the
old language.
8. **Engine edges in the studio:**
- A host `provider="MapLibre"` (mixed case) becomes "each map's own".
- A host `provider` is not published to `window.MAP_PROVIDER` unless the reader chose one.
- Choosing "each map's own" deletes a `window.MAP_PROVIDER` the host page set.