diff --git a/skills/make-a-doxa-map/SKILL.md b/skills/make-a-doxa-map/SKILL.md deleted file mode 100644 index 36c125242..000000000 --- a/skills/make-a-doxa-map/SKILL.md +++ /dev/null @@ -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 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** (` - -``` - -`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 diff --git a/skills/plugin-designer/SKILL.md b/skills/plugin-designer/SKILL.md new file mode 100644 index 000000000..4945fca54 --- /dev/null +++ b/skills/plugin-designer/SKILL.md @@ -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// 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 + ├── .vue one .vue = one PROFILE (one screen), FLAT in the folder + ├── .meta.json optional: {"type": "", "label": "…"} + ├── .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//.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/ +``` + +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` → `.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=` → `` | 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 + +``` + +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//*.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 `.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 `.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_` 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//`. +- 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= bun x vite build # just your bundle → public/dx-maps// +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= 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