@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.
- package/README.md +38 -7
- package/bin/onlymapjs.mjs +147 -11
- package/dist/{LercDecode.es-peqB7bxk.js → LercDecode.es-CgN9Gb9e.js} +1 -1
- package/dist/{basemap-BeKuGQJs.js → basemap-Bfn5Z__c.js} +168 -163
- package/dist/basemap.d.ts +3 -1
- package/dist/elements/om-map.d.ts +28 -0
- package/dist/{index-BKU3HvOO.js → index-C4cWRigY.js} +1 -1
- package/dist/{index-BPKvpqIk.js → index-CnitG1VX.js} +2 -2
- package/dist/{index-DzyVbu_T.js → index-KgO0MBqA.js} +12742 -12293
- package/dist/{index-DVnTYF5u.js → index-M8KfTTol.js} +1 -1
- package/dist/{index-Dg8SKcgV.js → index-lXrP3rPo.js} +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/layout-audit.d.ts +53 -0
- package/dist/{lerc-DEpGyngt.js → lerc-l-QFh62d.js} +2 -2
- package/dist/onlymap.standalone.js +21022 -20568
- package/dist/onlymapjs.js +73 -62
- package/dist/programmatic.d.ts +3 -1
- package/dist/{raster-CX7Y4jtJ.js → raster-BBA_oI-d.js} +2 -2
- package/dist/react/context.d.ts +9 -1
- package/dist/react/om-map.d.ts +3 -1
- package/dist/react/om-widget.d.ts +3 -1
- package/dist/react.js +333 -202
- package/dist/runtime-core.d.ts +12 -1
- package/dist/version.d.ts +1 -1
- package/dist/widget-layout.d.ts +16 -0
- package/docs/basemaps.md +1 -1
- package/docs/react.md +2 -1
- package/docs/testing.md +10 -0
- package/llms.txt +1 -1
- package/onlymapjs.html-data.json +18 -0
- package/package.json +1 -1
- package/skills/onlymapjs/SKILL.md +2 -1
- package/skills/onlymapjs/references/react.md +2 -2
- package/skills/onlymapjs/references/syntax.md +3 -3
- 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
|
|
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
|
-
##
|
|
175
|
+
## Quick-start guide
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
### Use OnlyMapJS with Claude Code and Codex
|
|
178
178
|
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
198
|
+
npx -y skills add NikaGeospatial/onlymapjs \
|
|
199
|
+
--skill onlymapjs \
|
|
200
|
+
--agent claude-code \
|
|
201
|
+
--agent codex
|
|
189
202
|
```
|
|
190
203
|
|
|
191
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
317
|
+
process.exitCode = 1;
|
|
182
318
|
}
|