skills/plugin-designer replaces make-a-doxa-map
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

The old skill predated MapLibre, the universal props, the dx-maps tree and the three outputs.
plugin-designer teaches an agent to design a plugin in plugin-designs/ against the Plugin Studio:
bundle = folder = one JS file; the starter (now a real minimal map) and its exact renames; reuse
library/ first, private src/, library changes to the build owner; the universal props (engine
auto/mapbox/maplibre, locale, token, token service) with their channels in order; seven engine
rules, each from a real bug, including parsing expressions headless with
@maplibre/maplibre-gl-style-spec (a transitive dependency, so the skill says so); localization
with right to left on the element only; the five capability levels; learning from the seven
production plugins (each rule mapped to the one that shows it best); dev in the studio, the one
build, the three outputs. Reviewed by studio and exp-shepherd; every path it names exists on
main; references nothing outside this repo.
This commit is contained in:
Doxa Maps 2026-09-26 06:43:05 +00:00
parent cfb47334a4
commit f66650462b
2 changed files with 249 additions and 274 deletions

View file

@ -1,274 +0,0 @@
---
name: make-a-doxa-map
description: >-
Use this skill when a user (typically a non-coder "map maker" / prayer
mobilizer) asks you to build a new map/plugin with the doxa-maps bundler, add
another screen to a map, embed one on a site, or ship one as a mobile app.
Teaches the 5 plugin capabilities, the 5 deployment methods, the
plugin-designs/ root-Vue pattern, the graduation path, and the exact
copy-the-starter -> `bun run build` flow. Grounded in the REAL package.json
scripts (dev/build/preview/phone) and the REAL structure (plugin-designs/,
library/, internal/).
---
# Make a doxa-map (use the plugin bundler)
You are the AI agent for a **map maker** — usually someone who does NOT write
code. Your job is to build maps for them by driving this bundler. This skill is
your operating manual: what the bundler is, how to make a new plugin by copying
the starter, and how to ship it.
> **The one thing to remember:** a new plugin is **one folder** under
> `plugin-designs/`, and **`bun run build`** turns every folder into a
> paste-anywhere `.js` custom element. Everything else is detail.
Run every command from the bundler repo root
(`.../doxa-maps-plugin-studio/`, the folder that has `package.json` and
`plugin-designs/`).
---
## 0. Mental model (read this first)
The bundler is a **JAMstack plugin bundler**: one build step turns each map into
a self-contained web component ("plugin") that plugs in anywhere. Four words:
| Word | Plain meaning |
|---|---|
| **bundle / plugin** | One folder under `plugin-designs/` = one plugin. It compiles into a **single `.js` file** you can paste on any page. "Bundle" and "plugin" are the same thing. |
| **profile** | One `.vue` file inside the folder = one **screen** (one map). A bundle can hold one profile or many. The `.vue` filename (minus `.vue`) IS the profile's name. |
| **root Vue / root.vue** | Each profile is a normal Vue component — the "root" of that one screen. This is the **root-Vue pattern**: a folder with a root Vue file = a plugin. |
| **`src/`** | The plugin's **private sandbox** (its own colors, components, helpers). Nothing here can affect another plugin. |
Three zones — always know which one a file is in:
| Zone | What | Edit it? |
|---|---|---|
| `library/` (imported as `@map/…`) | SHARED, reusable, parameterized building blocks — the map, legend, controls, search bar, composables, color/API registries | **No — reuse only.** A change here can break every map. |
| `plugin-designs/<your-map>/` | YOUR plugin: root `.vue` profiles (flat) + a `src/` sandbox | **Yes — build anything.** Nothing here leaks. |
| `internal/` | the scanner/build machinery (clean, tree, gallery, guards) | **Never touch.** |
Rule of thumb: **local by default.** Code only moves into `library/` after a
*second* plugin genuinely needs it (see the Graduation Path below).
---
## 1. The 5 PLUGIN CAPABILITIES (how much the author nests)
Capability = how much composition the plugin does. It is NOT "complexity" and it
is NOT a quality ranking — it is a ladder of nesting. The studio gallery sorts
**5 → 1** (most-composed first). Pick the LOWEST tier that does the job.
| # | Capability | What it is | Live example in this repo |
|---|---|---|---|
| 1 | **Single-element plugin** | one folder → one JS bundle → one custom element. Simplest, lightest. | `plugin-designs/doxa-vision-card/` (a pure single element, no map, renders instantly) |
| 2 | **Prop-table plugin** | one root Vue defines a **prop table** + **element table** → ONE plugin renders many parameterized variants | `plugin-designs/doxa-simple-map/` (Prayer / Engagement / Adoption from one plugin, via `simple-map.instances.json`) |
| 3 | **Monolithic plugin** | one root Vue nests many maps/layouts *internally* → one element. Downside: you can't embed just one inner tab. | `plugin-designs/doxa-research-map/` (research map: many maps + layout in one element) |
| 4 | **Nested-element SPA** | each inner map is its OWN plugin-design; a **layout** nests them → one element, but each inner map is *also* embeddable on its own AND the whole SPA embeds | `plugin-designs/my-upg-100-list/`, `plugin-designs/research-dashboard/`, `plugin-designs/cities-internet/` |
| 5 | **Nested-SPA / multi-SPA** | nest multiple SPAs into one SPA → a whole modular platform | the doxa-maps studio / mobile app: `internal/generate-gallery.mjs` emits `public/index.html` as a Vue nested-SPA (vertical tabs, each tab a plugin of a different tier) |
**How to choose:** one screen, no data variants → cap 1. One screen, several
preset variants → cap 2 (add an `*.instances.json`). Several screens fused into
one you never split → cap 3. Several screens that must *also* be individually
embeddable → cap 4. A whole platform of SPAs → cap 5 (the studio already IS this;
you rarely author a new cap-5 — you add cap 1–4 plugins and the studio composes
them for you).
---
## 2. The 5 DEPLOYMENT METHODS (where the SAME plugin runs)
Author once; the SAME built `.js` (or its standalone page) runs all of these.
Methods 1–3 are **proven**; 4–5 are **deferred POCs** (no build script on this
branch yet — do not promise a one-command WordPress/desktop build).
| # | Method | How | Status | Command |
|---|---|---|---|---|
| 1 | **iframe** on any/partner site | paste the `embed.iframe` snippet from `manifest.json` | ✅ proven | `bun run build` → host `public/doxa-maps/` |
| 2 | **Mobile app** (Capacitor, Android) | build wraps the studio SPA into an APK; no new code | ✅ proven | `bun run phone` |
| 3 | **Direct-JS** (`<script src>` + custom-element tag, SRI-hashed, props/events, Shadow DOM) | paste the `embed.script` snippet from `manifest.json` | ✅ proven | `bun run build`, then paste snippet |
| 4 | **WordPress plugin** | same iframe or `<script>` snippet in a WP **Custom HTML** block | ⚠️ deferred POC (no WP build script) | `bun run build`, paste snippet |
| 5 | **Rust desktop (Tauri)** | front-end → desktop shell | ⚠️ deferred POC (not scaffolded) | none exists yet |
Full command/install detail per method:
[`documentation/README-SECTION-3-cli-commands.md`](../../documentation/README-SECTION-3-cli-commands.md).
Every `bun run build` writes `public/doxa-maps/manifest.json` — the machine-
readable index of all bundles + profiles, each carrying ready-to-paste
`embed.iframe`, `embed.script` (with an SRI `integrity` hash), and raw
`iframeUrl` / `scriptUrl`. URLs are root-relative, so the whole `doxa-maps/`
folder is position-independent (drop it on Cloudflare/Vercel/`file://`).
---
## 3. Make a NEW plugin — the copy-the-starter flow (the main task)
The starter is **`plugin-designs/template-bundle/`** (it has its own README). It
is a tiny working bundle: two flat profiles that render a colored box (no Mapbox,
no data) plus a laid-out `src/` sandbox. **Copy it; never start from blank.**
### 3a. The 2-move happy path
```bash
# 1. copy the starter to your new plugin name (run from the repo root)
cp -r plugin-designs/template-bundle plugin-designs/my-plugin
# 2. build the one command
bun run build # → public/doxa-maps/my-plugin/my-plugin.js
```
The build auto-detects the new folder (it loops `plugin-designs/*/`), compiles it
to one IIFE `.js` custom element, runs the guards, and lists it in the studio.
### 3b. Rename exactly FOUR things so the copy has its own name
After `cp -r`, edit these four (nothing else) inside `plugin-designs/my-plugin/`:
1. **The folder name** — already done by `cp -r` (= bundle name = output
`my-plugin.js`).
2. **The tag in `index.js`** — `customElements.define('template-bundle', …)` →
`customElements.define('my-plugin', …)`. (There are two spots: the `.define`
call and the `!customElements.get('template-bundle')` guard.)
3. **The tag + `profile` strings in `index.html`** — the staging page mounts the
element, so replace `<template-bundle profile-config='{"profile":"…"}'>` tags
with `<my-plugin profile-config='{"profile":"my-screen"}'>`, and the
`template-bundle { … }` CSS selector.
4. **The `.vue` filenames** — rename `template-map-a.vue` to your screen's name
(e.g. `my-screen.vue`); the filename becomes the profile name. Delete
`template-map-b.vue` if you only want one screen.
Then `bun run build` again.
> **The flat rule:** profile `.vue` files sit **directly** in the plugin folder —
> there is NO `profiles/` subfolder. The entry discovers them with a flat glob
> (`import.meta.glob('./*.vue')`); a `.vue` in a subfolder would NOT be found.
> The small pieces a profile is built from live in `src/`.
### 3c. Fill in the map
1. **List what the map needs** — data, colors, controls (legend, search, share,
fullscreen…).
2. **Reuse from the library** by reference — `import { useMapInstance } from '@map/composables/useMapInstance.js'`. The menu of what `@map/…` offers is [`documentation/reference.md`](../../documentation/reference.md). **Never copy library code into your folder.**
3. **Put your own code in `src/`** — screens, components, hooks, helpers (imported by relative path, e.g. `import colors from './src/colors/colors.json'`). There is no alias for your sandbox — that boundary keeps it private.
4. **Set colors** in `src/colors/colors.json` (plain hex). For color that varies by data, copy `src/colors/example-mode.js` (a strategy `.js`).
### 3d. Add extra preset variants (capability 2)
To ship one plugin as several named embeddable variants (like Prayer / Engagement
/ Adoption from `doxa-simple-map`), add a `<profile>.instances.json` next to the
`.vue`. The build emits a standalone page per instance at
`my-plugin/<instance>/index.html`. This is the cap-2 prop-table pattern.
### 3e. Embed the result
From `public/doxa-maps/manifest.json`, or hand-write from the starter's shape:
```html
<script src="/doxa-maps/my-plugin/my-plugin.js"></script>
<my-plugin profile-config='{"profile":"my-screen"}'></my-plugin>
```
`profile` must match a `.vue` filename (without `.vue`).
---
## 4. Iterate live (the dev server)
```bash
bun run dev # Vite dev server at http://localhost:5173
```
- The plugin's staging page is
`http://localhost:5173/plugin-designs/my-plugin/index.html`.
- Edits hot-reload — this is how you show the map maker changes in near-real-time
and take feedback.
- `bun run preview` serves the last production build; `bun run serve` serves
`public/` (the built tree + studio) at `http://localhost:4173`.
---
## 5. The graduation path (why the root-Vue pattern matters)
A plugin starts **non-reusable** — its own components live in its `src/`.
**Graduation** = deliberately extracting a component into the shared `library/`
**only after** it is proven reusable AND parameterized.
- **Do NOT graduate prematurely.** Pushing non-parameterized code into `library/`
poisons every other plugin with bad examples / breakage. Local by default;
promote later — an expert move, not a first move.
- The bundler is a **refactor-cruncher**: take an open-source map app → make it a
plugin (a folder + root Vue) → make its components reusable/parameterized → now
it runs in all 5 deploy targets and every other plugin can share the pieces.
Practical rule for you as the agent: keep new work in the plugin's `src/`. Only
suggest moving something to `library/` when a *second* plugin needs the exact same
thing and it takes props (no hard-coded specifics).
---
## 6. The plugin-studio (the auto-generated gallery)
On build, `bun run gallery` (the last build step) writes `public/index.html` — a
**Category-5 Vue nested-SPA** that auto-detects every plugin and lists them sorted
**capability 5 → 1**. It's called a "studio" (not "gallery") because it's the
development location where all tiers sit in one place. You don't hand-edit it; it
regenerates from `manifest.json` every build. Your new plugin appears in it
automatically.
The same `public/index.html` is the Capacitor mobile-app launch entry
(`capacitor.config.json` `webDir: public`), so restoring/building the studio SPA
is also the deploy-method-2 mobile app.
---
## 7. Verify your work (what "done" looks like)
1. `bun run build` prints **ALL GUARDS PASS** (network-policy, iso2-map,
resource-join, tables-complete, bundles-shipped, map-paint) and ends with the
gallery writing `public/index.html`. If a guard fails, read its message — it
names the exact file/rule.
2. `public/doxa-maps/my-plugin/my-plugin.js` exists (the IIFE bundle) and
`public/doxa-maps/manifest.json` lists `my-plugin` with your profile(s) + embed
snippets.
3. `bun run dev` → the plugin renders at its staging URL and in the studio.
> **Concurrency note:** if a build fails mid-run with files vanishing (e.g. a
> `clean` wiped `public/doxa-maps/`), another process ran `bun run clean` on the
> same folder. Re-run `bun run build` cleanly (serialize it) — that is not a code
> bug.
---
## 8. Command cheat-sheet (all real, from `package.json`)
| Command | What it does |
|---|---|
| `bun install` | install deps into `node_modules/` (once; `npm install` also works) |
| `bun run dev` | Vite dev server at `:5173` (live plugin staging) |
| `bun run build` | clean → write token → build every `plugin-designs/*/` → tree → guards → studio. **The one command.** |
| `bun run preview` | serve the last production build |
| `bun run serve` | serve `public/` (built tree + studio) at `:4173` |
| `bun run phone` | build → Capacitor sync → assemble + install the Android APK (deploy method 2) |
| `BUNDLE=my-plugin bun run build:bundle` | build just one plugin (faster while iterating) |
---
## 9. Guardrails
- **Additive by default.** A new map = a NEW folder under `plugin-designs/`. Don't
edit other people's plugins or `internal/`.
- **Never edit `library/` to customize one map** — that's what `src/` is for.
- **Keep profile `.vue` files flat** in the plugin folder (no `profiles/` subdir).
- **Don't promise deploy 4/5 as one-command** — WordPress/Tauri are deferred POCs;
they reuse the method-1/3 artifacts.
- **Explain it in plain English** to the map maker — if you can't, they can't
follow. Simplicity is the goal.
## References (in this repo)
- `plugin-designs/template-bundle/README.md` — the starter's own README (copy-me detail)
- `plugin-designs/README.md` — the plugin-designs authoring guide (shared vs. yours, production vs. experimental)
- `documentation/README-SECTION-3-cli-commands.md` — CLI commands + installs per deploy method
- `documentation/reference.md` — the `@map/…` menu of reusable library pieces
- `documentation/tutorial.md` — 3 zones + 3 rules in one page
- `plugin-designs/doxa-simple-map/index.js` — a fuller, real-map reference entry

View file

@ -0,0 +1,249 @@
---
name: plugin-designer
description: >-
Use this skill to design and build a new map plugin in this repo's
plugin-designs/ folder, or to add a screen to an existing one. Teaches the
bundle = folder = one JS file pattern, reusing library/ before writing code,
the universal props every plugin must honour (map engine, language, Mapbox
token or token service), the rules for working under both MapLibre and
Mapbox, localization and right-to-left, the five capability levels, the one
build step, and the three outputs (web folder, mobile app, desktop app).
Works directly with the Plugin Studio (`bun run dev`).
---
# Plugin designer
You build map plugins for this repo. A **plugin** is a folder under `plugin-designs/`; one build
turns it into one JavaScript file that any page can embed as an HTML tag. Your plugin must work
the same under both map engines, in every language, and with or without a Mapbox token.
Run every command from the repo root (the folder with `package.json`).
---
## 1. The pattern: bundle = folder = one JS file
```
plugin-designs/<your-plugin>/ one FOLDER = one BUNDLE (the folder name is the bundle name)
├── index.js entry: registers the custom element, finds the profiles
├── index.html a staging page that mounts the element
├── <screen>.vue one .vue = one PROFILE (one screen), FLAT in the folder
├── <screen>.meta.json optional: {"type": "<capability>", "label": "…"}
├── <screen>.instances.json optional: named presets of one screen (capability 2)
├── data/ optional: the bundle's own data files (imported, so they ship inside the bundle)
└── src/ the bundle's PRIVATE code: components, colors, helpers
builds to ─────────────► public/dx-maps/<your-plugin>/<your-plugin>.js
```
- **Profiles are flat.** `index.js` finds them with `import.meta.glob('./*.vue')`; a `.vue` in a
subfolder is not found. The filename (without `.vue`) is the profile's name.
- **Elements live inside.** One bundle registers one tag named after the folder, and can hold more
elements for its own screens to nest (see `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json`).
- **Folders starting with `_` are skipped** by the build: a free place for work in progress.
- **New plugins appear in the Plugin Studio at once, and in the Research Gallery's Experimental
section automatically.** Moving one into a production section is a deliberate edit to the
gallery's curated sections (`plugin-designs/doxa-maps-spa/doxa-maps-spa.vue`), not your call alone.
## 2. Start by copying the starter
```bash
cp -r plugin-designs/template-bundle plugin-designs/<your-plugin>
```
Then rename exactly these, and nothing else:
1. the tag in `index.js` — both the `customElements.define('template-bundle', …)` call and the
`customElements.get('template-bundle')` guard;
2. the tag, the `profile` strings and the CSS selector in `index.html`;
3. the `.vue` filenames (`template-map-a.vue` → `<screen>.vue`); delete `template-map-b.vue` if
you need one screen.
`template-map-a.vue` is a real minimal map: it draws `data/places.geojson` on a basemap with one
translated title, and already follows the page's engine, language (with right to left) and token,
falling back to keyless MapLibre. **Replace `data/places.geojson` with your own points** and you have
a working map to build on. Its title key (`template.title`) lives in the library catalogues. Your own
translated text needs its own catalogue file in `library/i18n/locales/`, which is a library change
(section 3). Until that lands, plain English in your `.vue` is fine.
Keep the entry's `configureApp` exactly as the starter has it: it installs Pinia, installs i18n
**and** provides it as `appI18n`, and provides `profileModules`. The element's root
(`library/ProfileLoader.vue`) cannot call `useI18n()`; it drives the language through the
provided instance. Drop the provide and a host can no longer set your plugin's language.
## 3. Reuse `library/` first
`library/` (imported as `@map/…`) is shared by every plugin. Its menu is
`documentation/reference.md`.
- **Reuse by import, never by copy.** `import { useMapInstance } from '@map/composables/useMapInstance.js'`.
- **Your own code goes in your `src/`**, imported by relative path. Nothing there reaches another plugin.
- **Don't edit `library/` to customise one plugin.** A library change reaches every plugin that
imports it, so it needs every consumer rebuilt. On a team with a build owner, hand library
changes to that owner rather than landing them yourself.
- **Graduate late.** Move code into `library/` only when a second plugin needs the same thing
and it takes props instead of hard-coded specifics.
## 4. The universal props — every plugin honours these
A plugin never decides these for itself; it reads them from whoever embeds it. The Plugin Studio
header sets all of them, which is how you test yours.
| Prop | Values | Channels, first found wins | Default |
|---|---|---|---|
| **Map engine** (`provider`) | `mapbox` · `maplibre` · `auto` (any case) | `profile-config` `"provider"` → `?provider=` in the page address → `window.MAP_PROVIDER`; an explicit choice always wins | `auto`: Mapbox when a token is available (any row below), MapLibre otherwise. Confirm in `library/composables/useMapProvider.js`, never assume it |
| **Language** (`locale`) | one of the 11 in `library/i18n/locales/` | `profile-config` `"locale"` (or `"lang"`) → `?lang=` / `?locale=` → `<html lang>` | English |
| **Mapbox token** (`tk`) | a public `pk.…` token | `profile-config` `"tk"` → `?tk=` on a generated page → `window.MAP_TOKEN`; generated pages also keep a token pasted once in the tree (browser storage) | none. The embed snippet's `YOUR_MAPBOX_TOKEN` placeholder counts as no token |
| **Token service** (`tokenUrl`) | a URL that returns a `pk.` token | `profile-config` `"tokenUrl"` (the element asks it when it has no token; the studio header's field feeds this) → on generated pages also `window.MAP_TOKEN_URL` → the build config's `tokenUrl` → same-origin `/api/maps/token` | none |
```html
<your-plugin profile-config='{"profile":"<screen>","provider":"maplibre","locale":"ar"}'></your-plugin>
```
Read them through the library (`resolveMapProvider`, the injected `mapboxToken`, the i18n
instance), never by parsing the URL yourself, and never fetch a token service yourself: the
library asks it, accepts only a `pk.` answer, and a Mapbox map waits for it before it starts.
**Your plugin must draw under MapLibre with no token at all.** `library/composables/useMapProvider.js` and
`library/ProfileLoader.vue` are the authority when this table and the code disagree.
## 5. Two engines: rules that keep MapLibre and Mapbox equal
Write every map feature once, so it works under both engines. These rules each come from a
real bug:
1. **Ask the map which engine it is.** Use `engineFor(map)` / `isMapboxMap(map)` from
`@map/composables/useMapProvider.js`, never a global that may still be defined after a switch.
2. **Some APIs differ in shape.** `getProjection()` is `{name}` on Mapbox but `{type}` or
`undefined` on MapLibre; `setProjection` takes `'mercator'` on Mapbox but `{type:'mercator'}` on
MapLibre; `setConfigProperty` and light presets exist only on Mapbox. Wrap these calls in `try`,
and never assume the result's shape. An uncaught throw silently skips everything after it.
3. **Mapbox-only paint properties drop the whole layer on MapLibre.** Never write
`'circle-emissive-strength'` (or any `*-emissive-strength`) literally. Spread
`...emissivePaint(map, kind)` from `useMapProvider.js`.
4. **Fonts must exist on the engine's glyph server.** Under MapLibre, a Mapbox font name
(`DIN Offc Pro`, `Arial Unicode MS`) gets a 404, and the layer draws **no text** with no error.
Spread `...labelFont(map, {…})` from `@map/maps/label-font.js` into every symbol layer.
5. **`mapbox://` sources don't load on MapLibre.** The country polygons come from the Mapbox tileset
`mapbox.country-boundaries-v1` under Mapbox. Under MapLibre, use the local file
`boundaries/world-adm0.geojson` under the assets base. It names the country code `iso3`, where
the tileset says `iso_3166_1_alpha_3`, so normalise once. `library/composables/useMapLayers.js`
(the "region polygons, per engine" block) is the worked example.
6. **Load assets from the assets base, never a page-relative path.** Use
`resolveAssetsBase(profileConfig)` from `@map/utils/embedBase.js`. A hard-coded `./assets`
works in the built folder and breaks under `bun run dev`.
7. **Prove your expressions parse under MapLibre without a browser.** MapLibre's own parser,
`@maplibre/maplibre-gl-style-spec`, is installed as a dependency of `maplibre-gl`. It is NOT
declared in `package.json`, so a lockfile change can remove it: if the import fails, add it as a
dev dependency rather than dropping the check.
```js
import { expression, featureFilter } from '@maplibre/maplibre-gl-style-spec'
const e = expression.createExpression(['match', ['get', 'status'], 'reached', '#2e7d32', '#c62828'])
if (e.result !== 'success') throw new Error(e.value.map(x => x.message).join('; '))
const admits = featureFilter(['==', ['get', 'iso3'], 'TUN']).filter({ zoom: 2 }, { properties: { iso3: 'TUN' } })
```
Run each paint and filter expression your layers use against a few real features from the data.
## 6. Localization and right to left
- **Every word on screen comes from a catalogue** in `library/i18n/locales/<code>/*.json`, the same
key in all 11 languages. Components use `t('file.key')`; code that runs outside the Vue app uses
`tStatic(path, locale)` from `@map/i18n/index.js`.
- **Add a key to all 11 catalogues at once.** `bun run check:i18n-parity` fails when one is missing;
`bun run check:i18n-never-translated` flags a key that was copied, not translated.
- **Names from the data stay as the data has them** (people groups, places); translate only the
interface.
- **Right to left belongs to your element, not the page.** Arabic mirrors the plugin's own box; the
library sets `dir` on it. Never write `document.documentElement.dir` from a plugin: an embedded
plugin that does flips the whole partner page.
## 7. Choose the capability level
Pick the lowest level that does the job. Declare levels 3–5 in `<screen>.meta.json` `"type"`;
levels 1 and 2 are detected on their own.
| Level | Name (`type`) | What it is | Example |
|---|---|---|---|
| 1 | single element (`single`) | one screen, one element | `plugin-designs/prayer-walk-planner/` |
| 2 | preset element (`parameterized`) | one screen, several named presets from `<screen>.instances.json` | `plugin-designs/doxa-simple-map/` |
| 3 | multi-view element (`monolithic`) | many views fused inside one element | `plugin-designs/doxa-research-map/` |
| 4 | composite element (`nested-elements-mfe`) | nests other elements, each still embeddable alone, wired by props and events | `plugin-designs/dashboard-plus-grid/` (the Big Picture tab) |
| 5 | app shell (`nested-mfe-spa`) | nests whole apps | `plugin-designs/doxa-maps-spa/` (the Research Gallery) |
## 8. Learn from the reference plugins
The seven production plugins are the reference: each shows what a good plugin does. Before you
write a rule's code, read the plugin that shows it best. Find code by the name given here, not by
line number, because lines move.
| Rule | Read this | What to look at |
|---|---|---|
| Entry: i18n installed **and** provided; one bundle, two tags | Prayer Progress: `plugin-designs/doxa-simple-map/index.js` | `configureApp` and its `app.provide('appI18n', …)` |
| Presets from one screen (capability 2) | Prayer Progress: `plugin-designs/doxa-simple-map/simple-map.instances.json` | Prayer / Engagement / Adoption as three embeddable pages |
| Engine-asymmetric APIs, guarded | Regions: `plugin-designs/regions-dashboard/src/RegionCountryMap.vue` | `projectionOf` and `setProjectionOn` — both shapes, inside `try` |
| Polygons both engines can draw; data from the assets base | Cities with Internet: `plugin-designs/cities-internet/connectivity-map.vue` | `resolveAssetsBase(…)`, the `boundaries/world-adm0.geojson` fetch (polygon data with no `mapbox://` source), and `resolveNamedStyle(…, provider)` for a basemap that matches the engine |
| Interface text from the catalogues | Gospel Resources: `plugin-designs/gospel-resources/resources-map.vue` | its interface text goes through `t('…')` (about 80 calls) |
| Basemap labels in the reader's language; right to left | Prayer Progress: `plugin-designs/doxa-simple-map/doxa-simple-map.vue` | the `name_<locale>` label switch and `isRTL` |
| Interop: one element reacts to another's events | Simple Research: `plugin-designs/research-map-plus-grid/research-map-plus-grid.vue` | the map listening for the grid's `groups-filtered` event |
| Composite element (capability 4) | Big Picture: `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json` | `"composes"`: two elements, each still embeddable alone |
| One bundle, several embeddable screens | UPG-100: `plugin-designs/my-upg-100-list/` | `upg-100-mfe`, `upg-100-list` and `poster-wall` from one bundle |
| App shell (capability 5) | Research Gallery: `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue` | every production plugin as a tab, and the Experimental folder filled from the manifest |
`emissivePaint` and `labelFont` (section 5) have no production-plugin exemplar, because the
production plugins get them through the library's own layers. The starter uses both directly:
read the pin and label layers in `plugin-designs/template-bundle/template-map-a.vue`.
## 9. Develop in the Plugin Studio
```bash
bun install # once
bun run dev # the Plugin Studio at http://localhost:5173/
```
- Your plugin appears in the studio as soon as its folder exists, and edits reload live.
- One screen on its own: `http://localhost:5173/__leaf/<your-plugin>/<screen>`.
- Test the universal props from the studio header: switch the **engine**, the **language**
(include Arabic), and the **token** (none, a token, a token service). Your plugin must work in
every combination.
- `bun run check:dev-assets` checks that every map's data files load under the dev server.
## 10. Build: one step
```bash
BUNDLE=<your-plugin> bun x vite build # just your bundle → public/dx-maps/<your-plugin>/
BUILD_ALLOW=1 bun run build # every bundle, the tree, the manifest, the studio
```
The full build rewrites every plugin's output. It refuses to run without `BUILD_ALLOW=1`, and
on a team one agent owns it. Build only your own bundle unless you are that agent.
`public/dx-maps/manifest.json` then lists your plugin with ready-to-paste embed snippets.
## 11. The three outputs
| Output | What it is | How |
|---|---|---|
| **1. The web folder** | `public/dx-maps/`: every plugin, embeddable by **iframe** or **script tag**, from any host | copy the folder; snippets are in `manifest.json` |
| **2. The mobile app** | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir: public/dx-maps`) | `bun run phone` (Android) |
| **3. The desktop app** | the same folder in Tauri (`deployment/src-tauri/`), a proof of concept | `bun run tauri:build` |
WordPress (`deployment/wordpress/`) embeds the web folder's snippets; it's experimental.
## 12. Done means
- `BUNDLE=<your-plugin> bun x vite build` succeeds, and your plugin is in `manifest.json`.
- It works in the Plugin Studio under **both engines**, **with and without a token**, in
**English and Arabic**.
- Every new string is in all 11 catalogues (`bun run check:i18n-parity`).
- Your layer expressions parse under MapLibre (section 5, rule 7).
- You changed nothing in `library/` or in another plugin's folder, or you handed that change to
the build owner.
## References (all in this repo)
- `plugin-designs/README.md`: authoring guide, production vs experimental
- `plugin-designs/template-bundle/`: the starter, and its README
- `documentation/reference.md`: the `@map/…` library menu and the profile types
- `documentation/how-to.md`: recipes, including swapping the map engine
- `library/composables/useMapProvider.js`: engine resolution and engine helpers
- `library/maps/label-font.js`: fonts per engine
- `library/i18n/index.js`: languages, `tStatic`, right to left