@nika-js/onlymap 0.4.4 → 0.4.6

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/README.md CHANGED
@@ -69,7 +69,7 @@ Or with no build step at all, straight from a CDN:
69
69
 
70
70
  The bare package URL serves `dist/onlymap.standalone.js`, a single-file bundle built for exactly this (jsDelivr too). Use a CDN that serves the package's raw files — **not** a rebundling CDN like esm.sh, which re-splits the bundle into duplicate copies of the deck.gl/luma.gl runtime and breaks every layer's shader compilation.
71
71
 
72
- Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 528 unit/behavioral tests and 45 Playwright GPU tests.
72
+ Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 540 unit/behavioral tests and 53 Playwright GPU tests.
73
73
 
74
74
  The [examples](https://github.com/NikaGeospatial/onlymapjs/tree/main/examples) are the best tour: widgets, behaviors & overlays, basemaps, columnar/Arrow data, manual drawing, 3D models, scene lighting (with the native lighting widget), DEM terrain, a live WebSocket ship feed, and a polled driver fleet.
75
75
 
@@ -244,7 +244,9 @@ await h.pick({ layer: "quakes", featureId: "q1" }); // same code path as a real
244
244
  expect(overlay.shadowRoot.textContent).toContain("M 6.5");
245
245
  ```
246
246
 
247
- Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot test, and `await mapEl.ready` / the `om-map-ready` event to de-flake browser e2e tests. Full guide: [docs/testing.md](docs/testing.md).
247
+ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot test, and `await mapEl.ready` / the `om-map-ready` event to de-flake browser e2e tests.
248
+
249
+ **Layout, checked like types.** `npx @nika-js/onlymap check-layout <manifest.html>` audits a page's real rendered widget layout in headless Chromium — no screenshots, no eyeballing: it asserts no widgets overlap, each is hit-testable (not painted under the canvas), each sits in its slot, and same-slot edges are flush, across a viewport-width sweep, emitting structured `{severity, element, message, fix}` diagnostics and exiting 0/1. The same audit is importable as `auditLayout(page)` for your own Playwright suite. Full guide: [docs/testing.md](docs/testing.md).
248
250
 
249
251
  ## 3D
250
252
 
@@ -254,8 +256,8 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
254
256
 
255
257
  - **`OmMap.*`** — `validate`, `snapshotIR`, `registerLayer`, `registerWidget`, `registerAction`, `registerSource`, `registerFormat`, `registerBasemap`, `configureBasemap`, `configureData`, `configureTelemetry`, `configureLicense`, `getLayerSchema`
256
258
  - **`@nika-js/onlymap/deck`** — the bundled deck.gl classes (`CompositeLayer`, `TileLayer`, …) for building custom layer types: shims must extend the same class hierarchy the core renders with, not a second installed deck.gl copy. Recipe: [docs/custom-layers.md](docs/custom-layers.md)
257
- - **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing, origin}`, where `origin` is `"user"` for gesture-driven bursts vs `"programmatic"` for API/story moves — the echo-suppression signal for state sync) — the camera-persistence hook; `document.querySelector("om-map")` is fully typed
258
- - **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange(view, origin)` option (the `om-view-changed` twin). The React adapter rides it; usable directly from vanilla TS or other frameworks
259
+ - **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing, origin}`, where `origin` is `"user"` for gesture-driven bursts vs `"programmatic"` for API/story moves — the echo-suppression signal for state sync) — the camera-persistence hook; the `om-map-point` event (`detail = {coordinate: [lng,lat]|null, kind: "click"|"hover"}`) fires on every click/hover with the map coordinate, including empty-map clicks picks discard — the hook for custom capture tools the built-in draw widget doesn't cover; the `om-tileset-load` event (`detail = {layerId, tileset}`) surfaces a `Tile3DLayer`'s live deck `Tileset3D` for tools that need the real tileset (e.g. region export), not the IR; `document.querySelector("om-map")` is fully typed
260
+ - **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange(view, origin)` option (the `om-view-changed` twin), plus `onMapPoint` / `onTilesetLoad` options (the `om-map-point` / `om-tileset-load` twins). The React adapter rides it; usable directly from vanilla TS or other frameworks
259
261
  - **`getStore(token)`** — the external-store contract: per-token `{subscribe, getSnapshot}` stores (`viewport`/`selection`/`layers`/`data:<id>`) with cached immutable plain-data snapshots and `origin` tagging — directly consumable by `useSyncExternalStore` (the React adapter's own hooks ride it), MobX autoruns, Redux listeners, Zustand mirrors. ~20-line integration-tested recipes for Redux Toolkit, MobX/mobx-keystone, Zustand, and Jotai: [docs/external-stores.md](docs/external-stores.md)
260
262
  - **Testing** — `mountForTest`, and imports are SSR-safe (importing in Node/jsdom never touches browser globals)
261
263
 
package/bin/onlymapjs.mjs CHANGED
@@ -137,8 +137,21 @@ async function checkLayout(manifestArg) {
137
137
  }
138
138
  const context = await browser.newContext({ serviceWorkers: "block" });
139
139
  const page = await context.newPage();
140
+ const pageErrors = [];
141
+ page.on("pageerror", (error) => pageErrors.push(String(error).split("\n")[0]));
142
+ page.on("console", (msg) => {
143
+ if (msg.type() === "error") pageErrors.push(msg.text().split("\n")[0]);
144
+ });
140
145
  await page.goto(url, { waitUntil: "domcontentloaded" });
141
- const diagnostics = await auditLayout(page);
146
+ let diagnostics;
147
+ try {
148
+ diagnostics = await auditLayout(page);
149
+ } catch (error) {
150
+ // The page never produced a map. Its own console/pageerror output is the
151
+ // real cause — surface it so the failure is self-explaining.
152
+ const detail = pageErrors.length ? `\nPage errors:\n ${pageErrors.slice(0, 3).join("\n ")}` : "";
153
+ throw new Error(`${error instanceof Error ? error.message : String(error)}${detail}`);
154
+ }
142
155
  if (diagnostics.length === 0) {
143
156
  console.log("CLEAN: no widget layout problems at 360, 640, 768, or 1024px.");
144
157
  return 0;
@@ -19,7 +19,7 @@ async function v(N = {}) {
19
19
  if (n) {
20
20
  const { createRequire: I } = await import(
21
21
  /*webpackIgnore:true*/
22
- "./lerc-l-QFh62d.js"
22
+ "./lerc-CaQOmDCP.js"
23
23
  ).then((g) => g._);
24
24
  var O = I(import.meta.url);
25
25
  }
@@ -1,4 +1,4 @@
1
- import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-KgO0MBqA.js";
1
+ import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-DSkgI4Oc.js";
2
2
  const Cd = Math.PI / 180, gy = 180 / Math.PI;
3
3
  function em(Pe, Q = 0) {
4
4
  const me = Math.min(180, Pe) * Cd;
@@ -25217,7 +25217,7 @@ class Ry {
25217
25217
  * remount, since `customAttribution` is fixed at construction.
25218
25218
  */
25219
25219
  mountAttribution(Q) {
25220
- if (this.showAttribution && (this.attributionText = Q, this.attribution && (this.map.removeControl(this.attribution), this.attributionElement = void 0), this.attribution = new rm.AttributionControl({ compact: !0, ...Q ? { customAttribution: Q } : {} }), this.map.addControl(this.attribution), this.attributionHost)) {
25220
+ if (this.showAttribution && (this.attributionText = Q, this.attribution && (this.map.removeControl(this.attribution), this.attributionElement?.remove(), this.attributionElement = void 0), this.attribution = new rm.AttributionControl({ compact: !0, ...Q ? { customAttribution: Q } : {} }), this.map.addControl(this.attribution), this.attributionHost)) {
25221
25221
  const me = this.map.getContainer().querySelector(".maplibregl-ctrl-attrib");
25222
25222
  me && (me.setAttribute("data-om-mandated-chrome-item", "attribution"), me.style.margin = "0", this.attributionHost.appendChild(me), this.attributionElement = me);
25223
25223
  }
@@ -25272,7 +25272,7 @@ class Ry {
25272
25272
  for (const me of this.pendingCaptureRejects) me(Q);
25273
25273
  this.pendingCaptureRejects.clear();
25274
25274
  }
25275
- this.deckCaptures = [], this.attributionElement = void 0, this.overlay.finalize(), this.map.remove();
25275
+ this.deckCaptures = [], this.attributionElement?.remove(), this.attributionElement = void 0, this.overlay.finalize(), this.map.remove();
25276
25276
  }
25277
25277
  }
25278
25278
  export {
@@ -67,6 +67,8 @@ export declare class OmMapElement extends HTMLElementBase {
67
67
  private widgetLayer;
68
68
  private readonly slotContainers;
69
69
  private readonly mandatedChromeHosts;
70
+ /** Slots that host mandated chrome (badge/attribution) — set at host creation, so the collision-dim exemption is a slot-level flag, not a per-flush DOM scan. */
71
+ private readonly slotsWithMandatedChrome;
70
72
  private widgetResizeObserver;
71
73
  private widgetsFolded;
72
74
  private foldPassPending;
@@ -99,13 +101,22 @@ export declare class OmMapElement extends HTMLElementBase {
99
101
  * adapters mount/unmount the actual controls inside.
100
102
  */
101
103
  private ensureMandatedChromeHost;
104
+ /** True while an internal reparent (slotting/manual-flip) is moving a widget — its transient disconnect must NOT be read as an author removal. */
105
+ private inSlotReparent;
106
+ /** Wrap an internal reparent: suppress history AND flag it as a move so unregisterWidgetInternal keeps the fold/anchor bookkeeping. */
107
+ private reparent;
102
108
  slotWidgetInternal(el: HTMLElement, slot: WidgetSlot): void;
103
109
  /** Called by <om-widget> when its live `fold` attribute changes. */
104
110
  refreshWidgetFoldInternal(): void;
105
111
  private startWidgetFoldObserver;
112
+ private foldBreakpointRaw?;
113
+ private foldBreakpointCache;
114
+ /** Cached breakpoint resolution — re-probes only when the token string actually changes, not on every ResizeObserver tick. */
106
115
  private foldBreakpointPx;
107
116
  private measureWidgetFold;
108
117
  private updateWidgetFoldForWidth;
118
+ /** Lift the bottom-end row clear of the bottom drawer toggle while folded. Applied here AND in the fold pass so a lazily-created bottom-end container still gets it. */
119
+ private syncFoldBottomOffset;
109
120
  private scheduleFoldPass;
110
121
  private shouldFoldWidget;
111
122
  private ensureFoldDrawer;
@@ -172,6 +183,7 @@ export declare class OmMapElement extends HTMLElementBase {
172
183
  * modeled (headless has no rects; this no-ops there).
173
184
  */
174
185
  private dimSlotsAgainstOverlays;
186
+ /** widgets-toggle can sit in any author-chosen slot, so still scan for it (attribution/badge are covered by the slot flag). */
175
187
  private slotHasNeverHidesWidget;
176
188
  emit(event: string, payload?: Record<string, unknown>): void;
177
189
  /** Recenters (and optionally rezooms) the map — instant, not an animated fly. */
@@ -218,6 +230,20 @@ export declare class OmMapElement extends HTMLElementBase {
218
230
  */
219
231
  injectPickInternal(selection: Selection | null): void;
220
232
  injectDragPickInternal(selection: Selection): void;
233
+ /** Every click/hover map coordinate (spec: "Manual Drawing"): drives the
234
+ * internal draw controller AND fires the public `om-map-point` event, so
235
+ * consumer capture tools the built-in draw widget doesn't cover
236
+ * (rectangle/circle AOIs) can subscribe. The two consumers are
237
+ * DECOUPLED: the event dispatches first (dispatchEvent isolates a
238
+ * throwing listener), then the draw controller runs — so neither a
239
+ * throwing draw session nor a throwing listener can starve the other.
240
+ * Fires at pointer rate on hover (no debounce — vertex capture needs
241
+ * every point); heavy listeners should throttle their own work. */
242
+ private handleMapPoint;
243
+ /** Harness map-point injection (spec: "Consumer Testing Surface") — the
244
+ * same path a real deck click/hover coordinate takes, so the om-map-point
245
+ * event and custom capture tools are testable without a GPU. */
246
+ injectMapPointInternal(coordinate: [number, number] | null, kind?: "click" | "hover"): void;
221
247
  /** Harness setView (spec: "Consumer Testing Surface") — the one path that reaches pitch/bearing. */
222
248
  setViewInternal(partial: {
223
249
  longitude?: number;
@@ -302,6 +328,20 @@ declare global {
302
328
  /** "user" if any change in the settled burst came from a canvas gesture; "programmatic" for pure API/action/story moves. */
303
329
  origin: "user" | "programmatic";
304
330
  }>;
331
+ /** Every click/hover's map coordinate (null when the pointer is off any
332
+ * geometry deck can unproject) — the consumer hook for custom capture
333
+ * tools beyond the built-in draw widget. */
334
+ "om-map-point": CustomEvent<{
335
+ coordinate: [number, number] | null;
336
+ kind: "click" | "hover";
337
+ }>;
338
+ /** A Tile3DLayer finished loading its root tileset — detail carries the
339
+ * authored layer id and the live deck `Tileset3D` for tools (e.g. region
340
+ * export) that need the real tileset, not the IR. */
341
+ "om-tileset-load": CustomEvent<{
342
+ layerId: string;
343
+ tileset: unknown;
344
+ }>;
305
345
  }
306
346
  }
307
347
  export {};
@@ -1,5 +1,5 @@
1
- import { i as E, j as S, k as R, n as N, o as h, p as w, q as O, s as W, r as x, t as I, v as B, w as _, x as P, y as k, A as $, B as z, E as v, F, H as C, I as U, J as p, K as M } from "./index-KgO0MBqA.js";
2
- import { N as $e, R as ze, _ as ve, O as Ce, P as Ue, Q as Me, S as je, T as De, U as Je, X as qe, Y as He, Z as Ve, $ as Ge, a0 as Ke, a1 as Qe, a2 as Xe, a3 as Ye, a4 as Ze, a5 as et, a6 as tt, a7 as nt, a8 as rt, a9 as at, aa as st, ab as ot } from "./index-KgO0MBqA.js";
1
+ import { i as E, j as S, k as R, n as N, o as h, p as w, q as O, s as W, r as x, t as I, v as B, w as _, x as P, y as k, A as $, B as z, E as v, F, H as C, I as U, J as p, K as M } from "./index-DSkgI4Oc.js";
2
+ import { N as $e, R as ze, _ as ve, O as Ce, P as Ue, Q as Me, S as je, T as De, U as Je, X as qe, Y as He, Z as Ve, $ as Ge, a0 as Ke, a1 as Qe, a2 as Xe, a3 as Ye, a4 as Ze, a5 as et, a6 as tt, a7 as nt, a8 as rt, a9 as at, aa as st, ab as ot } from "./index-DSkgI4Oc.js";
3
3
  import { g as j, i as D } from "./table-accessors-CYWTzpQI.js";
4
4
  async function J(t, e, n = {}, r = {}) {
5
5
  const a = E(t), s = S.getWorkerFarm(n), { source: o } = n, c = { name: a, source: o };