A static, browser-only map (OpenStreetMap + Leaflet) with a Gregorian calendar as an equal first-class dimension. See docs/superpowers/specs/2026-07-26-universal-map-time-engine-design.md for the full design, docs/json-reference.md for a field-by-field reference of world.json, layer.json, strings.json, and the GeoJSON+temporal data format (docs/schemas/*.schema.json has the same shapes as JSON Schema, for editor autocomplete — add "$schema": "../../docs/schemas/world.schema.json" (adjust the relative path) to your own manifest files to pick it up), docs/api-reference.md for the internal function/module API, CHANGELOG.md for what's shipped beyond v1, and ROADMAP.md for what's next.
pnpm install
pnpm dev
Open the printed local URL. No backend, no paid services — pnpm build produces a static dist/ you can host anywhere (or serve locally with pnpm preview or any static file server).
pnpm test
- Create a new folder under
worlds/<your-world-id>/. - Add a
world.json(seeworlds/demo/world.jsonfor the shape). - Add one
*.layer.jsonper data layer underworlds/<your-world-id>/layers/, and the matching GeoJSON underworlds/<your-world-id>/data/. - Optionally add
strings.jsonfor your own UI text, and apluginsblock to activateparticipate. - Load it with
?world=<your-world-id>in the URL (e.g.http://localhost:5173/?world=my-world), or leave the query param off to getworlds/demo/by default. A world switcher UI is still intentionally out of scope for v1 (see the design spec's non-goals) — this is just a static id lookup, resolved once at page load. Per-domain deployment of a single world is in scope (see below), just not a UI for switching between worlds at runtime.
No engine code under src/engine/ needs to change to add a new world instance.
Every world can be opened on its own during development, and built + deployed as its own standalone site on its own domain — no code changes needed either way, just the world's own world.json/layers//data/.
Open one locally, with the rest of the app unaffected — pnpm dev, then append ?world=<id> to the printed URL:
| World | URL (dev) |
|---|---|
demo |
http://localhost:5173/ (default, no param needed) |
paranormal-spain |
http://localhost:5173/?world=paranormal-spain |
events-canary-islands |
http://localhost:5173/?world=events-canary-islands |
moon-map-photos |
http://localhost:5173/?world=moon-map-photos |
(Port may differ — use whatever pnpm dev actually prints.) A world switcher UI is intentionally out of scope for v1 — this is a static id lookup resolved once at page load, not a runtime menu.
Build one for its own domain — vite build --mode <world-id> --outDir builds/<world-id> bundles only that world's data (not every world under worlds/) and makes it load by default with no ?world= query param needed, so a visitor at that domain never sees worlds/demo/ or any other world's content at all. Each of the 3 real worlds already has a matching package.json script:
pnpm run build:paranormal-spain # -> builds/paranormal-spain/
pnpm run build:events-canary-islands # -> builds/events-canary-islands/
pnpm run build:moon-map-photos # -> builds/moon-map-photos/
Adding a new world that should also deploy standalone means adding one matching line to package.json's "scripts":
"build:<your-world-id>": "tsc --noEmit && vite build --mode <your-world-id> --outDir builds/<your-world-id>"
Preview a build before deploying it — the output is plain static files, so any static file server works, e.g.:
npx serve builds/paranormal-spain
Deploy — upload the contents of builds/<world-id>/ to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, an S3 bucket, etc.) and point your domain at it. base: './' in vite.config.ts means the build works whether it's served from a domain root or a subpath, unmodified. world.json can also set an optional favicon field (a path relative to that world's folder) to give that domain its own favicon, separate from the default one in index.html.
There's no dedicated kind for isochrones — precompute the polygons with whatever routing tool fits your data (routing engines/APIs, GIS tools, etc.) and ship them as a normal kind: "polygon" layer, exactly like worlds/demo/layers/regions.layer.json. Each isochrone gets properties.temporal if it should only apply on certain dates, and any taxonomy field (e.g. travel time bucket) like any other layer. This needs no engine code — the same reasoning that makes administrative regions "just temporal geometry" applies here. A layer that computes isochrones live (e.g. on click) would need an external routing service, which is out of scope for this static, no-backend engine.
- Two top-level views — Map and a full-screen Calendar view (day/week/month/year, a 12-month year-at-a-glance layout, and a per-day event agenda).
- Multi-calendar display support for Gregorian, Julian, Islamic, and Hebrew calendars via
calendar.systeminworld.json. - Multi-projection map display support for
EPSG:3857,EPSG:4326, and custom CRS definitions viamap.crs. - Calendar bar refinements with day/week/month/year granularity and a range slider, plus richer selection-card info rendering for links, images, and coordinates.
- Isolated per-domain world builds (
vite build --mode <world-id>, see above) ship only that world's data, plus per-worldfavicon. systems.time: falsehides all time/calendar UI for a world with no temporal data.- The calendar no longer caps navigation at "today" —
calendar.min/maxgovern the real bounds, so future-dated content (e.g. upcoming events) is reachable, not just past/current dates. - A thematic
welcomesplash (declarativewelcomeobject inworld.json) as a world's initial view, with a live feature-count teaser, before its CTA drops you into the map. - A multi-image
infoField(type: "image"resolving to more than one value) renders a thumbnail grid that opens a full-screen lightbox with prev/next, arrow-key navigation, and Escape-to-close. - Point-layer marker styling driven by any feature property, independent of
taxonomy:style.colorField/colorMap(a colored circle behind the icon) andstyle.badgeField/badgeMap(a sparse corner badge). - Satellite base layers can show place labels via an optional
labelsUrlsecond tile source layered on top. scripts/fetch-osm-boundary.mjspulls a real administrative boundary from OpenStreetMap (Overpass API) to use as aregionRole: "boundary"layer's source geojson, instead of hand-authoring one.
CalendarBar.ts now has a day/week/month/year granularity selector plus a range slider bounded by calendar.min/calendar.max (previously only a prev/next day stepper + native date picker), covering the design spec's Section 6 gap.
calendar.system (docs/json-reference.md) renders dates in Julian/Islamic/Hebrew calendars throughout the UI (calendar bar stepping and label, temporal-status text), and CalendarBar.ts's year/month/day fields edit directly in that system too, not just Gregorian.
map.crs (docs/json-reference.md) supports Leaflet's built-in EPSG:3857/EPSG:4326/Simple (flat pixel-space, e.g. indoor floor plans or game/fictional maps) plus fully custom projections via proj4leaflet.