@nika-js/onlymap 0.6.21 → 0.6.23

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/{LercDecode.es-B_dD2_mY.js → LercDecode.es-D8gAmIiR.js} +1 -1
  4. package/dist/attribute-resolution.d.ts +43 -1
  5. package/dist/{basemap-DK0AJ6ZH.js → basemap-7kviSfrp.js} +1 -1
  6. package/dist/elements/om-map.d.ts +2 -0
  7. package/dist/{geoparquet-fc2daxMh.js → geoparquet-CdNUwBZW.js} +1 -1
  8. package/dist/html-data.d.ts +14 -0
  9. package/dist/{index-DOPjev5l.js → index-CwCSSbZ_.js} +1 -1
  10. package/dist/{index-BwoceNAo.js → index-CyKFtivx.js} +1 -1
  11. package/dist/{index-9EEyGbU1.js → index-Dl2kmfXC.js} +1 -1
  12. package/dist/{index-CuBGRyG-.js → index-DncpMROY.js} +10482 -10389
  13. package/dist/{index-BNZFF535.js → index-DuNFpOy2.js} +2 -2
  14. package/dist/ir-snapshot.d.ts +2 -0
  15. package/dist/ir.d.ts +8 -0
  16. package/dist/{lerc-CMJO8KZO.js → lerc-CGMS_bHf.js} +2 -2
  17. package/dist/onlymap.standalone.js +22012 -21919
  18. package/dist/onlymapjs.js +1 -1
  19. package/dist/parse-manifest.d.ts +8 -0
  20. package/dist/programmatic.d.ts +2 -0
  21. package/dist/{raster-GMsuQARC.js → raster-BBShuKWm.js} +2 -2
  22. package/dist/{raster-pipeline-DNpKXgO3.js → raster-pipeline-GF1IWgfR.js} +1 -1
  23. package/dist/react.js +18 -17
  24. package/dist/runtime-core.d.ts +13 -1
  25. package/dist/version.d.ts +1 -1
  26. package/dist/{zarr-KiXluGoJ.js → zarr-i8xFRo88.js} +2 -2
  27. package/docs/stories.md +2 -0
  28. package/llms.txt +1 -0
  29. package/onlymapjs.attributes.json +1582 -0
  30. package/onlymapjs.html-data.json +282 -278
  31. package/package.json +2 -1
  32. package/skills/onlymapjs/SKILL.md +1 -1
  33. package/skills/onlymapjs/references/syntax.md +8 -2
package/dist/onlymapjs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { aA as e, aB as r, aC as t, aD as o, aE as b, aF as i, ap as l, aG as n, aH as S, aI as g, aJ as c, aK as E, aL as L, aM as T, aN as A, aO as d, aP as p, aQ as m, aR as _, aS as I, aT as u, aU as M, aV as y, aW as D, ao as O, aX as R, aw as f, aY as N, aZ as P, a_ as h, W as F, a$ as B, b0 as C, b1 as v, b2 as G, b3 as U, b4 as W, b5 as w, b6 as x, b7 as Y, b8 as H, b9 as X, ba as k, bb as J, bc as K, bd as V, be as z, bf as Q, bg as Z, bh as $, bi as j, bj as q, bk as aa, bl as sa, bm as ea, bn as ra, bo as ta, bp as oa, bq as ba, br as ia, bs as la, bt as na, bu as Sa, bv as ga, bw as ca, bx as Ea, by as La, bz as Ta, bA as Aa, bB as da, bC as pa, bD as ma, bE as _a, bF as Ia, bG as ua, bH as Ma, bI as ya, bJ as Da, bK as Oa, bL as Ra, bM as fa, bN as Na, bO as Pa, bP as ha, bQ as Fa, bR as Ba, bS as Ca, bT as va, bU as Ga, bV as Ua, bW as Wa, bX as wa, bY as xa, bZ as Ya, b_ as Ha, b$ as Xa, c0 as ka, c1 as Ja, c2 as Ka, c3 as Va, c4 as za, c5 as Qa, c6 as Za } from "./index-CuBGRyG-.js";
1
+ import { aA as e, aB as r, aC as t, aD as o, aE as b, aF as i, ap as l, aG as n, aH as S, aI as g, aJ as c, aK as E, aL as L, aM as T, aN as A, aO as d, aP as p, aQ as m, aR as _, aS as I, aT as u, aU as M, aV as y, aW as D, ao as O, aX as R, aw as f, aY as N, aZ as P, a_ as h, W as F, a$ as B, b0 as C, b1 as v, b2 as G, b3 as U, b4 as W, b5 as w, b6 as x, b7 as Y, b8 as H, b9 as X, ba as k, bb as J, bc as K, bd as V, be as z, bf as Q, bg as Z, bh as $, bi as j, bj as q, bk as aa, bl as sa, bm as ea, bn as ra, bo as ta, bp as oa, bq as ba, br as ia, bs as la, bt as na, bu as Sa, bv as ga, bw as ca, bx as Ea, by as La, bz as Ta, bA as Aa, bB as da, bC as pa, bD as ma, bE as _a, bF as Ia, bG as ua, bH as Ma, bI as ya, bJ as Da, bK as Oa, bL as Ra, bM as fa, bN as Na, bO as Pa, bP as ha, bQ as Fa, bR as Ba, bS as Ca, bT as va, bU as Ga, bV as Ua, bW as Wa, bX as wa, bY as xa, bZ as Ya, b_ as Ha, b$ as Xa, c0 as ka, c1 as Ja, c2 as Ka, c3 as Va, c4 as za, c5 as Qa, c6 as Za } from "./index-DncpMROY.js";
2
2
  export {
3
3
  e as ALL_POSITION_VALUES,
4
4
  r as AUDIT_EXEMPTIONS,
@@ -16,6 +16,14 @@ export interface ParseManifestOptions {
16
16
  * would evict a concurrently-running live map's compiled accessors).
17
17
  */
18
18
  snapshot?: boolean;
19
+ /** Structured warning sink (om-map routes to reportRuntimeErrorInternal → om-validation-error) for parse-time diagnostics that should reach `validate`-mode tooling, not only the console. */
20
+ report?: (entry: {
21
+ severity: "warning";
22
+ element: string;
23
+ attribute: string;
24
+ message: string;
25
+ fix: string;
26
+ }) => void;
19
27
  /** Controller/element identity for descriptor-owned transport lifetime. */
20
28
  dataOwner?: DataTransportOwner;
21
29
  }
@@ -70,6 +70,8 @@ export interface LayerDescriptor {
70
70
  props?: Record<string, unknown>;
71
71
  /** Per-layer terrain mode (spec: "Terrain") — the `terrain` attribute's twin; absent = the type default. */
72
72
  terrain?: "drape" | "offset" | "off";
73
+ /** Per-layer zoom visibility (public issue #38) — the `visible-zoom-range` attribute's twin: hidden outside min ≤ zoom < max (minzoom/maxzoom convention). Composes with `visible`. */
74
+ visibleZoomRange?: [number, number];
73
75
  }
74
76
  /** Camera state, both an input (options/setView) and an output (getViewState). */
75
77
  export interface CameraState {
@@ -1,4 +1,4 @@
1
- import { c as re, t as Ae, i as nt, a as Re, s as rt, C as ot, b as it, F as at, A as st, d as lt, R as he, e as ct, p as ut, m as dt, f as ht, g as pt, h as gt } from "./raster-pipeline-DNpKXgO3.js";
1
+ import { c as re, t as Ae, i as nt, a as Re, s as rt, C as ot, b as it, F as at, A as st, d as lt, R as he, e as ct, p as ut, m as dt, f as ht, g as pt, h as gt } from "./raster-pipeline-GF1IWgfR.js";
2
2
  import pe from "./index-CW1n5LdO.js";
3
3
  function mt(e, t) {
4
4
  const n = e.length / 3, r = new Uint8ClampedArray(n * 4), o = 0, i = n, a = n * 2;
@@ -1069,7 +1069,7 @@ A.set(m.Zstd, () => import("./zstd-jXobGRcq.js").then((e) => e.decode));
1069
1069
  A.set(m.Jpeg, () => Promise.resolve(oe));
1070
1070
  A.set(m.Jpeg6, () => Promise.resolve(oe));
1071
1071
  A.set(m.Webp, () => Promise.resolve(oe));
1072
- A.set(m.Lerc, () => import("./lerc-CMJO8KZO.js").then((e) => e.l).then((e) => e.decode));
1072
+ A.set(m.Lerc, () => import("./lerc-CGMS_bHf.js").then((e) => e.l).then((e) => e.decode));
1073
1073
  async function ie(e, t, n) {
1074
1074
  const r = A.get(t);
1075
1075
  if (!r)
@@ -1,5 +1,5 @@
1
1
  import { w as ae } from "./mgrs-BY9bIvp4.js";
2
- import { am as ce, an as le, ao as ee, ap as te, aq as ue, b as he, ar as de, l as Z, as as fe, d as pe, at as me, au as ge, av as ve, aw as ne } from "./index-CuBGRyG-.js";
2
+ import { am as ce, an as le, ao as ee, ap as te, aq as ue, b as he, ar as de, l as Z, as as fe, d as pe, at as me, au as ge, av as ve, aw as ne } from "./index-DncpMROY.js";
3
3
  function Pe(o, e, t) {
4
4
  const { projectedCorners: n } = e, { topLeft: s, topRight: r, bottomRight: a, bottomLeft: i } = n, c = t(s[0], s[1]), u = t(r[0], r[1]), l = t(a[0], a[1]), d = t(i[0], i[1]), f = [
5
5
  c,
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  var Re = Object.defineProperty;
2
2
  var Fe = (t, e, s) => e in t ? Re(t, e, { enumerable: !0, configurable: !0, writable: !0, value: s }) : t[e] = s;
3
3
  var oe = (t, e, s) => Fe(t, typeof e != "symbol" ? e + "" : e, s);
4
- import { jsxs as X, jsx as O } from "react/jsx-runtime";
4
+ import { jsxs as U, jsx as O } from "react/jsx-runtime";
5
5
  import { createContext as Pe, useContext as xe, forwardRef as Ce, useRef as b, useState as $, useId as ce, useCallback as se, useMemo as M, useLayoutEffect as ie, useEffect as F, useImperativeHandle as Te, useSyncExternalStore as de } from "react";
6
6
  import { MANAGED_SLOTS as ke, slotContainerStyle as Me, FOLD_SIDES as A, MapController as je, parseWidgetStyle as Le, foldToggleSlot as $e, foldToggleStyle as De, foldPanelStyle as _e, foldDrawerStyle as Ae, resolveFoldBreakpointPx as Ie, shouldFoldAtWidth as Ke, DEFAULT_FOLD_BREAKPOINT_PX as ze, resolveSlot as He, foldSideForSlot as Ne, isStoreToken as We } from "@nika-js/onlymap";
7
7
  import { createPortal as ue } from "react-dom";
@@ -11,14 +11,14 @@ function W(t) {
11
11
  if (!e) throw new Error(`[onlymapjs] ${t} must be rendered inside <OmMap>.`);
12
12
  return e;
13
13
  }
14
- function Q(t, e) {
14
+ function X(t, e) {
15
15
  if (t === e) return !0;
16
16
  const s = Object.keys(t ?? {}), n = Object.keys(e ?? {});
17
17
  return s.length !== n.length ? !1 : s.every((i) => t[i] === e[i]);
18
18
  }
19
19
  function Be(t, e) {
20
20
  const { props: s, updateTriggers: n, ...i } = t, { props: p, updateTriggers: c, ...l } = e;
21
- return Q(i, l) && Q(s, p) && Q(n, c);
21
+ return X(i, l) && X(s, p) && X(n, c);
22
22
  }
23
23
  class qe {
24
24
  constructor(e) {
@@ -48,7 +48,7 @@ const ot = Ce(function(e, s) {
48
48
  () => Object.fromEntries(N.map((r) => [r, null]))
49
49
  ), [R, I] = $(!1), [q, me] = $(Je), [ye, ge] = $(
50
50
  () => Object.fromEntries(A.map((r) => [r, 0]))
51
- ), [be, V] = $(null), Z = `om-react-fold-${ce().replaceAll(":", "")}`, he = b(new Map(A.map((r) => [r, /* @__PURE__ */ new Set()]))), ee = d ? JSON.stringify(d) : "", K = b({
51
+ ), [be, V] = $(null), Q = `om-react-fold-${ce().replaceAll(":", "")}`, he = b(new Map(A.map((r) => [r, /* @__PURE__ */ new Set()]))), ee = d ? JSON.stringify(d) : "", K = b({
52
52
  badge: null,
53
53
  attribution: null
54
54
  }), ve = se((r) => {
@@ -82,8 +82,8 @@ const ot = Ce(function(e, s) {
82
82
  if (!r) return;
83
83
  let o, u = ze;
84
84
  const f = (_) => {
85
- const U = getComputedStyle(r).getPropertyValue("--om-widget-fold-breakpoint").trim();
86
- U !== o && (o = U, u = Ie(U, r)), I((Ee) => g && Ke(_, u, Ee));
85
+ const Z = getComputedStyle(r).getPropertyValue("--om-widget-fold-breakpoint").trim();
86
+ Z !== o && (o = Z, u = Ie(Z, r)), I((Ee) => g && Ke(_, u, Ee));
87
87
  };
88
88
  if (f(r.getBoundingClientRect().width || (d ? d === !0 ? 800 : d.width ?? 800 : 0)), typeof ResizeObserver > "u") return;
89
89
  const S = new ResizeObserver(([_]) => f(_.contentRect.width));
@@ -160,7 +160,7 @@ const ot = Ce(function(e, s) {
160
160
  () => a && G && k ? { controller: a, layers: G, overlayPane: k, corners: C, widgetsHidden: !!v, widgetsFolded: R, foldPanels: q, registerFoldedWidget: te } : null,
161
161
  [a, G, k, C, v, R, q, te]
162
162
  );
163
- return /* @__PURE__ */ X("div", { ref: E, className: y, style: { position: "relative", overflow: "hidden", ...x }, children: [
163
+ return /* @__PURE__ */ U("div", { ref: E, className: y, style: { position: "relative", overflow: "hidden", ...x }, children: [
164
164
  /* @__PURE__ */ O("div", { ref: T, style: { position: "absolute", inset: 0 } }),
165
165
  /* @__PURE__ */ O(
166
166
  "div",
@@ -169,7 +169,7 @@ const ot = Ce(function(e, s) {
169
169
  style: { position: "absolute", top: 0, left: 0, zIndex: 10, pointerEvents: "none" }
170
170
  }
171
171
  ),
172
- /* @__PURE__ */ O("div", { "data-om-widget-layer": "", style: we, children: N.map((r) => /* @__PURE__ */ X(
172
+ /* @__PURE__ */ O("div", { "data-om-widget-layer": "", style: we, children: N.map((r) => /* @__PURE__ */ U(
173
173
  "div",
174
174
  {
175
175
  "data-om-widget-slot": r,
@@ -181,7 +181,7 @@ const ot = Ce(function(e, s) {
181
181
  children: [
182
182
  R && A.filter((o) => $e(o) === r && ye[o] > 0).map((o) => {
183
183
  const u = be === o;
184
- return /* @__PURE__ */ X(
184
+ return /* @__PURE__ */ U(
185
185
  "div",
186
186
  {
187
187
  "data-om-fold-drawer": o,
@@ -197,7 +197,7 @@ const ot = Ce(function(e, s) {
197
197
  type: "button",
198
198
  "data-om-fold-toggle": o,
199
199
  "aria-expanded": u,
200
- "aria-controls": `${Z}-${o}`,
200
+ "aria-controls": `${Q}-${o}`,
201
201
  "aria-label": `${u ? "Close" : "Open"} ${o} map controls`,
202
202
  style: De(),
203
203
  onClick: () => {
@@ -213,7 +213,7 @@ const ot = Ce(function(e, s) {
213
213
  /* @__PURE__ */ O(
214
214
  "div",
215
215
  {
216
- id: `${Z}-${o}`,
216
+ id: `${Q}-${o}`,
217
217
  ref: Oe[o],
218
218
  "data-om-fold-panel": o,
219
219
  role: "region",
@@ -277,19 +277,20 @@ const ot = Ce(function(e, s) {
277
277
  flush: "flush",
278
278
  refresh: "refresh",
279
279
  terrain: "terrain",
280
+ visibleZoomRange: "visibleZoomRange",
280
281
  updateTriggers: "updateTriggers"
281
- }, Ue = /* @__PURE__ */ new Set([...Object.keys(pe), "onClick", "onHover"]);
282
- function Xe(t) {
282
+ }, Ze = /* @__PURE__ */ new Set([...Object.keys(pe), "onClick", "onHover"]);
283
+ function Ue(t) {
283
284
  const e = {};
284
285
  for (const [n, i] of Object.entries(t))
285
- !Ue.has(n) && i !== void 0 && (e[n] = i);
286
+ !Ze.has(n) && i !== void 0 && (e[n] = i);
286
287
  const s = {};
287
288
  for (const [n, i] of Object.entries(pe))
288
289
  s[i] = t[n];
289
290
  return Object.keys(e).length > 0 && (s.props = e), s;
290
291
  }
291
292
  function st(t) {
292
- const { controller: e, layers: s } = W("OmLayer"), { id: n } = t, i = Xe(t);
293
+ const { controller: e, layers: s } = W("OmLayer"), { id: n } = t, i = Ue(t);
293
294
  F(() => {
294
295
  s.upsert(i);
295
296
  }), F(() => () => s.remove(n), [s, n]);
@@ -338,7 +339,7 @@ function it({ position: t = "top-start", order: e, fold: s, className: n, style:
338
339
  function le(t, e, s, n) {
339
340
  return e && (t == null ? void 0 : t.layerId) !== e ? !1 : !s || (t ? t.type : n) === s;
340
341
  }
341
- const Qe = {
342
+ const Xe = {
342
343
  "top-left": "translate(0, 0)",
343
344
  "top-center": "translate(-50%, 0)",
344
345
  "top-right": "translate(-100%, 0)",
@@ -382,7 +383,7 @@ function at(t) {
382
383
  w.style.visibility = "hidden";
383
384
  return;
384
385
  }
385
- w.style.visibility = "visible", w.style.transform = `translate(${D}px, ${R}px) ${Qe[B ?? "bottom-center"]}`;
386
+ w.style.visibility = "visible", w.style.transform = `translate(${D}px, ${R}px) ${Xe[B ?? "bottom-center"]}`;
386
387
  }),
387
388
  [e]
388
389
  ), F(() => {
@@ -345,6 +345,8 @@ export declare class RuntimeCore {
345
345
  private bimGeoreference;
346
346
  /** Rendered deck layer id → authored manifest/controller id (terrain uses fresh renderer ids). */
347
347
  private renderedLayerIds;
348
+ /** Zoom-visibility gate fingerprint (see computeZoomGateKey) — camera moves rebuild layers only when this changes. */
349
+ private zoomGateKey;
348
350
  /** The decoded property table for a pick-features layer, or undefined before its first tile. */
349
351
  getFeatureTable(layerId: string): Record<string, unknown>[] | undefined;
350
352
  /**
@@ -519,7 +521,7 @@ export declare class RuntimeCore {
519
521
  * consumers rendering exports must add provider credits themselves.
520
522
  * Headless rejects (no renderer); pre-ready rejects (await map.ready).
521
523
  */
522
- /** The basemap's own settle state (style + tiles for the current view) — true when standalone/headless or the chunk hasn't landed. */
524
+ /** The basemap's own settle state (style + tiles for the current view) — true when standalone/headless. NOT vacuously true while a basemap-mode adapter is absent (initial chunk load, a determinism-flip remount): a settle or paced gate polling that window must hold until the mount lands, not capture a frame with no basemap canvas. */
523
525
  basemapIdle(): boolean;
524
526
  /** deck's own per-layer readiness (async layer/pipeline init) — whenSettled gates on this too; a capture before it is stably missing content (issue #37's flaky first frame). Headless/basemap-pending count as ready. */
525
527
  layersReady(): boolean;
@@ -571,6 +573,16 @@ export declare class RuntimeCore {
571
573
  */
572
574
  reconcile(irs: LayerIR[]): void;
573
575
  private buildLayers;
576
+ /** Fingerprint of every zoom-gated layer's in/out state at the current zoom — "" when no layer authors a range (the overwhelmingly common case, so refreshZoomGates is two property reads per camera move). */
577
+ private computeZoomGateKey;
578
+ /**
579
+ * Camera-move hook for `visible-zoom-range` (public issue #38): rebuilds
580
+ * layers ONLY when some layer's gate crosses its boundary — never per
581
+ * frame. Called from every viewport-change signal site (deck's
582
+ * onViewStateChange, the basemap adapter's move events, and the instant
583
+ * camera paths that bypass both).
584
+ */
585
+ private refreshZoomGates;
574
586
  private applyLayers;
575
587
  /**
576
588
  * The sanctioned per-frame DOM bypass (spec: "Map Stories / per-frame
package/dist/version.d.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * the build rootDir, and a `define` would need repeating across vite/vitest/
6
6
  * vite-node configs.
7
7
  */
8
- export declare const LIBRARY_VERSION = "0.6.21";
8
+ export declare const LIBRARY_VERSION = "0.6.23";
@@ -1,5 +1,5 @@
1
- import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, g as pr, h as mr } from "./raster-pipeline-DNpKXgO3.js";
2
- import { ap as gr } from "./index-CuBGRyG-.js";
1
+ import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, g as pr, h as mr } from "./raster-pipeline-GF1IWgfR.js";
2
+ import { ap as gr } from "./index-DncpMROY.js";
3
3
  import $t from "./index-CW1n5LdO.js";
4
4
  var Et;
5
5
  function h(e, t, n) {
package/docs/stories.md CHANGED
@@ -135,6 +135,8 @@ Each paced frame emits `om-paced-tick` on the story (`detail = {t, waitedMs}`);
135
135
 
136
136
  Effect verbs animate on the **story clock** during a paced run: `fade`, `pulse`, `trace`, and `populate` are evaluated as functions of story time, so a 2 s trace spans exactly 2 s of output frames — half-drawn outlines and mid-fade opacities land in the frames exactly as authored, however long each frame takes to capture. (One exception: `trace follow` is skipped while the story drives the camera — the paced route owns it.)
137
137
 
138
+ Paced frames also wait for the **basemap** to settle — camera done, tiles loaded, label placement converged — so recorded basemap takes carry their labels on every frame, not just static ones. For exactly reproducible first frames, author a start camera (`center`/`zoom` on `<om-map>`): without one, the initial pose is "wherever the camera is at first capture", which can land differently between runs.
139
+
138
140
  ### Recording a story to video
139
141
 
140
142
  ```bash
package/llms.txt CHANGED
@@ -49,6 +49,7 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
49
49
  - Fixed-route 3D-tile pre-loading: `<om-story warm-tiles>` pre-fetches AND parses every 3D tileset's tiles along the story's fly-to route in the background at load (deck's own flight arc, sampled), so a flyby plays sharp instead of "blurry then clear" — or dispatch the `warm-tiles` action (`{story?, samples?, budget?}`; default budget raises each tileset cache to 256 MB, raise-only) manually before a take. Completion: `om-tiles-warmed` on `<om-map>`. Persistent per-layer knob: `load-options='{"tileset":{"maximumMemoryUsage":512}}'`.
50
50
  - Load-paced ("clean") flyby: `<om-story paced>` steps its own clock frame by frame (optionally `paced="60"` story-fps, default 30), drives the camera itself along the fly-to route, and never advances while any 3D tileset is still refining — NO frame ever shows unrefined tiles, at the cost of wall-clock time (playback is not real-time; use for recorded takes or heavy tilesets — Google Photorealistic 3D Tiles — where no pre-warm fits the flight in cache). Per-frame `om-paced-tick` on the story (`detail = {t, waitedMs}`; `waitedMs > 0` = that frame paused for tiles). Composes with `warm-tiles` (warm first → shorter waits). Paced runs: only `fly-to` steps steer the camera (`zoom-to-feature` etc. are skipped with a warning), and user gestures do NOT pause playback — use the player widget or `story-pause`. VIDEO OUTPUT: `npx onlymapjs record map.html --out flyby.mp4` (needs dev-installed playwright; ffmpeg for assembly, else PNG frames + the command to run) plays the story paced in headless Chromium and writes a video where every frame is fully refined — widgets/overlays/attribution included; options `--story/--fps/--width/--height/--scale/--gpu/--keep-frames/--timeout/--max-hold` (`--gpu` = hardware rendering instead of headless software GL — ~3× shorter tile holds on heavy 3D scenes, recommended); frames survive a deadline hit in `<out>.frames/` for salvage. Per-frame tile waits are capped by `paced-max-hold` on `<om-story>` (duration grammar, default 10s; `--max-hold` sets it) — frames that keep hitting the cap may stay slightly blurry; raise it, or set `"none"` (`--max-hold none`) for an absolute gate: guaranteed-sharp takes, only the overall timeout bounds the run. Custom recorders: `storyEl.setPacedCapture(async (tick) => {...})` — awaited per frame BEFORE that frame's om-paced-tick, so capture is race-free and the ended-state tick means all frames captured. Effect verbs (fade/pulse/trace/populate) animate on the STORY clock during paced runs — recorded frames capture traces half-drawn and fades mid-flight exactly as authored (`trace follow` is skipped; the paced route owns the camera).
51
51
  - `<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. Scene actions are story-steppable AND scrub-capturable: `set-basemap`, `set-lighting` (a sunset story: steps walking sun-elevation down; a bare preset step is a clean reset), and `set-terrain` all rewind on seek — the story captures the map's scene attributes before first play. 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).
52
+ - Zoom visibility: `visible-zoom-range="[8, 14]"` on ANY layer type — scale-dependent visibility (hidden outside min ≤ zoom < max, the minzoom/maxzoom convention; the tile-only visible-min-zoom/visible-max-zoom deck props remain tile-layer-specific). Composes with `visible`; a zoom-hidden layer stays in the legend/switcher/ctx. For CI that emits om-layer markup, validate attributes per layer type against the shipped `onlymapjs.attributes.json` (generated from the same registry the runtime checks — `universal` + `layers[<type>]`), not the flat per-tag union in the IntelliSense html-data file. Also: a layer whose REQUIRED position accessor resolves undefined for every row (e.g. a TextLayer fed GeoJSON with no get-position) now warns at runtime with the exact fix (`get-position="$geometry.coordinates"`) instead of silently drawing nothing.
52
53
  - Filtering: `filter-field="magnitude" filter-range="[4, 10]"` on a layer (GPU-side, live-updatable via the `filter-layer` action); pair with `<om-widget type="filter" layer="..." field="...">`. For an epoch-millisecond field, make the slider labels readable with `<om-widget type="filter" layer="quakes" field="time" format="date" date-style="datetime" time-zone="UTC"></om-widget>`. Up to 4 numeric dimensions at once via `filter-fields='[{"field":"magnitude","range":[4,10]},{"field":"time","range":[…]}]'` (JSON array, additive to filter-field/filter-range — wins if both are authored) — one `<om-widget type="filter">` per field, each moves its own dimension independently (filter-layer merges the range onto the matching field rather than replacing the whole filter); a row must pass every active dimension (AND). A dimension can't be added live — the full set is declared up front in filter-fields. Categorical filtering is a SEPARATE mechanism (deck.gl's own discrete keep-list test, not a range) with its own attributes: `filter-category="fuel" filter-categories='["Coal","Gas"]'` (single) or `filter-category-fields='[{"field":"fuel","categories":[...]},...]'` (up to 4); the SAME `<om-widget type="filter">` auto-renders checkboxes instead of a slider when its `field` is declared categorically (mode is inferred from the layer's own filter, never a separate widget attribute) — one checkbox per distinct value present in the data, with its row count. A category dimension with no keep-list is dropped from the active filter (there is no "matches everything" category the way a numeric range has [-Infinity, Infinity]). Numeric and categorical filters on the same layer combine — a row must pass both. `ctx.stats`/`ctx.dataInViewport` respect whichever kind(s) are active by default (`{filtered:false}` opts out). CLASSIFIED SYMBOLOGY: `classify-by="<numeric field>"` (+ `classify-scale="quantile|equal-interval|jenks"`, `classify-classes="2-12"` default 5, `classify-ramp="viridis|plasma|inferno|magma|cividis|turbo|blues|greens|oranges|purples|reds|ylorrd|rdbu|spectral"`) computes class breaks FROM THE DATA at reconcile time, installs the fill-color accessor and the auto classes legend — use it when the user asks for graduated/choropleth styling WITHOUT hand-authoring domains; an authored get-fill-color/color always wins (validation warns on the conflict); URL-backed layers classify when their data arrives. TEMPORAL PLAYBACK: `<om-widget type="time-slider" layer="…" field="<numeric/epoch-ms field>" duration="20s" window="<span in field units>" loop format="date" date-style time-zone>` — play/pause/scrub emitting the ordinary filter-layer action (cumulative from the domain start, or a sliding window with `window`); manifest stays the source of truth so undo/story/external filter edits re-sync the thumb — prefer it over hand-rolling a playback loop.
53
54
  - Routing & tracking are two library-owned layer types (not `PathLayer`/`IconLayer` hand-wired) that expand into ordinary `PathLayer`/`IconLayer` instances internally, same pattern as `BIMLayer`→`Tile3DLayer`. `<om-layer type="Route" geometry='{"type":"LineString","coordinates":[[lng,lat],...]}'>` draws a styled route (casing + line + origin/destination pins) from geometry you already have — resolves SYNCHRONOUSLY, no network. `<om-layer type="Route" origin="[lng,lat]" destination="[lng,lat]" provider="nika" profile="driving">` (+ optional `waypoints`) resolves one ASYNCHRONOUSLY via a `RoutingProvider` named by `provider` — `"nika"` is registered by default but its endpoint is an UNVERIFIED PLACEHOLDER until NIKA's real routing service ships (register a working one with `OmMap.registerRoutingProvider(name, provider)` — a ~15-line adapter over OSRM's keyless public demo server (`router.project-osrm.org/route/v1/{profile}/{lng},{lat};{lng},{lat}?geometries=geojson&overview=full`, map `distance`/`duration`/`legs` onto `distanceMeters`/`durationSec`/`legs[]`) is the verified keyless real-data recipe; the skill's syntax.md carries it in full). `geometry` wins outright if both are authored (validation warns). `color`/`casing-color` style the line; `follow="fit-route"` auto-fits the camera once resolved. `<om-layer type="Tracking" get-position="[$lng,$lat]">` renders ONE moving entity (v1 — a fleet is one `Tracking` layer per vehicle) with bearing-derived icon rotation; position data arrives through the ORDINARY `data`/`source` mechanism, no separate tracking-subscription API. `bearing-field` (default `"bearing"`) names the plain field to rotate by (checks `properties.<field>` on GeoJSON rows, `<field>` directly on flat rows); `interpolate-ms` (default `1000`) glides the marker between two fixes via the per-frame channel instead of jumping; `follow="follow"` eases the camera along with it, same timing. `color`/`size` style the marker; `icon="arrow|car|motorcycle"` picks the shape (default arrow — all nose-up, baked in `color`, unknown names fall back with a validation warning). Tail modes: on the ROUTE layer, `progress-from="<tracking-layer-id>"` + `tail="none"` (client view — only current position → destination renders, origin pin dropped) or `tail="dim"` (operator view — traveled portion darkened; `tail-color` overrides) split the route at the marker's interpolated position per frame; default full ignores the split; validation warns on partial wiring.
54
55