template-bundle: the starter's first screen is now a real map
A new writer's first copy used to draw a coloured box. template-map-a.vue now draws the points in data/places.geojson on a basemap with one translated title, through the library's own pieces (useMapInstance, resolveMapProvider, labelFont, emissivePaint). It follows the page's engine, locale (right-to-left included) and token, and with no token it runs on keyless MapLibre, so it works with no account. template-map-b.vue stays as the second-screen example, so the README, the tutorial and the plugin-designer skill (rename a, delete b) stay true. The README gains the map's pieces table and the dx-maps paths. Library change: library/i18n/locales/<11>/template.json (key template.title) is eagerly globbed into every bundle, so consumers need rebuilding (rule 5B).
This commit is contained in:
parent
031b9b38bf
commit
22b92ae043
16 changed files with 151 additions and 52 deletions
3
library/i18n/locales/ar/template.json
Normal file
3
library/i18n/locales/ar/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "خريطتي الأولى"
|
||||
}
|
||||
3
library/i18n/locales/de/template.json
Normal file
3
library/i18n/locales/de/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Meine erste Karte"
|
||||
}
|
||||
3
library/i18n/locales/en/template.json
Normal file
3
library/i18n/locales/en/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "My first map"
|
||||
}
|
||||
3
library/i18n/locales/es/template.json
Normal file
3
library/i18n/locales/es/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Mi primer mapa"
|
||||
}
|
||||
3
library/i18n/locales/fr/template.json
Normal file
3
library/i18n/locales/fr/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Ma première carte"
|
||||
}
|
||||
3
library/i18n/locales/hi/template.json
Normal file
3
library/i18n/locales/hi/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "मेरा पहला नक्शा"
|
||||
}
|
||||
3
library/i18n/locales/it/template.json
Normal file
3
library/i18n/locales/it/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "La mia prima mappa"
|
||||
}
|
||||
3
library/i18n/locales/pt/template.json
Normal file
3
library/i18n/locales/pt/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Meu primeiro mapa"
|
||||
}
|
||||
3
library/i18n/locales/ro/template.json
Normal file
3
library/i18n/locales/ro/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Prima mea hartă"
|
||||
}
|
||||
3
library/i18n/locales/ru/template.json
Normal file
3
library/i18n/locales/ru/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Моя первая карта"
|
||||
}
|
||||
3
library/i18n/locales/zh/template.json
Normal file
3
library/i18n/locales/zh/template.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "我的第一张地图"
|
||||
}
|
||||
|
|
@ -3,8 +3,14 @@
|
|||
**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 (two trivial profiles that render a colored box, no Mapbox, no
|
||||
data) plus a `src/` sandbox laid out for your real code.
|
||||
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)
|
||||
|
||||
|
|
@ -24,7 +30,7 @@ If you've never touched this repo, here are the only 4 words you need:
|
|||
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 (your paste-anywhere plugin)
|
||||
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
|
||||
|
|
@ -70,8 +76,9 @@ needs it — an expert move, not a first move. To see everything `library/` offe
|
|||
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" ← FLAT at the bundle root
|
||||
├── template-map-b.vue ← profile "template-map-b" ← FLAT at the bundle root
|
||||
├── 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/
|
||||
```
|
||||
|
|
@ -81,7 +88,21 @@ template-bundle/
|
|||
(`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/doxa-maps/template-bundle/template-bundle.js`.
|
||||
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
|
||||
|
||||
|
|
@ -93,7 +114,7 @@ There's no alias for your sandbox — your code is always relative. That boundar
|
|||
## Embed it
|
||||
|
||||
```html
|
||||
<script src="/doxa-maps/template-bundle/template-bundle.js"></script>
|
||||
<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>
|
||||
```
|
||||
|
|
|
|||
11
plugin-designs/template-bundle/data/places.geojson
vendored
Normal file
11
plugin-designs/template-bundle/data/places.geojson
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{ "type": "Feature", "properties": { "name": "Nairobi" }, "geometry": { "type": "Point", "coordinates": [36.82, -1.29] } },
|
||||
{ "type": "Feature", "properties": { "name": "Cairo" }, "geometry": { "type": "Point", "coordinates": [31.24, 30.04] } },
|
||||
{ "type": "Feature", "properties": { "name": "Delhi" }, "geometry": { "type": "Point", "coordinates": [77.21, 28.61] } },
|
||||
{ "type": "Feature", "properties": { "name": "Jakarta" }, "geometry": { "type": "Point", "coordinates": [106.85, -6.21] } },
|
||||
{ "type": "Feature", "properties": { "name": "Lima" }, "geometry": { "type": "Point", "coordinates": [-77.04, -12.05] } },
|
||||
{ "type": "Feature", "properties": { "name": "Istanbul" }, "geometry": { "type": "Point", "coordinates": [28.98, 41.01] } }
|
||||
]
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
* profile, all compiled into the single template-bundle.js.
|
||||
*
|
||||
* Usage from a host page:
|
||||
* <script src="/doxa-maps/template-bundle/template-bundle.js"></script>
|
||||
* <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>
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,65 +1,99 @@
|
|||
<script setup>
|
||||
/**
|
||||
* template-map-a.vue — example profile "A"
|
||||
* template-map-a.vue — example profile "A": a minimal REAL map. Copy this.
|
||||
*
|
||||
* @description Minimal example profile A — a colored placeholder, no Mapbox or data.
|
||||
* @description Minimal working map: one data layer from data/, one translated string, both engines.
|
||||
* @profile template-map-a
|
||||
* @bundle template-bundle
|
||||
* @element template-bundle
|
||||
*
|
||||
* This file sits DIRECTLY in the bundle folder (plugin-designs/template-bundle/),
|
||||
* NOT in a nested profiles/ subfolder. Its filename without `.vue` —
|
||||
* `template-map-a` — is the profile name you request from the host page:
|
||||
* This file sits DIRECTLY in the bundle folder, NOT in a profiles/ subfolder. Its filename without
|
||||
* `.vue` is the profile name a host requests:
|
||||
*
|
||||
* <template-bundle profile-config='{"profile":"template-map-a"}'></template-bundle>
|
||||
*
|
||||
* A profile is a normal Vue component. It can read the host's profile-config via
|
||||
* inject('profileConfig') — the same channel the real map profiles use — without
|
||||
* any prop-drilling across the web-component boundary.
|
||||
*
|
||||
* It also shows the sandbox import boundary: this profile pulls its accent color
|
||||
* from src/colors/colors.json via a RELATIVE path (./src/...). Your own sandbox
|
||||
* code is always a relative import; the @map/... alias is only for the shared
|
||||
* library. Edit the hex values in that JSON and rebuild to recolor — no code.
|
||||
* WHAT A NEW MAP GETS BY COPYING THIS (every piece is the library's, so it behaves like the others):
|
||||
* - the ENGINE, the LOCALE (+ right-to-left) and the Mapbox TOKEN are the page's universal props
|
||||
* (documentation/reference-universal-props.md). useMapInstance reads the engine; ProfileLoader
|
||||
* gives the locale, `dir`, and the token (inject('mapboxToken')). Nothing is hard-coded here.
|
||||
* - WITH NO TOKEN it runs on keyless MapLibre, so a first copy works with no account at all.
|
||||
* - one DATA layer from this bundle's own data/ folder (a static import ships it inside the bundle,
|
||||
* so it loads on any host with no extra request);
|
||||
* - one TRANSLATED string: t('template.title'), from library/i18n/locales/<lc>/template.json;
|
||||
* - label fonts and emissive paint that are right on either engine (labelFont, emissivePaint).
|
||||
* - a pin colour from THIS bundle's src/colors/colors.json: your own code is a RELATIVE import;
|
||||
* @map/... is only for the shared library. Edit the hex and rebuild to recolour.
|
||||
* Replace data/places.geojson with your own points, rename the title key, and you have your map.
|
||||
*/
|
||||
import { inject, computed } from 'vue'
|
||||
import { inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||||
import { resolveMapProvider, emissivePaint } from '@map/composables/useMapProvider.js'
|
||||
import { labelFont } from '@map/maps/label-font.js'
|
||||
import colors from './src/colors/colors.json'
|
||||
import places from './data/places.geojson?raw'
|
||||
|
||||
const config = inject('profileConfig', null)
|
||||
const note = computed(() => config?.value?.note || '(no note passed in profile-config)')
|
||||
const accent = colors.pins.selected
|
||||
const { t } = useI18n()
|
||||
const profileConfig = inject('profileConfig', null)
|
||||
const mapboxToken = inject('mapboxToken', ref(''))
|
||||
|
||||
// The page's engine choice (profile-config provider → ?provider= → window.MAP_PROVIDER). With no
|
||||
// token, Mapbox cannot draw at all, so a keyless page gets MapLibre. (Once the library's AUTO default
|
||||
// lands, resolveMapProvider does this itself and this line can go.)
|
||||
const asked = resolveMapProvider(profileConfig)
|
||||
const provider = asked === 'mapbox' && !mapboxToken.value ? 'maplibre' : asked
|
||||
|
||||
const mapEl = ref(null)
|
||||
const error = ref('')
|
||||
const { map, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapEl,
|
||||
accessToken: mapboxToken.value,
|
||||
provider,
|
||||
// Under MapLibre, useMapInstance swaps this mapbox:// style for its free (OpenFreeMap) basemap.
|
||||
style: 'mapbox://styles/mapbox/light-v11',
|
||||
center: [40, 15],
|
||||
zoom: 1.4,
|
||||
})
|
||||
|
||||
function addPlaces(m) {
|
||||
m.addSource('places', { type: 'geojson', data: JSON.parse(places) })
|
||||
m.addLayer({
|
||||
id: 'places-dots', type: 'circle', source: 'places',
|
||||
paint: { 'circle-radius': 7, 'circle-color': colors.pins.selected, 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2, ...emissivePaint(m) },
|
||||
})
|
||||
m.addLayer({
|
||||
id: 'places-labels', type: 'symbol', source: 'places',
|
||||
layout: { 'text-field': ['get', 'name'], 'text-size': 12, 'text-offset': [0, 1.2], 'text-anchor': 'top', ...labelFont(m, { weight: 'medium' }) },
|
||||
paint: { 'text-color': '#3b463d', 'text-halo-color': '#ffffff', 'text-halo-width': 1.5 },
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try { await initializeMap() } catch (e) { error.value = String(e?.message || e); return }
|
||||
const m = map.value
|
||||
if (!m) { error.value = 'map engine not loaded'; return }
|
||||
// add the data once the style is ready (it may already be, so do not wait for an event that passed)
|
||||
if (m.isStyleLoaded()) addPlaces(m)
|
||||
else m.once('load', () => addPlaces(m))
|
||||
})
|
||||
onBeforeUnmount(() => destroy())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tpl tpl--a">
|
||||
<h2>Profile A</h2>
|
||||
<p>Served from <code>template-map-a.vue</code> in the <code>template-bundle</code> bundle.</p>
|
||||
<p class="note">note: {{ note }}</p>
|
||||
<p class="swatch">
|
||||
accent from <code>src/colors/colors.json</code>:
|
||||
<span class="dot" :style="{ background: accent }"></span> {{ accent }}
|
||||
</p>
|
||||
<div class="tpl-map">
|
||||
<div ref="mapEl" class="tpl-map__canvas"></div>
|
||||
<div class="tpl-map__title">{{ t('template.title') }}</div>
|
||||
<div v-if="error" class="tpl-map__error">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tpl {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
text-align: center;
|
||||
.tpl-map { position: relative; width: 100%; height: 100%; min-height: 360px; }
|
||||
.tpl-map__canvas { position: absolute; inset: 0; }
|
||||
.tpl-map__title {
|
||||
position: absolute; top: 12px; inset-inline-start: 12px; z-index: 2;
|
||||
background: #ffffff; color: #3b463d; border: 1px solid #d1d4cd; border-radius: 8px;
|
||||
padding: 6px 10px; font: 600 14px/1.3 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
/* MATERIAL LIGHT (2026-09-21). Was background #0b1f2a / color #7dd3fc.
|
||||
★ THIS FILE IS THE COPY-ME STARTER, so a palette violation here PROPAGATES: the skill's recipe
|
||||
is "copy template-bundle and rename it", which means every future plugin would inherit a dark
|
||||
surface the house style forbids. Fixing the template is worth more than fixing one leaf.
|
||||
The two profiles still differ visibly (a uses the accent tint, b a cooler border) — that
|
||||
distinction is the point of shipping two, and it survives on the light palette. */
|
||||
.tpl--a { background: #f3f3f1; color: #3b463d; border: 1px solid #92b195; }
|
||||
.tpl h2 { font-size: 22px; font-weight: 700; }
|
||||
.tpl code { background: rgba(59, 70, 61, 0.08); padding: 1px 6px; border-radius: 4px; }
|
||||
.tpl .note { font-size: 12px; opacity: 0.7; }
|
||||
.tpl .swatch { font-size: 12px; opacity: 0.85; display: flex; align-items: center; gap: 6px; }
|
||||
.tpl .dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
|
||||
.tpl-map__error { position: absolute; bottom: 12px; inset-inline-start: 12px; z-index: 2; color: #b3412f; background: #fff; padding: 6px 10px; border-radius: 6px; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
*
|
||||
* A SECOND profile living in the SAME bundle folder, alongside
|
||||
* template-map-a.vue (both flat, no profiles/ subfolder). Both compile into the
|
||||
* single output file app/doxa-maps/template-bundle/template-bundle.js — one
|
||||
* single output file public/dx-maps/template-bundle/template-bundle.js — one
|
||||
* bundle, two profiles.
|
||||
* The host chooses which one to show with the `profile` field:
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue