doxa-maps/plugin-designs/template-bundle
Doxa Maps 5ab4af2e44 Five plugins were silently monolingual, because the bootstrap was copied sixteen times
Goal (c), and it turned into a goal (d) fix. Every profile element is
defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four
steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped.

ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from
profile-config. Without the provide the watcher holds null and the drive NEVER RUNS --
while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the
host. The element renders a convincing MIXTURE of the reader's language and English, and
nothing throws, because the t() shim falls back to its key. It reads as "a few strings
not translated yet".

SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and
my-upg-100-list. regions-plugin censused all 19 entry files independently and found the
same five and no sixth. The measurement is regions-dashboard's, from when they hit it:
a Spanish catalogue present, es set, and every t() string still English.

All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed
by construction rather than by five separate edits. Behaviour is otherwise identical --
same options, same element names, same per-plugin extras. wall-of-people-groups needed
the helper to hand its pinia back, because it takes a store off its own instance and a
helper that hid pinia would have given it a second one: two stores and a silent state
split.

★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function
BODIES; eleven sites are not `function buildXElement()` at all, they are
`const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in
the census as a caveat and it was the headline within the hour -- a 4x undercount of the
item it recommended. So this gate greps for the SHAPE, not for a known list: a check
written around the four that matched would have passed the twelve that did not.

check-element-bootstrap has two arms. No entry may build a profile element by hand, and
the helper itself must provide appI18n -- PLANTED as a negative case, so the regression
cannot return by editing library instead of a plugin. It also fails if it finds fewer
than ten entries, because a detection that stops matching reports the same green as a
tree with no problems.

THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate
matched the helper's DOCSTRING, so it would have passed a helper that only mentions the
provide in prose -- comments are stripped now, because a check that reads comments is
reading the author's intentions rather than the program. And the plant itself used /m
instead of /gm, so it deleted the documentation and left the code, then reported that it
could not detect an absence that was not there.
2026-09-28 17:40:34 +00:00
..
data template-bundle: the starter's first screen is now a real map 2026-09-26 04:38:12 +00:00
src Move internal files out of the repo; fix five dead links; a fuller delegation diagram 2026-09-25 05:14:10 +00:00
index.html Rename app-profiles -> plugin-designs; build green under new naming (load-bearing paths updated, guards pass) 2026-09-17 18:18:18 +00:00
index.js Five plugins were silently monolingual, because the bootstrap was copied sixteen times 2026-09-28 17:40:34 +00:00
README.md template-bundle: the starter's first screen is now a real map 2026-09-26 04:38:12 +00:00
template-map-a.vue template-bundle: the starter's first screen is now a real map 2026-09-26 04:38:12 +00:00
template-map-b.vue template-bundle: the starter's first screen is now a real map 2026-09-26 04:38:12 +00:00

template-bundle — copy this to start a new plugin

This folder is the STARTER. Copy it to make a new plugin. That's the whole idea.

Your starting point. Copy this folder, then build your map inside the copy — don't start from blank. It's a tiny working bundle plus a src/ sandbox laid out for your real code:

  • template-map-a.vue is a real map. It draws the points in data/places.geojson on a basemap, shows one translated title, and follows the page's engine, language (including right-to-left) and Mapbox token. It needs no account: with no token it runs on the free MapLibre engine. Swap in your own points and it is your map.
  • template-map-b.vue is a second, trivial screen (a coloured box). It only shows that one bundle can hold several screens. Delete it if you want one.

In plain English (read this first)

If you've never touched this repo, here are the only 4 words you need:

Word Plain-English meaning
plugin / bundle One folder = one plugin. It compiles into a single .js file you can paste onto any web page. "Bundle" and "plugin" mean the same thing here — the folder you copy.
profile One .vue file inside the folder = one screen (one map). A bundle can hold one profile or many. You pick which screen shows at embed time with a profile name.
root.vue Each profile IS a normal Vue component — it's the "root" (the top of the tree) for that one screen. The filename (minus .vue) is the profile's name. Everything that screen draws hangs off this one file.
src/ Your private sandbox — your own colors, components, and helpers. Nothing in here can affect another plugin. Shared building blocks live in library/ (imported as @map/…) and you never edit those.

Make a new plugin in 2 moves:

# 1. copy this starter to a new name
cp -r plugin-designs/template-bundle plugin-designs/my-plugin

# 2. build (the ONE command)
bun run build      # → public/dx-maps/my-plugin/my-plugin.js  (your paste-anywhere plugin)

Then rename the tag + profile strings (the "exactly four things" list below) so your copy has its own name, and rebuild. That's it — the build auto-detects your new folder, compiles it to one .js custom element, and lists it in the gallery.

Proven: a copy of this folder (cp -r + rename the 4 strings) runs bun run build and emits a working <my-plugin> custom element .js bundle that passes every build guard. If it builds for the starter, it builds for your copy.

Two zones — know which one a file is in

Zone What Edit it?
library/ (imported as @map/…) shared, reusable building blocks — map, legends, toolbars, composables, color + API registries No — reuse only. A change here can break every other map.
plugin-designs/<your-map>/src/ your private sandbox — screens, colors, API wiring, components, utils Yes — build anything. Nothing here leaks to another map.

Rule of thumb: local by default. Code only moves into library/ once a second map genuinely needs it — an expert move, not a first move. To see everything library/ offers, read ../../documentation/reference.md (that's the menu you pick from).

Build a new map — the steps

  1. Copy this folder → plugin-designs/<your-map>/, then change exactly four things:
    • the folder name (= bundle name = output file <your-map>.js),
    • the tag in index.js (customElements.define('<your-map>', …)),
    • the tag + profile strings in index.html (the staging page must mount your tag),
    • the .vue filenames — rename template-map-a.vue to your screen's name and delete template-map-b.vue if you only want one screen (filename = the profile name). Nothing else. Check it: bun run dev → your map renders at http://localhost:5173/plugin-designs/<your-map>/index.html.
  2. List what your map needs — data, colors, controls (legend, search, share, fullscreen…).
  3. Pick pieces from ../../documentation/reference.md and import them by reference (@map/…) — never copy library code into your folder.
  4. Put your own code in src/ — screens, components, hooks, helpers.
  5. Set colors in src/colors/colors.json (plain hex, no code). For color that varies by data, use a strategy .js in src/colors/ (copy example-mode.js).
  6. Build — bun run build (or just this one: BUNDLE=<your-map> bun run build:bundle).

What's in this folder

template-bundle/
├── index.js              ← bundle entry: registers <template-bundle>, globs ./*.vue
├── index.html            ← dev staging page (the build needs it to find the bundle)
├── template-map-a.vue    ← profile "template-map-a": THE MAP  ← FLAT at the bundle root
├── template-map-b.vue    ← profile "template-map-b": a box  ← FLAT at the bundle root
├── data/places.geojson   ← the map's points (imported by template-map-a.vue, ships inside the .js)
└── src/                  ← YOUR SANDBOX (each subfolder has its own README)
    ├── api/  colors/  components/  composables/  utils/

The flat rule: profile .vue files sit directly in the bundle folder — there is no profiles/ subfolder, because the entry discovers them with a flat glob (import.meta.glob('./*.vue')). A .vue inside a profiles/ subfolder would NOT be found. Keep your profile screens flat; the small pieces they're built from live in src/.

Everything builds into one file: public/dx-maps/template-bundle/template-bundle.js.

The map's pieces (template-map-a.vue)

Piece Where it comes from To change it
the points data/places.geojson (a static import, so no extra request on the host) replace the file with your own GeoJSON points
the pin colour src/colors/colors.json → pins.selected edit the hex
the engine (Mapbox or MapLibre) the page: profile-config provider, ?provider=, or window.MAP_PROVIDER; no token → MapLibre nothing: leave it to the page
the language and right-to-left the page's locale, given by the library's ProfileLoader nothing: leave it to the page
the title t('template.title') from library/i18n/locales/<lc>/template.json see below

Your own text. Translations live in library/ (every locale in one place), so a new key there is a library change: ask the build owner to land it. Until then, a plain English string in your .vue is fine.

The two ways you import

  • Reuse from the library → the @map/… alias: import { useMapInstance } from '@map/composables/useMapInstance.js'
  • Your own sandbox code → a relative path: import colors from './src/colors/colors.json'

There's no alias for your sandbox — your code is always relative. That boundary keeps it private.

Embed it

<script src="/dx-maps/template-bundle/template-bundle.js"></script>
<template-bundle profile-config='{"profile":"template-map-a"}'></template-bundle>
<template-bundle profile-config='{"profile":"template-map-b"}'></template-bundle>

(profile must match a .vue filename in this folder, without .vue. Add a third profile by dropping another flat .vue next to these and rebuilding — no config to edit.)

See also

  • src/README.md — sandbox layout · ../README.md — the plugin-designs authoring guide
  • ../../documentation/tutorial.md — 3 zones + 3 rules in one page · ../../documentation/reference.md — the @map/… menu
  • ../doxa-simple-map/index.js — a fuller, real-map reference entry