← All releases·15 November 2026

Element Editor 0.2.0 — first npm release

First public release on npm. Five packages, all at 0.2.0 and MIT-licensed:

  • @element-editor/react: the drop-in <ElementEditor /> on MapLibre GL, and on Leaflet at @element-editor/react/leaflet.
  • @element-editor/adapter-maplibre and @element-editor/adapter-leaflet: editing for a map you already have.
  • @element-editor/ui: the toolbars, panels, data table and GeoJSON inspector, for your own layout.
  • @element-editor/core: stores, types and helpers shared by all of them.

New to Element? Start with the quick start; the rest of this entry lists what changed since the 0.1 pre-release builds. 0.1.0 was never published to npm, so Breaking and Deprecated only matter if you built against one of those: see Migrating to 0.2.

Breaking

  • @element-editor/react is now the MapLibre drop-in. <ElementEditor /> owns a MapLibre GL map instead of a Leaflet map, and @element-editor/react/style.css bundles maplibre-gl.css instead of Leaflet's. Install maplibre-gl ^5.5 next to it (npm i @element-editor/react maplibre-gl). The package depends on @element-editor/core, @element-editor/ui and @element-editor/adapter-maplibre instead of bundling them.
  • The Leaflet drop-in moved to @element-editor/react/leaflet, stylesheet @element-editor/react/leaflet/style.css. Its peers are optional and no longer installed for you: npm i @element-editor/react @element-editor/adapter-leaflet leaflet react-leaflet (no maplibre-gl needed). Same component, props and ref.
  • Engine-specific props. tileLayer, renderer and raster basemaps exist only on /leaflet; vector or raster basemaps only on the default entry (basemap, defaultBasemap and onBasemapChange are on both, with each entry's basemap type). On the default entry tileLayer / renderer are ignored with a development warning.
  • Peer dependencies: react / react-dom ^19.2 (React 18 is no longer supported), react-leaflet ^5, leaflet ^1.9, maplibre-gl ^5.5 (the Terrain basemap needs 5.5; was >=4). Every peer of @element-editor/react except React is optional, so install the ones for the entry you use; maplibre-gl is also an optional peer of @element-editor/adapter-maplibre.
  • CSS: the stylesheets no longer apply Tailwind's global preflight to the host page. Base styles are scoped to [data-elm-theme] elements and the editor's portals. Adapter users must render the map and all editor UI inside an element with data-elm-theme="light" or "dark"; portaled UI of your own should carry data-elm-portal.
  • Events: every user-facing message goes through onEvent; with onEvent set (managed mode) the editor shows no toasts at all.
  • Drawing tools stay active after each shape by default. "Return to select after each shape" (Settings, Cmd+J) is now off for new users; a user who already set it either way keeps their choice (elm_autoExitAfterDraw).
  • The GeoJSON inspector opens docked beside the map by default instead of as a floating window; a user's saved Dock / Undock choice still wins. config.ui.geojsonEditorOpen: 'docked' now means the same as true.
  • Keyboard shortcuts: T toggles the data table (0.1.0: V) and A toggles the transform tool (was T). Bindings a user customised are kept.
  • maxFeatures / setMaxFeatures moved from uiStore to dataStore (@element-editor/core). The maxFeatures prop is unchanged. The adding actions (duplicateFeature, paste, break apart, …) now return boolean; dataStore.addFeature returns the stored feature or null.
  • Errors: ElementEditorUIProvider's onError now fires for every runtime failure, not only render crashes (check error.code === 'crash' for those), with an ElementEditorError (still an Error). @element-editor/core: DataStoreOptions.onError / UIStoreOptions.onError and uiStore.reportError take an ElementEditorError (plus { quiet }) instead of a string; dataStore.copySelection() returns Promise<boolean>. Hint-like messages (a vertex that can't be deleted, an invalid zoom or coordinate, polygons-only boolean ops, nothing to export or fit) are warning events instead of error.
  • uiStore.bounds is now a GeoJSON bbox [west, south, east, north] (was [[south, west], [north, east]]), whatever form the bounds prop was given in. Only affects code that reads the store.
  • Licence key removed: the licenseKey prop and evaluation-mode warning are gone (the community edition is MIT, see Changed).
  • theme is the mode only: 'light' | 'dark' | 'auto'. The object form (CSS variables for the editor root) is removed: pass them through style (style={{ '--elm-accent': '#7c3aed' }}) or use accentColor. An object passed to theme anyway is ignored with a development warning.
  • Removed:
    • The 0.1 --elm-* surface, border, text and shadow tokens that no component read. They are replaced by working design tokens (see Added); --elm-radius and --elm-font keep their names, now with an effect.
    • Self-intersection (kinks) checking: polygons with crossing edges are not flagged.
    • @element-editor/core: the unused importWorker, exportWorker, validationWorker and a duplicate geometryWorker. Import, export and validation run on the main thread; the GeometryWorkerPayload type (what runGeometryWorkerOperation takes) stays.
    • @element-editor/adapter-maplibre: DrawingCursorHint (it never rendered).
    • config.ui.corners and config.toolbar.position (and the types Corner and CornerItem): place controls with config.ui.layout. toolbar.position: 'top-left' → 'toolbar' in topLeft; ui.corners.<corner>: { items, direction } → <corner>: { items, direction }; to keep the default and move a few controls, withLayout({ move: { … } }). toolbar.direction stays. See Migrating to 0.2, section 9.

Added

  • Late initialData. Data that arrives after mount (null / undefined / an empty collection → data, the usual fetch-then-render) is applied once while the editor is still empty and unedited, and the drop-ins fit it as they would on mount. Any later change is ignored with a development warning: replace data with ref.setFeatureCollection() or remount with a new key.
  • ref.setFeatureCollection(data, { silent, undoable }). silent: true loads without firing onChange / onFeature* (so data you just fetched isn't saved straight back); undoable: false loads without an undo step and clears the history, like initialData.
  • More ref methods and onToolChange: getFeature(id), getTool(), canUndo(), canRedo() and validate() ({ valid, errors } for every feature, changes nothing). onToolChange(tool | null) prop on both drop-ins and both adapters' ElementEditorControls.
  • Built-in file drop on both drop-ins: drop a .geojson / .json file anywhere on the editor to import it like Cmd/Ctrl+O (replaces the data in one undo step, fits the map, reports skipped features). fileDrop={false} turns it off; it is off in readonly.
  • Narrow layout below 640 px (the editor's own width, not the viewport): the layers panel and docked inspector overlay the map and start collapsed, rows wrap their buttons, the search box collapses to an icon and the editing toolbars stay inside the map.
  • Escape closes windows (shortcuts, settings, schema editor, the floating GeoJSON inspector) and returns focus to the button that opened them; on the map, Escape first cancels the active tool or selection.
  • Data table: a Type column (icon plus Point / Line / Area and the shape's size); Lat / Lng columns only while the table lists a point.
  • Lines get a thin casing (a light or dark halo under the stroke, by basemap) so they stay readable over busy tiles. MapLibre layer elm-features-line-casing.
  • AGENTS.md in every package: a guide for coding agents (entry decision table, installs, minimal examples, Next.js, data loading, ref API, error codes, common mistakes), type-checked in CI.
  • Ref API: getMap() (typed maplibregl.Map on the default entry and adapter-maplibre, L.Map on /leaflet and adapter-leaflet; null until mounted), getSelection(), select(ids, { fit }), setTool(tool | null), addFeature(f) (returns the stored feature or null), updateFeature(id, patch), removeFeatures(ids) (returns the count). Mutations behave like user edits (validation, maxFeatures, undo, onChange / onFeature*); { undoable: false } (also on updateFeatures) applies a change without an undo step and keeps it when the user undoes earlier edits. Bad input returns null / false / 0 with a development warning.
  • Map setup props on the drop-ins: initialView ({ center: [lng, lat], zoom }, { bounds } or 'data'; wins over the remembered view), mapOptions (extra maplibregl.Map / L.Map options, typed per entry, minus the ones Element sets itself, which are dropped with a development warning) and onMapReady(map) (once per map, after Element's layers are on it: add your own layers there). Start view when initialView is omitted: the remembered view, else a fit of initialData (first visit), else a fit of bounds, else Europe. Adapters: onReady(map) on both ElementEditorControls.
  • MapLibre: Element survives map.setStyle. After any style replacement (your own basemap switcher, MapTiler SDK style switching, setStyle with or without diff, a style URL or object) Element puts its sources, layers (on top), images and the selection highlight back by itself and refills them with the current features. Your own layers aren't Element's to restore: re-add them from a style.load listener added in onMapReady / onReady (it runs after Element's repair, so an elm-* beforeId works), and your addImage images from a styleimagemissing listener. A style without glyphs gets every Element layer but the text-feature one (development warning).
  • onError on <ElementEditor> (both entries), ElementEditorControls (both adapters), ElementEditorUIProvider and EventProvider: called first for every runtime failure with an ElementEditorError (code from a small closed union such as 'rate-limited', 'timeout', 'invalid-data', 'limit-reached', 'crash'; a source such as 'basemap', 'picker', 'routing', 'search', 'import'; an honest English message; cause, featureId). Also fires with onEvent set and for quiet failures that show no toast. Return false to stop the built-in toast and the onEvent error event, or a string to show that text instead (translation). The onEvent error event carries code and source. OverpassError is an ElementEditorError (new code 'invalid-data').
  • config.network: every third-party server in one place, per editor (both drop-ins and ElementEditorUIProvider for the adapters). vectorTiles, dem (with encoding), osmRaster, glyphs and sprite rewrite the built-in basemaps (a URL, or MapLibre source fields merged over the built-in source); overpass (endpoint, timeoutMs), routing (provider: 'ors' | 'osrm', url, apiKey, attribution) and nominatim (url, email sent as a query parameter, attribution) set the Picker's, Route tool's and search box's servers; fetch is used for every request Element makes itself. false turns a service off and hides what needs it (routing: false the Route tool, nominatim: false the search box, overpass: false the Leaflet Picker; on MapLibre the Picker then reads only the vector tiles: "No shape here in the basemap"). Headers for map resources go through mapOptions.transformRequest. Two editors on one page no longer share any of these. See docs/guides/third-party-services.md. Types NetworkConfig, NetworkSource, RoutingConfig, NominatimConfig (@element-editor/core).
  • @element-editor/core: fetchRoute(waypoints, { provider, url, apiKey, fetch, signal }) and fetchOverpassFeatures(lat, lon, radius, click, { endpoint, timeoutMs, fetch, signal }) take the per-editor settings per call (a bare AbortSignal still works). Failed lookups reject with OverpassError (code: 'rate-limited' | 'busy' | 'timeout' | 'network'); types FetchRouteOptions, OverpassOptions, OverpassErrorCode. uiStore.unavailableTools / isToolAvailable(tool): the tools config.network turns off.
  • Attribution follows the services in use: "Geocoding by Nominatim" only while the search box is offered (and your nominatim.attribution on your own server), a new "© openrouteservice.org by HeiGIT" credit while the Route tool routes through ORS, and a development warning when a tile source points at another server without an attribution (the default provider's credit is dropped; the OpenStreetMap data credit stays).
  • A development warning the first time the Route tool snaps through the public OSRM demo server (the default): it is car-only, rate-limited and not for production.
  • Built-in basemaps for the MapLibre drop-in: Streets, Terrain, Light and Dark (OpenFreeMap vector tiles; Streets and Terrain with Mapterhorn hillshade) and OpenStreetMap (raster), plus 'auto', which follows the editor theme (light: Streets, dark: Dark). See docs/guides/basemaps.md.
  • <ElementEditor> (MapLibre) props defaultBasemap (starting basemap, default 'auto'; the user's pick is remembered in localStorage elm_basemap and wins on reload), basemap (controlled by your app: changing it switches the basemap, nothing is read from or written to localStorage, it may be off the picker's list; dev warning when a picker shows and onBasemapChange is missing, and for an id-less style object passed anew every render), onBasemapChange (the user's pick or ref.setBasemap(), as the basemaps entry; 'auto' stays 'auto'; not called for theme flips or prop changes), basemaps (picker choices; false hides the picker). The sprite defaults to the jsDelivr copy at the package version (self-host it with config.network.sprite). Custom basemaps: a vector style URL or StyleSpecification, or raster { url, attribution }; give each an id so the picker can tell them apart and remember the pick.
  • Basemap picker with thumbnails (inlined, no requests), as the new 'basemap' layout control (first in the default bottomLeft; hidden when the editor has no picker; can't go in a group).
  • @element-editor/adapter-maplibre/basemaps entry: BASEMAPS, BasemapInput, BuiltinBasemapId, resolveBasemap, loadBasemapStyle, setBasemap (switches style and keeps Element's layers and images; the latest call wins), DEFAULT_SPRITE_URL, DEFAULT_GLYPHS_URL, type BasemapNetwork (the { network } option of loadBasemapStyle / setBasemap). BasemapControl and useBasemap in the main entry for adapter hosts.
  • Basemap switches keep your own layers. setBasemap / BasemapControl (and so the drop-in's picker and 'auto' following the theme) carry over Element's elm-* layers and every other source and layer that wasn't part of the previous basemap, in their order relative to Element's layers and above the new basemap. Layers or sources whose id the new basemap also uses give way to it. Images added with map.addImage other than Element's are still dropped by the style switch.
  • Leaflet drop-in: optional basemaps?: (RasterBasemap | 'osm')[] (one entry replaces OpenStreetMap; two or more add a picker, choice remembered in elm_basemap_leaflet; entries told apart by id, else url), plus basemap / defaultBasemap / onBasemapChange as on the MapLibre drop-in. tileLayer overrides them all (dev warning). New RasterBasemap type from @element-editor/ui and both react entries; LeafletBasemap (RasterBasemap | 'osm') from /leaflet.
  • Ref (both drop-ins): getBasemap() (the current choice) and setBasemap(b) (switches like a user pick, fires onBasemapChange, isn't remembered; with a controlled basemap it only fires onBasemapChange).
  • Picker (MapLibre) reads shapes from the basemap's vector tiles first, merging tile-split pieces; Overpass is the fallback.
  • config.ui.layout: one placement model for every <ElementEditor> control. Ten slots (topLeft, topCenter, topRight, leftCenter, rightCenter, bottomLeft, bottomCenter, bottomRight, statusLeft, statusRight) map to ordered entries: a control id ('toolbar', 'undo', 'redo', 'fitBounds', 'locate', 'zoomIndicator', 'table', 'schema', 'geojson', 'settings', 'keyboard', 'applyChanges', 'attribution', 'basemap'), 'separator', or { group: [...] }. Unlisted controls are hidden, except the map attribution. statusLeft / statusRight add a thin status bar under the map; 'attribution' there moves the map credits into it. Slots on one map edge never overlap. Slot-specific types: a misplaced control is a compile error. withLayout(patch, base = DEFAULT_LAYOUT) moves, adds or hides a few controls and keeps the rest (layout: withLayout({ move: { table: 'topRight' }, hide: ['keyboard'] })). Development warnings name both places of a control listed twice (and point to withLayout), and flag a layout that leaves the toolbar unplaced while drawing is possible, 'applyChanges' unplaced with onSubmit, or undo/redo unplaced with keyboard shortcuts off. New @element-editor/ui exports: withLayout, LayoutSlot, MapSlot, StatusSlot, LayoutControl, LayoutGroup, MapGroup, StatusGroup, LayoutEntry, MapEntry, StatusEntry, LayoutSlotConfig, MapSlotConfig, StatusSlotConfig, LayoutConfig, LayoutPatch, SlotFor, ResolvedLayout, DEFAULT_LAYOUT, UndoButton, RedoButton, ControlGroup, IconButtonSize, TooltipSide; both react entries re-export the layout types, DEFAULT_LAYOUT and withLayout. IconButton, FitBoundsButton, LocateButton, ZoomIndicator take size?: 'md' | 'sm' | 'group' and tooltipSide. Style hooks: data-elm-slot, data-elm-group, data-elm-separator.
  • slots covers all eight map slots (topCenter, leftCenter, rightCenter, bottomCenter added). The tools prop takes any DrawTool ('picker', 'measure', 'search' too).
  • config.persistKey / config.persist. persistKey namespaces everything an editor remembers in localStorage (elm_theme → elm_<persistKey>_theme, …), so several editors on one origin keep separate state; persist: false reads and writes nothing. Works on both drop-ins and through ElementEditorUIProvider config. @element-editor/core: PersistScope, scopedKey, and an optional scope argument on readStorage / writeStorage / readPersisted / writePersisted.
  • Feature colours on every entry point. featureColor (flat prop on both drop-ins and both adapters' <ElementEditorUIProvider>, or config.featureColor; the flat prop wins) sets the default colour of every feature's outline, fill and point. getFeatureStyle is a prop of both drop-ins (same signature as the adapters'), and FeatureStyle is exported from @element-editor/react and /leaflet. Precedence: selected, then getFeatureStyle per key, then featureColor, then the default slate; a hovered feature keeps its colour. Leaflet: getFeatureStyle also colours point pins, fillColor falls back to color like on MapLibre, and it may return undefined/null.
  • Design tokens for full custom theming. 23 CSS variables restyle the whole editor chrome, per mode and per editor: --elm-font, --elm-font-mono, --elm-radius, --elm-radius-control, --elm-shadow, --elm-shadow-raised, --elm-shadow-control, --elm-surface, --elm-surface-raised, --elm-surface-sunken, --elm-surface-hover, --elm-control-hover, --elm-text, --elm-text-secondary, --elm-text-muted, --elm-text-subtle, --elm-text-disabled, --elm-border, --elm-border-subtle, --elm-border-strong, --elm-focus-ring, --elm-tooltip, --elm-tooltip-text. Set them in a stylesheet (on [data-elm-theme="light"] / "dark", a class on the editor root, or :root) or through style; defaults are the previous look, unchanged. Panels, toolbars, menus, windows, the data table, the GeoJSON inspector's chrome, toasts, tooltips, the basemap and OSM trace pickers, map popups and the attribution chrome all follow them. Element copies the tokens the editor root resolves onto its body-level portals, so a rule like .brand-editor[data-elm-theme="dark"] { … } reaches modals, menus and tooltips too (this also applies to --elm-accent / --elm-primary-N set in a stylesheet). See Theming.
  • One-colour branding: <ElementEditor accentColor>, config.accentColor, <ElementEditorUIProvider accentColor> or CSS --elm-accent. The whole primary 50–950 scale (buttons, selection, map highlights, handles, previews) is generated from it, live. @element-editor/core: accentPalette, readAccentPalette, resolveColor, parseColor, mixOklab, DEFAULT_ACCENT, DEFAULT_FEATURE_COLOR, ACCENT_STEPS, AccentPalette / AccentShade. @element-editor/ui: useAccentPalette, useAccentStyle / accentStyle, useApplyAccentToRoot.
  • config.ui.geojsonEditorOpen?: boolean | 'docked': open the GeoJSON inspector on mount (initial state only; a saved Dock/Undock choice wins). Docked GeoJSON inspector (dock toggle in the window title bar; remembered in elm_code_docked). New @element-editor/ui export GeoJSONDockPlaceholder (Suspense fallback).
  • bounds (both drop-ins, adapter-leaflet ElementEditorControls) takes a GeoJSON bbox [west, south, east, north]; adapter-maplibre maxBounds takes one too. ref.flyTo({ center: [lng, lat], zoom? }). New types BBox, BoundsInput, FlyToOptions (core and both react entries) and ElementEditorBaseProps (both react entries); BasemapInput / BuiltinBasemapId from @element-editor/react.
  • @element-editor/adapter-leaflet and the Leaflet drop-in: renderer?: 'canvas' | 'svg' (default 'canvas': unselected polygons and lines share one canvas; 'svg' keeps one <path> per feature).
  • ElementEditorUIProvider (exported from @element-editor/ui, re-exported by both adapters): toaster prop, default true. Each provider and <ElementEditor> renders its own <Toaster> with per-instance ids, so adapter users don't add one and a second editor never shows a toast twice. useToasterId(), useIsEventForEditor(), EmittedEvent; error / warning / success events may carry a description.
  • @element-editor/adapter-leaflet: FeatureStyleProvider, useFeatureStyle, FeatureStyle (parity with adapter-maplibre).
  • 'use client' at the top of both @element-editor/react entries, @element-editor/ui (all entries) and both adapters, so Next.js App Router Server Components can import them. @element-editor/core and adapter-maplibre/basemaps stay server-importable.
  • Enter finishes a line, polygon, route, slice or cut even with ui.keyboardShortcuts: false.
  • Data table: paste text from the clipboard into a selected editable cell; sort arrows and truncating headers.
  • Layers panel: right-click a feature row → Zoom to feature.
  • --elm-bottom-stack-offset CSS variable on the <ElementEditor> root (bottom edge of the bottom-center editing toolbars).
  • @element-editor/core: readStorage / writeStorage (never throw), dataStore.canAddFeatures(n), dataStore.deleteFeatures(ids, { undoable }) (one undo step, returns the count) and isGeometry.

Changed

  • Plain GeoJSON in, strict GeoJSON out. initialData, ref.setFeatureCollection(), ref.importGeoJSON() and ref.addFeature() (both drop-ins and both adapters) are typed with the new FeatureCollectionInput / GeoJSONFeatureInput (exported by @element-editor/core and both @element-editor/react entries): metadata optional, feature id optional (string | number), properties may be null. Element stores them normalised (UUID for a missing id, numbers as strings, {} properties, the default schema). getFeatureCollection(), onChange and events still return the strict FeatureCollection / GeoJSONFeature. The input types accept @types/geojson as is: 3D positions, 6-value bbox, GeometryCollection, geometry: null (such a feature is skipped and reported through onError, code: 'invalid-data').
  • Loading initialData no longer fires onChange / onFeature* (it did, twice under StrictMode): the host already has that data.
  • submit() and validate() check every feature. Imported data with a missing required field used to pass submit().
  • readonly closes every write path, not only the drawing tools: data table edits and paste, layer rename and drag-reorder, the schema editor, typing in the GeoJSON inspector, editing shortcuts, import (Cmd+O, file drop) and undo / redo. Selection, measuring, search, copy, export and the panels still work; the ref API is not restricted.
  • Boolean operations are one undo step (union, subtract, intersect, exclude), as are duplicate, layers drag-reorder and group rename. Union of many features used to push one entry per input, so the first Cmd+Z emptied the map.
  • Simplify, Smooth and Buffer work on lines (LineString / MultiLineString): simplify and smooth keep the line open with its endpoints, buffer turns it into a corridor polygon.
  • Circles, rectangles and routes become plain shapes on the first point edit. Entering vertex edit shows a notice; the first committed point change drops _drawType (and a route's _waypoints) in the same undo step. A rectangle's corners can be deleted like any point (it used to refuse with "Convert to polygon first.", an action that didn't exist).
  • GeoJSON inspector: invalid text is underlined, a status line says where ("Invalid JSON at line 42 — no changes apply until it's fixed") and jumps there on click; text that isn't applied is kept as a draft when the inspector closes or docks.
  • Undo / redo keep the selection (and an open vertex edit or transform) for the features that still exist.
  • Transform: clicking a feature stays in transform; Escape, A or a click on empty map exits. A drag anywhere inside the box moves the whole selection, the same on both engines.
  • Fly / fit frame the visible map area: every fit, fly-to and pan (selection, search, zoom to feature, ref.flyTo / fitBounds, select(ids, { fit: true })) centres the target in the part of the map the open panels leave visible, so map.getCenter() can differ from the target while a panel is open.
  • MapLibre: on dark basemaps the default feature colour is a lighter slate (#94a3b8); lines are drawn 3.5 px wide by default.
  • Toasts follow the editor's theme; search Enter goes to the highlighted (or first) result.
  • Replace import is one undo step (Cmd/Ctrl+O, ref.importGeoJSON, setFeatureCollection) instead of writing a file backup: one undo restores the previous features, schemas and groups.
  • @element-editor/adapter-maplibre no longer moves the map on mount. ElementEditorControls used to fit the map to the store's features when it mounted (an undocumented FitBounds; in practice only the MapLibre drop-in hit it, and it overrode the remembered view on every load). The drop-ins now choose the start view (see initialView); adapter users fit with ref.fitBounds. FitBounds stays exported.
  • bounds sets the start view when nothing else does (no initialView, no remembered view, no initialData): the map opens fitted to it instead of at Europe.
  • Licence: the community edition is MIT (all packages).
  • npm metadata: every package has repository, homepage, bugs, keywords and publishConfig (public, with provenance), and ships LICENSE, README.md and AGENTS.md.
  • Packaging: CommonJS entries are .cjs files (the packages are "type": "module"); exports maps use per-condition types with .d.cts twins, so types resolve under moduleResolution: node16 / nodenext and for require. Declarations reference the other packages by name and every bare import is external (@element-editor/react dist 4.2 MB → 152 KB). Subpath entries (/leaflet, /basemaps, /primitives, /internal) also resolve under moduleResolution: node10 (typesVersions); no declaration maps pointing at unpublished sources.
  • The geometry worker is inlined and loaded on first use (no bundler configuration). Buffer, Simplify and Smooth previews run in it (latest slider value wins; 30 s timeout), and so do Cut hole and Slice (a cut on a 50k-vertex polygon took over 2 s on the main thread), with a main-thread fallback where no worker can start.
  • The MapLibre drop-in renders on the server without errors: it outputs its loading shell and doesn't touch window, document or localStorage there. Keep ssr: false in Next.js anyway (see the Next.js guide); the Leaflet drop-in can't render on the server at all.
  • Keyboard shortcuts and Escape act only on the editor that has focus or the last pointer press (its [data-elm-theme] root); two editors, and the host's own inputs and modals, no longer receive each other's keys.
  • routingApiKey is per editor instance instead of module-wide (and now deprecated, see below).
  • The search box no longer sends a User-Agent header (browsers drop or preflight it); identify your app to Nominatim with config.network.nominatim.email.
  • config.mode and the theme prop no longer write the theme to localStorage, so one editor's config doesn't leak into others.
  • Invalid initialData reports an error event and starts empty instead of crashing; a single Feature is wrapped in a FeatureCollection.
  • Picker / Overpass: a busy or rate-limited server (429/502/503/504) is retried twice, honouring Retry-After up to 10 s; each attempt times out after 15 s; network errors aren't retried; Escape cancels a lookup in progress.
  • @element-editor/core: react peer ^19.2.0 like the other packages; the new GeometryWorkerPayload type covers the buffer/simplify/smooth payloads.
  • UI: toasts are compact and quiet (no icons or per-type colours, inverted against the editor theme; new ElementToaster export); delete/remove actions are no longer red; buttons that open a window or panel show a filled icon while it's open; tooltips flip and clamp to stay on screen; the layers-panel toggle sits in the panel header; every zoom-to action flies in Leaflet too; panels, table, inspector and attribution move together when a panel opens or closes; the feature panel's Source JSON is syntax-highlighted; Settings → Geometry drawing mode reads "Visual" / "Spatial".
  • The Polygon tool is called Area everywhere in the UI (toolbar, shortcuts panel "Draw area", messages). Its id stays 'polygon' (tools, setTool) and GeoJSON types are unchanged. Unnamed features are labelled in plain words (Point / Points, Line / Lines, Area / Areas, Mixed) instead of "Polygon", "MultiPolygon", "MultiLine", "Collection". The Measure tool's mode buttons read "Measure distance" / "Measure area".
  • Map attribution follows the OSM Foundation attribution guidelines on both drop-ins: expanded at load, folded into the "i" button on the first map interaction or after five seconds (Leaflet used to start folded; MapLibre folded only on a drag).

Removed from the public API

Each package's root entry now exports only its documented API. Nothing here was ever published, so there is no deprecation period. Helpers the packages share among themselves moved to @element-editor/core/internal (new) and @element-editor/ui/internal, which are not covered by semver and not meant for application code.

  • @element-editor/core, moved to /internal: the Mercator and geometry math (project, unproject, toRadians, getMercatorLatCorrectionRatio, applyVisualTransform, translateGeometryVisually, translateGeometrySpatially, mapPositions, rdp), multi-geometry parts (isMultiGeometry, getPartCount, getPartGeometry, setPartGeometry, getPartBBox, getPartLabel, offsetGeometry, offsetSimpleGeometry), topology (findTopologicalNeighbors, groupNeighborsByFeature, applyTopoMove, commitTopoMove, commitTopoInsert, commitTopoDelete, transformUpdates, NeighborVertex, TopoUpdate), placeBox / PlaceMode / PlaceBounds, createClickRepeatGuard, CLICK_SLOP_PX, CLICK_TOOL_MODES, normalizeBounds, normalizeFlyToTarget, normalizeFitBoundsTarget, warnOnce, isDevBuild, httpErrorCode, loadErrorCode, routingError, routingAttribution, overrideSource, OSM_ATTRIBUTION, the picker helpers (tileCandidates, tileCandidateResult, unionPieces, overpassRadiusForZoom, addPickedFeature, OSM_PICK_MIN_ZOOM, OSM_PICK_OVERLAY_COLORS, TileFeature, TileCandidate), schema defaults (DEFAULT_SCHEMAS, BOUNDARIES_SCHEMA_ID, FIELD_TYPES, slugify, uniqueKey), shortcut bindings (CUSTOMIZABLE_SHORTCUTS, FIXED_SHORTCUTS, CustomizableShortcut, loadCustomBindings, saveCustomBindings, getEffectiveKey, getCurrentKey, findConflict, syncShortcutCache), GeometryWorkerRequest, GeometryWorkerResponse, setGeometryRunner, GeometryRunner, featureLimitMessage, freeGeometry, canProcessShape, isLineGeometryType, countShapeVertices, makeCircleCoords, makeRectCoords, resolveUnitSystem.
  • @element-editor/core, no longer exported: getBBox, exportLayerGeoJSON, getBaseGeometryCategory, mergeGeometries, canVertexEdit, findSharedEdgeNeighbors, applyTopoInsert, applyTopoDelete, SharedEdgeNeighbor, clearOverpassCache, OVERPASS_DEFAULTS, tileFeatureToResult, overpassResultToFeature, DEFAULT_SCHEMA, getRoutingApiKey, ORS_DEFAULT_URL, OSRM_DEFAULT_URL, ORS_ATTRIBUTION, STORAGE_KEY, FixedShortcut, AnyShortcut, getGeometryRunner.
  • @element-editor/core, kept and now documented as public: the store state types DataStore, UIStore, UndoEntry (new export), PanelPrefs (new export), StoreFeaturePatch (new export: what the store's updateFeature(s) take), DialogState, CompletedMeasurement, DataStoreOptions, UIStoreOptions, FeaturePatch. GeoJSONFeature._validation is marked @internal.
  • @element-editor/ui: useContextMenu moved to /internal; useDragWindow, useResizablePanel and useSearchState are no longer exported. New: the DataStore and UIStore types, re-exported next to useDataStore / useUIStore / useGeoStore.
  • @element-editor/adapter-leaflet: FeatureLayer (was deprecated: never mounted, ignored featureColor; use ElementEditorControls) and the FitBoundsInteraction alias. FitBounds is now the component that fits the map to the features once on mount, like the MapLibre one (the removed FeatureLayer module's FitBounds re-fitted on every change).
  • @element-editor/adapter-maplibre: the FitBoundsInteraction alias (use FitBounds), initFeatureLayers / cleanupFeatureLayers, initPreviewLayers / cleanupPreviewLayers, initSnapLayers / cleanupSnapLayers, initProcessingPreviewLayers / cleanupProcessingPreviewLayers, initMeasurePreviewLayers / cleanupMeasurePreviewLayers, INTERACTIVE_FEATURE_LAYERS (ElementEditorControls adds and removes Element's sources and layers itself, also after style changes), emptyFC, bboxToMapLibre, useCursorWhileMode, useModifierDragCursor. @element-editor/adapter-maplibre/basemaps: finishStyle, carryOverElm.

Deprecated

All of these still work in 0.2. The first three log a one-time development warning.

  • bounds={[[south, west], [north, east]]} ([lat, lng] pairs) on both drop-ins and adapter-leaflet: pass a bbox [west, south, east, north]. The warning prints the converted value. Type LegacyLatLngBounds.
  • ref.flyTo([lat, lng], zoom): use ref.flyTo({ center: [lng, lat], zoom }).
  • The routingApiKey prop (both drop-ins and both adapters' ElementEditorControls; dev warning): use config.network.routing.apiKey, which wins when both are set.
  • @element-editor/core: setRoutingApiKey() (module-global; two editors clobber each other). Use config.network.routing.apiKey, or { apiKey } to fetchRoute. fetchRoute(waypoints, signal) with a bare AbortSignal still reads the global.

Fixed

  • Snapping: MapLibre snapping works above ~60° latitude (a coarse pre-filter in degrees missed nearby targets there), and every geometry type is a snap target on both engines (MultiLineString and GeometryCollection were not). A MultiPoint offers only its points: the cursor no longer snaps to lines between its positions (it was treated as a line).
  • Altitude (Z) is kept through move, scale and rotate, point drags, shared-point edits and coordinate edits; a point inserted on an edge takes the mean altitude of its neighbours. Transforms used to drop it.
  • Spatial drawing mode: moving a shape keeps its true size (each point keeps its great-circle distance and bearing from the anchor); it drifted at high latitudes.
  • Import: a feature with a malformed geometry (an unknown type, or coordinates missing or not an array) is skipped and counted instead of aborting the whole import; an empty geometry (coordinates: []) loads without crashing. Empty geometries no longer export a [null, null, null, null] bbox; a merge import that adds nothing no longer creates an undo step.
  • Simplify on Leaflet is undoable; Measure no longer leaves a "0 m" label behind.
  • Hover feedback: the pointer cursor no longer sticks after hovering a feature (MapLibre: the hover wrote pointer and the Cmd/Alt cursor helper then restored it as the "previous" cursor; Leaflet: a move inside the canvas's 32 ms hit-test throttle was never tested, so a quick exit left the hover, cursor and name on). Lines are hovered, named and clicked within ~8 px on both engines (Leaflet canvas tolerance and SVG hit path, a transparent MapLibre hit layer elm-features-line-hit). No name tooltip in vertex edit. A feature just added under a still cursor shows no name and is no snap target until the cursor leaves it; a point placed that way is drawn one accent step darker (primary-600) instead of under a snap ring.
  • A keyboard shortcut now flashes its button pressed (~140 ms), as if clicked: undo / redo, the drawing tools, measure, transform, cut hole, slice, break apart, duplicate, delete, and the table / GeoJSON / schema / layers panel / shortcuts / zoom toggles. Buttons opt in with data-elm-shortcut="<shortcut id>" (shortcutId prop on ToolButton, IconButton, EditToolbarBtn); only the editor that handled the key flashes.
  • No blue focus outline around the whole map after clicking it and pressing a shortcut key (Chrome turns on :focus-visible for the focused Leaflet container / MapLibre canvas at the first plain key press).
  • Pressing I / T no longer types the letter into the GeoJSON inspector it just opened and focused.
  • <ElementEditor>: the top and bottom control rows and the contextual toolbars end at the right-hand panels (layers panel and docked GeoJSON inspector), so the toolbar no longer covers the docked inspector's header and centres on the visible map.
  • Import keeps Element's own foreign members: hidden and _drawType survived export but were dropped on import (initialData, importGeoJSON, file import), so a reloaded circle or rectangle lost its editing constraints. A numeric feature id 0 was replaced by a UUID; it is now '0'.
  • Leaflet: a pin's hover colour could stick after the cursor left (hover swapped the icon element, and the new one never got a mouseout). The tint is now set on the existing element; same for MultiPoint and GeometryCollection pins.
  • MapLibre parity with Leaflet: a freehand loop closed on its start point (or a click-placed circle) keeps the new shape selected (the trailing click cleared it); Escape clears a multi-selection on both engines whatever has focus (it worked only on Leaflet, through a separate handler); in vertex edit only the hovered edge's midpoint is shown.
  • Transform works the same on both engines: Shift scales uniformly from every handle (MapLibre ignored it on edge handles; on Leaflet, Shift on an edge handle could only grow the shape), Alt / Option scales from the box centre (it did nothing on Leaflet) and combines with Shift, and rotation turns around the centre of the box (MapLibre used the average of the shape's points, so an uneven shape drifted while rotating). Pressing or releasing Shift or Alt mid-drag updates the preview at once. The box centre and side handles sit at the visual middle of the box on the map.
  • Transform (move / scale / rotate) with shared-edge editing on: a neighbour followed only the last shared vertex; it now follows all of them. A transform, a multi-feature drag and a shared-vertex edit are one store commit (one onChange) instead of one per feature.
  • Loading initialData is no longer an undo step: Cmd+Z right after load emptied the map. importGeoJSON(raw, 'replace', { undoable: false }) loads without one and clears the history.
  • MapLibre: a basemap or theme change while the drop-in's first basemap style was still loading was lost (useBasemap with initialApplied treated the value current when the map arrived as already shown).
  • Leaflet drop-in: a basemaps entry without maxZoom made the map's max zoom NaN, so fitting (select(ids, { fit: true })) threw.
  • Failures that were silent or mislabelled now reach the user or onError: a failed basemap switch (and a failed style URL after a switch) and a basemap catalog chunk that doesn't load (was an unhandled rejection) are reported; a refused system clipboard no longer shows "Copied selection" (it says the copy works in this editor only) and the copy buttons in the feature panel and inspector report it quietly (one was an unhandled rejection); a refused routing API key (401/403) says so instead of "Couldn't snap to road", and Leaflet reports routing failures as errors like MapLibre; a Nominatim 429 says to wait instead of "check your connection"; Overpass answering HTML is invalid-data instead of a network error; buffer / simplify / smooth preview failures and worker timeouts reach onError quietly; Cmd/Ctrl+O reports features skipped for a missing or unsupported geometry or a repeated id in a warning, applies the 100 MB import cap and says "Not valid JSON" instead of the raw parser message.
  • MapLibre: the selected feature's highlight no longer disappears after a full style rebuild (a basemap switch that MapLibre can't diff, or WebGL context restore).
  • Switching drawing tools discards the previous tool's route waypoints, freehand and measure points (they could linger).
  • dataStore.deleteFeature with an unknown id no longer adds an empty undo step and a spurious onChange.
  • GeoJSON inspector: undo inside the inspector could wipe every feature (the editor's own updates were recorded in its undo history); features without a valid geometry are never committed from it.
  • bounds is applied on mount, not only when it changes (both engines).
  • MapLibre drop-in: a basemap style that fails to load (network, bad URL) no longer leaves the editor without a map; it mounts on a blank map and reports an error event.
  • MapLibre drop-in: 'auto' keeps following the theme when the basemap picker is hidden.
  • MapLibre: text features (map labels) didn't render on the built-in basemaps (their font 404'd); they now use Noto Sans.
  • The default entry imports under plain Node ESM (no named imports from CommonJS maplibre-gl) and works with the MapTiler SDK's maplibre-gl build.
  • Leaflet drop-in: a single basemaps entry was ignored; it now replaces OpenStreetMap.
  • Picker: leaving the tool cancels its lookup and removes its overlay (Leaflet); an aborted lookup no longer removes a newer lookup's pulse (both engines).
  • Development warnings reach integrators. They were compiled out of the published build (import.meta.env.DEV is false in a library build); they now follow your bundler's process.env.NODE_ENV and stay silent in production builds. New ones: Element's stylesheet missing, an editor with zero width or height, adapter UI outside a data-elm-theme element.
  • Basemap picker: Escape closes it without deselecting features; focus moves into the panel and back; aria-expanded / aria-controls.
  • Enter on a focused button activates it instead of toggling vertex edit.
  • Inside the drop-in, fit-bounds, locate, zoom and the ref's flyTo / fitBounds silently did nothing when a map-controller provider was nested in another.
  • MapLibre drop-in: no setStyle race on mount (also under StrictMode); maplibre-gl.css no longer collapses the canvas to 400px. The basemap picker panel anchors to its button.
  • MapLibre: after finishing a shape under a still cursor, the hover tooltip could name a different feature.
  • Cursors: grabbing while panning during a tool; crosshair restored after a Picker candidate pick and after Cmd/Alt-drag mid-tool.
  • Data table: pasting into a cell did nothing; the selected-cell ring clears on an outside or header click; no focus outline around the whole grid.
  • GeoJSON inspector edits that removed features never reached the store; they now apply in real time and the selection follows the code cursor.
  • Feature and schema edits are undoable (cell edits, side-panel Save, rename, map label edits, schema and field add/rename/delete; one step each). deleteSchema refuses to remove the last schema; addSchema(name, fields?) returns the new id.
  • maxFeatures is enforced for every add path (drawing, duplicate, Alt-drag, paste, break apart, slice, buffer-as-new, Picker, GeoJSON editor, import, which is truncated with truncated: true).
  • importGeoJSON('replace') (and setFeatureCollection, StrictMode initialData) dropped incoming features whose ids already existed in the editor.
  • updateFeature / updateFeatureValidation with an unknown id, or validation results that add nothing new, no longer fire onChange; onChange reuses the same object for unchanged features, so you can compare by reference.
  • <ElementEditor layout="split">: the adapter no longer resets the layout to 'panel' after mounting; prop changes no longer trigger React's "Cannot update a component while rendering a different component" warning.
  • Layers panel with thousands of features: no blank list deep inside a large group; selecting a feature scrolls its row into view inside groups.
  • --elm-primary-* passed through style (or set on any editor root) takes effect; map highlights honour per-shade overrides and stylesheet-only accent changes.
  • The OSM trace candidate list rendered light in dark mode (no theme attribute on its portal); dragged rows and groups in the layers list now keep the editor's mode too.
  • <ElementEditor style>: its CSS variables (--elm-accent, --elm-primary-N, …) also reach Element's body-level portals (modals, context menus, tooltips, the basemap picker, the Picker's candidate list), which used to stay blue unless the accent came from accentColor. TypeScript accepts --* keys in style without a cast.
  • Storage access in sandboxed or storage-blocked iframes no longer crashes the editor.
  • Boolean operation buttons say what they do. Subtract's said "bottom minus top", but it cuts every other selected shape out of the one selected first: its tooltip and accessible name are now "Subtract: cut the others out of the first selected" (Union, Intersect and Exclude got short descriptions too).
  • Slice: a line that doesn't split the shape (it stops inside it or misses it) no longer notches it with an undo step while also reporting a failure. Nothing changes and the message says so: "The line didn't cut the shape apart, so nothing changed. Draw it all the way across the shape." A multi-part shape counts as sliced only when it gains a part. Same in the worker and the main-thread fallback.
  • Data table: boolean fields show a toggle in each row instead of a text input. Click it, or press Space / Enter on the selected cell, to switch it (one undo step each, also when switched back quickly); values are stored as true / false. Pasting yes / no, true / false, 1 / 0 sets it, other text is ignored; boolean columns sort off before on; readonly disables the toggle. The feature panel's toggle no longer shows a value stored as the text "false" as on.
  • Smaller layout fixes: dragged windows no longer trail the cursor; the attribution "i" no longer touches the last bottomRight button; bottom-left buttons no longer cover the editing toolbar on narrow editors; the Schema window is at least 450px wide.

Performance

pnpm test:perf (5k mixed features and one 50k-vertex polygon), first benchmark run of this cycle vs. the release candidate (packages/react/e2e/perf/results/baseline.md → refactor-3a31d98.md):

  • Leaflet, 5k features: load longest task 4,471 → 69 ms (features mount progressively; all mounted at ~0.9 s). Pan blocking time 8,160 → 0 ms, frame p50 200 → 17 ms. Zoom blocking time 1,298 → 58 ms; box-select 972 → 31 ms.
  • MapLibre, 50k-vertex polygon: entering vertex edit froze the page (longest task 42 s); now 159 ms (vertex handles capped at 500 per ring, sampled in view).
  • GeoJSON inspector open, 5k features: longest task 316 → 64 ms (Leaflet), 278 ms → no long task (MapLibre); typing re-parses only the edited feature.
  • Panel resize, 5k features: each frame's style recalc dropped from ~65 to ~9 ms (the scoped preflight's selectors were evaluated on every element). Leaflet panel-resize blocking time 2,180 → 28 ms, MapLibre 314 → 0 ms.
  • Committing an edit no longer re-does work for every feature: MapLibre 5k drag blocking time 69 → 14 ms. Layers-panel rows are a fixed 36 px and no longer re-measure on scroll.

Outside the benchmark:

  • Smaller eager bundle (gzipped JS that a fresh Vite app loads up front): MapLibre drop-in 669 → 495 KB (−26%), Leaflet drop-in 419 → 272 KB (−35%). The data table, layers list and schema editor are really lazy now (the @element-editor/ui entry re-exported them statically), the clipping / buffer code loads on first use, the basemap thumbnails are a separate chunk, and Settings / Shortcuts mount only while open.

Changes since the 0.2 pre-releases

Only matters if you built against a pre-release of main; none of these APIs were in 0.1.0.

  • @element-editor/ui: resolveLayout, LAYOUT_SLOTS, isControlVisible and visibleLayoutEntries moved to @element-editor/ui/internal, which is not covered by semver. resolveConfig() and DEFAULT_LAYOUT stay in the main entry.
  • DEFAULT_LAYOUT is deep-frozen; mutating it throws in strict mode. Change it with withLayout({ move: { attribution: 'statusRight' } }).
  • LayoutConfig is slot-specific: map slots take MapEntrys, status slots StatusEntrys, so 'attribution' or a bare 'separator' in a map slot, 'toolbar' / 'applyChanges' in the status bar and { items, direction } on a status slot are compile errors (they were dropped at runtime with a warning).
  • ResolvedConfig['ui'] has layout instead of corners, and ResolvedConfig['toolbar'].position reflects the toolbar's layout slot.
  • @element-editor/ui: removed danger from ContextMenu items, the danger prop of EditToolbarBtn, Button's 'danger' variant, the ToolBar export and the @element-editor/ui/style.css export (import the stylesheet of @element-editor/react, /leaflet or an adapter; each includes the UI styles).
  • Third-party servers moved to config.network (per editor): configureOverpass() and its OverpassConfig type are removed (use config.network.overpass + fetch, or the options of fetchOverpassFeatures); the spriteUrl prop of the MapLibre drop-in and of BasemapControl, and the spriteUrl option of useBasemap / loadBasemapStyle / setBasemap, are removed (use config.network.sprite, or the { network } option); uiStore.routingApiKey / setRoutingApiKey (store) are removed.
  • BasemapControl on your own map leaves your style alone until there is a real choice: with no value, no explicit defaultValue and nothing remembered under storageKey, it shows 'auto' selected but applies nothing until the user picks. Pass defaultValue (or value) to apply a basemap on mount.