@element-editor/ui

UI components for building custom editor layouts with adapter packages.


Purpose

@element-editor/ui is not needed when using @element-editor/react (either entry) — the full editor UI is included there. Install @element-editor/ui when using @element-editor/adapter-leaflet or @element-editor/adapter-maplibre and you want toolbars, panels, and modals with control over their placement in your layout.

npm install @element-editor/ui

Peer dependencies: react and react-dom ^19.2 (React 18 is not supported; the data table needs React 19.2). @element-editor/ui has no stylesheet export of its own: the old @element-editor/ui/style.css is gone. Its styles ship inside each adapter's stylesheet, so import @element-editor/adapter-leaflet/style.css or @element-editor/adapter-maplibre/style.css (or @element-editor/react/style.css).

Render every component inside an element with data-elm-theme="light" or "dark". The stylesheet applies Tailwind's base styles only there (and in the editor's own portals), so it never restyles your page; components outside it look unstyled.

The geometry worker is inlined and loaded lazily on the first geometry operation, so no bundler configuration (worker plugins, optimizeDeps exclusions) is needed.

Every entry point (@element-editor/ui, /primitives, /internal) starts with a 'use client' directive, so a Next.js App Router Server Component can import it (the components still run only in the browser).


Store / Context

GeoStoreProvider

import { GeoStoreProvider } from '@element-editor/ui'

Creates an isolated Zustand store instance shared between ElementEditorControls and all @element-editor/ui components in the same tree. Must be an ancestor of both the adapter ElementEditorControls and any UI components.

Also re-exported by @element-editor/adapter-leaflet and @element-editor/adapter-maplibre for convenience. All three are the same component.

StoreContext

The React context object holding the store instances. Advanced use only — prefer useDataStore and useUIStore hooks.


Providers

ElementEditorUIProvider (exported here and re-exported by both adapters) sets up ElementEditorErrorBoundary, ElementEditorConfigProvider, ShortcutConfigProvider, EventProvider (with its own <Toaster>) and MapControllerProvider in one component. Use the providers below individually only if you need finer control.

ShortcutConfigProvider

import { ShortcutConfigProvider } from '@element-editor/ui'

Required for keyboard shortcut display and the useShortcutConfig hook. Wrap the component tree that contains any UI component displaying shortcut keys.

EventProvider

import { EventProvider } from '@element-editor/ui'

Routes every user-facing message (errors, warnings, confirmations, undo) to toasts or to your onEvent handler. Required by ElementEditorControls and most UI components (useEmit throws without it).

PropTypeDefaultDescription
onEvent(e: ElementEditorEvent) => void—Receive every event yourself. When set, the editor shows no toasts at all. Read once at mount.
onError(error: ElementEditorError) => void | false | string—Called first for every runtime failure; return false to stop the toast / onEvent error event, or a string to replace the toast text. A nested EventProvider with only onError runs it, then hands the event to the enclosing provider. See Events.
toasterbooleanfalseRender this instance's own sonner <Toaster> and tag its toasts with a per-instance id, so a host <Toaster> or a second editor never shows them twice. Ignored when onEvent is set. With the default false, toasts go to an untagged <Toaster> your app renders. (ElementEditorUIProvider passes true.)
<EventProvider onEvent={(e) => handleEvent(e)}>
  {children}
</EventProvider>

Store-level errors (for example a failed cut or slice, or invalid initialData) reach the same EventProvider whether it sits above or below GeoStoreProvider.

ThemeContext.Provider

import { ThemeContext } from '@element-editor/ui'

<ThemeContext.Provider value="dark">
  {children}
</ThemeContext.Provider>

Passes the resolved theme ('light' or 'dark') to portaled components that render outside the normal DOM hierarchy. Required for correct dark mode in modals.

MapControllerProvider

import { MapControllerProvider } from '@element-editor/ui'

Provides the MapController context to descendants. Required for FitBoundsButton and any component that calls useMapController(). Included in the adapters' ElementEditorUIProvider, and also provided by @element-editor/adapter-maplibre's ElementEditorControls for its children. With @element-editor/adapter-leaflet, use ElementEditorUIProvider or add it yourself above both MapContainer and the UI. Zoom-to actions (fit bounds, zoom to a feature) fly to their target.


Toolbars

ComponentDescription
FloatingToolbarDrawing tool selector. Reads enabled tools from the store. Typically positioned top-center above the map.
EditingToolbarEditing action buttons (boolean operations, transform, delete, duplicate). Appears when features are selected.
ActiveDrawingToolbar"Finish drawing" and "Cancel" buttons. Appears while actively drawing.
FitBoundsButtonButton that flies the map to fit all visible features. Requires MapControllerProvider in the tree.
ZoomIndicatorClick-to-edit current zoom level badge. Reads currentZoom from the store, calls mapController.flyTo on commit. A click opens the input with the caret at the end; the Z shortcut opens it with the number selected. Validation errors emit via EventProvider. Optional minZoom/maxZoom props (default 1–22).
LocateButtonGeolocation button — calls navigator.geolocation and flies the map to the user's location. Errors emit via EventProvider. Optional zoom prop (default 15).
ReadonlyBanner"View-only mode" pill rendered at the bottom-center when readonly is on. Replaces the empty space where editing toolbars would normally sit. No props.
BooleanOperationsBoolean operation buttons (union, intersect, subtract, exclude, cut hole, slice) as a standalone component. Lazy-loaded: render it inside <Suspense>.
import {
  FloatingToolbar, EditingToolbar, ActiveDrawingToolbar, UndoRedoToolbar,
  FitBoundsButton, ZoomIndicator, LocateButton, ReadonlyBanner,
  BooleanOperations, SimplifyPanel, SmoothPanel, BufferPanel,
  FeatureMapContextMenu,
} from '@element-editor/ui'

Panels

ComponentDescription
LayersPanelFeature list sidebar with layer visibility toggles, group management, and schema selector. Its open/close toggle sits in the panel header, left of the "Layers" label, while the panel is open; when it's closed, the toggle is a floating button at the top-right edge of the map, level with the header. Shortcut / by default. Right-click a feature row for its menu, which starts with Zoom to feature.
FeatureSidePanelProperty editing panel for the currently selected feature. Used in split layout. Requires a dialogState prop. Its Source section shows the feature's GeoJSON with syntax highlighting.
FeatureEditModalProperty form for a selected feature, rendered as a floating modal. Requires a dialogState prop. Lazy-loaded: render it inside <Suspense>.
FeatureListThe feature list as a standalone component (without the panel chrome). Lazy-loaded (it carries the drag-and-drop library): render it inside <Suspense>.
FeatureRowSingle row in the feature list. featureLabel(feature) is exported alongside as a labelling helper.
GroupRowGroup row in the feature list.
PartSubRowSub-row for individual parts of a multi-geometry feature.
SchemaEditorSchema field management UI as a standalone component. Lazy-loaded: render it inside <Suspense>.
DataTablePanelFull spreadsheet view of all features and their properties, rendered in a resizable panel. Lazy-loaded: render it inside <Suspense>.
DataTableThe data grid as a standalone component (without the panel chrome). Lazy-loaded (it carries the data grid library): render it inside <Suspense>.
BasemapPickerEngine-neutral basemap switcher: an icon button opening a panel of thumbnail tiles. Props: options: { id, label, thumbnail?: string | [string, string] }[], value, onChange(id), label? (default 'Basemap'), size?, tooltipSide?. It applies nothing; the host reacts to onChange. The panel stays open while the user picks basemaps or works on the map; only the button again or Escape closes it. label is the button label and the panel's accessible name (no visible title). The drop-ins and BasemapControl (adapter-maplibre) use it. Types BasemapPickerOption, BasemapPickerProps.
import {
  LayersPanel, FeatureSidePanel, FeatureEditModal,
  FeatureList, FeatureRow, featureLabel,
  GroupRow, PartSubRow,
  SchemaEditor, DataTablePanel, DataTable, BasemapPicker,
} from '@element-editor/ui'

Note: FeatureEditModal, DataTablePanel, DataTable, FeatureList, SchemaEditor, BooleanOperations, SchemaModal, GeoJSONModal, ShortcutsModal, and SettingsModal are exported as lazy() components. Each call site must be wrapped in its own <Suspense> — sharing one boundary across multiple lazy siblings causes already-loaded ones to unmount when a new sibling suspends.


Modals

ComponentDescription
ShortcutsModalKeyboard shortcut reference panel. Lazy-loaded.
SettingsModalEditor settings — snap toggle, basemap picker, geometry mode, units. Lazy-loaded.
SchemaModalSchema editor — add, edit, and remove schemas and fields. Lazy-loaded. Accepts an onClose callback.
GeoJSONModalRaw GeoJSON inspector and inline editor. Lazy-loaded. Accepts an onClose callback. Opens docked beside the map unless the user last undocked it (floating window).
GeoJSONDockPlaceholderNot lazy. Use it as the GeoJSONModal Suspense fallback: an empty dock frame while the chunk loads (nothing when the inspector opens floating), so the controls don't jump.

All four are lazy-loaded (React.lazy), so their dependencies (CodeMirror, the data grid) load in separate chunks. Render each inside its own <Suspense> (React throws without one; a boundary shared by several lazy components unmounts the loaded ones whenever another starts loading). ShortcutsModal and SettingsModal render nothing while closed; SchemaModal and GeoJSONModal render whenever mounted, so mount them only while open:

import { Suspense } from 'react'
import {
  ShortcutsModal,
  SettingsModal,
  SchemaModal,
  GeoJSONModal,
  useUIStore,
} from '@element-editor/ui'

function Modals() {
  const schemaOpen = useUIStore((s) => s.schemaModalOpen)
  const toggleSchema = useUIStore((s) => s.toggleSchemaModal)
  const codeOpen = useUIStore((s) => s.codeModalOpen)
  const toggleCode = useUIStore((s) => s.toggleCodeModal)

  return (
    <>
      <Suspense fallback={null}><ShortcutsModal /></Suspense>
      <Suspense fallback={null}><SettingsModal /></Suspense>
      {schemaOpen && <Suspense fallback={null}><SchemaModal onClose={toggleSchema} /></Suspense>}
      {codeOpen && <Suspense fallback={null}><GeoJSONModal onClose={toggleCode} /></Suspense>}
    </>
  )
}

Error boundary

ElementEditorErrorBoundary

import { ElementEditorErrorBoundary } from '@element-editor/ui'

<ElementEditorErrorBoundary onError={(err) => console.error(err)}>
  {children}
</ElementEditorErrorBoundary>

Catches render/runtime errors in the editor tree and shows a minimal recovery UI. onError gets the caught error wrapped as an ElementEditorError with code: 'crash', source: 'editor' (the original error is its cause). Mounted automatically by @element-editor/react's <ElementEditor> and by both adapters' ElementEditorUIProvider.


Primitives

ExportKindDescription
ButtonComponentStandard button with variant prop ('primary', 'secondary', etc.).
InputComponentSingle-line text input.
TextareaComponentMulti-line text input.
SelectInputComponentStyled select element.
KbdComponentKeyboard key display (light background).
KbdDarkComponentKeyboard key display (dark background).
KbdInvertComponentKeyboard key display (inverted).
TooltipComponentAccessible tooltip wrapper. Accepts content, shortcut, and side props. It flips to the other side and is clamped so it stays on screen.
IconButtonComponent48×48 card-style icon button matching the editor visual. Tooltip-wrapped, supports active/disabled states. While active, a Phosphor icon child is drawn filled in the accent colour (aria-pressed is set). Use for custom controls slotted via slots.<corner> so they line up with built-in buttons. shortcutId (also on ToolButton / EditToolbarBtn) names the shortcut that triggers the same action (data-elm-shortcut), so pressing the key flashes the button pressed.
ToolButtonComponentIcon button for toolbars.
EditToolbarBtnComponentButton styled for the editing toolbar.
ToolbarDividerComponentVisual divider between toolbar sections.
ContextMenuComponentRight-click context menu. Pairs with isContextMenuOpen() and the CONTEXT_MENU_MARKER_ATTR data-attribute marker.
SearchDropdownComponentSearchable dropdown input.
SearchDropdownHandleTypeRef type for SearchDropdown.
import {
  Button, Input, Textarea, SelectInput,
  Kbd, KbdDark, KbdInvert,
  Tooltip, IconButton, ToolButton, EditToolbarBtn, ToolbarDivider,
  ContextMenu, isContextMenuOpen, CONTEXT_MENU_MARKER_ATTR,
  SearchDropdown,
} from '@element-editor/ui'
import type { SearchDropdownHandle } from '@element-editor/ui'

Hooks

useDataStore(selector)

const features = useDataStore((s) => s.features)
const selectedIds = useDataStore((s) => s.selectedIds)

Subscribes to the data Zustand store. See DataStore in the Types reference.

useUIStore(selector)

const tableOpen = useUIStore((s) => s.tableOpen)
const toggleTable = useUIStore((s) => s.toggleTable)

Subscribes to the UI Zustand store (modal state, tool state, panel visibility).

useGeoStore()

const { dataStore, uiStore } = useGeoStore()

Returns the raw Zustand store instances for imperative access. Their state types, DataStore and UIStore, are exported as types from @element-editor/ui and @element-editor/core (see DataStore and UIStore):

import type { DataStore } from '@element-editor/ui'

const selectCount = (s: DataStore) => s.features.length

useShortcutConfig()

const { getKey } = useShortcutConfig()
const key = getKey('draw-polygon') // returns current binding, e.g. 'g'

Returns the current key binding for any ShortcutId. Reflects custom bindings saved by the user. Must be called inside ShortcutConfigProvider.

useElementEditorConfig()

const cfg = useElementEditorConfig()
cfg.mode            // 'light' | 'dark' | 'system'
cfg.toolbar.position  // the toolbar's ui.layout slot as a ToolbarPosition
cfg.ui.dataTable
cfg.units.length

Returns the resolved config (with defaults applied and the user's persisted units override merged in). Must be called inside ElementEditorConfigProvider.

useUnitsOverride()

const { override, setUnits, resetUnits } = useUnitsOverride()
setUnits({ length: 'metric' })

Read/write the user's persisted units override. Persists to localStorage. Use this from settings UIs; consumers reading current values should prefer useElementEditorConfig().units.

resolveConfig(config?)

import { resolveConfig } from '@element-editor/ui'
const resolved = resolveConfig({ mode: 'dark' })

Pure helper that applies defaults to a ElementEditorConfig and returns a ResolvedConfig. Useful for previewing a config object without rendering a provider.

useMapController()

const controller = useMapController()
controller.flyTo?.([48.86, 2.35], 12)
controller.fitBounds?.([0, 0, 1, 1])
controller.panTo?.([48.86, 2.35])
controller.getCenter?.()           // returns [lng, lat]
controller.setZoom?.(15)
controller.getZoomBounds?.()       // { min, max }
controller.getMap?.()              // the engine's map (backs ref.getMap())

Returns the MapController object. Methods may be null if no map controller has been registered yet. A custom map bridge should set getMap: () => map so ref.getMap() works.

useEmit()

const emit = useEmit()
emit({ type: 'success', message: 'Saved.' })

Returns a stable function to emit a ElementEditorEvent. Must be called inside EventProvider. When onEvent is provided to EventProvider, emitted events are forwarded to that handler instead of firing sonner toasts. Use it instead of calling sonner's toast() directly, so onEvent sees every message and toasts land in the right editor's <Toaster>.

The argument is an EmittedEvent: a ElementEditorEvent that may also carry description, a second line the default toast shows under the message:

emit({ type: 'error', message: 'Save failed.', description: 'The server returned 503. Try again.' })

useToasterId()

import { useToasterId } from '@element-editor/ui'
import { toast } from 'sonner'

const toasterId = useToasterId()
toast.loading('Uploading…', { toasterId })

Returns the sonner toaster id of this editor's own <Toaster>, or undefined when toasts go to your app's untagged <Toaster>. Pass it to direct sonner calls that have no ElementEditorEvent equivalent (such as toast.loading), so the toast shows in the right place. Skip such toasts when useIsManaged() is true.

useTheme()

const theme = useTheme() // 'light' | 'dark'

Returns the current resolved theme from ThemeContext.

useAccentPalette(el)

import { useAccentPalette } from '@element-editor/ui'

const accent = useAccentPalette(map.getContainer()) // { 50: '#…', …, 950: '#…' }

Returns the accent palette (shades 50–950 as hex strings) in effect for el, read from the inherited --elm-accent. For map layers that need concrete colours (Leaflet SVG paths, MapLibre paint). Re-reads when the style or class attribute of the nearest [data-elm-theme] ancestor or of <html> changes, and returns the same object while the colour is unchanged. HTML inside the editor can use var(--color-primary-N) instead. Same math as accentPalette() in @element-editor/core; see Theming.

useAccentStyle() / accentStyle(color)

import { useAccentStyle } from '@element-editor/ui'

const accentStyle = useAccentStyle()
createPortal(<div data-elm-portal style={accentStyle}>…</div>, document.body)

useAccentStyle() returns { '--elm-accent': config.accentColor }, or undefined when no accent is configured. It also carries the accent and design tokens the editor root resolves (--elm-surface, --elm-radius, …, from your stylesheet or style; under adapters, from the [data-elm-theme] wrapper of the map), and inside <ElementEditor> the CSS variables of its style prop, which win as they do on the root. Spread it into the style of your own body-level portal roots so they match the editor; the root must also carry data-elm-theme (the editor's mode, from useTheme()) or data-elm-portal. accentStyle(color) is the accent alone, without the hook.

useApplyAccentToRoot(container)

Writes config.accentColor as --elm-accent onto the nearest [data-elm-theme] ancestor of container (else onto container). Both adapters' ElementEditorControls already call it with the map container; call it only in a fully custom integration that doesn't use ElementEditorControls.

useIsManaged()

const managed = useIsManaged() // true when onEvent was provided to EventProvider

Returns true when the host application provided an onEvent handler. Use to skip toasts that have no ElementEditorEvent equivalent.

useKeyboardShortcuts()

import { useKeyboardShortcuts } from '@element-editor/ui'

useKeyboardShortcuts(map.getContainer())

Registers keyboard listeners for drawing modes, undo/redo, delete, and finish-drawing. Already mounted by both adapters' ElementEditorControls (and so by @element-editor/react); calling it again makes every shortcut fire twice. Call it only in a fully custom integration that doesn't use ElementEditorControls.

Pass the map container. Shortcuts are then scoped to this editor: a key acts only when the event comes from inside the editor's root (the nearest [data-elm-theme] ancestor of the container), or when focus is neutral (the page body, or an Element Editor portal) and the last pointer press was inside the root. A single editor on the page also handles body-focused keys before any click. Without a container, the listeners act page-wide.

useIsEventForEditor()

import { useIsEventForEditor } from '@element-editor/ui'

const isForThisEditor = useIsEventForEditor()
useEffect(() => {
  const onKey = (e: KeyboardEvent) => {
    if (!isForThisEditor(e)) return
    // act on this editor
  }
  window.addEventListener('keydown', onKey)
  return () => window.removeEventListener('keydown', onKey)
}, [isForThisEditor])

Returns a function that tells whether a DOM event belongs to this editor, using the same scope as useKeyboardShortcuts. Use it to gate your own window or document keyboard listeners that act on editor state, so a second editor or your host page doesn't trigger them. Must be called inside GeoStoreProvider.

Portals

Any UI you render with createPortal outside the editor's data-elm-theme element (a menu, picker or modal attached to body) should carry a data-elm-portal attribute on its root. ElementEditor then applies its base styles inside it, and focus or clicks inside it still count as inside the editor, so shortcuts and Escape keep working there. Spread useAccentStyle() into its style so it uses the editor's accent colour.

useAdapterControls(opts)

import { useAdapterControls } from '@element-editor/ui'

const api = useAdapterControls({ initialData, schema, readonly, onChange, /* … */ })
useImperativeHandle(ref, () => api, [api])

The shared editor-shell wiring used by <ElementEditor> and both adapter ElementEditorControls. It owns prop→store sync (readonly / tools / maxFeatures / layout; with engine set, as the adapters do, the tools config.network turns off go to uiStore.unavailableTools), the initialData load (on mount, and once more for data that arrives late while the editor is empty and unedited; no undo step, no onChange) and schema mount, the data-change subscription (onChange / per-feature callbacks / selection), onZoomChange / onFeatureSelect / onToolChange subscriptions, and returns the ElementEditorImperativeAPI. Pass the map type to get a typed getMap(): useAdapterControls<maplibregl.Map>(…).

Most consumers do not need this hook directly — use <ElementEditor> or the adapter ElementEditorControls. Reach for it when building a fully custom layout that wraps neither.

useDataChangeSubscription(store, callbacks)

Granular per-feature subscription primitive used internally by useAdapterControls. Exposed for advanced cases that need to react to add / update / delete / selection changes outside the imperative-API surface.

useNominatim()

const { results, loading, error } = useNominatim(query)

Debounced (400 ms) geocoding of query (2+ characters) against the editor's config.network.nominatim server (default nominatim.openstreetmap.org/search; email is sent as a query parameter) through config.network.fetch. A failure sets error and reaches onError quietly (source: 'search').

useGeoJSONDrop(opts)

Headless file-drop + file-picker primitive for importing GeoJSON. Returns spread-friendly prop getters and a programmatic open() trigger — no opinions about styling. The hook parses + validates the file and hands the resulting FeatureCollection back via onImport; the caller decides what to do with it.

const { getRootProps, getInputProps, isDragActive, open } = useGeoJSONDrop({
  onImport: (fc, file) => {
    ref.current?.importGeoJSON(fc, 'replace')
    toast.success(`Loaded ${file.name}`)
  },
  onError: (err) => toast.error(describe(err)),
})

return (
  <div {...getRootProps()}>
    <input {...getInputProps()} />
    <button onClick={open}>Import…</button>
    {isDragActive && <div className="overlay">Drop to import</div>}
  </div>
)

Options

OptionTypeDefaultDescription
onImport(fc, file) => void—Called once the file parses to a valid FeatureCollection.
onError(err: GeoJSONDropError) => void—Called for wrong-type, too-large, malformed-JSON, or not-a-FeatureCollection inputs.
maxBytesnumber100 * 1024 * 1024Maximum accepted byte length. Matches the default import cap (DEFAULT_MAX_IMPORT_BYTES).
acceptstring[]['.geojson', '.json', 'application/geo+json', 'application/json']File extensions and/or MIME types to accept.
disabledbooleanfalseWhen true, drag/drop + file picker become no-ops.

Error shape (GeoJSONDropError)

  • { kind: 'wrong-type', file }
  • { kind: 'too-large', bytes, maxBytes }
  • { kind: 'invalid-json', cause }
  • { kind: 'not-feature-collection', received }

Note: Spread getRootProps() on a container with pointer-events: auto. A pointer-events: none host silently swallows drag events.

useGeoJSONExport(opts)

Symmetric counterpart to useGeoJSONDrop — headless primitive for saving the current ElementEditor state as GeoJSON. Three entry points: trigger a download, fetch the text to POST to a backend, or build a Blob to stream elsewhere.

const { exportToFile, exportToString, exportToBlob, isExporting } = useGeoJSONExport({
  getFeatureCollection: () => ref.current?.getFeatureCollection(),
  filename: (fc) => `element-editor-${fc.features.length}-features`,
  onExport: (_fc, file) => toast.success(`Exported ${file.name}`),
  onError: (err) => toast.error(describeExportError(err)),
})

return <button onClick={exportToFile} disabled={isExporting}>Export</button>

Options

OptionTypeDefaultDescription
getFeatureCollection() => FeatureCollection | null | undefined—Reads the FC lazily on every call.
filenamestring | (fc) => string'features'.geojson appended automatically if missing.
indentnumber2Pretty-print indent. Pass 0 for compact.
onExport(fc, { name, bytes }) => void—Fires after a successful file download.
onError(err: GeoJSONExportError) => void—Fires on empty FC or serialization error.
disabledbooleanfalseMakes every method a no-op.

Returned methods

  • exportToFile(): string \| null — triggers the browser download. Returns the resolved filename, or null when empty.
  • exportToString(): string \| null — synchronous, returns serialized JSON text.
  • exportToBlob(): Blob \| null — returns a Blob typed application/geo+json.

Error shape (GeoJSONExportError)

  • { kind: 'empty' } — FC was null/empty/zero features.
  • { kind: 'serialization-error', cause } — JSON.stringify threw.

Types

ElementEditorConfig / ResolvedConfig

Exported from @element-editor/ui (re-exported by adapters and @element-editor/react). See the ElementEditor React reference for the full shape. ResolvedConfig is what useElementEditorConfig() returns — same shape with all defaults applied.

DrawTool

type DrawTool =
  | 'polygon' | 'rectangle' | 'circle' | 'line' | 'point' | 'freehand' | 'route'
  | 'picker'  | 'measure'   | 'search'

Layout types and withLayout

LayoutConfig and its parts (MapSlot, StatusSlot, MapEntry, StatusEntry, LayoutPatch, …) and withLayout(patch, base = DEFAULT_LAYOUT) are documented in the ElementEditor React reference.

ToolbarPosition

ResolvedConfig.toolbar.position, derived from the toolbar's ui.layout slot.

type ToolbarPosition =
  | 'top-left'    | 'top-center'    | 'top-right'
  | 'left-center' | 'right-center'
  | 'bottom-left' | 'bottom-center' | 'bottom-right'

UnitsOverride

type UnitsOverride = Partial<{
  length: 'metric' | 'imperial' | 'auto'
  area: 'metric' | 'imperial' | 'auto'
  display: 'human' | 'raw'
  precision: number
}>

MapController

interface MapController {
  flyTo: ((latlng: [number, number], zoom: number) => void) | null
  panTo: ((latlng: [number, number]) => void) | null
  fitBounds: ((target: [number, number][] | [number, number, number, number]) => void) | null
  getCenter: (() => [number, number]) | null  // returns [lng, lat]
  setZoom: ((zoom: number) => number) | null
  getZoomBounds: (() => { min: number; max: number }) | null
  getMap?: () => unknown  // the engine's map (maplibregl.Map / L.Map), behind the public ref.getMap()
}

ElementEditorImperativeAPI / ImportResult

The shape returned by useAdapterControls() and the ref type of every ElementEditorControls. See the ElementEditor React reference for the full shape.

ElementEditorEvent

See the Events reference for the full union type.

NominatimResult

Result object from the Nominatim geocoding API.

FeatureContextMenuData

Data shape passed to FeatureMapContextMenu.


Geometry utilities

These are re-exported from @element-editor/ui for use in custom integrations.

ExportDescription
runGeometryWorkerOperationRun a geometry operation in the background worker: polygon boolean ops (union, intersect, subtract, exclude), cut-hole, slice-shape, buffer, simplify, smooth. Returns a Promise<Geometry>. Rejects after 30 s ({ timeoutMs } option) or if the worker fails. The payload type is GeometryWorkerPayload from @element-editor/core.
disposeGeometryWorkerTerminate the geometry worker instance. Call on unmount if using runGeometryWorkerOperation directly.
triggerGeoJSONDownload(text, filename?)Trigger a browser download of a GeoJSON string. The download primitive used by exportAs and useGeoJSONExport.

This is the only worker ElementEditor uses. It's inlined into the package and loaded on the first call. The editor uses it for polygon boolean operations, cut hole, slice and the buffer, simplify and smooth previews. Import, export and validation run on the main thread.

import { runGeometryWorkerOperation, disposeGeometryWorker } from '@element-editor/ui'

Full export list

// Store / context
export { GeoStoreProvider, StoreContext, useDataStore, useUIStore, useGeoStore }
export type { DataStore, UIStore }   // re-exported from @element-editor/core
export { MapControllerProvider, useMapController, useSetMapController }
export type { MapController }
export { ThemeContext, useTheme, ThemeReportingProvider, useReportThemeFromRoot }
export type { ResolvedTheme }
export { EventProvider, useEmit, useIsManaged, useToasterId }
export type { EmittedEvent }
export { ShortcutConfigProvider, useShortcutConfig }
export { ElementEditorConfigProvider, useElementEditorConfig, useUnitsOverride, resolveConfig, DEFAULT_LAYOUT, withLayout }
export type { ElementEditorConfig, ResolvedConfig, DrawTool, ToolbarPosition, UnitsOverride, RasterBasemap }
export type { LayoutConfig, LayoutSlot, MapSlot, StatusSlot, LayoutControl, LayoutEntry, MapEntry, StatusEntry, LayoutGroup, MapGroup, StatusGroup, LayoutSlotConfig, MapSlotConfig, StatusSlotConfig, LayoutPatch, SlotFor, ResolvedLayout }

// Events and errors
export type { ElementEditorEvent, ElementEditorErrorHandler, ErrorCode, ErrorSource }
export { ElementEditorError }   // re-exported from @element-editor/core

// Panels & modals (lazy: GeoJSONModal, SchemaModal, FeatureEditModal, DataTablePanel, SettingsModal, ShortcutsModal)
export { LayersPanel, FeatureSidePanel, FeatureEditModal }
export { FeatureList, FeatureRow, featureLabel, GroupRow, PartSubRow }
export { SchemaEditor, SchemaModal, GeoJSONModal, GeoJSONDockPlaceholder }
export { DataTable, DataTablePanel, ShortcutsModal, SettingsModal }
export { preloadEditorPanelsWhenIdle }

// Toolbars (lazy: BooleanOperations)
export { FloatingToolbar, EditingToolbar, UndoRedoToolbar, UndoButton, RedoButton, ControlGroup, ActiveDrawingToolbar }
export { VertexSelectionBar }
export { FitBoundsButton, ZoomIndicator, LocateButton, ReadonlyBanner, BooleanOperations }
export { SimplifyPanel, SmoothPanel, BufferPanel }
export { FeatureMapContextMenu }
export type { FeatureContextMenuData }

// Primitives
export { Button, ContextMenu, isContextMenuOpen, CONTEXT_MENU_MARKER_ATTR, Input, Textarea, SelectInput }
export { Kbd, KbdDark, KbdInvert }
export { SearchDropdown }
export type { SearchDropdownHandle }
export { ToolButton, IconButton, Tooltip, EditToolbarBtn, ToolbarDivider, ElementToaster }
export type { IconButtonSize, TooltipSide }
export { BasemapPicker }
export type { BasemapPickerOption, BasemapPickerProps }

// Hooks
export { useKeyboardShortcuts, useIsEventForEditor, useDataChangeSubscription }
export { useNominatim }
export type { NominatimResult }
export { useGeoJSONDrop }
export type { UseGeoJSONDropOptions, UseGeoJSONDropReturn, GeoJSONDropError }
export { useGeoJSONExport }
export type { UseGeoJSONExportOptions, UseGeoJSONExportReturn, GeoJSONExportError }
export { useAccentPalette, useAccentStyle, useApplyAccentToRoot, accentStyle }
export { useAdapterControls }
export type { UseAdapterControlsOptions, ElementEditorImperativeAPI, MutationOptions, SetFeatureCollectionOptions, ValidationReport, ImportResult }
export { triggerGeoJSONDownload, saveGeoJSONFile }

// Geometry utilities
export { runGeometryWorkerOperation, disposeGeometryWorker }

// Error boundary (mounted by ElementEditorUIProvider)
export { ElementEditorErrorBoundary }

// Full UI context stack for adapter setups (re-exported by both adapters)
export { ElementEditorUIProvider }
export type { ElementEditorUIProviderProps }

@element-editor/ui/primitives is a second entry point with the base UI components.

@element-editor/ui/internal is plumbing shared with the adapters and the @element-editor/react drop-in shell. It is not public API and not covered by semver: anything in it can change or disappear in any release. Don't import it from your app; for layout, resolveConfig(), DEFAULT_LAYOUT and withLayout are in the main entry.

DEFAULT_LAYOUT is deep-frozen, because every editor on the page shares it. Change it with withLayout instead of mutating it: withLayout({ move: { attribution: 'statusRight' } }).