@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
@@ -60,6 +60,16 @@ export interface RuntimeCoreCallbacks {
60
60
  */
61
61
  onRuntimeError?: (entry: ValidationEntry) => void;
62
62
  }
63
+ /**
64
+ * Stable front-end-owned hosts for legally required map chrome. Keeping the
65
+ * hosts in the managed slot tree lets framework and HTML front-ends share
66
+ * the same collision-free layout without giving the renderer ownership of
67
+ * either front-end's DOM.
68
+ */
69
+ export interface MandatedChromeHosts {
70
+ badge?: HTMLElement;
71
+ attribution?: HTMLElement;
72
+ }
63
73
  /** Headless mode config (spec: "Consumer Testing Surface") — an explicit size, since jsdom layout reports 0×0. */
64
74
  export interface HeadlessOptions {
65
75
  width: number;
@@ -141,7 +151,8 @@ export declare class RuntimeCore {
141
151
  private readonly gateWarned;
142
152
  private badge?;
143
153
  private unsubscribeLicense?;
144
- constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions, basemapOptions?: BasemapRuntimeOptions);
154
+ private readonly chromeHosts;
155
+ constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions, basemapOptions?: BasemapRuntimeOptions, chromeHosts?: MandatedChromeHosts);
145
156
  /** `basemap` attribute → concrete style, logging resolution problems (unknown preset / missing key) — the demo-style fallback still renders. */
146
157
  private resolveBasemap;
147
158
  /**
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.4.3";
8
+ export declare const LIBRARY_VERSION = "0.4.4";
@@ -17,6 +17,22 @@ export declare const MANAGED_SLOTS: readonly ["top-start", "top-center", "top-en
17
17
  export type ManagedSlot = (typeof MANAGED_SLOTS)[number];
18
18
  /** A widget's resolved placement: a managed slot, or the author-styled manual tier. */
19
19
  export type WidgetSlot = ManagedSlot | "manual";
20
+ /** Responsive fold drawers follow the four logical sides of the map. */
21
+ export type FoldSide = "top" | "end" | "bottom" | "start";
22
+ export declare const FOLD_SIDES: readonly FoldSide[];
23
+ /**
24
+ * Slot → responsive drawer. Corner widgets follow their block side (top or
25
+ * bottom); the two side-center slots follow their logical inline side. This
26
+ * is the issue-#21 grouping: one disclosure per map side, never one global
27
+ * drawer.
28
+ */
29
+ export declare function foldSideForSlot(slot: ManagedSlot): FoldSide;
30
+ /** The managed slot that owns each drawer toggle, keeping it in normal flow. */
31
+ export declare function foldToggleSlot(side: FoldSide): ManagedSlot;
32
+ /** Shared drawer plumbing styles for the HTML and React front-ends. */
33
+ export declare function foldDrawerStyle(side?: FoldSide): Record<string, string | number>;
34
+ export declare function foldPanelStyle(side: FoldSide): Record<string, string | number>;
35
+ export declare function foldToggleStyle(): Record<string, string | number>;
20
36
  /** Legacy 4-corner vocabulary (pre-0.4.1) — accepted forever, mapped LOGICALLY. */
21
37
  export declare const LEGACY_POSITION_ALIASES: Readonly<Record<string, ManagedSlot>>;
22
38
  /** Every accepted `position` value (validation + html-data). */
package/docs/basemaps.md CHANGED
@@ -78,7 +78,7 @@ Registered names show up in the switcher widget, validation, and (after `npx @ni
78
78
 
79
79
  ## Attribution
80
80
 
81
- Map data providers require attribution — OSM, OpenFreeMap, and CARTO all carry it in their style sources, and OnlyMapJS renders MapLibre's compact attribution control automatically whenever a basemap is active. Preset-level `attribution` text is appended. If you render your own credits, opt out with `attribution="false"` on `<om-map>` — but make sure your replacement satisfies the provider's terms.
81
+ Map data providers require attribution — OSM, OpenFreeMap, and CARTO all carry it in their style sources, and OnlyMapJS renders MapLibre's compact attribution control automatically whenever a basemap is active. Preset-level `attribution` text is appended. The control is an in-flow member of the managed `bottom-end` slot in both HTML and React, so widgets placed there stack above it instead of covering it. It remains visible during hide-all and prevents its slot from collision-dimming. If you render your own credits, opt out with `attribution="false"` on `<om-map>` — but make sure your replacement satisfies the provider's terms.
82
82
 
83
83
  ## Validation
84
84
 
package/docs/react.md CHANGED
@@ -56,6 +56,7 @@ Actions that mutate manifest attributes (`toggle-layer`, `show-overlay`, `fade`,
56
56
  | `center` `zoom` `pitch` `bearing` | Initial camera; later prop changes move the camera (instant). While they're unchanged, user panning is never fought. |
57
57
  | `basemap` | Same contract as the attribute — a preset name (`"positron"`, `"dark-matter"`, `"osm"`, …), a style URL, or omit for standalone deck.gl. Changing the prop switches live (camera + layers survive); `basemapKey` supplies the MapTiler key. See [docs/basemaps.md](basemaps.md). |
58
58
  | `headless` | `true` or `{ width, height }` — no renderer, real projection math (tests under jsdom/happy-dom). |
59
+ | `widgetStyle` / `widgetsHidden` | Layout-token sugar and hide-all. Hide-all preserves component state and hides authored managed widgets; provider attribution and the license badge remain visible. |
59
60
  | `onReady` | Renderer up + first commit + no data URL still loading. |
60
61
  | `onViewStateChange` | Every camera change, with the current `CameraState`. |
61
62
  | `onRuntimeError` | deck.gl-level failures in the structured validation shape. |
@@ -75,7 +76,7 @@ Give it a size (`style`/`className`) — it renders a `position: relative` div.
75
76
 
76
77
  ### `<OmWidget>`
77
78
 
78
- A positioning shell: `position="top-left|top-right|bottom-left|bottom-right"` + your JSX. Widgets sharing a corner stack; the shell re-enables pointer events over the map.
79
+ A positioning shell: one of the eight logical, RTL-aware managed slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`), a legacy corner alias, or `manual`, plus your JSX. Same-slot widgets stack with flush edges and a shared gap. At map widths ≤640px, managed widgets move into accessible per-side drawers; set `fold="never"` on essential controls, `widgetsFold={false}` on `<OmMap>` to opt out, or customize `--om-widget-fold-breakpoint`. Provider attribution is an in-flow member of `bottom-end`; the license badge is an in-flow member of `bottom-start`, so required chrome and your JSX never cover each other. The shell re-enables pointer events over the map.
79
80
 
80
81
  ### `<OmOverlay>`
81
82
 
package/docs/testing.md CHANGED
@@ -129,6 +129,16 @@ A *failing* fetch also settles readiness (the layer is just empty) — `mountFor
129
129
 
130
130
  Keep this thin — two to five tests — because the logic is already covered below. The library gives you three tools that remove the usual flakiness:
131
131
 
132
+ For a no-test-code widget layout check, install Playwright and run:
133
+
134
+ ```bash
135
+ npm install --save-dev playwright
136
+ npx playwright install chromium
137
+ npx onlymapjs check-layout public/dashboard.html
138
+ ```
139
+
140
+ `check-layout` serves the manifest over loopback HTTP (so relative data URLs work), executes it in an isolated headless Chromium context, opens responsive drawers, and checks real geometry/hit-testing at 360, 640, 768, and 1024px. It prints one JSON diagnostic per problem and exits 0 when clean or 1 on layout errors. Because it executes the page, only run it on manifests you trust.
141
+
132
142
  1. **`await mapEl.ready`** — resolves when the renderer initialized *and* the first reconcile ran *and* every declared `data` URL settled. Never `waitForTimeout`.
133
143
  2. **`mapEl.projectInternal([lng, lat])`** — derive click/hover pixels from the map's own projection instead of hardcoding coordinates.
134
144
  3. **Typed lookups** — `document.querySelector("om-map")` is fully typed (no casts) once the library is imported anywhere in your typechecked graph.
package/llms.txt CHANGED
@@ -26,7 +26,7 @@ OnlyMapJS is NOT raw deck.gl and NOT generic HTML/JSX. The rules below are the d
26
26
 
27
27
  - `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style; any scheme fetch supports, including desktop asset protocols like Tauri's, query strings fine), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). Terrain: `terrain="terrarium"` (keyless AWS DEM; also `maptiler-terrain` with a key, a raw `{z}/{x}/{y}` DEM URL + `terrain-decoder="terrarium|mapbox-rgb|<JSON>"`, or `off`) raises a 3D surface — geographic layers DRAPE onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D-model layers sit ON it), `terrain-exaggeration` scales relief, `terrain-texture` drapes imagery; terrain REPLACES an active basemap while on (restored when off); `set-terrain` action, `terrain` watch token, `OmMap.registerTerrain(name, {...})` for more sources. Scene lighting for 3D content: `lighting="daylight|studio|flat|custom"` (absent = deck defaults) with `lighting-ambient`/`lighting-sun`/`lighting-sun-azimuth`/`lighting-sun-elevation`/`lighting-camera` overrides and `lighting-sun-date` (ISO/epoch — solar-position sun computed at the map center, wins over azimuth/elevation); attribute-backed (undoable, live-editable), story-steppable via the `set-lighting {lighting, sunAzimuth, ...}` action (`lighting="default"` removes the attribute set; a bare preset is a clean reset — stale overrides clear), native UI via `<om-widget type="lighting">` (preset radios + tuning sliders), widget watch token `lighting`. GeoJsonLayer extrudes polygons declaratively: `extruded get-elevation="$height"` (+ `wireframe`). `widgets-hidden` attribute (or the `set-widgets-visible {visible}` action / `<om-widget type="widgets-toggle">` button) hides every widget WITHOUT destroying state — attribution never hides (license); transient (not an undo step) but story-steppable, so a step can clear chrome for a cinematic take. Slots auto-dim while an open `<om-overlay>` popup covers them (position stability over the popup dodging; `widgets-dim="off"` disables). `validate` attribute enables live validation + on-page error panel. Include a `map-id="<random UUID>"` on complete pages (identifies the map artifact for usage telemetry — not a visitor id; the page author deletes it to opt out); `telemetry="off"` disables usage telemetry + library-error reporting for the map (global: `OmMap.configureTelemetry({ disabled: true })`; schema: docs/telemetry.md). Free plan limits: 5 layers, 25k rows per layer — layers past a limit don't render and validation says why; a `license-key="om_live_…"` attribute (publishable, origin-restricted) or `OmMap.configureLicense(key)` lifts limits and removes the corner badge. When a user's map exceeds free limits, say so and point at the validation message rather than silently restructuring their data. Events on the element: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`, `om-view-changed` (camera settled after a move, debounced; `detail = {longitude, latitude, zoom, pitch, bearing, origin}` where `origin` is `"user"` for gesture bursts vs `"programmatic"` for API/story moves — the camera-persistence hook, and the echo-suppression signal when syncing camera state to an app store). `await mapEl.snapshot()` returns a canvas-only PNG dataURL of the scene (basemap + layers at device pixels; `{as:"blob"}` for files) — DOM widgets/overlays/attribution are NOT captured, so exports must render provider credits themselves.
28
28
  - `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (all 33 bundled, plus the native `COGLayer` raster type), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). Every type's full attribute list ships in the package's `onlymapjs.html-data.json` — consult it instead of guessing attribute names or reading the minified dist. TextLayer's default font atlas covers ASCII only: set `character-set` when label text carries other glyphs (`—`, `·`, accents) or deck warns and renders them blank. External layer classes register via `OmMap.registerLayer({type, deckClass, props})` — build them on `@nika-js/onlymap/deck` (the bundled `CompositeLayer`/`TileLayer`/… re-exports), never a separately-installed deck.gl (different class hierarchy, breaks in the renderer); function-valued props ride the subclass's `static defaultProps`; register at module top level BEFORE the manifest mounts (see docs/custom-layers.md). Data: `data` URL (JSON, GeoJSON, CSV/TSV `.csv` — parsed to typed columns — Shapefile `.shp` (+`.dbf` attributes) and KML `.kml` as GeoJSON features, or Arrow IPC `.arrow`/`.feather` — large point datasets stay columnar, GeoArrow line/polygon geometry becomes GeoJSON features, zstd-compressed IPC is handled; other formats plug in via `OmMap.registerFormat({match, parse})`; data URLs accept any scheme the runtime's fetch supports — desktop webviews (Tauri, Electron) pass asset-protocol URLs straight in), inline `<script type="application/json">` (row arrays or column-oriented `{"columns": {"lon": [...], "lat": [...]}}`; must be a DIRECT child of the `<om-layer>`, and when present it wins — omit the `data` attribute), or `wss://` streaming (`key="mmsi"` upserts entities in place, `flush="250ms"` coalesces bursts, `source="name"` selects a `OmMap.registerSource` decoder plugin), or a polled REST snapshot (`refresh="5s"` re-fetches and replaces — for live endpoints that return the full current state). Authenticated endpoints: call `OmMap.configureData({ headers: {...} })` in a script — never put tokens in attributes. `$field` accessors work identically on all of them — never write column-index code yourself. One columnar restriction: the `js` full-JS opt-in is not allowed on Arrow/columnar layers (validation will tell you; use `$field` accessors instead). For 3D models use `type="ScenegraphLayer"` with `scenegraph="./model.glb"` (required) and `get-orientation="[0, $heading, 90]"` — the roll of 90 stands Y-up glTF models upright; see docs/3d-assets.md. GeoTIFF/COG rasters use `type="COGLayer"` with `src="./dem.tif"` (NOT `data` — rasters stream tiles by Range request, they are not parsed rows): `min`/`max` set the rescale window (default 0–255; ALWAYS set them for float/16-bit data like DEMs), `colormap` picks a bundled ramp for single-band sources (gray, viridis, plasma, inferno, magma, cividis, rdylgn, rdbu, spectral, terrain, jet, turbo), `nodata` overrides the source sentinel (renders transparent); plain 8-bit RGB COGs need no styling attributes; restretch/recolor are GPU uniforms (no refetch) and the legend ramp derives from colormap+min/max automatically. Sources must be Cloud-Optimized (`gdal_translate -of COG` otherwise).
29
- - `<om-widget type="legend|layer-switcher|basemap-switcher|lighting|zoom-controls|undo-redo|scale-bar|attribution|filter|vega-lite" position="bottom-end">` — static UI panels. `position` takes one of 8 managed slots (logical, RTL-aware: `top-start|top-center|top-end|center-start|center-end|bottom-start|bottom-center|bottom-end`; legacy corners `top-left` etc. alias) — same-slot widgets stack with flush edges and a shared gap (never overlap); `order="1"` orders within a slot; adjacent compact button widgets (zoom-controls, undo-redo, widgets-toggle) auto-merge into ONE control group with dividers (`cluster="false"` opts a widget out); `position="manual"` renders a plain block you place with your own CSS (even outside the map). Layout tokens on `<om-map>`: `widget-style="gap:10 opacity:0.9 inset:16"` (keys inset/gap/inset-x/-y/gap-x/-y/opacity/radius/size, px except opacity) or the `--om-widget-inset-x/-y/-gap-x/-y/-opacity/-radius` custom properties. Built-ins are themeable from page CSS via custom properties (they inherit through the shadow root): `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` — full set: `--om-widget-bg/-fg/-muted/-border/-hover-bg/-accent`; scope to a single widget with an `om-widget[type=legend]` selector instead. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers', 'history', 'basemap']` (`layers` also fires on visibility/filter changes; `basemap` on basemap switches; `history` on undo/redo availability), `this.$(sel)`, `vegaEmbed`/`d3` as globals.
29
+ - `<om-widget type="legend|layer-switcher|basemap-switcher|lighting|zoom-controls|undo-redo|scale-bar|attribution|filter|vega-lite" position="bottom-end">` — static UI panels. `position` takes one of 8 managed slots (logical, RTL-aware: `top-start|top-center|top-end|center-start|center-end|bottom-start|bottom-center|bottom-end`; legacy corners `top-left` etc. alias) — same-slot widgets stack with flush edges and a shared gap (never overlap); provider attribution joins `bottom-end` and the license badge joins `bottom-start` as in-flow members, so required chrome never covers a widget; `order="1"` orders within a slot; adjacent compact button widgets (zoom-controls, undo-redo, widgets-toggle) auto-merge into ONE control group with dividers (`cluster="false"` opts a widget out); `position="manual"` renders a plain block you place with your own CSS (even outside the map). At map widths ≤640px managed widgets auto-fold into one accessible drawer per map side; `fold="never"` exempts an essential widget, `widgets-fold="off"` opts the map out, `--om-widget-fold-breakpoint` changes the threshold. Layout tokens on `<om-map>`: `widget-style="gap:10 opacity:0.9 inset:16"` (keys inset/gap/inset-x/-y/gap-x/-y/opacity/radius/size, px except opacity) or the `--om-widget-inset-x/-y/-gap-x/-gap-y/-opacity/-radius/-fold-breakpoint` custom properties. Built-ins are themeable from page CSS via custom properties (they inherit through the shadow root): `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` — full set: `--om-widget-bg/-fg/-muted/-border/-hover-bg/-accent`; scope to a single widget with an `om-widget[type=legend]` selector instead. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers', 'history', 'basemap']` (`layers` also fires on visibility/filter changes; `basemap` on basemap switches; `history` on undo/redo availability), `this.$(sel)`, `vegaEmbed`/`d3` as globals.
30
30
  - `<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). `{{field}}` interpolates the picked feature HTML-escaped; `{{{field}}}` is raw (avoid). For labels on many features use `PopupLayer`, not overlays.
31
31
  - `<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`, `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`, `undo`, `redo`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
32
32
  - 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")`.
@@ -162,6 +162,15 @@
162
162
  "name": "widgets-hidden",
163
163
  "description": "Hide-all: every widget goes visibility:hidden (state survives — no removal) except attribution and widgets-toggle, which never hide. Toggle via the set-widgets-visible action or <om-widget type=\"widgets-toggle\">; transient UI, not an undo step; story-scrub-capturable."
164
164
  },
165
+ {
166
+ "name": "widgets-fold",
167
+ "description": "Responsive auto-fold is on by default at map widths ≤ --om-widget-fold-breakpoint (640px): managed widgets move into one accessible drawer per map side. Set \"off\" to disable.",
168
+ "values": [
169
+ {
170
+ "name": "off"
171
+ }
172
+ ]
173
+ },
165
174
  {
166
175
  "name": "widgets-dim",
167
176
  "description": "Set \"off\" to disable collision-dim: by default a slot dims (--om-widget-opacity-dimmed, 0.35) while an open <om-overlay> popup covers it, rather than the popup dodging.",
@@ -1723,6 +1732,15 @@
1723
1732
  }
1724
1733
  ]
1725
1734
  },
1735
+ {
1736
+ "name": "fold",
1737
+ "description": "Set \"never\" to keep an essential control outside responsive side drawers on narrow maps.",
1738
+ "values": [
1739
+ {
1740
+ "name": "never"
1741
+ }
1742
+ ]
1743
+ },
1726
1744
  {
1727
1745
  "name": "watch",
1728
1746
  "description": "Space-separated watch tokens: viewport, selection, layers, history, basemap, lighting, terrain, widgets, data:<layerId>."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nika-js/onlymap",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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": {
@@ -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.4.3` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.4.3/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.4.4` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.4.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.
27
27
 
28
28
  ## React Projects
29
29
 
@@ -66,6 +66,7 @@ Load the smallest reference needed for the task:
66
66
  - Basemap choice or user-switchable basemaps -> `basemap` presets (`positron`, `liberty`, `dark-matter`, `osm`, ...) + `<om-widget type="basemap-switcher">`; MapTiler custom styles via a style URL or `basemap-key`.
67
67
  - Undoable UI (step back after layer toggles, filter changes, basemap switches, sketch edits) -> `<om-widget type="undo-redo">`; Cmd/Ctrl-Z works even without the widget. Camera moves and story playback are not undo steps.
68
68
  - Hide all map chrome (a clean/cinematic frame, a screenshot, a story beat) -> `widgets-hidden` attribute on `<om-map>`, the `set-widgets-visible` action, or a `<om-widget type="widgets-toggle">` button. State survives (not destroyed); attribution never hides. Do NOT hand-roll `display:none`.
69
+ - Keep mobile chrome usable -> rely on the default map-width auto-fold into per-side drawers; mark only essential controls `fold="never"`. Use `widgets-fold="off"` only when the user explicitly wants fixed wide-layout chrome.
69
70
  - Group adjacent map buttons (zoom + undo + toggle into one control group) -> just place compact button widgets in the same `position` slot; they auto-cluster. `cluster="false"` opts one out. Do NOT build a wrapper widget.
70
71
  - GeoTIFF/COG raster (DEM, satellite imagery, NDVI) -> `<om-layer type="COGLayer" src="…tif">` with `min`/`max`/`colormap` for single-band data (see syntax.md — `src`, not `data`).
71
72
  - Live entity updates -> `wss://` stream with `key` and optional `source` decoder.
@@ -48,9 +48,9 @@ function StatsPanel({ onToggle }) {
48
48
 
49
49
  ## Component surface
50
50
 
51
- - **`<OmMap>`** — `center`/`zoom`/`pitch`/`bearing` (initial; later changes move the camera, unchanged props never fight user panning), `basemap`, `headless`, `widgetStyle` (layout-token sugar, the `widget-style` attribute's twin: `"gap:10 opacity:0.9"` → `--om-widget-*` custom properties), `widgetsHidden` (hide-all: hides the slot containers via visibility widget state survives; MapLibre attribution is outside the layer and never hides), `onReady`, `onViewStateChange`, `onRuntimeError`. Give it a size via `style`/`className`. `ref` exposes the imperative `MapController` handle: `flyTo`, `setView`, `emit`, `getLayers`, `getSelection`, `injectPick`, `ready` (promise), `project`.
51
+ - **`<OmMap>`** — `center`/`zoom`/`pitch`/`bearing` (initial; later changes move the camera, unchanged props never fight user panning), `basemap`, `headless`, `widgetStyle` (layout-token sugar, the `widget-style` attribute's twin: `"gap:10 opacity:0.9"` → `--om-widget-*` custom properties), `widgetsHidden` (hide-all: hides authored managed widget wrappers without removal, so widget state survives; provider attribution and the license badge stay visible in their slots), `widgetsFold` (default true; map-width responsive side drawers, set false to opt out), `onReady`, `onViewStateChange`, `onRuntimeError`. Give it a size via `style`/`className`. `ref` exposes the imperative `MapController` handle: `flyTo`, `setView`, `emit`, `getLayers`, `getSelection`, `injectPick`, `ready` (promise), `project`.
52
52
  - **`<OmLayer>`** — `id` + `type` (any registered deck.gl layer type) + deck props. `data`: stable inline reference or URL string (full Data Layer: CSV/Arrow/Shapefile/KML formats, `ws(s)://` streams via `source`/`streamKey`/`flush`, `refresh` polling). `label`/`color` feed `ctx.layers`; `filterField`/`filterRange` = GPU filter; `onClick`/`onHover` receive the flattened picked object (`onHover(null)` = pointer left).
53
- - **`<OmWidget>`** — positioning shell: `position` takes one of 8 managed slots (logical, RTL-aware: `top-start|top-center|top-end|center-start|center-end|bottom-start|bottom-center|bottom-end`; legacy corners `top-left` etc. alias) + arbitrary JSX. Same-slot widgets stack with flush edges and a shared gap; `order={1}` sets deterministic in-slot ordering. `position="manual"` renders a plain block at the JSX site — note it sits inside OmMap's overflow-hidden box, so for UI OUTSIDE the map render your own element next to `<OmMap>` and drive the map via `useOmMap()`/the ref instead. (Automatic button-clustering of adjacent compact widgets, and collision-dim of slots under an open overlay, are HTML-lane only — in React, compose your own control group in JSX and dim via your own state.)
53
+ - **`<OmWidget>`** — positioning shell: `position` takes one of 8 managed slots (logical, RTL-aware: `top-start|top-center|top-end|center-start|center-end|bottom-start|bottom-center|bottom-end`; legacy corners `top-left` etc. alias) + arbitrary JSX. Same-slot widgets stack with flush edges and a shared gap; at map widths ≤640px they auto-fold into per-side drawers (`fold="never"` exempts an essential control); provider attribution joins `bottom-end` and the license badge joins `bottom-start` as in-flow members; `order={1}` sets deterministic in-slot ordering. `position="manual"` renders a plain block at the JSX site — note it sits inside OmMap's overflow-hidden box, so for UI OUTSIDE the map render your own element next to `<OmMap>` and drive the map via `useOmMap()`/the ref instead. (Automatic button-clustering of adjacent compact widgets, and collision-dim of slots under an open overlay, are HTML-lane only — in React, compose your own control group in JSX and dim via your own state.)
54
54
  - **`<OmOverlay>`** — geo-anchored HTML with managed projection/tracking/culling. `anchor={[lng, lat]}` or `anchorFrom="selection"` (+ `layer` to scope which picks move it); children may be `(selection) => JSX`; `anchorOffset` (default `bottom-center`); `interactive={false}` for hover-following tooltips.
55
55
  - **`useOmMap(watch?)`** — the same `ctx` contract HTML widget scripts get, typed: `layers`, `viewport`, `selection`, `emit`, `data()`, `dataInViewport()`, `stats()`. Watch tokens: `"viewport"`, `"selection"`, `"layers"`, `"data:<layerId>"`. Rides `useSyncExternalStore` (tearing-safe; `ctx` identity stable until a watched token fires).
56
56
  - **`controller.getStore(token)`** — framework-free `{subscribe, getSnapshot}` per watch token (cached plain-data snapshots; viewport snapshots carry `origin: "user"|"programmatic"` for echo-free two-way binding). Use it to sync map state into Redux/MobX/Zustand/Jotai — recipes in docs/external-stores.md; never mirror row arrays into a store (use the `data:<id>` version stamp + `ctx.data()`).
@@ -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.4.3/dist/onlymapjs.css">
20
- <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.4.3"></script>
19
+ <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.4.4/dist/onlymapjs.css">
20
+ <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.4.4"></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).
@@ -215,7 +215,7 @@ Built-ins:
215
215
  - `widgets-toggle` — one button hiding/showing all OTHER widgets (`widgets-hidden` attribute / `set-widgets-visible {visible}` action; bare payload toggles). Hidden = visibility, never removal — widget state survives; attribution and the toggle itself never hide. Transient (not an undo step), story-scrub-capturable.
216
216
  - `undo-redo` — undo/redo buttons over the manifest history (layer toggles, filters, basemap switches, element edits, drawn sketches). Keyboard works without the widget: Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z, Ctrl-Y. Camera moves, hover effects, and story playback are not undo steps.
217
217
 
218
- Positions — 8 managed slots (logical, RTL-aware): `top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`. Legacy corner names (`top-left`, `top-right`, `bottom-left`, `bottom-right`) are aliases. Same-slot widgets stack in one library-owned flex container: flush edges, shared gap — never overlapping. `order="1"` sets deterministic in-slot ordering (default: DOM order). Adjacent COMPACT button widgets (zoom-controls, undo-redo, widgets-toggle) in one slot auto-merge into a single control group (shared radius/shadow, 1px dividers); `cluster="false"` keeps one out — validation warns if set on a non-compact widget. `position="manual"` opts out of management: the widget renders as a plain block you place with your own CSS (even outside the map, e.g. in an app header, driving the map through actions). Layout tokens: `--om-widget-inset-x/-y` (slot inset, default 12px), `--om-widget-gap-x/-y` (stack gap, default 8px), `--om-widget-opacity`, `--om-widget-opacity-dimmed` (default 0.35 — the collision-dim level), `--om-widget-radius` — or the no-CSS sugar attribute `<om-map widget-style="gap:10 opacity:0.9 inset:16">` (keys: inset, gap, inset-x/-y, gap-x/-y, opacity, radius, size; numbers are px except opacity).
218
+ Positions — 8 managed slots (logical, RTL-aware): `top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`. Legacy corner names (`top-left`, `top-right`, `bottom-left`, `bottom-right`) are aliases. Same-slot widgets stack in one library-owned flex container: flush edges, shared gap — never overlapping. `order="1"` sets deterministic in-slot ordering (default: DOM order). Adjacent COMPACT button widgets (zoom-controls, undo-redo, widgets-toggle) in one slot auto-merge into a single control group (shared radius/shadow, 1px dividers); `cluster="false"` keeps one out — validation warns if set on a non-compact widget. At map widths ≤640px, managed widgets auto-fold into top/end/bottom/start disclosure drawers; `fold="never"` keeps an essential control outside, `widgets-fold="off"` opts the map out, and `--om-widget-fold-breakpoint` changes the map-width threshold. `position="manual"` opts out of management: the widget renders as a plain block you place with your own CSS (even outside the map, e.g. in an app header, driving the map through actions). Layout tokens: `--om-widget-inset-x/-y` (slot inset, default 12px), `--om-widget-gap-x/-y` (stack gap, default 8px), `--om-widget-opacity`, `--om-widget-opacity-dimmed` (default 0.35 — the collision-dim level), `--om-widget-radius`, `--om-widget-fold-breakpoint` — or the no-CSS sugar attribute `<om-map widget-style="gap:10 opacity:0.9 inset:16">` (keys: inset, gap, inset-x/-y, gap-x/-y, opacity, radius, size; numbers are px except opacity).
219
219
 
220
220
  Theming: built-in widgets read `--om-widget-*` CSS custom properties, which inherit through their shadow roots — so plain page CSS themes them, no JS:
221
221
 
@@ -94,6 +94,21 @@ Use `vi.stubGlobal("fetch", ...)` for URL data and `vi.stubGlobal("WebSocket", .
94
94
 
95
95
  Free-plan license gates (5 layers, 25k rows per layer) apply in headless tests exactly as in production — gated layers emit errors on the validation stream instead of rendering. If the page under test legitimately exceeds the limits, call `OmMap.configureLicense("om_live_…")` in test setup; keys verify offline, so CI needs no network. Telemetry never fires from headless maps.
96
96
 
97
+ ## Layout Audit
98
+
99
+ Run the public browser-truth audit against a trusted HTML manifest:
100
+
101
+ ```bash
102
+ npm install --save-dev playwright
103
+ npx playwright install chromium
104
+ npx onlymapjs check-layout ./map.html
105
+ ```
106
+
107
+ The command serves the manifest and its relative assets on loopback, tests
108
+ 360/640/768/1024px map layouts, prints JSON diagnostics, and exits nonzero on
109
+ layout or disclosure failures. It executes the page's scripts, so do not run
110
+ it against untrusted HTML.
111
+
97
112
  ## Browser/E2E Testing
98
113
 
99
114
  Use Playwright only for pixels, real GPU picking, basemap composition, or asset rendering.