# 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 (``). 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 | `` | | 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 ``** 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.