skills/plugin-designer replaces make-a-doxa-map
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:
parent
cfb47334a4
commit
f66650462b
2 changed files with 249 additions and 274 deletions
|
|
@ -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
|
||||
249
skills/plugin-designer/SKILL.md
Normal file
249
skills/plugin-designer/SKILL.md
Normal 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
|
||||
Loading…
Reference in a new issue