@nika-js/onlymap 0.4.3 → 0.4.4

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 (35) hide show
  1. package/README.md +38 -7
  2. package/bin/onlymapjs.mjs +147 -11
  3. package/dist/{LercDecode.es-peqB7bxk.js → LercDecode.es-CgN9Gb9e.js} +1 -1
  4. package/dist/{basemap-BeKuGQJs.js → basemap-Bfn5Z__c.js} +168 -163
  5. package/dist/basemap.d.ts +3 -1
  6. package/dist/elements/om-map.d.ts +28 -0
  7. package/dist/{index-BKU3HvOO.js → index-C4cWRigY.js} +1 -1
  8. package/dist/{index-BPKvpqIk.js → index-CnitG1VX.js} +2 -2
  9. package/dist/{index-DzyVbu_T.js → index-KgO0MBqA.js} +12742 -12293
  10. package/dist/{index-DVnTYF5u.js → index-M8KfTTol.js} +1 -1
  11. package/dist/{index-Dg8SKcgV.js → index-lXrP3rPo.js} +1 -1
  12. package/dist/index.d.ts +4 -2
  13. package/dist/layout-audit.d.ts +53 -0
  14. package/dist/{lerc-DEpGyngt.js → lerc-l-QFh62d.js} +2 -2
  15. package/dist/onlymap.standalone.js +21022 -20568
  16. package/dist/onlymapjs.js +73 -62
  17. package/dist/programmatic.d.ts +3 -1
  18. package/dist/{raster-CX7Y4jtJ.js → raster-BBA_oI-d.js} +2 -2
  19. package/dist/react/context.d.ts +9 -1
  20. package/dist/react/om-map.d.ts +3 -1
  21. package/dist/react/om-widget.d.ts +3 -1
  22. package/dist/react.js +333 -202
  23. package/dist/runtime-core.d.ts +12 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/widget-layout.d.ts +16 -0
  26. package/docs/basemaps.md +1 -1
  27. package/docs/react.md +2 -1
  28. package/docs/testing.md +10 -0
  29. package/llms.txt +1 -1
  30. package/onlymapjs.html-data.json +18 -0
  31. package/package.json +1 -1
  32. package/skills/onlymapjs/SKILL.md +2 -1
  33. package/skills/onlymapjs/references/react.md +2 -2
  34. package/skills/onlymapjs/references/syntax.md +3 -3
  35. package/skills/onlymapjs/references/testing.md +15 -0
package/README.md CHANGED
@@ -81,7 +81,7 @@ A handful of elements, one rule: **attributes are kebab-case versions of deck.gl
81
81
  |---|---|
82
82
  | `<om-map>` | The map. `center`, `zoom`, `pitch`, `bearing`; `basemap` takes a free preset (`positron`, `liberty`, `dark-matter`, `osm`, …), a style URL, or `"none"` (standalone canvas) — and switches **live**; `validate` for a live on-page error panel. |
83
83
  | `<om-layer>` | Any of **34 layer types** by name — all of deck.gl's core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, …) plus the built-in `PopupLayer` for WebGL badges/labels at scale and the native `COGLayer` for GeoTIFF rasters. `id` required; `label`/`color` feed the legend. |
84
- | `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar`, `attribution`, `filter`, `draw`, `vega-lite` (live charts). Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all chrome without destroying it — attribution never hides. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable). Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
84
+ | `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar`, `attribution`, `filter`, `draw`, `vega-lite` (live charts). Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths ≤640px, managed widgets automatically move into accessible top/end/bottom/start drawers; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
85
85
  | `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. `{{field}}` interpolates the picked feature, HTML-escaped by default. |
86
86
  | `<om-behavior>` | Declarative interactions: `on="click|hover|drag|load|data-loaded"` → a named action. |
87
87
  | `<om-story>` | A storyboard: `<om-step>` children fire actions on a timeline. Controlled by the `player` widget, behaviors, or `storyEl.play()/pause()/seek()`. |
@@ -172,23 +172,54 @@ That opt-in command updates `.vscode/settings.json` and copies the `!`-prefixed
172
172
 
173
173
  The package also ships `!`-prefixed manifest snippets (`node_modules/@nika-js/onlymap/.vscode/onlymap.code-snippets`) — type `!starter`, `!map`, `!layer`, `!draw`, etc. to scaffold a well-formed element.
174
174
 
175
- ## LLM Skill
175
+ ## Quick-start guide
176
176
 
177
- The npm package and public mirror include a portable Skill at `skills/onlymapjs`. Skill-aware agents can install that folder to learn the OnlyMapJS manifest syntax, authoring patterns, and validation workflow instead of treating the library like raw deck.gl.
177
+ ### Use OnlyMapJS with Claude Code and Codex
178
178
 
179
- With the Vercel Labs `skills` CLI, install it from the public GitHub repo:
179
+ The public repo includes a portable `onlymapjs` Skill that teaches coding agents the manifest syntax, React adapter, common map patterns, and validation/testing workflow. Install the library in the app you want to map:
180
180
 
181
181
  ```bash
182
+ npm install @nika-js/onlymap
183
+ ```
184
+
185
+ Then, from that app's repository root, install the Skill for your agent:
186
+
187
+ ```bash
188
+ # Claude Code
189
+ npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs --agent claude-code
190
+
191
+ # Codex
182
192
  npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs --agent codex
183
193
  ```
184
194
 
185
- Or install globally for Codex:
195
+ Using both agents in the same repository? Install it for both in one command:
186
196
 
187
197
  ```bash
188
- npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs --agent codex --global
198
+ npx -y skills add NikaGeospatial/onlymapjs \
199
+ --skill onlymapjs \
200
+ --agent claude-code \
201
+ --agent codex
189
202
  ```
190
203
 
191
- To inspect it without installing:
204
+ Start or restart Claude Code or Codex in that repository, then describe the map you want. Mention the installed Skill explicitly when you want to guarantee it is used:
205
+
206
+ ```text
207
+ Use the installed OnlyMapJS skill to build a full-screen map in this project.
208
+ Inspect my data before choosing accessors. Add a legend and click details,
209
+ include a no-JavaScript fallback, validate the result, and run the relevant tests.
210
+ ```
211
+
212
+ For an existing React app:
213
+
214
+ ```text
215
+ Use the installed OnlyMapJS skill to add this map to my React app. Use the
216
+ @nika-js/onlymap/react adapter, preserve the app's existing state patterns,
217
+ and verify the result with the library's headless testing tools.
218
+ ```
219
+
220
+ The Skill steers HTML projects toward declarative `om-*` manifests and React projects toward the first-party adapter. It also tells the agent to use `OmMap.validate()`, `OmMap.snapshotIR()`, and the headless harness rather than guessing whether the generated map works.
221
+
222
+ To inspect the Skill before installing it:
192
223
 
193
224
  ```bash
194
225
  npx -y skills add NikaGeospatial/onlymapjs --list
package/bin/onlymapjs.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { dirname, extname, join, resolve, sep } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
7
  const CUSTOM_DATA = "./node_modules/@nika-js/onlymap/onlymapjs.html-data.json";
@@ -9,16 +10,148 @@ const SNIPPET_FILE = "onlymap.code-snippets";
9
10
  function usage() {
10
11
  console.log(`Usage:
11
12
  onlymapjs init [--force]
13
+ onlymapjs check-layout <manifest.html>
12
14
  onlymapjs --help
13
15
 
14
16
  Commands:
15
17
  init Configure the current project for OnlyMapJS authoring in VS Code.
18
+ check-layout
19
+ Load a browser-runnable manifest in isolated headless Chromium,
20
+ audit real widget geometry at 360/640/768/1024px, and exit 0/1.
16
21
 
17
22
  Options:
18
23
  --force Overwrite .vscode/${SNIPPET_FILE} if it already exists.
19
24
  `);
20
25
  }
21
26
 
27
+ const MIME_TYPES = {
28
+ ".html": "text/html; charset=utf-8",
29
+ ".js": "text/javascript; charset=utf-8",
30
+ ".mjs": "text/javascript; charset=utf-8",
31
+ ".css": "text/css; charset=utf-8",
32
+ ".json": "application/json; charset=utf-8",
33
+ ".geojson": "application/geo+json; charset=utf-8",
34
+ ".csv": "text/csv; charset=utf-8",
35
+ ".svg": "image/svg+xml",
36
+ ".png": "image/png",
37
+ ".jpg": "image/jpeg",
38
+ ".jpeg": "image/jpeg",
39
+ ".webp": "image/webp",
40
+ ".wasm": "application/wasm",
41
+ };
42
+
43
+ function isInside(root, candidate) {
44
+ return candidate === root || candidate.startsWith(`${root}${sep}`);
45
+ }
46
+
47
+ function browserManifestHtml(text) {
48
+ // A plain static server cannot resolve package bare specifiers. Rewrite
49
+ // the root OnlyMapJS entry only in the ephemeral response; relative data
50
+ // and all authored source files keep their original URLs.
51
+ return text
52
+ .replaceAll('"@nika-js/onlymap"', '"/__onlymapjs__/dist/onlymapjs.js"')
53
+ .replaceAll("'@nika-js/onlymap'", "'/__onlymapjs__/dist/onlymapjs.js'")
54
+ .replaceAll('"@nika-js/onlymap/onlymapjs.css"', '"/__onlymapjs__/dist/onlymapjs.css"')
55
+ .replaceAll("'@nika-js/onlymap/onlymapjs.css'", "'/__onlymapjs__/dist/onlymapjs.css'");
56
+ }
57
+
58
+ async function startStaticServer(manifestPath, packageRoot) {
59
+ const manifestRoot = dirname(manifestPath);
60
+ const server = createServer((req, res) => {
61
+ try {
62
+ const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://127.0.0.1").pathname);
63
+ let target;
64
+ let root;
65
+ let isManifest = false;
66
+ if (pathname === "/") {
67
+ target = manifestPath;
68
+ root = manifestRoot;
69
+ isManifest = true;
70
+ } else if (pathname.startsWith("/__onlymapjs__/")) {
71
+ root = packageRoot;
72
+ target = resolve(root, pathname.slice("/__onlymapjs__/".length));
73
+ } else {
74
+ root = manifestRoot;
75
+ target = resolve(root, pathname.slice(1));
76
+ }
77
+ if (!isInside(root, target) || !existsSync(target) || !statSync(target).isFile()) {
78
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
79
+ res.end("Not found");
80
+ return;
81
+ }
82
+ const type = MIME_TYPES[extname(target).toLowerCase()] ?? "application/octet-stream";
83
+ const content = readFileSync(target);
84
+ res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
85
+ res.end(isManifest ? browserManifestHtml(content.toString("utf8")) : content);
86
+ } catch (error) {
87
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
88
+ res.end(error instanceof Error ? error.message : String(error));
89
+ }
90
+ });
91
+ await new Promise((resolveListen, reject) => {
92
+ server.once("error", reject);
93
+ server.listen(0, "127.0.0.1", resolveListen);
94
+ });
95
+ const address = server.address();
96
+ if (!address || typeof address === "string") throw new Error("Could not allocate the local layout-audit server.");
97
+ return { server, url: `http://127.0.0.1:${address.port}/` };
98
+ }
99
+
100
+ async function checkLayout(manifestArg) {
101
+ if (!manifestArg) throw new Error("check-layout needs a manifest HTML path.\nUsage: onlymapjs check-layout <manifest.html>");
102
+ const manifestPath = resolve(process.cwd(), manifestArg);
103
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
104
+ throw new Error(`Manifest not found: ${manifestPath}`);
105
+ }
106
+ if (extname(manifestPath).toLowerCase() !== ".html") {
107
+ throw new Error(`check-layout expects an .html manifest, received: ${manifestPath}`);
108
+ }
109
+
110
+ let playwright;
111
+ try {
112
+ playwright = await import("playwright");
113
+ } catch (error) {
114
+ if (error?.code === "ERR_MODULE_NOT_FOUND" || String(error).includes("Cannot find package 'playwright'")) {
115
+ throw new Error(
116
+ "check-layout needs Playwright in this project.\n" +
117
+ "Install it with:\n npm install --save-dev playwright\n npx playwright install chromium",
118
+ );
119
+ }
120
+ throw error;
121
+ }
122
+
123
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
124
+ const { auditLayout } = await import(new URL("../dist/onlymapjs.js", import.meta.url));
125
+ const { server, url } = await startStaticServer(manifestPath, packageRoot);
126
+ let browser;
127
+ try {
128
+ console.log(`Auditing ${manifestPath}`);
129
+ console.log("Note: check-layout executes this page's scripts inside an isolated headless Chromium context.");
130
+ try {
131
+ browser = await playwright.chromium.launch({ headless: true });
132
+ } catch (error) {
133
+ if (String(error).includes("Executable doesn't exist") || String(error).includes("browserType.launch")) {
134
+ throw new Error(`Chromium is not installed for Playwright.\nRun:\n npx playwright install chromium\n\n${String(error)}`);
135
+ }
136
+ throw error;
137
+ }
138
+ const context = await browser.newContext({ serviceWorkers: "block" });
139
+ const page = await context.newPage();
140
+ await page.goto(url, { waitUntil: "domcontentloaded" });
141
+ const diagnostics = await auditLayout(page);
142
+ if (diagnostics.length === 0) {
143
+ console.log("CLEAN: no widget layout problems at 360, 640, 768, or 1024px.");
144
+ return 0;
145
+ }
146
+ for (const diagnostic of diagnostics) console.error(JSON.stringify(diagnostic));
147
+ console.error(`FAILED: ${diagnostics.length} layout diagnostic(s).`);
148
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
149
+ } finally {
150
+ await browser?.close();
151
+ await new Promise((resolveClose) => server.close(resolveClose));
152
+ }
153
+ }
154
+
22
155
  function parseJsonc(text) {
23
156
  let out = "";
24
157
  let inString = false;
@@ -167,16 +300,19 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
167
300
  process.exit(0);
168
301
  }
169
302
 
170
- const [command, ...rest] = args;
171
- if (command !== "init") {
172
- console.error(`Unknown command: ${command}\n`);
173
- usage();
174
- process.exit(1);
175
- }
176
-
177
303
  try {
178
- init({ force: rest.includes("--force") });
304
+ const [command, ...rest] = args;
305
+ if (command === "init") {
306
+ init({ force: rest.includes("--force") });
307
+ } else if (command === "check-layout") {
308
+ if (rest.length !== 1) throw new Error("Usage: onlymapjs check-layout <manifest.html>");
309
+ process.exitCode = await checkLayout(rest[0]);
310
+ } else {
311
+ console.error(`Unknown command: ${command}\n`);
312
+ usage();
313
+ process.exitCode = 1;
314
+ }
179
315
  } catch (err) {
180
316
  console.error(err instanceof Error ? err.message : String(err));
181
- process.exit(1);
317
+ process.exitCode = 1;
182
318
  }
@@ -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-DEpGyngt.js"
22
+ "./lerc-l-QFh62d.js"
23
23
  ).then((g) => g._);
24
24
  var O = I(import.meta.url);
25
25
  }