@nika-js/onlymap 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/onlymap.code-snippets +4 -4
- package/LICENSE.md +22 -12
- package/README.md +27 -6
- package/dist/actions.d.ts +2 -2
- package/dist/badge.d.ts +4 -0
- package/dist/{basemap-Bn4TmZtQ.js → basemap-BdvB-SHy.js} +1 -1
- package/dist/ctx.d.ts +10 -1
- package/dist/elements/om-map.d.ts +1 -0
- package/dist/error-reporting.d.ts +17 -0
- package/dist/history.d.ts +53 -0
- package/dist/html-data.d.ts +2 -2
- package/dist/{index-3UyMg0Md.js → index-BVQ0s1Zh.js} +1 -1
- package/dist/{index-oE1Kouy1.js → index-BxzDKZpu.js} +14312 -13798
- package/dist/{index-D2zVsZ79.js → index-CJOMgp8k.js} +1 -1
- package/dist/{index-BZs_x9Dx.js → index-CvBHiOGD.js} +2 -2
- package/dist/{index-DSjndBOf.js → index-Vc8QfNEf.js} +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/internal-ids.d.ts +7 -0
- package/dist/ir-diff.d.ts +1 -1
- package/dist/license.d.ts +64 -0
- package/dist/onlymapjs.js +27 -24
- package/dist/onlymapjs.umd.cjs +290 -280
- package/dist/runtime-core.d.ts +6 -0
- package/dist/telemetry-schema.d.ts +53 -0
- package/dist/telemetry.d.ts +60 -0
- package/dist/version.d.ts +8 -0
- package/docs/react.md +1 -0
- package/docs/telemetry.md +78 -0
- package/docs/testing.md +2 -2
- package/llms.txt +4 -3
- package/onlymapjs.html-data.json +32 -0
- package/package.json +6 -3
- package/skills/onlymapjs/SKILL.md +4 -1
- package/skills/onlymapjs/references/syntax.md +8 -0
- package/skills/onlymapjs/references/testing.md +2 -0
package/dist/runtime-core.d.ts
CHANGED
|
@@ -90,6 +90,12 @@ export declare class RuntimeCore {
|
|
|
90
90
|
private basemapOptions?;
|
|
91
91
|
/** Bumped on every renderer (re)init — a superseded lazy chunk load must not install its adapter over a newer renderer. */
|
|
92
92
|
private rendererGeneration;
|
|
93
|
+
/** Pre-gate descriptors — what a license settle re-reconciles from (lastIRs holds the GATED set the renderer sees). */
|
|
94
|
+
private pregateIRs;
|
|
95
|
+
/** (layer, reason) pairs already reported — gate errors fire once per violation, not per reconcile. */
|
|
96
|
+
private readonly gateWarned;
|
|
97
|
+
private badge?;
|
|
98
|
+
private unsubscribeLicense?;
|
|
93
99
|
constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions, basemapOptions?: BasemapRuntimeOptions);
|
|
94
100
|
/** `basemap` attribute → concrete style, logging resolution problems (unknown preset / missing key) — the demo-style fallback still renders. */
|
|
95
101
|
private resolveBasemap;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The telemetry WIRE SCHEMA (spec: "Monetization Gates / Usage telemetry";
|
|
3
|
+
* public doc: docs/telemetry.md — keep all three in sync). Deliberately a
|
|
4
|
+
* types-only module with zero DOM/browser references: the ingestion Worker
|
|
5
|
+
* (cloud/workers/telemetry, WebWorker lib) type-imports this same file, so
|
|
6
|
+
* client and server can never drift apart silently — a schema change breaks
|
|
7
|
+
* the other side's typecheck in the same commit.
|
|
8
|
+
*/
|
|
9
|
+
export interface TelemetryLayerSnapshot {
|
|
10
|
+
type: string;
|
|
11
|
+
rows: number;
|
|
12
|
+
streaming: boolean;
|
|
13
|
+
refresh: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Library-caused error report (spec: "Monetization Gates / Usage telemetry" — library-error reporting). Scrubbed: no manifest content, no layer data, no page paths. */
|
|
16
|
+
export interface TelemetryErrorReport {
|
|
17
|
+
event: "library_error";
|
|
18
|
+
pageLoadId: string;
|
|
19
|
+
version: string;
|
|
20
|
+
/** Hostname only — never the path. */
|
|
21
|
+
origin: string;
|
|
22
|
+
dev: boolean;
|
|
23
|
+
/** Hash of message + top own-code frame — the dedup/grouping key. */
|
|
24
|
+
signature: string;
|
|
25
|
+
message: string;
|
|
26
|
+
/** Top stack lines from the library's own code, query strings stripped. */
|
|
27
|
+
frames: string[];
|
|
28
|
+
ua: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TelemetrySnapshot {
|
|
31
|
+
event: "map_ready";
|
|
32
|
+
/** Random per page load — dedups beacon retries & multi-map pages. NOT persistent, NOT a visitor id. */
|
|
33
|
+
pageLoadId: string;
|
|
34
|
+
/** The authored `map-id` attribute — identifies the map artifact, not the visitor. */
|
|
35
|
+
mapId: string | null;
|
|
36
|
+
version: string;
|
|
37
|
+
/** "free" until the license module (monetization M1) exists. */
|
|
38
|
+
plan: string;
|
|
39
|
+
keyId: string | null;
|
|
40
|
+
/** Hostname only — never the path. */
|
|
41
|
+
origin: string;
|
|
42
|
+
frontend: "html" | "react" | "programmatic";
|
|
43
|
+
renderer: "maplibre" | "standalone";
|
|
44
|
+
/** Dev context (localhost et al.) — the only beacons the server may GeoIP (country code, IP discarded). */
|
|
45
|
+
dev: boolean;
|
|
46
|
+
layers: TelemetryLayerSnapshot[];
|
|
47
|
+
story: {
|
|
48
|
+
steps: number;
|
|
49
|
+
} | null;
|
|
50
|
+
widgets: string[];
|
|
51
|
+
draw: boolean;
|
|
52
|
+
undoRedo: boolean;
|
|
53
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage telemetry — client side (spec: "Monetization Gates / Usage
|
|
3
|
+
* telemetry", phase M3 client half). One snapshot per <om-map> instance per
|
|
4
|
+
* page load, built when that map reaches `ready` (manifest fully parsed —
|
|
5
|
+
* late mutations go uncounted, by design). Deployment-scoped, never
|
|
6
|
+
* visitor-scoped: the payload describes the page's use of the library.
|
|
7
|
+
*
|
|
8
|
+
* LIVE by default since the ingestion Worker deployment (2026-07-14): the
|
|
9
|
+
* default endpoint is the first-party Worker at om-api.nika.eco (spec's
|
|
10
|
+
* "beacon must not ship before something is listening" — it is listening).
|
|
11
|
+
* Disable with `configureTelemetry({ disabled: true })`, per map with
|
|
12
|
+
* `telemetry="off"`, or clear the endpoint (`{ endpoint: undefined }`).
|
|
13
|
+
* Disclosure: LICENSE.md §11; public schema: docs/telemetry.md.
|
|
14
|
+
*
|
|
15
|
+
* Non-negotiables (spec, binding):
|
|
16
|
+
* - telemetry never affects function: everything is wrapped, fire-and-
|
|
17
|
+
* forget, no retry, silent on failure; nothing awaits it;
|
|
18
|
+
* - no PII, no page URLs (hostname only), no visitor identifiers —
|
|
19
|
+
* `pageLoadId` dies with the page; `map-id` identifies the map ARTIFACT;
|
|
20
|
+
* - opt-out: `OmMap.configureTelemetry({ disabled: true })` or
|
|
21
|
+
* `telemetry="off"` on the map element;
|
|
22
|
+
* - headless maps (consumer test suites) never report.
|
|
23
|
+
*/
|
|
24
|
+
import type { LayerIR } from "./ir";
|
|
25
|
+
import type { TelemetrySnapshot, TelemetryErrorReport } from "./telemetry-schema";
|
|
26
|
+
export type { TelemetrySnapshot, TelemetryLayerSnapshot } from "./telemetry-schema";
|
|
27
|
+
export interface TelemetryConfig {
|
|
28
|
+
/** Kill switch — disables the map-ready beacon and library-error reports globally. */
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** Ingestion endpoint — the first-party Worker by default; explicitly set `undefined` to silence all sends. */
|
|
31
|
+
endpoint?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The first-party ingestion Worker (cloud/workers/telemetry) — a domain we control, never a vendor's. */
|
|
34
|
+
export declare const DEFAULT_TELEMETRY_ENDPOINT = "https://om-api.nika.eco/v1/t";
|
|
35
|
+
/** Merge-assign: only keys present in `partial` change (so `{ endpoint: undefined }` explicitly clears). */
|
|
36
|
+
export declare function configureTelemetry(partial: TelemetryConfig): void;
|
|
37
|
+
/** Read-only view for the error-reporting module (same gates, same switch). */
|
|
38
|
+
export declare function getTelemetryConfig(): Readonly<TelemetryConfig>;
|
|
39
|
+
/** One id per page load, shared by every map on the page. */
|
|
40
|
+
export declare const pageLoadId: string;
|
|
41
|
+
/** Hostname-only origin + dev flag — shared with error reporting. */
|
|
42
|
+
export declare function hostContext(): {
|
|
43
|
+
origin: string;
|
|
44
|
+
dev: boolean;
|
|
45
|
+
};
|
|
46
|
+
export declare function buildMapSnapshot(mapEl: Element, layerIRs: ReadonlyMap<string, LayerIR>, frontend?: TelemetrySnapshot["frontend"]): TelemetrySnapshot;
|
|
47
|
+
/**
|
|
48
|
+
* Fire-and-forget POST. `text/plain` is deliberate: it keeps the request
|
|
49
|
+
* CORS-simple (no preflight from every customer origin) — the Worker parses
|
|
50
|
+
* the body as JSON regardless of the content type. sendBeacon survives page
|
|
51
|
+
* unload; `fetch keepalive` is the fallback where sendBeacon is missing.
|
|
52
|
+
* Never throws, never retries, nothing observes the result.
|
|
53
|
+
*/
|
|
54
|
+
export declare function postPayload(payload: TelemetrySnapshot | TelemetryErrorReport): void;
|
|
55
|
+
/**
|
|
56
|
+
* The om-map `ready` hook — one call per map instance per page load
|
|
57
|
+
* (guarded by the caller's readyFired latch). All gates checked here so the
|
|
58
|
+
* call site stays a single unconditional line.
|
|
59
|
+
*/
|
|
60
|
+
export declare function reportMapReady(mapEl: Element, layerIRs: ReadonlyMap<string, LayerIR>): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library version, as a compile-time constant (telemetry payloads,
|
|
3
|
+
* future diagnostics). Kept in sync with package.json by a drift test
|
|
4
|
+
* (telemetry.test.ts) rather than a JSON import — package.json sits outside
|
|
5
|
+
* the build rootDir, and a `define` would need repeating across vite/vitest/
|
|
6
|
+
* vite-node configs.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LIBRARY_VERSION = "0.2.3";
|
package/docs/react.md
CHANGED
|
@@ -116,5 +116,6 @@ React ≥ 18 is an optional peer dependency — it's only loaded if you import `
|
|
|
116
116
|
|
|
117
117
|
- **Stories** as React components (a `<Story>`/timeline hook is on the roadmap) — an HTML `<om-story>` needs the HTML front-end.
|
|
118
118
|
- **The draw widget** — HTML front-end only for now.
|
|
119
|
+
- **Undo/redo** — manifest history is a DOM-front-end feature; in React your state (and its undo) belongs to the app. `ctx.history` reads `{ canUndo: false, canRedo: false }` here.
|
|
119
120
|
- Per-feature `trace` (it animates via runtime manifest elements) — whole-layer `trace` on a TripsLayer works.
|
|
120
121
|
- A `scale()` helper mirroring the expression language — use `d3-scale` or plain functions.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Telemetry
|
|
2
|
+
|
|
3
|
+
OnlyMapJS reports **one usage snapshot per map, per page load** — sent when a map reaches `ready` — and, separately, **errors caused by the library's own code**. This page documents exactly what is (and is not) collected, and how to turn it off.
|
|
4
|
+
|
|
5
|
+
> **Status: active.** Reports go to the first-party endpoint `https://om-api.nika.eco/v1/t` (never to a third-party domain from your pages). Disclosure lives in the license (LICENSE.md §11). Both opt-outs below are always honored.
|
|
6
|
+
|
|
7
|
+
## What a snapshot contains
|
|
8
|
+
|
|
9
|
+
The payload is **deployment-scoped**: it describes the page's use of the library, never the visitor.
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"event": "map_ready",
|
|
14
|
+
"pageLoadId": "…", // random UUID per page load — dedups retries; dies with the page
|
|
15
|
+
"mapId": "…" | null, // the authored map-id attribute (see below)
|
|
16
|
+
"version": "0.2.3",
|
|
17
|
+
"plan": "free", "keyId": null,
|
|
18
|
+
"origin": "dashboard.example.com", // hostname ONLY — never the path or query
|
|
19
|
+
"frontend": "html", // html | react | programmatic
|
|
20
|
+
"renderer": "maplibre" | "standalone",
|
|
21
|
+
"dev": false, // true on localhost / *.local
|
|
22
|
+
"layers": [ { "type": "ScatterplotLayer", "rows": 1200, "streaming": false, "refresh": false } ],
|
|
23
|
+
"story": { "steps": 7 } | null,
|
|
24
|
+
"widgets": ["legend", "basemap-switcher"], // widget types only
|
|
25
|
+
"draw": false, "undoRedo": false
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
What is **never** collected: page paths or URLs, your data or its contents, coordinates, IP addresses in the payload, cookies, or any persistent visitor identifier. `pageLoadId` is regenerated on every page load and cannot link visits. Snapshots are also never sent from `headless` maps, so test suites stay silent.
|
|
30
|
+
|
|
31
|
+
## Library-error reporting
|
|
32
|
+
|
|
33
|
+
Unexpected exceptions **from the library's own code** — never your page scripts, never other libraries (the stack must point into OnlyMapJS's own bundle), and never manifest/validation mistakes (those are surfaced to you in the dev error panel instead) — are reported so bugs get fixed:
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
{
|
|
37
|
+
"event": "library_error",
|
|
38
|
+
"pageLoadId": "…", "version": "0.2.3",
|
|
39
|
+
"origin": "dashboard.example.com", // hostname only
|
|
40
|
+
"dev": false,
|
|
41
|
+
"signature": "…", // hash of message + top frame — the grouping key
|
|
42
|
+
"message": "…", // truncated; query strings stripped
|
|
43
|
+
"frames": ["…"], // top library-code stack lines; query strings stripped
|
|
44
|
+
"ua": "…" // browser user-agent string
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Stacks are scrubbed before sending: query strings are stripped from every URL (they can carry keys), and no manifest content, layer data, or accessor source is ever included. At most one report per distinct error per page load, capped at five per page. The same opt-outs below disable error reporting — one switch, no fine print.
|
|
49
|
+
|
|
50
|
+
## `map-id` — identifying the map, not the visitor
|
|
51
|
+
|
|
52
|
+
An optional authored attribute:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<om-map map-id="0f2c6a1e-88f7-4c3e-9d41-7b1f3f9f2ab7" ...>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It identifies the **map artifact** — the same id on every visit by every visitor — so usage can distinguish "one popular dashboard" from "many different maps". It is the same category as an analytics measurement id in page source: author-controlled, nothing stored on the visitor's device. **Opt out by deleting or changing it**; it is never required, and validation never asks for it. The VS Code `!map` snippet generates one automatically.
|
|
59
|
+
|
|
60
|
+
## Opting out
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
OmMap.configureTelemetry({ disabled: true }); // global — kills usage snapshots AND error reports
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<om-map telemetry="off" ...> <!-- per map -->
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Clearing the endpoint (`OmMap.configureTelemetry({ endpoint: undefined })`) is a third, equivalent switch: no endpoint, no network.
|
|
71
|
+
|
|
72
|
+
## The binding rules
|
|
73
|
+
|
|
74
|
+
These are design constraints, not promises of restraint:
|
|
75
|
+
|
|
76
|
+
1. **Telemetry never affects function.** Nothing in the library waits on, retries, or reacts to a report; sends are fire-and-forget (`navigator.sendBeacon`) and silent on failure. Error reporting can never itself throw into your page.
|
|
77
|
+
2. **No endpoint, no network.** The send layer is a no-op when the endpoint is cleared.
|
|
78
|
+
3. **Dev-context geography only.** Only `dev: true` reports (a developer's own localhost) are GeoIP-resolved at the server — to a country code, with the IP discarded unwritten. Production reports are never GeoIP'd and visitor IPs are never stored.
|
package/docs/testing.md
CHANGED
|
@@ -122,8 +122,8 @@ h = await mountForTest(PAGE); // resolves after the mocked fetch
|
|
|
122
122
|
|
|
123
123
|
A *failing* fetch also settles readiness (the layer is just empty) — `mountForTest` never hangs on a bad URL.
|
|
124
124
|
|
|
125
|
-
**What's real at this tier:** validation, accessor execution, `ctx.stats`/`data`/`dataInViewport`, declarative + viewport filtering, behaviors → actions, overlay anchoring/culling/interpolation (real Mercator math), widget reactivity, XSS escaping, columnar row materialization.
|
|
126
|
-
**What isn't:** pixels, GPU attribute recompute, basemap compositing, and CDN-loaded widgets (`vega-lite` is browser-only — assert its *data* here via `ctx.stats`, its rendering at tier 3 if at all).
|
|
125
|
+
**What's real at this tier:** validation, accessor execution, `ctx.stats`/`data`/`dataInViewport`, declarative + viewport filtering, behaviors → actions, overlay anchoring/culling/interpolation (real Mercator math), widget reactivity, XSS escaping, columnar row materialization — **and the free-plan license gates** (5 layers / 25k rows per layer), which apply in headless tests exactly as in production so a passing suite can't hide a gated page. If your page legitimately exceeds the free limits, configure your license key in test setup — keys are publishable and verify **offline**, so CI needs no network or secrets vault: `OmMap.configureLicense("om_live_…")`. Gate violations surface as errors on the validation stream (`om-validation-error` / the `validate` attribute), naming the limit.
|
|
126
|
+
**What isn't:** pixels, GPU attribute recompute, basemap compositing, and CDN-loaded widgets (`vega-lite` is browser-only — assert its *data* here via `ctx.stats`, its rendering at tier 3 if at all). Telemetry is also silent here — headless maps never report.
|
|
127
127
|
|
|
128
128
|
## Tier 3 — visual: Playwright
|
|
129
129
|
|
package/llms.txt
CHANGED
|
@@ -16,11 +16,12 @@ OnlyMapJS is NOT raw deck.gl and NOT generic HTML/JSX. The rules below are the d
|
|
|
16
16
|
|
|
17
17
|
## Element vocabulary
|
|
18
18
|
|
|
19
|
-
- `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). `validate` attribute enables live validation + on-page error panel.
|
|
19
|
+
- `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). `validate` attribute enables live validation + on-page error panel. Include a `map-id="<random UUID>"` on complete pages (identifies the map artifact for usage telemetry — not a visitor id; the page author deletes it to opt out); `telemetry="off"` disables usage telemetry + library-error reporting for the map (global: `OmMap.configureTelemetry({ disabled: true })`; schema: docs/telemetry.md). Free plan limits: 5 layers, 25k rows per layer — layers past a limit don't render and validation says why; a `license-key="om_live_…"` attribute (publishable, origin-restricted) or `OmMap.configureLicense(key)` lifts limits and removes the corner badge. When a user's map exceeds free limits, say so and point at the validation message rather than silently restructuring their data.
|
|
20
20
|
- `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (all 33 bundled), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). Data: `data` URL (JSON, GeoJSON, CSV/TSV `.csv` — parsed to typed columns — Shapefile `.shp` (+`.dbf` attributes) and KML `.kml` as GeoJSON features, or Arrow IPC `.arrow`/`.feather` — large point datasets stay columnar, GeoArrow line/polygon geometry becomes GeoJSON features, zstd-compressed IPC is handled; other formats plug in via `OmMap.registerFormat({match, parse})`), inline `<script type="application/json">` (row arrays or column-oriented `{"columns": {"lon": [...], "lat": [...]}}`), or `wss://` streaming (`key="mmsi"` upserts entities in place, `flush="250ms"` coalesces bursts, `source="name"` selects a `OmMap.registerSource` decoder plugin), or a polled REST snapshot (`refresh="5s"` re-fetches and replaces — for live endpoints that return the full current state). Authenticated endpoints: call `OmMap.configureData({ headers: {...} })` in a script — never put tokens in attributes. `$field` accessors work identically on all of them — never write column-index code yourself. One columnar restriction: the `js` full-JS opt-in is not allowed on Arrow/columnar layers (validation will tell you; use `$field` accessors instead). For 3D models use `type="ScenegraphLayer"` with `scenegraph="./model.glb"` (required) and `get-orientation="[0, $heading, 90]"` — the roll of 90 stands Y-up glTF models upright; see docs/3d-assets.md.
|
|
21
|
-
- `<om-widget type="legend|layer-switcher|basemap-switcher|zoom-controls|scale-bar|attribution|filter|vega-lite" position="bottom-right">` — static UI panels. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers']
|
|
21
|
+
- `<om-widget type="legend|layer-switcher|basemap-switcher|zoom-controls|undo-redo|scale-bar|attribution|filter|vega-lite" position="bottom-right">` — static UI panels. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers', 'history', 'basemap']` (`layers` also fires on visibility/filter changes; `basemap` on basemap switches; `history` on undo/redo availability), `this.$(sel)`, `vegaEmbed`/`d3` as globals.
|
|
22
22
|
- `<om-overlay id="..." anchor-from="selection">` — rich geo-anchored HTML (≤ ~20 per map). Anchors: `anchor="[lng, lat]"` (static), `anchor-from="selection"` (follows picks), or `anchor-layer="regions" anchor-feature-id="mission"` (anchored to a feature's own geometry — bbox center — no coordinates in markup; `{{field}}` interpolates that feature's attributes). `{{field}}` interpolates the picked feature HTML-escaped; `{{{field}}}` is raw (avoid). For labels on many features use `PopupLayer`, not overlays.
|
|
23
|
-
- `<om-behavior on="click|hover|drag|load|data-loaded" layer="..." action="...">` — declarative interaction. Built-in actions: `show-overlay`, `hide-overlay`, `show-tooltip`, `hide-tooltip`, `toggle-layer`, `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
|
|
23
|
+
- `<om-behavior on="click|hover|drag|load|data-loaded" layer="..." action="...">` — declarative interaction. Built-in actions: `show-overlay`, `hide-overlay`, `show-tooltip`, `hide-tooltip`, `toggle-layer`, `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`, `undo`, `redo`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
|
|
24
|
+
- Undo/redo is built in: user-facing manifest changes (layer toggles, filter changes, basemap switches, element add/remove, drawn sketches) are recorded automatically — the manifest is the state. `<om-widget type="undo-redo">` renders the buttons; Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z, and Ctrl-Y work on any map (text inputs keep their native undo). Camera moves, hover effects, and story playback are deliberately NOT undo steps. Widget scripts: `ctx.history.canUndo/canRedo` with watch token `history`; `ctx.emit("undo")`/`ctx.emit("redo")`.
|
|
24
25
|
- `<om-fallback>` — static no-JS fallback, direct child of `<om-map>` (one per map, no attributes, plain HTML content — links allowed). Shown ONLY where scripts never run (chat-app/email file previews — iOS QuickLook renders HTML attachments with JS off — file managers, sandboxed webviews); hidden automatically once the map boots. GOOD PRACTICE: include one on every complete page, especially pages that may be shared as a file ("This interactive map requires JavaScript — open this file in a web browser", plus a hosted-version link when one exists). Without one, the stylesheet shows a generic text-only banner. The gate is pure CSS (`om-map:not(:defined)` in onlymapjs.css), so the CSS must load without JS — a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages; a bundler-emitted stylesheet is fine in npm projects.
|
|
25
26
|
- Animation: `transition="get-fill-color 800ms, get-radius 400ms"` on a layer GPU-animates prop changes (also smooths streaming updates via `get-position`). Camera: the `fly-to` action takes `center`/`zoom`/`pitch`/`bearing`/`duration` (e.g. `duration="2s"`) — use it in behaviors or `data-emit` buttons; `zoom-to-feature` also accepts `duration`.
|
|
26
27
|
- `<om-story id="tour" autoplay loop interrupt="pause|ignore">` — a storyboard of `<om-step>` children. Each step: `action="..."` + payload attributes (same kebab-case rule as behaviors) + `duration`/`delay`/`parallel` timing. Steps REFERENCE layers/overlays by id (`layer=`/`target=`) — a step must NEVER contain elements (validation error). Control: `<om-widget type="player" story="tour">`, the story-play/story-pause/story-seek actions, or `storyEl.play()/pause()/seek(ms)`. Seeking restores initial state then applies steps before T; use declarative payloads (e.g. `action="toggle-layer" visible="true"`, not bare toggles) so scrubbing is deterministic. Effect verbs as bare step attributes: `<om-step fade layer="regions" duration="1s">` (opacity reveal — start the layer at `opacity="0"`), `pulse` (attention flash), `trace` (progressive draw — whole-layer needs a TripsLayer; add `feature-id="..."` to make ONE polygon/line draw itself on inside any layer, or use it from a click behavior for click-to-trace), `populate` (rows drop in one by one — ordered by the authored filter-field, a payload `field`, or data order).
|
package/onlymapjs.html-data.json
CHANGED
|
@@ -77,6 +77,23 @@
|
|
|
77
77
|
"name": "validate",
|
|
78
78
|
"description": "Run manifest validation and show the on-page error panel."
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
"name": "map-id",
|
|
82
|
+
"description": "Optional authored UUID identifying this map artifact (telemetry dedup across visits/pages — not a visitor id). Opt-out: delete it."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "license-key",
|
|
86
|
+
"description": "OnlyMapJS license token (om_live_…) — lifts the free-tier limits (5 layers / 25k rows per layer) and removes the badge. Publishable, origin-restricted; or call OmMap.configureLicense once."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "telemetry",
|
|
90
|
+
"description": "Set \"off\" to disable usage telemetry for this map (global opt-out: OmMap.configureTelemetry).",
|
|
91
|
+
"values": [
|
|
92
|
+
{
|
|
93
|
+
"name": "off"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
80
97
|
{
|
|
81
98
|
"name": "headless",
|
|
82
99
|
"description": "No renderer — parsing/IR only (tests, SSR)."
|
|
@@ -1415,6 +1432,12 @@
|
|
|
1415
1432
|
},
|
|
1416
1433
|
{
|
|
1417
1434
|
"name": "set-basemap"
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
"name": "undo"
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
"name": "redo"
|
|
1418
1441
|
}
|
|
1419
1442
|
]
|
|
1420
1443
|
},
|
|
@@ -1465,6 +1488,9 @@
|
|
|
1465
1488
|
},
|
|
1466
1489
|
{
|
|
1467
1490
|
"name": "basemap-switcher"
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
"name": "undo-redo"
|
|
1468
1494
|
}
|
|
1469
1495
|
]
|
|
1470
1496
|
},
|
|
@@ -1680,6 +1706,12 @@
|
|
|
1680
1706
|
},
|
|
1681
1707
|
{
|
|
1682
1708
|
"name": "set-basemap"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
"name": "undo"
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
"name": "redo"
|
|
1683
1715
|
}
|
|
1684
1716
|
]
|
|
1685
1717
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nika-js/onlymap",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Interactive WebGL maps from declarative HTML — a custom-element manifest drives deck.gl: rendering, data loading, live updates, picking, widgets, and validation. No build step.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -82,14 +82,16 @@
|
|
|
82
82
|
"dev": "vite",
|
|
83
83
|
"build": "npm run typecheck && vite build && vite build --config vite.react.config.ts && npm run build:types",
|
|
84
84
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
85
|
-
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.e2e.json --noEmit",
|
|
85
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.e2e.json --noEmit && tsc -p cloud/workers/telemetry/tsconfig.json",
|
|
86
86
|
"test": "vitest run",
|
|
87
87
|
"prepublishOnly": "npm run build",
|
|
88
88
|
"try": "vite-node dev/expr-repl.mjs",
|
|
89
89
|
"test:e2e": "playwright test",
|
|
90
90
|
"gen:html-data": "vite-node dev/generate-html-data.ts",
|
|
91
91
|
"gen:public": "npm run gen:html-data && vite-node dev/build-public.ts",
|
|
92
|
-
"test:public": "npm run gen:public -- --dry-run --strict"
|
|
92
|
+
"test:public": "npm run gen:public -- --dry-run --strict",
|
|
93
|
+
"deploy:telemetry": "wrangler deploy --config cloud/workers/telemetry/wrangler.toml",
|
|
94
|
+
"dev:telemetry": "wrangler dev --config cloud/workers/telemetry/wrangler.toml"
|
|
93
95
|
},
|
|
94
96
|
"comment:deps": "This library ships a fully self-contained bundle in dist/ (verified: zero external bare imports), so it has NO runtime dependencies — deck.gl, loaders.gl, MapLibre, Arrow, d3, acorn, etc. are build-time-only and get bundled by Vite. Keeping them here (not in dependencies) is what stops every consumer install from pulling ~250 MB it never uses.",
|
|
95
97
|
"devDependencies": {
|
|
@@ -128,6 +130,7 @@
|
|
|
128
130
|
"vite": "^6.0.0",
|
|
129
131
|
"vite-node": "^6.0.0",
|
|
130
132
|
"vitest": "^4.1.9",
|
|
133
|
+
"wrangler": "^4.110.0",
|
|
131
134
|
"ws": "^8.21.0"
|
|
132
135
|
}
|
|
133
136
|
}
|
|
@@ -23,7 +23,7 @@ Use OnlyMapJS as a declarative HTML map library. Write custom elements such as `
|
|
|
23
23
|
</script>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
For no-build CDN examples, use a published module URL such as `https://esm.sh/@nika-js/onlymap@0.2.
|
|
26
|
+
For no-build CDN examples, use a published module URL such as `https://esm.sh/@nika-js/onlymap@0.2.3`.
|
|
27
27
|
|
|
28
28
|
## React Projects
|
|
29
29
|
|
|
@@ -62,6 +62,7 @@ Load the smallest reference needed for the task:
|
|
|
62
62
|
- Many labels/badges -> `<om-layer type="PopupLayer">`.
|
|
63
63
|
- Guided tour or narrative sequence -> `<om-story>` with `<om-step>` siblings that reference existing layers/overlays by id.
|
|
64
64
|
- Basemap choice or user-switchable basemaps -> `basemap` presets (`positron`, `liberty`, `dark-matter`, `osm`, ...) + `<om-widget type="basemap-switcher">`; MapTiler custom styles via a style URL or `basemap-key`.
|
|
65
|
+
- Undoable UI (step back after layer toggles, filter changes, basemap switches, sketch edits) -> `<om-widget type="undo-redo">`; Cmd/Ctrl-Z works even without the widget. Camera moves and story playback are not undo steps.
|
|
65
66
|
- Live entity updates -> `wss://` stream with `key` and optional `source` decoder.
|
|
66
67
|
- REST snapshot that changes over time -> `refresh="5s"`.
|
|
67
68
|
- User sketching -> `data="draw:sketch"` layer plus `<om-widget type="draw" target="sketch">`.
|
|
@@ -71,6 +72,8 @@ Load the smallest reference needed for the task:
|
|
|
71
72
|
|
|
72
73
|
When creating a map page, output a complete runnable HTML file unless the user asks for a fragment. Include CSS only as needed for page sizing or custom widgets/overlays. Keep the first screen the usable map, not a landing page.
|
|
73
74
|
|
|
75
|
+
Include a `map-id="<random UUID>"` attribute on `<om-map>` when creating a new complete page (generate a fresh UUID — never copy one from an example). It identifies the map artifact for usage telemetry, not the visitor; the author can delete it to opt out.
|
|
76
|
+
|
|
74
77
|
Include an `<om-fallback>` element (a short "this map requires JavaScript — open in a browser" message, optionally with a hosted-version link) as a direct child of `<om-map>` on any complete page. For the fallback to render in no-JS previews, `onlymapjs.css` must load without JavaScript — a real `<link rel="stylesheet">` or inlined `<style>`, not only a runtime `import` (bundler-emitted stylesheets are fine).
|
|
75
78
|
|
|
76
79
|
When modifying an existing page, preserve the user's data URLs, layer ids, and styling unless the request requires changing them.
|
|
@@ -36,6 +36,9 @@ Common attributes:
|
|
|
36
36
|
- `basemap` — a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm`; keyed `maptiler-streets|dataviz|satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a MapLibre style URL, or `none` (standalone canvas). The attribute is live: writing it switches the basemap in place (camera and layers survive). Register more with `OmMap.registerBasemap(name, { style })`.
|
|
37
37
|
- `attribution="false"` to opt out of the automatic provider-attribution control (only if you render equivalent credits yourself)
|
|
38
38
|
- `validate` to show live validation errors during authoring
|
|
39
|
+
- `map-id="<random UUID>"` — identifies the map artifact for usage telemetry (not a visitor id). Include one on new complete pages, freshly generated — never copied from an example; deleting it is the opt-out.
|
|
40
|
+
- `telemetry="off"` disables usage telemetry for this map (global: `OmMap.configureTelemetry({ disabled: true })`)
|
|
41
|
+
- `license-key="om_live_…"` — lifts the free-plan limits (5 layers, 25k rows per layer) and removes the corner badge; publishable origin-restricted token, safe in page source (or `OmMap.configureLicense(key)` once). Free-plan violations don't break the map: the offending layer isn't rendered and validation names the limit.
|
|
39
42
|
- `headless width="800" height="600"` for test harness use
|
|
40
43
|
|
|
41
44
|
Example:
|
|
@@ -180,6 +183,7 @@ Built-ins:
|
|
|
180
183
|
- `vega-lite`
|
|
181
184
|
- `player`
|
|
182
185
|
- `basemap-switcher` — radio list of presets; `options="positron dark-matter osm"` (default: every keyless registered preset)
|
|
186
|
+
- `undo-redo` — undo/redo buttons over the manifest history (layer toggles, filters, basemap switches, element edits, drawn sketches). Keyboard works without the widget: Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z, Ctrl-Y. Camera moves, hover effects, and story playback are not undo steps.
|
|
183
187
|
|
|
184
188
|
Positions: `top-left`, `top-right`, `bottom-left`, `bottom-right`.
|
|
185
189
|
|
|
@@ -215,8 +219,11 @@ Widget context:
|
|
|
215
219
|
- `ctx.stats(id, field, { scope: "viewport" })`
|
|
216
220
|
- `ctx.selection`
|
|
217
221
|
- `ctx.viewport`
|
|
222
|
+
- `ctx.history` — `{ canUndo, canRedo }`; re-render on changes via the `history` watch token
|
|
218
223
|
- `ctx.emit(action, payload)`
|
|
219
224
|
|
|
225
|
+
Watch tokens: `data:<layerId>`, `viewport`, `selection`, `layers` (fires on layer add/remove, visibility, and filter changes), `basemap`, `history`.
|
|
226
|
+
|
|
220
227
|
Use `this.$()` and `this.root`; widgets render in shadow DOM.
|
|
221
228
|
|
|
222
229
|
### `<om-overlay>`
|
|
@@ -278,6 +285,7 @@ Common built-in actions:
|
|
|
278
285
|
- `zoom-to-feature`
|
|
279
286
|
- `filter-layer`
|
|
280
287
|
- `set-basemap` — payload `{ basemap }`; writes the `<om-map basemap>` attribute
|
|
288
|
+
- `undo`, `redo` — step the manifest history (no payload)
|
|
281
289
|
- `zoom-in`, `zoom-out`
|
|
282
290
|
- `fly-to`
|
|
283
291
|
- story actions: `story-play`, `story-pause`, `story-seek`
|
|
@@ -92,6 +92,8 @@ Harness operations:
|
|
|
92
92
|
|
|
93
93
|
Use `vi.stubGlobal("fetch", ...)` for URL data and `vi.stubGlobal("WebSocket", ...)` for streams.
|
|
94
94
|
|
|
95
|
+
Free-plan license gates (5 layers, 25k rows per layer) apply in headless tests exactly as in production — gated layers emit errors on the validation stream instead of rendering. If the page under test legitimately exceeds the limits, call `OmMap.configureLicense("om_live_…")` in test setup; keys verify offline, so CI needs no network. Telemetry never fires from headless maps.
|
|
96
|
+
|
|
95
97
|
## Browser/E2E Testing
|
|
96
98
|
|
|
97
99
|
Use Playwright only for pixels, real GPU picking, basemap composition, or asset rendering.
|