A state-machine-powered layer management library for map applications. Model your map contents as layers and layer groups, then control visibility, opacity, and ordering from any UI framework and any mapping library.
| Package | Version | Description |
|---|---|---|
@ulm/core |
Core state machine library — framework and map-library agnostic | |
@ulm/leaflet |
Leaflet adapter — syncs manager state to a Leaflet map |
LayerTimeInfo using @internationalized/date for date ranges.LayerManagerAdapter to connect any mapping library.npm install @ulm/core
import { LayerManager } from '@ulm/core';
interface LayerData {
url: string;
}
const manager = new LayerManager<LayerData>({
onLayerAdded(info) {
console.log('added:', info.layerId);
},
onVisibilityChanged(info, visible) {
console.log(info.layerId, 'visible:', visible);
},
});
manager.addLayer({
layerConfig: {
layerId: 'basemap',
layerName: 'Basemap',
layerType: 'layer',
parentId: null,
layerData: { url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' },
},
visible: true,
});
manager.setVisibility('basemap', false);
manager.destroy();
See @ulm/core for the full API reference.
createLayerManagerMachine is exported for direct XState usage (e.g. integrating with @xstate/react):
import { createLayerManagerMachine } from '@ulm/core';
import { createActor } from 'xstate';
const actor = createActor(createLayerManagerMachine<LayerData>(), {
input: { allowNestedGroupLayers: true },
});
actor.start();
If you're using LayerManager, the raw actor is also available via manager.actor.
LayerManager owns state; adapters subscribe to its emitted events and perform map-library side-effects (add/remove layers, sync visibility, opacity). Implement the LayerManagerAdapter interface and attach it with manager.setAdapter(adapter).
import { LeafletLayerManagerAdapter } from '@ulm/leaflet';
manager.setAdapter(new LeafletLayerManagerAdapter(map));
// manager.setAdapter(null) detaches and calls adapter.unregister()
See @ulm/leaflet for a complete example, or implement LayerManagerAdapter yourself for other mapping libraries (OpenLayers, Mapbox, etc.).
| Example | Description |
|---|---|
examples/simple |
Minimal vanilla TypeScript — demonstrates LayerManager with plain DOM |
examples/leaflet |
React + Leaflet — @ulm/leaflet adapter, nested groups, layer list UI |
To run an example:
npm install # install all workspace dependencies
npm run dev # starts all dev servers via Turbo
packages/
core/ @ulm/core — state machine library
leaflet/ @ulm/leaflet — Leaflet adapter
examples/
simple/ vanilla TypeScript example
leaflet/ React + Leaflet example
Commits follow Conventional Commits:
type(scope): description
A scope is required. It names the package that changed: core, leaflet, examples, repo, or deps.
| Type | Use for | In the changelog |
|---|---|---|
feat |
User-visible capability | Added |
fix |
Defect repair | Fixed |
perf |
Measurable speed or resource change | Changed |
revert |
Undoes an earlier commit | Changed |
refactor, test, docs, build, ci, chore |
Internal work | Not shown |
feat and fix that remove or drop something land under Removed. Mark a breaking change with ! before the colon.
npm run changelog:preview # print the pending section
npm run changelog:draft # prepend it to CHANGELOG.md
npm run version:next # print the next version
Edit the drafted section before tagging. @ulm/core and @ulm/leaflet ship on the same tag, vX.Y.Z. The current release is v1.0.0. v1.0.1 and v1.0.2 already belong to the previous single package. If npm run version:next prints one of those, tag the next free version instead. See CHANGELOG.md.