← 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-maplibreand@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/reactis now the MapLibre drop-in.<ElementEditor />owns a MapLibre GL map instead of a Leaflet map, and@element-editor/react/style.cssbundlesmaplibre-gl.cssinstead of Leaflet's. Installmaplibre-gl^5.5next to it (npm i @element-editor/react maplibre-gl). The package depends on@element-editor/core,@element-editor/uiand@element-editor/adapter-maplibreinstead 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(nomaplibre-glneeded). Same component, props and ref. - Engine-specific props.
tileLayer,rendererand rasterbasemapsexist only on/leaflet; vector or rasterbasemapsonly on the default entry (basemap,defaultBasemapandonBasemapChangeare on both, with each entry's basemap type). On the default entrytileLayer/rendererare 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/reactexcept React is optional, so install the ones for the entry you use;maplibre-glis 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 withdata-elm-theme="light"or"dark"; portaled UI of your own should carrydata-elm-portal. - Events: every user-facing message goes through
onEvent; withonEventset (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 astrue. - Keyboard shortcuts:
Ttoggles the data table (0.1.0:V) andAtoggles the transform tool (wasT). Bindings a user customised are kept. maxFeatures/setMaxFeaturesmoved fromuiStoretodataStore(@element-editor/core). ThemaxFeaturesprop is unchanged. The adding actions (duplicateFeature, paste, break apart, …) now returnboolean;dataStore.addFeaturereturns the stored feature ornull.- Errors:
ElementEditorUIProvider'sonErrornow fires for every runtime failure, not only render crashes (checkerror.code === 'crash'for those), with anElementEditorError(still anError).@element-editor/core:DataStoreOptions.onError/UIStoreOptions.onErroranduiStore.reportErrortake anElementEditorError(plus{ quiet }) instead of a string;dataStore.copySelection()returnsPromise<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) arewarningevents instead oferror. uiStore.boundsis now a GeoJSON bbox[west, south, east, north](was[[south, west], [north, east]]), whatever form theboundsprop was given in. Only affects code that reads the store.- Licence key removed: the
licenseKeyprop and evaluation-mode warning are gone (the community edition is MIT, see Changed). themeis the mode only:'light' | 'dark' | 'auto'. The object form (CSS variables for the editor root) is removed: pass them throughstyle(style={{ '--elm-accent': '#7c3aed' }}) or useaccentColor. An object passed tothemeanyway 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-radiusand--elm-fontkeep their names, now with an effect. - Self-intersection (
kinks) checking: polygons with crossing edges are not flagged. @element-editor/core: the unusedimportWorker,exportWorker,validationWorkerand a duplicategeometryWorker. Import, export and validation run on the main thread; theGeometryWorkerPayloadtype (whatrunGeometryWorkerOperationtakes) stays.@element-editor/adapter-maplibre:DrawingCursorHint(it never rendered).config.ui.cornersandconfig.toolbar.position(and the typesCornerandCornerItem): place controls withconfig.ui.layout.toolbar.position: 'top-left'→'toolbar'intopLeft;ui.corners.<corner>: { items, direction }→<corner>: { items, direction }; to keep the default and move a few controls,withLayout({ move: { … } }).toolbar.directionstays. See Migrating to 0.2, section 9.
- The 0.1
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 withref.setFeatureCollection()or remount with a newkey. ref.setFeatureCollection(data, { silent, undoable }).silent: trueloads without firingonChange/onFeature*(so data you just fetched isn't saved straight back);undoable: falseloads without an undo step and clears the history, likeinitialData.- More ref methods and
onToolChange:getFeature(id),getTool(),canUndo(),canRedo()andvalidate()({ 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/.jsonfile anywhere on the editor to import it likeCmd/Ctrl+O(replaces the data in one undo step, fits the map, reports skipped features).fileDrop={false}turns it off; it is off inreadonly. - 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.
Escapecloses windows (shortcuts, settings, schema editor, the floating GeoJSON inspector) and returns focus to the button that opened them; on the map,Escapefirst 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()(typedmaplibregl.Mapon the default entry and adapter-maplibre,L.Mapon/leafletand adapter-leaflet;nulluntil mounted),getSelection(),select(ids, { fit }),setTool(tool | null),addFeature(f)(returns the stored feature ornull),updateFeature(id, patch),removeFeatures(ids)(returns the count). Mutations behave like user edits (validation,maxFeatures, undo,onChange/onFeature*);{ undoable: false }(also onupdateFeatures) applies a change without an undo step and keeps it when the user undoes earlier edits. Bad input returnsnull/false/0with a development warning. - Map setup props on the drop-ins:
initialView({ center: [lng, lat], zoom },{ bounds }or'data'; wins over the remembered view),mapOptions(extramaplibregl.Map/L.Mapoptions, typed per entry, minus the ones Element sets itself, which are dropped with a development warning) andonMapReady(map)(once per map, after Element's layers are on it: add your own layers there). Start view wheninitialViewis omitted: the remembered view, else a fit ofinitialData(first visit), else a fit ofbounds, else Europe. Adapters:onReady(map)on bothElementEditorControls. - MapLibre: Element survives
map.setStyle. After any style replacement (your own basemap switcher, MapTiler SDK style switching,setStylewith or withoutdiff, 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 astyle.loadlistener added inonMapReady/onReady(it runs after Element's repair, so anelm-*beforeIdworks), and youraddImageimages from astyleimagemissinglistener. A style withoutglyphsgets every Element layer but the text-feature one (development warning). onErroron<ElementEditor>(both entries),ElementEditorControls(both adapters),ElementEditorUIProviderandEventProvider: called first for every runtime failure with anElementEditorError(codefrom a small closed union such as'rate-limited','timeout','invalid-data','limit-reached','crash'; asourcesuch as'basemap','picker','routing','search','import'; an honest Englishmessage;cause,featureId). Also fires withonEventset and for quiet failures that show no toast. Returnfalseto stop the built-in toast and theonEventerror event, or a string to show that text instead (translation). TheonEventerrorevent carriescodeandsource.OverpassErroris anElementEditorError(new code'invalid-data').config.network: every third-party server in one place, per editor (both drop-ins andElementEditorUIProviderfor the adapters).vectorTiles,dem(withencoding),osmRaster,glyphsandspriterewrite 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) andnominatim(url,emailsent as a query parameter,attribution) set the Picker's, Route tool's and search box's servers;fetchis used for every request Element makes itself.falseturns a service off and hides what needs it (routing: falsethe Route tool,nominatim: falsethe search box,overpass: falsethe Leaflet Picker; on MapLibre the Picker then reads only the vector tiles: "No shape here in the basemap"). Headers for map resources go throughmapOptions.transformRequest. Two editors on one page no longer share any of these. Seedocs/guides/third-party-services.md. TypesNetworkConfig,NetworkSource,RoutingConfig,NominatimConfig(@element-editor/core).@element-editor/core:fetchRoute(waypoints, { provider, url, apiKey, fetch, signal })andfetchOverpassFeatures(lat, lon, radius, click, { endpoint, timeoutMs, fetch, signal })take the per-editor settings per call (a bareAbortSignalstill works). Failed lookups reject withOverpassError(code:'rate-limited' | 'busy' | 'timeout' | 'network'); typesFetchRouteOptions,OverpassOptions,OverpassErrorCode.uiStore.unavailableTools/isToolAvailable(tool): the toolsconfig.networkturns off.- Attribution follows the services in use: "Geocoding by Nominatim" only while the search box is offered (and your
nominatim.attributionon 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 anattribution(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). Seedocs/guides/basemaps.md. <ElementEditor>(MapLibre) propsdefaultBasemap(starting basemap, default'auto'; the user's pick is remembered inlocalStorageelm_basemapand wins on reload),basemap(controlled by your app: changing it switches the basemap, nothing is read from or written tolocalStorage, it may be off the picker's list; dev warning when a picker shows andonBasemapChangeis missing, and for an id-less style object passed anew every render),onBasemapChange(the user's pick orref.setBasemap(), as thebasemapsentry;'auto'stays'auto'; not called for theme flips or prop changes),basemaps(picker choices;falsehides the picker). The sprite defaults to the jsDelivr copy at the package version (self-host it withconfig.network.sprite). Custom basemaps: a vector style URL orStyleSpecification, or raster{ url, attribution }; give each anidso 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 defaultbottomLeft; hidden when the editor has no picker; can't go in a group). @element-editor/adapter-maplibre/basemapsentry: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, typeBasemapNetwork(the{ network }option ofloadBasemapStyle/setBasemap).BasemapControlanduseBasemapin 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'selm-*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 withmap.addImageother 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 inelm_basemap_leaflet; entries told apart byid, elseurl), plusbasemap/defaultBasemap/onBasemapChangeas on the MapLibre drop-in.tileLayeroverrides them all (dev warning). NewRasterBasemaptype from@element-editor/uiand both react entries;LeafletBasemap(RasterBasemap | 'osm') from/leaflet. - Ref (both drop-ins):
getBasemap()(the current choice) andsetBasemap(b)(switches like a user pick, firesonBasemapChange, isn't remembered; with a controlledbasemapit only firesonBasemapChange). - 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/statusRightadd 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 towithLayout), and flag a layout that leaves the toolbar unplaced while drawing is possible,'applyChanges'unplaced withonSubmit, or undo/redo unplaced with keyboard shortcuts off. New@element-editor/uiexports: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_LAYOUTandwithLayout.IconButton,FitBoundsButton,LocateButton,ZoomIndicatortakesize?: 'md' | 'sm' | 'group'andtooltipSide. Style hooks:data-elm-slot,data-elm-group,data-elm-separator.slotscovers all eight map slots (topCenter,leftCenter,rightCenter,bottomCenteradded). Thetoolsprop takes anyDrawTool('picker','measure','search'too).config.persistKey/config.persist.persistKeynamespaces everything an editor remembers inlocalStorage(elm_theme→elm_<persistKey>_theme, …), so several editors on one origin keep separate state;persist: falsereads and writes nothing. Works on both drop-ins and throughElementEditorUIProvider config.@element-editor/core:PersistScope,scopedKey, and an optional scope argument onreadStorage/writeStorage/readPersisted/writePersisted.- Feature colours on every entry point.
featureColor(flat prop on both drop-ins and both adapters'<ElementEditorUIProvider>, orconfig.featureColor; the flat prop wins) sets the default colour of every feature's outline, fill and point.getFeatureStyleis a prop of both drop-ins (same signature as the adapters'), andFeatureStyleis exported from@element-editor/reactand/leaflet. Precedence: selected, thengetFeatureStyleper key, thenfeatureColor, then the default slate; a hovered feature keeps its colour. Leaflet:getFeatureStylealso colours point pins,fillColorfalls back tocolorlike on MapLibre, and it may returnundefined/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 throughstyle; 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-Nset 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 inelm_code_docked). New@element-editor/uiexportGeoJSONDockPlaceholder(Suspense fallback).bounds(both drop-ins,adapter-leafletElementEditorControls) takes a GeoJSON bbox[west, south, east, north]; adapter-maplibremaxBoundstakes one too.ref.flyTo({ center: [lng, lat], zoom? }). New typesBBox,BoundsInput,FlyToOptions(core and both react entries) andElementEditorBaseProps(both react entries);BasemapInput/BuiltinBasemapIdfrom@element-editor/react.@element-editor/adapter-leafletand 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):toasterprop, defaulttrue. 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/successevents may carry adescription.@element-editor/adapter-leaflet:FeatureStyleProvider,useFeatureStyle,FeatureStyle(parity with adapter-maplibre).'use client'at the top of both@element-editor/reactentries,@element-editor/ui(all entries) and both adapters, so Next.js App Router Server Components can import them.@element-editor/coreandadapter-maplibre/basemapsstay server-importable.Enterfinishes a line, polygon, route, slice or cut even withui.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-offsetCSS 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) andisGeometry.
Changed
- Plain GeoJSON in, strict GeoJSON out.
initialData,ref.setFeatureCollection(),ref.importGeoJSON()andref.addFeature()(both drop-ins and both adapters) are typed with the newFeatureCollectionInput/GeoJSONFeatureInput(exported by@element-editor/coreand both@element-editor/reactentries):metadataoptional, featureidoptional (string | number),propertiesmay benull. Element stores them normalised (UUID for a missing id, numbers as strings,{}properties, the default schema).getFeatureCollection(),onChangeand events still return the strictFeatureCollection/GeoJSONFeature. The input types accept@types/geojsonas is: 3D positions, 6-valuebbox,GeometryCollection,geometry: null(such a feature is skipped and reported throughonError,code: 'invalid-data'). - Loading
initialDatano longer firesonChange/onFeature*(it did, twice under StrictMode): the host already has that data. submit()andvalidate()check every feature. Imported data with a missing required field used to passsubmit().readonlycloses 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+Zemptied 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,Aor 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, somap.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
Entergoes 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-maplibreno longer moves the map on mount.ElementEditorControlsused to fit the map to the store's features when it mounted (an undocumentedFitBounds; 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 (seeinitialView); adapter users fit withref.fitBounds.FitBoundsstays exported.boundssets the start view when nothing else does (noinitialView, no remembered view, noinitialData): 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,keywordsandpublishConfig(public, with provenance), and shipsLICENSE,README.mdandAGENTS.md. - Packaging: CommonJS entries are
.cjsfiles (the packages are"type": "module");exportsmaps use per-condition types with.d.ctstwins, so types resolve undermoduleResolution: node16/nodenextand forrequire. Declarations reference the other packages by name and every bare import is external (@element-editor/reactdist 4.2 MB → 152 KB). Subpath entries (/leaflet,/basemaps,/primitives,/internal) also resolve undermoduleResolution: 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,documentorlocalStoragethere. Keepssr: falsein Next.js anyway (see the Next.js guide); the Leaflet drop-in can't render on the server at all. - Keyboard shortcuts and
Escapeact 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. routingApiKeyis per editor instance instead of module-wide (and now deprecated, see below).- The search box no longer sends a
User-Agentheader (browsers drop or preflight it); identify your app to Nominatim withconfig.network.nominatim.email. config.modeand thethemeprop no longer write the theme tolocalStorage, so one editor's config doesn't leak into others.- Invalid
initialDatareports anerrorevent and starts empty instead of crashing; a singleFeatureis wrapped in aFeatureCollection. - Picker / Overpass: a busy or rate-limited server (429/502/503/504) is retried twice, honouring
Retry-Afterup to 10 s; each attempt times out after 15 s; network errors aren't retried;Escapecancels a lookup in progress. @element-editor/core:reactpeer^19.2.0like the other packages; the newGeometryWorkerPayloadtype covers the buffer/simplify/smooth payloads.- UI: toasts are compact and quiet (no icons or per-type colours, inverted against the editor theme; new
ElementToasterexport); 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 typesDataStore,UIStore,UndoEntry(new export),PanelPrefs(new export),StoreFeaturePatch(new export: what the store'supdateFeature(s)take),DialogState,CompletedMeasurement,DataStoreOptions,UIStoreOptions,FeaturePatch.GeoJSONFeature._validationis marked@internal.@element-editor/ui:useContextMenumoved to/internal;useDragWindow,useResizablePanelanduseSearchStateare no longer exported. New: theDataStoreandUIStoretypes, re-exported next touseDataStore/useUIStore/useGeoStore.@element-editor/adapter-leaflet:FeatureLayer(was deprecated: never mounted, ignoredfeatureColor; useElementEditorControls) and theFitBoundsInteractionalias.FitBoundsis now the component that fits the map to the features once on mount, like the MapLibre one (the removedFeatureLayermodule'sFitBoundsre-fitted on every change).@element-editor/adapter-maplibre: theFitBoundsInteractionalias (useFitBounds),initFeatureLayers/cleanupFeatureLayers,initPreviewLayers/cleanupPreviewLayers,initSnapLayers/cleanupSnapLayers,initProcessingPreviewLayers/cleanupProcessingPreviewLayers,initMeasurePreviewLayers/cleanupMeasurePreviewLayers,INTERACTIVE_FEATURE_LAYERS(ElementEditorControlsadds 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 andadapter-leaflet: pass a bbox[west, south, east, north]. The warning prints the converted value. TypeLegacyLatLngBounds.ref.flyTo([lat, lng], zoom): useref.flyTo({ center: [lng, lat], zoom }).- The
routingApiKeyprop (both drop-ins and both adapters'ElementEditorControls; dev warning): useconfig.network.routing.apiKey, which wins when both are set. @element-editor/core:setRoutingApiKey()(module-global; two editors clobber each other). Useconfig.network.routing.apiKey, or{ apiKey }tofetchRoute.fetchRoute(waypoints, signal)with a bareAbortSignalstill 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 (
MultiLineStringandGeometryCollectionwere not). AMultiPointoffers 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
coordinatesmissing 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
pointerand 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 layerelm-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>"(shortcutIdprop onToolButton,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-visiblefor the focused Leaflet container / MapLibre canvas at the first plain key press). - Pressing
I/Tno 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:
hiddenand_drawTypesurvived export but were dropped on import (initialData,importGeoJSON, file import), so a reloaded circle or rectangle lost its editing constraints. A numeric feature id0was 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
initialDatais 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 (
useBasemapwithinitialAppliedtreated the value current when the map arrived as already shown). - Leaflet drop-in: a
basemapsentry withoutmaxZoommade the map's max zoomNaN, 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 isinvalid-datainstead of a network error; buffer / simplify / smooth preview failures and worker timeouts reachonErrorquietly; 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.deleteFeaturewith an unknown id no longer adds an empty undo step and a spuriousonChange.- 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.
boundsis 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
errorevent. - 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'smaplibre-glbuild. - Leaflet drop-in: a single
basemapsentry 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.DEVisfalsein a library build); they now follow your bundler'sprocess.env.NODE_ENVand stay silent in production builds. New ones: Element's stylesheet missing, an editor with zero width or height, adapter UI outside adata-elm-themeelement. - Basemap picker:
Escapecloses it without deselecting features; focus moves into the panel and back;aria-expanded/aria-controls. Enteron a focused button activates it instead of toggling vertex edit.- Inside the drop-in, fit-bounds, locate, zoom and the ref's
flyTo/fitBoundssilently did nothing when a map-controller provider was nested in another. - MapLibre drop-in: no
setStylerace on mount (also under StrictMode);maplibre-gl.cssno 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).
deleteSchemarefuses to remove the last schema;addSchema(name, fields?)returns the new id. maxFeaturesis enforced for every add path (drawing, duplicate, Alt-drag, paste, break apart, slice, buffer-as-new, Picker, GeoJSON editor, import, which is truncated withtruncated: true).importGeoJSON('replace')(andsetFeatureCollection, StrictModeinitialData) dropped incoming features whose ids already existed in the editor.updateFeature/updateFeatureValidationwith an unknown id, or validation results that add nothing new, no longer fireonChange;onChangereuses 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 throughstyle(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 fromaccentColor. TypeScript accepts--*keys instylewithout 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/Enteron the selected cell, to switch it (one undo step each, also when switched back quickly); values are stored astrue/false. Pastingyes/no,true/false,1/0sets 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
bottomRightbutton; 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/uientry 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,isControlVisibleandvisibleLayoutEntriesmoved to@element-editor/ui/internal, which is not covered by semver.resolveConfig()andDEFAULT_LAYOUTstay in the main entry.DEFAULT_LAYOUTis deep-frozen; mutating it throws in strict mode. Change it withwithLayout({ move: { attribution: 'statusRight' } }).LayoutConfigis slot-specific: map slots takeMapEntrys, status slotsStatusEntrys, 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']haslayoutinstead ofcorners, andResolvedConfig['toolbar'].positionreflects the toolbar's layout slot.@element-editor/ui: removeddangerfromContextMenuitems, thedangerprop ofEditToolbarBtn,Button's'danger'variant, theToolBarexport and the@element-editor/ui/style.cssexport (import the stylesheet of@element-editor/react,/leafletor an adapter; each includes the UI styles).- Third-party servers moved to
config.network(per editor):configureOverpass()and itsOverpassConfigtype are removed (useconfig.network.overpass+fetch, or the options offetchOverpassFeatures); thespriteUrlprop of the MapLibre drop-in and ofBasemapControl, and thespriteUrloption ofuseBasemap/loadBasemapStyle/setBasemap, are removed (useconfig.network.sprite, or the{ network }option);uiStore.routingApiKey/setRoutingApiKey(store) are removed. BasemapControlon your own map leaves your style alone until there is a real choice: with novalue, no explicitdefaultValueand nothing remembered understorageKey, it shows'auto'selected but applies nothing until the user picks. PassdefaultValue(orvalue) to apply a basemap on mount.