@nika-js/onlymap 0.7.4 → 0.7.5

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/CHANGELOG.md CHANGED
@@ -8,6 +8,11 @@ Note: npm collapsed a few closely-spaced releases — the GPX/FlatGeobuf (0.5.4)
8
8
  and GeoParquet (0.5.5) work shipped to npm together as **0.5.6**, so npm's
9
9
  version list jumps 0.5.3 → 0.5.6. Each logical version is listed here regardless.
10
10
 
11
+ ## 0.7.5 — 2026-09-11
12
+
13
+ ### Added
14
+ - **Share any map as one file: `npx @nika-js/onlymap export map.html`.** Writes a portable copy that opens on any computer — relative data is fetched and embedded with format detection preserved (GeoJSON, CSV, Arrow, FlatGeobuf, GeoParquet and friends), library references become pinned CDN tags, and a no-JS fallback is added if missing. Warns when embedded data is heavy, and says exactly which sources must stay network-hosted (tile streams, COGs, shapefiles) and why.
15
+
11
16
  ## 0.7.4 — 2026-09-04
12
17
 
13
18
  ### Changed
package/README.md CHANGED
@@ -192,6 +192,8 @@ npx @nika-js/onlymap init
192
192
 
193
193
  That opt-in command updates `.vscode/settings.json` and copies the `!`-prefixed manifest snippets into `.vscode/onlymap.code-snippets`. It never runs automatically during install.
194
194
 
195
+ **Share a map as one file:** `npx @nika-js/onlymap export map.html` writes a portable copy that opens on any computer — every relative data URL is fetched and embedded (format detection preserved: GeoJSON, CSV, Arrow, FlatGeobuf, GeoParquet and friends keep working), relative library references become pinned CDN tags, and a no-JS `<om-fallback>` is added if missing. Heavy embedded data gets a size warning; basemaps, terrain, tile streams, COGs and shapefiles stay network-backed (the tool says which and why).
196
+
195
197
  ```jsonc
196
198
  // .vscode/settings.json
197
199
  { "html.customData": ["./node_modules/@nika-js/onlymap/onlymapjs.html-data.json"] }
package/bin/onlymapjs.mjs CHANGED
@@ -13,10 +13,18 @@ function usage() {
13
13
  onlymapjs init [--force]
14
14
  onlymapjs check-layout <manifest.html>
15
15
  onlymapjs record <manifest.html> [options]
16
+ onlymapjs export <manifest.html> [options]
16
17
  onlymapjs --help
17
18
 
18
19
  Commands:
19
20
  init Configure the current project for OnlyMapJS authoring in VS Code.
21
+ export Write a portable copy of a map page that opens on any computer:
22
+ every relative data/src/scenegraph URL is fetched and embedded as
23
+ a data: URL (format detection preserved — CSV, GeoJSON, Arrow,
24
+ FlatGeobuf, GeoParquet all keep working), relative library
25
+ references become pinned CDN tags, and an <om-fallback> is added
26
+ if missing. Remote http(s) URLs, basemaps, terrain, and tile
27
+ streams are left as-is — those still need network access.
20
28
  check-layout
21
29
  Load a browser-runnable manifest in isolated headless Chromium,
22
30
  audit real widget geometry at 360/640/768/1024px, and exit 0/1.
@@ -30,6 +38,12 @@ Commands:
30
38
  Options (init):
31
39
  --force Overwrite .vscode/${SNIPPET_FILE} if it already exists.
32
40
 
41
+ Options (export):
42
+ --out <file> Output path (default: <manifest>.export.html).
43
+ --root <dir> Directory that root-absolute URLs ("/data/x.json") resolve
44
+ against (default: the manifest's own directory).
45
+ --force Overwrite an existing output file.
46
+
33
47
  Options (record):
34
48
  --story <id> Which <om-story> to record (default: the first one).
35
49
  --out <file> Output path; .mp4 (H.264) or .webm (VP9). Default <manifest>.mp4.
@@ -537,6 +551,226 @@ function writeJson(path, value) {
537
551
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
538
552
  }
539
553
 
554
+ // --- export: portable single-file map pages --------------------------------
555
+
556
+ /** Attributes that may carry a fetchable single-file URL worth embedding. */
557
+ const EXPORT_URL_ATTRS = ["data", "src", "scenegraph"];
558
+
559
+ /**
560
+ * Extension → data-URL MIME. The library's format detection reads the URL
561
+ * extension (which survives as a `#name.ext` suffix — base64 contains no
562
+ * dots, so the suffix is the first extension-shaped match) and, for some
563
+ * formats, the Content-Type; simple MIMEs only, since a dotted MIME like
564
+ * application/vnd.google-earth.kml could shadow the suffix.
565
+ */
566
+ const EXPORT_MIME = {
567
+ json: "application/json",
568
+ geojson: "application/json",
569
+ jsonl: "application/json",
570
+ csv: "text/csv",
571
+ tsv: "text/tab-separated-values",
572
+ kml: "application/xml",
573
+ gpx: "application/xml",
574
+ fgb: "application/octet-stream",
575
+ parquet: "application/octet-stream",
576
+ geoparquet: "application/octet-stream",
577
+ arrow: "application/octet-stream",
578
+ feather: "application/octet-stream",
579
+ glb: "model/gltf-binary",
580
+ ifc: "application/octet-stream",
581
+ png: "image/png",
582
+ jpg: "image/jpeg",
583
+ jpeg: "image/jpeg",
584
+ webp: "image/webp",
585
+ };
586
+
587
+ /** Single files that cannot be embedded: the reader needs more than these bytes. */
588
+ const EXPORT_SKIP_EXTENSIONS = {
589
+ shp: "shapefiles fetch .dbf/.prj sidecars next to the .shp URL",
590
+ tif: "COGs stream by HTTP Range request",
591
+ tiff: "COGs stream by HTTP Range request",
592
+ zarr: "Zarr stores are directories of chunk files",
593
+ };
594
+
595
+ const EXPORT_WARN_FILE_BYTES = 1 * 1024 * 1024; // per-file "this is getting heavy"
596
+ const EXPORT_WARN_TOTAL_BYTES = 5 * 1024 * 1024; // whole-page ceiling worth flagging
597
+
598
+ const formatBytes = (n) => (n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)} MB` : `${Math.ceil(n / 1024)} KB`);
599
+
600
+ /**
601
+ * Attribute values legally contain ">" (accessor expressions like
602
+ * `$value > 100 ? …`), so tags are scanned with quote awareness instead of
603
+ * a `[^>]*` regex.
604
+ */
605
+ function* scanTags(html, tagName) {
606
+ const open = new RegExp(`<${tagName}(?=[\\s/>])`, "gi");
607
+ let m;
608
+ while ((m = open.exec(html))) {
609
+ let i = m.index + m[0].length;
610
+ let quote = null;
611
+ while (i < html.length) {
612
+ const c = html[i];
613
+ if (quote) {
614
+ if (c === quote) quote = null;
615
+ } else if (c === '"' || c === "'") quote = c;
616
+ else if (c === ">") break;
617
+ i += 1;
618
+ }
619
+ yield { start: m.index, end: i + 1, text: html.slice(m.index, i + 1) };
620
+ }
621
+ }
622
+
623
+ function readTagAttr(tagText, attr) {
624
+ const m = new RegExp(`\\b${attr}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i").exec(tagText);
625
+ return m ? { value: m[2] ?? m[3], start: m.index, length: m[0].length, raw: m[0] } : null;
626
+ }
627
+
628
+ function urlExtensionOf(url) {
629
+ return /\.(\w+)(?:\?|#|$)/.exec(url)?.[1]?.toLowerCase();
630
+ }
631
+
632
+ async function exportStandalone(rest) {
633
+ const valueFlags = new Set(["--out", "--root"]);
634
+ const positional = [];
635
+ for (let i = 0; i < rest.length; i += 1) {
636
+ if (valueFlags.has(rest[i])) i += 1; // skip the flag's value
637
+ else if (!rest[i].startsWith("--")) positional.push(rest[i]);
638
+ }
639
+ if (positional.length !== 1) throw new Error("Usage: onlymapjs export <manifest.html> [--out file] [--root dir] [--force]");
640
+ const flagValue = (flag) => {
641
+ const i = rest.indexOf(flag);
642
+ return i >= 0 ? rest[i + 1] : undefined;
643
+ };
644
+ const inputPath = resolve(positional[0]);
645
+ if (!existsSync(inputPath)) throw new Error(`No such file: ${inputPath}`);
646
+ const inputDir = dirname(inputPath);
647
+ const rootDir = resolve(flagValue("--root") ?? inputDir);
648
+ const outPath = resolve(flagValue("--out") ?? inputPath.replace(/\.html?$/i, "") + ".export.html");
649
+ if (resolve(outPath) === inputPath) throw new Error("--out must differ from the input file.");
650
+ if (existsSync(outPath) && !rest.includes("--force")) throw new Error(`${outPath} already exists; pass --force to overwrite it.`);
651
+
652
+ let html = readFileSync(inputPath, "utf8");
653
+ const warnings = [];
654
+ const notes = [];
655
+ const inlined = [];
656
+ let totalBytes = 0;
657
+
658
+ // 1. Embed relative data/src/scenegraph URLs on <om-layer> tags.
659
+ const edits = []; // { start, end, replacement } over the ORIGINAL html, applied back-to-front
660
+ for (const tag of scanTags(html, "om-layer")) {
661
+ for (const attr of EXPORT_URL_ATTRS) {
662
+ const found = readTagAttr(tag.text, attr);
663
+ if (!found || !found.value) continue;
664
+ const url = found.value;
665
+ if (/^(https?:|wss?:|data:|blob:|draw:)/i.test(url)) {
666
+ if (/^wss?:/i.test(url)) notes.push(`${attr}="${url}" is a live stream — it will need network access wherever the file opens.`);
667
+ continue; // remote/self-describing URLs travel as-is
668
+ }
669
+ if (/\{[zxy]\}/.test(url)) {
670
+ notes.push(`${attr}="${url}" is a tile template — tile streams cannot be embedded and stay network-backed.`);
671
+ continue;
672
+ }
673
+ const ext = urlExtensionOf(url);
674
+ if (ext && EXPORT_SKIP_EXTENSIONS[ext]) {
675
+ warnings.push(`${attr}="${url}" was NOT embedded (${EXPORT_SKIP_EXTENSIONS[ext]}). Host it at an absolute URL for the exported file to work elsewhere.`);
676
+ continue;
677
+ }
678
+ const cleanPath = decodeURIComponent(url.split(/[?#]/)[0]);
679
+ const filePath = cleanPath.startsWith("/") ? join(rootDir, cleanPath) : resolve(inputDir, cleanPath);
680
+ if (!existsSync(filePath)) {
681
+ warnings.push(`${attr}="${url}" resolves to ${filePath}, which does not exist — left untouched.`);
682
+ continue;
683
+ }
684
+ const bytes = readFileSync(filePath);
685
+ const mime = (ext && EXPORT_MIME[ext]) || "application/json";
686
+ const name = basename(cleanPath);
687
+ const dataUrl = `data:${mime};base64,${bytes.toString("base64")}#${encodeURIComponent(name)}`;
688
+ edits.push({
689
+ start: tag.start + found.start,
690
+ end: tag.start + found.start + found.length,
691
+ replacement: `${attr}="${dataUrl}"`,
692
+ });
693
+ totalBytes += bytes.length;
694
+ inlined.push({ url, bytes: bytes.length });
695
+ if (bytes.length > EXPORT_WARN_FILE_BYTES) {
696
+ warnings.push(
697
+ `${name} is ${formatBytes(bytes.length)} — heavy for an embedded page (base64 adds ~33%). ` +
698
+ `Consider hosting it at an absolute URL instead of embedding.`,
699
+ );
700
+ }
701
+ }
702
+ }
703
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
704
+ html = html.slice(0, edit.start) + edit.replacement + html.slice(edit.end);
705
+ }
706
+
707
+ // 2. Pin relative library references to the CDN at this package's version.
708
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
709
+ const version = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version;
710
+ const cdnScript = `https://unpkg.com/@nika-js/onlymap@${version}`;
711
+ const cdnCss = `https://unpkg.com/@nika-js/onlymap@${version}/dist/onlymapjs.css`;
712
+ let sawLibScript = false;
713
+ let sawLibCss = false;
714
+ const libEdits = [];
715
+ for (const tag of scanTags(html, "script")) {
716
+ const src = readTagAttr(tag.text, "src");
717
+ if (!src?.value) continue;
718
+ const isLib = /@nika-js\/onlymap|onlymapjs?\.(m?js|ts)|onlymap\.standalone\.js|\/src\/index\.ts/.test(src.value);
719
+ if (!isLib) continue;
720
+ sawLibScript = true;
721
+ if (/^https?:/i.test(src.value)) continue; // already CDN/hosted
722
+ libEdits.push({ start: tag.start + src.start, end: tag.start + src.start + src.length, replacement: `src="${cdnScript}"` });
723
+ }
724
+ for (const tag of scanTags(html, "link")) {
725
+ const href = readTagAttr(tag.text, "href");
726
+ if (!href?.value || !/onlymapjs\.css/.test(href.value)) continue;
727
+ sawLibCss = true;
728
+ if (/^https?:/i.test(href.value)) continue;
729
+ libEdits.push({ start: tag.start + href.start, end: tag.start + href.start + href.length, replacement: `href="${cdnCss}"` });
730
+ }
731
+ for (const edit of libEdits.sort((a, b) => b.start - a.start)) {
732
+ html = html.slice(0, edit.start) + edit.replacement + html.slice(edit.end);
733
+ }
734
+ if (!sawLibScript || !sawLibCss) {
735
+ // A bare-manifest page (no library tags at all): inject the pinned pair
736
+ // so the file boots anywhere. Before </head> when one exists, else at top.
737
+ const inject =
738
+ (sawLibCss ? "" : `<link rel="stylesheet" href="${cdnCss}" />\n`) +
739
+ (sawLibScript ? "" : `<script type="module" src="${cdnScript}"></script>\n`);
740
+ const headClose = /<\/head>/i.exec(html);
741
+ html = headClose ? html.slice(0, headClose.index) + inject + html.slice(headClose.index) : inject + html;
742
+ notes.push("The page had no OnlyMapJS reference — pinned CDN tags were injected.");
743
+ }
744
+
745
+ // 3. A no-JS fallback for file-preview contexts (the export's whole audience).
746
+ if (!/<om-fallback[\s>]/i.test(html)) {
747
+ const mapOpen = [...scanTags(html, "om-map")][0];
748
+ if (mapOpen) {
749
+ const fallback =
750
+ `\n <om-fallback><p><strong>This interactive map requires JavaScript.</strong><br/>` +
751
+ `If you are seeing this in a file preview, open the file in a web browser such as Chrome, Safari, or Firefox.</p></om-fallback>`;
752
+ html = html.slice(0, mapOpen.end) + fallback + html.slice(mapOpen.end);
753
+ notes.push("Added a generic <om-fallback> (none was authored).");
754
+ }
755
+ }
756
+
757
+ writeFileSync(outPath, html);
758
+
759
+ for (const { url, bytes } of inlined) console.log(` embedded ${url} (${formatBytes(bytes)})`);
760
+ if (totalBytes > EXPORT_WARN_TOTAL_BYTES) {
761
+ warnings.push(
762
+ `Embedded data totals ${formatBytes(totalBytes)} (~${formatBytes(Math.ceil(totalBytes * 1.34))} as base64) — ` +
763
+ `the exported page will be slow to open and share. Host the heavy layers at absolute URLs instead.`,
764
+ );
765
+ }
766
+ for (const note of notes) console.log(` note: ${note}`);
767
+ for (const warning of warnings) console.warn(` WARNING: ${warning}`);
768
+ const outSize = statSync(outPath).size;
769
+ console.log(`\nWrote ${outPath} (${formatBytes(outSize)}). Library pinned to @nika-js/onlymap@${version} via CDN.`);
770
+ console.log("Basemaps, terrain, and any remote/tiled sources still need network access when the file is opened.");
771
+ return 0;
772
+ }
773
+
540
774
  function init({ force = false } = {}) {
541
775
  const cwd = process.cwd();
542
776
  const vscodeDir = join(cwd, ".vscode");
@@ -583,6 +817,8 @@ try {
583
817
  process.exitCode = await checkLayout(rest[0]);
584
818
  } else if (command === "record") {
585
819
  process.exitCode = await recordStory(rest);
820
+ } else if (command === "export") {
821
+ process.exitCode = await exportStandalone(rest);
586
822
  } else {
587
823
  console.error(`Unknown command: ${command}\n`);
588
824
  usage();
@@ -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-C1z6aAA4.js"
22
+ "./lerc-BIQMg0FV.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, a as Sd, z as oc, 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-BirHZYjb.js";
1
+ import { C as ny, L as sy, M as oy, m as ay, c as ka, a as Sd, z as oc, 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-DkimKgOm.js";
2
2
  const Cd = Math.PI / 180, gy = 180 / Math.PI;
3
3
  function em(Pe, J = 0) {
4
4
  const me = Math.min(180, Pe) * Cd;
@@ -1,4 +1,4 @@
1
- import { r as p, n as d } from "./index-BirHZYjb.js";
1
+ import { r as p, n as d } from "./index-DkimKgOm.js";
2
2
  function g(t) {
3
3
  if (t == null) return !0;
4
4
  const e = t, n = e.id?.code;
@@ -1,4 +1,4 @@
1
- import { ae as _t, af as Ct } from "./index-BirHZYjb.js";
1
+ import { ae as _t, af as Ct } from "./index-DkimKgOm.js";
2
2
  function Yt(e, r, t = 2, i, o = "xy") {
3
3
  const s = r && r.length, l = s ? r[0] * t : e.length;
4
4
  let c = Ut(e, 0, l, t, !0, i && i[0], o);
@@ -1,5 +1,5 @@
1
- import { i as E, j as S, k as R, o as N, p as h, q as w, s as O, t as W, v as x, w as I, x as B, y as _, A as P, B as k, E as $, F as z, H as v, I as F, J as C, K as U, N as p, O as M } from "./index-BirHZYjb.js";
2
- import { P as $e, R as ze, _ as ve, Q as Ce, S as Ue, T as Me, U as je, X as De, Y as Je, Z as qe, $ as He, a0 as Ve, a1 as Ge, a2 as Ke, a3 as Qe, a4 as Xe, a5 as Ye, a6 as Ze, a7 as et, a8 as tt, a9 as nt, aa as rt, ab as at, ac as st, ad as ot } from "./index-BirHZYjb.js";
1
+ import { i as E, j as S, k as R, o as N, p as h, q as w, s as O, t as W, v as x, w as I, x as B, y as _, A as P, B as k, E as $, F as z, H as v, I as F, J as C, K as U, N as p, O as M } from "./index-DkimKgOm.js";
2
+ import { P as $e, R as ze, _ as ve, Q as Ce, S as Ue, T as Me, U as je, X as De, Y as Je, Z as qe, $ as He, a0 as Ve, a1 as Ge, a2 as Ke, a3 as Qe, a4 as Xe, a5 as Ye, a6 as Ze, a7 as et, a8 as tt, a9 as nt, aa as rt, ab as at, ac as st, ad as ot } from "./index-DkimKgOm.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 };
@@ -1,4 +1,4 @@
1
- import { ag as Be, ah as le, ai as ce, aj as De, ak as Oe, al as ke } from "./index-BirHZYjb.js";
1
+ import { ag as Be, ah as le, ai as ce, aj as De, ak as Oe, al as ke } from "./index-DkimKgOm.js";
2
2
  import { y as ve, a as he, z as G, R as J, A as ue, C as Fe, F as je, n as xe, S as Ie, E as Ne } from "./recordbatch-Bpc0uxFn.js";
3
3
  import { g as V, a as fe, b as Le, c as Ue, d as Me, e as qe, m as Ve } from "./table-accessors-CYWTzpQI.js";
4
4
  import { c as $e } from "./convert-arrow-schema-DrAihRf9.js";
@@ -30692,7 +30692,7 @@ const bee = {
30692
30692
  }, vee = {
30693
30693
  match: (t, e) => ["csv", "tsv"].includes(po(t) ?? "") || /text\/(csv|tab-separated-values)/i.test(e ?? ""),
30694
30694
  parse: async (t) => {
30695
- const [e, i, n] = await Promise.all([t.text(), import("./index-Dw5kfjba.js"), import("./index-D3prxKma.js")]), r = await n.parse(e, i.CSVLoader, { csv: { shape: "object-row-table" } });
30695
+ const [e, i, n] = await Promise.all([t.text(), import("./index-CiGUw6GD.js"), import("./index-CJ0pib8I.js")]), r = await n.parse(e, i.CSVLoader, { csv: { shape: "object-row-table" } });
30696
30696
  return _ee(r.data) ?? r.data;
30697
30697
  }
30698
30698
  };
@@ -30714,7 +30714,7 @@ const xee = {
30714
30714
  parse: async (t, e) => {
30715
30715
  t.body?.cancel().catch(() => {
30716
30716
  });
30717
- const [i, n] = await Promise.all([import("./index-D3prxKma.js"), import("./index-Cgi3Rxih.js")]), r = t.url || e, s = /* @__PURE__ */ new Map(), o = (y) => {
30717
+ const [i, n] = await Promise.all([import("./index-CJ0pib8I.js"), import("./index-HrEJtZoQ.js")]), r = t.url || e, s = /* @__PURE__ */ new Map(), o = (y) => {
30718
30718
  const v = String(y);
30719
30719
  let x = s.get(v);
30720
30720
  return x || (x = FN(v), s.set(v, x)), x.then((C) => C.clone());
@@ -30727,13 +30727,13 @@ const xee = {
30727
30727
  }, wee = {
30728
30728
  match: (t, e) => po(t) === "kml" || /application\/vnd\.google-earth\.kml/i.test(e ?? ""),
30729
30729
  parse: async (t) => {
30730
- const [e, i, n] = await Promise.all([t.text(), import("./index-D3prxKma.js"), import("./index-BpEYiuZn.js")]);
30730
+ const [e, i, n] = await Promise.all([t.text(), import("./index-CJ0pib8I.js"), import("./index-Bwu1DFHP.js")]);
30731
30731
  return Vl(await i.parse(e, n.KMLLoader));
30732
30732
  }
30733
30733
  }, Cee = /* @__PURE__ */ new Set(["waypoint", "track", "route"]), See = {
30734
30734
  match: (t, e) => po(t) === "gpx" || /application\/gpx\+xml/i.test(e ?? ""),
30735
30735
  parse: async (t, e) => {
30736
- const [i, n, r] = await Promise.all([t.text(), import("./index-D3prxKma.js"), import("./index-BpEYiuZn.js")]), o = ((await n.parse(i, r.GPXLoader)).features ?? []).map((l) => {
30736
+ const [i, n, r] = await Promise.all([t.text(), import("./index-CJ0pib8I.js"), import("./index-Bwu1DFHP.js")]), o = ((await n.parse(i, r.GPXLoader)).features ?? []).map((l) => {
30737
30737
  const u = l.properties?._gpxType, d = u === "trk" ? "track" : u === "rte" ? "route" : "waypoint";
30738
30738
  return { ...l, properties: { ...l.properties, _gpxKind: d } };
30739
30739
  }), a = e.split("#")[1]?.toLowerCase().replace(/s$/, "");
@@ -30766,7 +30766,7 @@ const xee = {
30766
30766
  t.arrayBuffer(),
30767
30767
  import("./index-BYbMtNVH.js"),
30768
30768
  import("./index-CgPV7QlM.js"),
30769
- import("./geoparquet-BoPBi8wO.js")
30769
+ import("./geoparquet-RBC0ZJNm.js")
30770
30770
  ]), o = new Uint8Array(i);
30771
30771
  if (o.length < 8 || o[0] !== 80 || o[1] !== 65 || o[2] !== 82 || o[3] !== 49)
30772
30772
  throw new Error(`data="${e}": not a Parquet file (missing PAR1 magic bytes).`);
@@ -68656,7 +68656,7 @@ Un({
68656
68656
  type: "COGLayer",
68657
68657
  carriesRasterWindow: !0,
68658
68658
  carriesRasterIdentify: !0,
68659
- loadClass: () => import("./raster-B_pyNdlk.js").then((t) => t.r).then((t) => t.OmCOGLayer),
68659
+ loadClass: () => import("./raster-Dsugvobw.js").then((t) => t.r).then((t) => t.OmCOGLayer),
68660
68660
  props: [
68661
68661
  { attr: "src", kind: "scalar", deckProp: "geotiff", type: "string", required: !0 },
68662
68662
  // COG v2 (issue #13): 1-based band selection — a single band (colormap-
@@ -68684,7 +68684,7 @@ Un({
68684
68684
  });
68685
68685
  Un({
68686
68686
  type: "ZarrLayer",
68687
- loadClass: () => import("./zarr-_lE_wpg2.js").then((t) => t.OmZarrLayer),
68687
+ loadClass: () => import("./zarr-CRysSFhD.js").then((t) => t.OmZarrLayer),
68688
68688
  props: [
68689
68689
  { attr: "src", kind: "scalar", deckProp: "src", type: "string", required: !0 },
68690
68690
  { attr: "variable", kind: "scalar", deckProp: "variable", type: "string" },
@@ -81256,7 +81256,7 @@ class NU {
81256
81256
  `[onlymapjs] basemap="${e}" (Mapbox GL) is not implemented yet in this version — falling back to standalone (no basemap). Use a maplibre-* basemap instead (no token required).`
81257
81257
  );
81258
81258
  else {
81259
- this.mode = "basemap", this.basemapAttr = e, import("./basemap-2L7QA0ic.js").then(({ MapLibreBasemapAdapter: s }) => {
81259
+ this.mode = "basemap", this.basemapAttr = e, import("./basemap-CtEc5BCi.js").then(({ MapLibreBasemapAdapter: s }) => {
81260
81260
  this.destroyed || i !== this.rendererGeneration || (this.basemap = new s(
81261
81261
  this.parent,
81262
81262
  // this.basemapAttr, not the captured param: a style switch that
@@ -82775,7 +82775,7 @@ function WU({ layerIRs: t, core: e, mapEl: i, selection: n, history: r, featureT
82775
82775
  }
82776
82776
  };
82777
82777
  }
82778
- const jU = "0.7.4", t3e = "https://om-api.nika.eco/v1/t", to = { endpoint: t3e };
82778
+ const jU = "0.7.5", t3e = "https://om-api.nika.eco/v1/t", to = { endpoint: t3e };
82779
82779
  function i3e(t) {
82780
82780
  for (const e of Object.keys(t))
82781
82781
  to[e] = t[e];
@@ -87181,7 +87181,7 @@ async function IRe(t) {
87181
87181
  e(t);
87182
87182
  }
87183
87183
  async function ZMe() {
87184
- return import("./raster-B_pyNdlk.js").then((t) => t.r);
87184
+ return import("./raster-Dsugvobw.js").then((t) => t.r);
87185
87185
  }
87186
87186
  const PRe = {
87187
87187
  registerLayer: Un,
@@ -1,4 +1,4 @@
1
- import { ak as Bt, az as Xt, aA as Yt, aB as Us } from "./index-BirHZYjb.js";
1
+ import { ak as Bt, az as Xt, aA as Yt, aB as Us } from "./index-DkimKgOm.js";
2
2
  import { w as gs, t as Hs, f as Ws, m as Qs } from "./mgrs-BY9bIvp4.js";
3
3
  import { c as Xs } from "./convert-arrow-schema-DrAihRf9.js";
4
4
  import { y as Ys, A as bt, a as Ks, z as Vs, R as Zs } from "./recordbatch-Bpc0uxFn.js";
@@ -1,4 +1,4 @@
1
- import { C as a, D as s } from "./raster-B_pyNdlk.js";
1
+ import { C as a, D as s } from "./raster-Dsugvobw.js";
2
2
  const D = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
3
3
  __proto__: null
4
4
  }, Symbol.toStringTag, { value: "Module" }));
@@ -8,7 +8,7 @@ var t;
8
8
  })(t || (t = {}));
9
9
  let l = !1;
10
10
  async function d() {
11
- const e = await import("./LercDecode.es-5mg89-0f.js");
11
+ const e = await import("./LercDecode.es-DYwn8YTS.js");
12
12
  return l || (await e.load(), l = !0), e;
13
13
  }
14
14
  async function i(e, r) {
@@ -95155,7 +95155,7 @@ function OfA({ layerIRs: e, core: A, mapEl: t, selection: i, history: r, feature
95155
95155
  }
95156
95156
  };
95157
95157
  }
95158
- const qfA = "0.7.4", xue = "https://om-api.nika.eco/v1/t", JE = { endpoint: xue };
95158
+ const qfA = "0.7.5", xue = "https://om-api.nika.eco/v1/t", JE = { endpoint: xue };
95159
95159
  function Lue(e) {
95160
95160
  for (const A of Object.keys(e))
95161
95161
  JE[A] = e[A];
package/dist/onlymapjs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { aC as e, aD as r, aE as t, aF as o, aG as b, aH as i, ap as l, aI as n, aJ as S, aK as c, aL as E, aM as L, aN as g, aO as T, aP as _, aQ as A, aR as d, aS as p, aT as I, aU as m, aV as D, aW as M, aX as u, aY as y, aZ as O, a_ as f, ao as R, a$ as N, aw as P, b0 as F, b1 as h, b2 as C, W as B, b3 as v, b4 as x, b5 as G, b6 as U, b7 as W, b8 as w, b9 as Y, ba as X, bb as H, bc as k, bd as J, be as K, bf as V, bg as z, bh as Q, bi as Z, bj as $, bk as j, bl as q, bm as aa, bn as sa, bo as ea, bp as ra, bq as ta, br as oa, bs as ba, bt as ia, bu as la, bv as na, bw as Sa, bx as ca, by as Ea, bz as La, bA as ga, bB as Ta, bC as _a, bD as Aa, bE as da, bF as pa, bG as Ia, bH as ma, bI as Da, bJ as Ma, bK as ua, bL as ya, bM as Oa, bN as fa, bO as Ra, bP as Na, bQ as Pa, bR as Fa, bS as ha, bT as Ca, bU as Ba, bV as va, bW as xa, bX as Ga, bY as Ua, bZ as Wa, b_ as wa, b$ as Ya, c0 as Xa, c1 as Ha, c2 as ka, c3 as Ja, c4 as Ka, c5 as Va, c6 as za, c7 as Qa, c8 as Za, c9 as $a, ca as ja, cb as qa, cc as as } from "./index-BirHZYjb.js";
1
+ import { aC as e, aD as r, aE as t, aF as o, aG as b, aH as i, ap as l, aI as n, aJ as S, aK as c, aL as E, aM as L, aN as g, aO as T, aP as _, aQ as A, aR as d, aS as p, aT as I, aU as m, aV as D, aW as M, aX as u, aY as y, aZ as O, a_ as f, ao as R, a$ as N, aw as P, b0 as F, b1 as h, b2 as C, W as B, b3 as v, b4 as x, b5 as G, b6 as U, b7 as W, b8 as w, b9 as Y, ba as X, bb as H, bc as k, bd as J, be as K, bf as V, bg as z, bh as Q, bi as Z, bj as $, bk as j, bl as q, bm as aa, bn as sa, bo as ea, bp as ra, bq as ta, br as oa, bs as ba, bt as ia, bu as la, bv as na, bw as Sa, bx as ca, by as Ea, bz as La, bA as ga, bB as Ta, bC as _a, bD as Aa, bE as da, bF as pa, bG as Ia, bH as ma, bI as Da, bJ as Ma, bK as ua, bL as ya, bM as Oa, bN as fa, bO as Ra, bP as Na, bQ as Pa, bR as Fa, bS as ha, bT as Ca, bU as Ba, bV as va, bW as xa, bX as Ga, bY as Ua, bZ as Wa, b_ as wa, b$ as Ya, c0 as Xa, c1 as Ha, c2 as ka, c3 as Ja, c4 as Ka, c5 as Va, c6 as za, c7 as Qa, c8 as Za, c9 as $a, ca as ja, cb as qa, cc as as } from "./index-DkimKgOm.js";
2
2
  export {
3
3
  e as ALL_POSITION_VALUES,
4
4
  r as AUDIT_EXEMPTIONS,
@@ -1,5 +1,5 @@
1
- import { c as se, t as ze, i as wt, a as Be, s as St, C as Pt, b as bt, F as Ct, A as Lt, d as It, R as fe, e as Gt, p as vt, m as Et, f as Ft, g as At, r as _e, h as jt, j as Rt, k as Ut, l as Mt, n as Ot } from "./raster-pipeline-CJCRg21V.js";
2
- import { ax as Kt, ay as xt } from "./index-BirHZYjb.js";
1
+ import { c as se, t as ze, i as wt, a as Be, s as St, C as Pt, b as bt, F as Ct, A as Lt, d as It, R as fe, e as Gt, p as vt, m as Et, f as Ft, g as At, r as _e, h as jt, j as Rt, k as Ut, l as Mt, n as Ot } from "./raster-pipeline-rqJJRCEo.js";
2
+ import { ax as Kt, ay as xt } from "./index-DkimKgOm.js";
3
3
  import ye from "./index-CW1n5LdO.js";
4
4
  function Dt(e, t) {
5
5
  const n = e.length / 3, r = new Uint8ClampedArray(n * 4), o = 0, i = n, a = n * 2;
@@ -1070,7 +1070,7 @@ j.set(f.Zstd, () => import("./zstd-jXobGRcq.js").then((e) => e.decode));
1070
1070
  j.set(f.Jpeg, () => Promise.resolve(le));
1071
1071
  j.set(f.Jpeg6, () => Promise.resolve(le));
1072
1072
  j.set(f.Webp, () => Promise.resolve(le));
1073
- j.set(f.Lerc, () => import("./lerc-C1z6aAA4.js").then((e) => e.l).then((e) => e.decode));
1073
+ j.set(f.Lerc, () => import("./lerc-BIQMg0FV.js").then((e) => e.l).then((e) => e.decode));
1074
1074
  async function ce(e, t, n) {
1075
1075
  const r = j.get(t);
1076
1076
  if (!r)
@@ -1,5 +1,5 @@
1
1
  import { w as de } from "./mgrs-BY9bIvp4.js";
2
- import { am as he, an as fe, ao as se, ap as oe, aq as me, b as pe, ar as ge, l as Z, as as ve, d as be, at as xe, au as Pe, av as ye, aw as ie } from "./index-BirHZYjb.js";
2
+ import { am as he, an as fe, ao as se, ap as oe, aq as me, b as pe, ar as ge, l as Z, as as ve, d as be, at as xe, au as Pe, av as ye, aw as ie } from "./index-DkimKgOm.js";
3
3
  function Te(r, e, t) {
4
4
  const { projectedCorners: n } = e, { topLeft: s, topRight: o, bottomRight: i, bottomLeft: a } = n, c = t(s[0], s[1]), u = t(o[0], o[1]), l = t(i[0], i[1]), h = t(a[0], a[1]), f = [
5
5
  c,
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.7.4";
8
+ export declare const LIBRARY_VERSION = "0.7.5";
@@ -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, j as pr, k as mr } from "./raster-pipeline-CJCRg21V.js";
2
- import { ap as gr } from "./index-BirHZYjb.js";
1
+ import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, j as pr, k as mr } from "./raster-pipeline-rqJJRCEo.js";
2
+ import { ap as gr } from "./index-DkimKgOm.js";
3
3
  import $t from "./index-CW1n5LdO.js";
4
4
  var Et;
5
5
  function h(e, t, n) {
package/llms.txt CHANGED
@@ -42,9 +42,9 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
42
42
  - `<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). Selection-anchored overlays scope with `layer="…"` (one layer's picks only) and `selection-type="click"|"hover"` (one pick type only) — a click-opened popup should ALWAYS set `selection-type="click"`, else merely hovering any pickable feature drags it there and re-templates it against the hovered object; with it, hover is inert and a click on empty space still dismisses. `{{field}}` interpolates the picked feature HTML-escaped; `{{{field}}}` is raw (avoid); `{{z}}` is the pick's ELEVATION in meters, present only when a `pickable="3d"` layer ran deck's depth pass for that pick (absent — not `0` — otherwise, so "no elevation" is distinguishable from sea level). `clip-to-map` (opt-in) hides the overlay when its own BOX would spill past the map viewport rather than only when its anchor leaves — for small transient tips that track the cursor; an overhanging absolutely-positioned box inflates the page's scrollable overflow and the scrollbar -> map resize -> reprojection loop shows as view jitter. For labels on many features use `PopupLayer`, not overlays. STYLING: overlay content renders inside a shadow root (style isolation, like widgets) — page stylesheets/classes do NOT reach it; use inline `style="…"` on the content or a `<style>` element INSIDE the overlay (children move into the shadow root wholesale, so it applies there); inheritable props + CSS custom properties pierce.
43
43
  - `<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`, `set-pickable` (`{layer, pickable: true|false|"3d"}` — runtime picking/popup toggle, story-capturable; the per-layer popup on/off switch a viewer-facing export needs), `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`, `undo`, `redo`; scene/tool actions `set-lighting`, `set-terrain`, `set-clip-box` (`{min,max,invert?,highlight?}` / `{clear:true}`), `clip-box-edit` (`{editing}`), `export-region-3d` (`{target?, format?:"glb"|"b3dm"}` — what the draw widget's `export-3d` button emits), the draw actions `draw-mode` (`{target?:"sketch", mode:"point"|"line"|"polygon"|null}` — null exits drawing), `draw-commit`, `draw-cancel`, `draw-delete` (removes the last shape), `draw-clear`, `draw-config` (`{target?, autosave?, fillColor?, lineColor?}`) and `draw-save` (`{target?, format?:"download"|"file-system"|"both"}`) — every one of these drives the same store a `data="draw:<target>"` layer reads, so a custom toolbar can replace `<om-widget type="draw">` entirely; and the measure actions `measure-mode` (`{mode:"distance"|"area"|"volume"|null}`), `measure-units`, `measure-clear`, `measure-config` (`{profile?, baseSurface?, density?, swell?, shrink?, deadband?}`), `measure-flat-target-plane` (`{flat}`). One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
44
44
  - 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")`.
45
- - `<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) refined by the `scripting` media feature on 2023+ engines: with scripting DISABLED the fallback shows instantly; with scripting ENABLED it never flashes during a slow load and only surfaces after a ~4s grace with load-failure wording (blocked/unreachable bundle). 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.
45
+ - `<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. To PRODUCE a shareable single file, `npx @nika-js/onlymap export map.html` embeds every relative data/src/scenegraph URL as a data: URL (format detection preserved — the original extension rides a #name.ext fragment), pins relative library refs to the CDN, adds a generic om-fallback when missing, and warns when embedded data is heavy (>1 MB per file / >5 MB total) or when a source cannot be embedded (shapefile sidecars, COG Range streams, Zarr directories, {z}/{x}/{y} templates — those need hosting) ("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) refined by the `scripting` media feature on 2023+ engines: with scripting DISABLED the fallback shows instantly; with scripting ENABLED it never flashes during a slow load and only surfaces after a ~4s grace with load-failure wording (blocked/unreachable bundle). 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.
46
46
  - 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`.
47
- - Pull-model frame rendering (external video frameworks such as Remotion): mark the map `data-om-recording` (the determinism switch — instant camera, no transitions/gesture interrupts, byte-stable `snapshot()`), then per frame `story.seek(t, {interpolateCamera: true})` + `await mapEl.whenSettled()` + capture; the same frame is byte-identical across processes and orderings. Story effect verbs (fade/pulse/trace/populate) evaluate as pure functions of story time under the switch — a seek landing mid-trace renders the half-drawn outline; only effects dispatched OUTSIDE a story (behaviors, ctx.emit) snap to their end state. `onlymapjs record` remains the built-in push-model path.
47
+ - Pull-model frame rendering (external video frameworks such as Remotion): mark the map `data-om-recording` (the determinism switch — instant camera, no transitions/gesture interrupts, byte-stable `snapshot()`), then per frame `story.seek(t, {interpolateCamera: true})` + `await mapEl.whenSettled()` + capture; the same frame is byte-identical across processes and orderings. Story effect verbs (fade/pulse/trace/populate) evaluate as pure functions of story time under the switch — a seek landing mid-trace renders the half-drawn outline; only effects dispatched OUTSIDE a story (behaviors, ctx.emit) snap to their end state. `onlymapjs record` remains the built-in push-model path; `@nika-js/onlymap-remotion` packages the pull-model path for Remotion.
48
48
  - Travel-map animation: a `trace` story step takes `follow` (camera rides the drawing tip along the path) and `easing="linear|ease-in|ease-out|ease-in-out"`; combine with a TripsLayer route + pins toggled by later steps, and export with `npx onlymapjs record`.
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).
@@ -62,6 +62,8 @@ UI panel (legend, chart, stats) → `<om-widget>`. Rich HTML at one map location
62
62
 
63
63
  In a React codebase, do NOT render om-* elements from JSX (React and the library would contend over the same DOM). Use the first-party adapter instead: `import { OmMap, OmLayer, OmWidget, OmOverlay, useOmMap } from "@nika-js/onlymap/react"` — camelCase deck.gl props, accessors as plain JS functions (`getFillColor={d => ...}`, no expression language), interactions as `onClick`/`onHover` handlers, widget state via the `useOmMap(watchTokens)` hook (tearing-safe: it rides `useSyncExternalStore` over the controller's per-token stores). To sync map state into Redux/MobX/Zustand/Jotai, use `controller.getStore(token)` — a framework-free `{subscribe, getSnapshot}` per watch token with cached plain-data snapshots and `origin: "user"|"programmatic"` tagging for echo-free two-way camera binding; ~20-line recipes: [docs/external-stores.md](docs/external-stores.md). Guide: [docs/react.md](docs/react.md).
64
64
 
65
+ If the target is a VIDEO built in Remotion — titles/overlays composited over the map, map content reacting per frame, or one composition rendered as many videos from data — use the separate `@nika-js/onlymap-remotion` package on top of this one (it takes this package as an exact peer). It wraps the pull-model seams below into `<OmMapVideo>`, whose prop selects the camera source (a GeoJSON `route`, a `places` list, `keyframes`, or the manifest's own story) and derives the composition's duration from that same data. It ships its own `llms.txt` and skill. For a plain flyby video with nothing layered on top, `npx onlymapjs record` here is simpler and needs no extra package.
66
+
65
67
  If the target is an Expo/React Native MOBILE app, use the separate `@nika-js/onlymap-native` package instead of this one: same layer vocabulary, but accessors are OnlyMap expression STRINGS (`getPosition="[$lon, $lat]"` — functions cannot cross its JSON bridge), descriptors are plain JSON, and native UI goes beside the map (no om-* elements, no `<OmWidget>`/`<OmOverlay>` components). That package ships its own `llms.txt` and skill; follow those for native work.
66
68
 
67
69
  ## Docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nika-js/onlymap",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Declarative deck.gl maps for HTML and React — interactive WebGL mapping with GeoJSON/CSV/Arrow data, MapLibre basemaps, widgets, popups, and live streams from a custom-element manifest or typed React components. TypeScript, no build step.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: onlymapjs
3
- description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), routes and directions (a styled A-to-B route line, OSRM or another routing engine, click-to-route), live vehicle/rider/delivery tracking (a moving marker gliding between GPS fixes with a follow camera), a React Native or Expo mobile map (route to the separate @nika-js/onlymap-native package), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
3
+ description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), routes and directions (a styled A-to-B route line, OSRM or another routing engine, click-to-route), live vehicle/rider/delivery tracking (a moving marker gliding between GPS fixes with a follow camera), a React Native or Expo mobile map (route to the separate @nika-js/onlymap-native package), a Remotion map video or programmatic map flyover with overlays (route to the separate @nika-js/onlymap-remotion package), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
4
4
  ---
5
5
 
6
6
  # OnlyMapJS
@@ -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 pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.7.4` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.7.4/dist/onlymapjs.css">`. Never a rebundling CDN (esm.sh, skypack): re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
26
+ For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.7.5` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.7.5/dist/onlymapjs.css">`. Never a rebundling CDN (esm.sh, skypack): re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
27
27
 
28
28
  ## React Projects
29
29
 
@@ -66,6 +66,7 @@ Load the smallest reference needed for the task:
66
66
 
67
67
  ## Authoring Decisions
68
68
 
69
+ - Target is a Remotion VIDEO (overlays composited over the map, map content reacting per frame, many videos from one composition) -> the separate `@nika-js/onlymap-remotion` package layered on this one: `<OmMapVideo>` with a `route`/`places`/`keyframes` prop, duration derived from the same data. It ships its own llms.txt and skill. A plain flyby with nothing on top needs no extra package — use `npx onlymapjs record`.
69
70
  - Target is an Expo/React Native MOBILE app -> the separate `@nika-js/onlymap-native` package, not this one: same layer vocabulary, but accessors are OnlyMap expression STRINGS (`getPosition="[$lon, $lat]"` — functions cannot cross its JSON bridge), descriptors are plain JSON, no om-* elements and no widget/overlay components (build native UI beside the map). It ships its own llms.txt and skill; follow those for native work.
70
71
  - UI panel, control, chart, legend, stats, filter, or draw toolbar -> `<om-widget>`.
71
72
  - Sparse rich HTML at one geographic location -> `<om-overlay>`.
@@ -16,8 +16,8 @@ Vite/npm project:
16
16
  Static CDN page (raw-file CDNs only — unpkg/jsDelivr; never esm.sh or another rebundling CDN, which duplicates the WebGL runtime and breaks layer shaders):
17
17
 
18
18
  ```html
19
- <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.7.4/dist/onlymapjs.css">
20
- <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.7.4"></script>
19
+ <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.7.5/dist/onlymapjs.css">
20
+ <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.7.5"></script>
21
21
  ```
22
22
 
23
23
  Always include `onlymapjs.css` — it carries the MapLibre basemap styles and the no-JS fallback rules (`<om-fallback>` / default banner). For the fallback to work in script-disabled previews it must load without JavaScript: a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages (a bundler-emitted stylesheet is fine in npm projects).
@@ -632,6 +632,7 @@ Rules:
632
632
  - No attributes; plain HTML content — links work, so include a hosted-version URL when one exists.
633
633
  - Without an `<om-fallback>`, the stylesheet shows a generic text-only banner instead.
634
634
  - Requires `onlymapjs.css` to load without JavaScript (see Import Patterns above).
635
+ - `npx @nika-js/onlymap export map.html` produces the shareable single file: relative data embedded as data: URLs (format detection intact), library refs pinned to the CDN, a generic fallback injected when missing, size warnings for heavy data. Shapefiles, COGs, Zarr, and tile templates stay network-backed.
635
636
  - Timing (2023+ browsers, via the CSS `scripting` media feature): with scripting disabled the fallback shows instantly; with scripting enabled it never flashes during a slow load — it appears only after a ~4s grace, with load-failure wording, when the bundle is blocked or unreachable. Older engines keep a 400ms reveal delay.
636
637
 
637
638
  Example: