Concepts¶
A few ideas run through the whole framework. Read this before you start editing the settings files.
The shell / user-surface boundary¶
The project splits its files into your files and shell code:
app/
├── projects/ ┐
│ ├── demo/ │ one folder per app (a "project"):
│ │ ├── settings.json│ settings.json, layers.json, layers/,
│ │ ├── layers.json ├─ USER SURFACE (yours to edit)
│ │ └── … │ basemaps.json, theme.json, symbology.json,
│ └── city-parcels/ │ popups.tsx, info.md, public/
├── widgets/ ┘ shared by every project
│
├── templates/basic/ ─ the starter npm run new copies (never built)
├── schema/ ─ JSON Schema for hover help in the .json files
├── src/ ─ SHELL (framework code; rarely touched)
│ ├── core/ │ state, layer loaders, popups, theme, registry
│ └── shell/ │ AppShell, TopBar, panels, MapView, …
├── scripts/ ─ npm commands (dev, build, new, …)
└── index.html, vite.config.ts, …
Everything you configure lives in a project folder, and custom widgets live in app/widgets/. One install can hold many projects; each command takes the project's name (npm run dev demo). The shell reads the chosen project's config and renders the app. You get the shell with the clone but rarely edit it, so shell updates can be pulled in without touching your files.
Two path aliases mark the boundary in imports:
@/…→app/src/…(the shell)@user/…→ the chosen project's folder,app/projects/<project>/(your files)
So shell code reads your config as @user/settings, @user/popups and so on, and never knows which project it's running.
The settings object¶
Your project's config is plain JSON. settings.json holds branding, the map's starting view, UI settings, map controls, the context menu, per-widget settings (search, measure, draw, bookmarks), where each widget is placed, and whether the app is commercial.
{
"$schema": "../../schema/settings.schema.json",
"branding": { "title": "My App" },
"ui": { "layout": { "showInfo": true } },
"map": { "style": "https://tiles.openfreemap.org/styles/positron", "center": [-98.5, 39.5], "zoom": 4 },
"topBarWidgets": [{ "widgetName": "measure" }],
"sidePanelWidgets": [{ "widgetName": "layer-list" }, { "widgetName": "legend" }],
"floatingWidgets": []
}
The Configuration section documents it field by field. A few parts live in their own files, to keep settings.json readable. The app reads them into the same settings:
| File | Becomes | See |
|---|---|---|
layers.json |
settings.layers |
Adding Layers |
basemaps.json |
settings.basemaps |
Map & Camera |
theme.json |
settings.theme |
Theme |
symbology.json |
named styles for { "kind": "ref" } |
Symbology |
So the docs write a setting's path as settings.layers, settings.theme.preset and so on, whichever file it's written in. Layer data files go in the layers/ folder or are hosted elsewhere. Popup templates are React components, so they stay code, in popups.tsx.
Every setting has a description, default and docs link built in. Each JSON file's first line, "$schema", points VS Code (and other editors that read JSON Schema) at that file's schema in app/schema/. Hover a key to read about it; autocomplete lists the keys and values each spot takes. JSON has no comments, so explanations live in the hover text and in these docs.
Checking your files¶
npm run dev and npm run build check all five JSON files before the app starts. Unknown keys are errors, so a typo like "zooom" doesn't pass silently. Every problem in every file is listed at once, each with its file and the exact spot:
app/projects/demo: has 2 problems in its JSON:
layers.json → layers[3]: Unrecognized key: "zooom"
settings.json → ui.popups.height: Invalid input
In npm run dev the list shows in the terminal and over the page; fix the files and save, and the page reloads. A build stops until they're fixed. A few checks that depend on more than one setting (a layout collision, a search provider that doesn't allow commercial use) still run when the app starts, as before.
Widgets are auto-discovered¶
A widget is a self-contained folder under app/widgets/. At build time the shell finds every app/widgets/*/widget.config.ts and reads its manifest, so adding a folder is enough. There's no central registry to edit.
Each manifest lists the surfaces the widget can be placed on:
top-bar: an icon button in the top bar; its UI opens in a panel beside the map, opposite the side railside-panel: an icon in the side rail; its UI slides out beside the mapfloating: a button floating over a corner of the map
Discovery skips folders whose name starts with _. That's how _template stays out of your app.
You place a widget by adding its id in settings.json:
"topBarWidgets": [{ "widgetName": "measure" }, { "widgetName": "draw" }],
"sidePanelWidgets": [{ "widgetName": "layer-list" }, { "widgetName": "legend" }],
"floatingWidgets": [{ "slot": "top-left", "widgets": [{ "widgetName": "search" }] }],
"mobileWidgets": ["layer-list", "legend", "measure", "draw"]
On phones the top bar becomes a carousel of the widgets in mobileWidgets, and each opens in a bottom sheet.
A widget can be placed on more than one surface. See the Widgets catalog and Building a Custom Widget.
The resolve / merge order¶
Almost every setting is optional and has a default, so removing a key falls back instead of erroring. When a setting can be set at more than one level, the most specific one wins:
kit default ← global setting ← per-item override
A popup's trigger, for example, resolves as the kit default ("click") ← settings.ui.popups.trigger ← the layer's own popup.trigger. Widget headers, layer-list actions and label styling work the same way. Wherever these docs say "defaults to X", X is the start of one of these chains.
So you can set something once globally and override it for one layer or one widget, without repeating yourself.
The boot lifecycle¶
When the app loads:
main.tsxapplies the theme and UI CSS variables before the first render, so nothing flashes, then mounts<AppShell>.<AppShell>validates the layout config and renders the top bar, panels and map, in the desktop or mobile layout depending on screen width.<MapView>creates the MapLibre map and adds the native controls. Once the style loads, it registers your layers and popup handlers.- Switching basemaps swaps only the basemap's own layers, underneath yours. Your data, toggles and popups aren't touched.
Internals goes deeper for contributors. Day to day, you'll use Configuration and Widgets.