Internals — Overview¶
This section is for contributors working on the shell (app/src/) — the framework code, as opposed to the user surface you configure. If you're building an app, you want Configuration and Widgets instead.
Directory map¶
app/src/
├── main.tsx # entry: applies theme + UI vars, mounts AppShell
├── index.css # Tailwind + @theme token vars + global rules
├── core/ # framework logic (no JSX chrome)
│ ├── types.ts # the Settings type + all config types
│ ├── store.ts # the Zustand store
│ ├── hooks.ts # useMap / useMapReady
│ ├── registry.ts # widget auto-discovery + resolveWidget
│ ├── widget-types.ts # WidgetManifest / WidgetPlacement
│ ├── theme.ts # applyTheme / applyUiVars (tokens → CSS vars)
│ ├── storageKey.ts # appId-namespaced localStorage keys
│ ├── layout.ts # resolveLayout / validateLayout
│ ├── layers/ # the layer pipeline (see Layers & Loaders)
│ ├── popups/ # popup resolve + handlers (see Popups)
│ └── legend/ # symbology introspection (see Legend)
└── shell/ # the React chrome
├── AppShell.tsx # the root: desktop/mobile branch + layout regions
├── MapView.tsx # creates the MapLibre map; registers layers/popups
├── TopBar / LeftPanel / RightPanel / FloatingSlots / MobileShell …
├── PopupHost / PopupContent / ContextMenu / InfoModal / LoadingOverlay
└── ConfirmDialog.tsx # shared confirm modal
The boot lifecycle¶
main.tsxapplies the theme and UI CSS variables synchronously (so the first paint has the right colors — no flash), then mounts<AppShell>.<AppShell>validates the layout config (a same-side collision throws here), readssettings.ui.infoto decide whether to auto-open the info modal, and branches onisMobileto render the desktop or mobile shell. The desktop shell lays out the top bar above a row of left slot · map · right slot, with the rail and top-bar panel placed by side.<MapView>creates the MapLibre map, registers native controls, and on the'load'event flipsisMapReady. A second effect then registers declarative layers and popup handlers.- On
style.load(every basemap switch, sincesetStylewipes sources), layers + popups re-register. Per-layer teardowns run first so persistent listeners (e.g. FlatGeobuf'smoveend) don't stack.
The store¶
State lives in one Zustand store (core/store.ts) read via useStore. Key slices:
| Slice | Purpose |
|---|---|
map / isMapReady |
The MapLibre instance + readiness |
isMobile |
Drives the desktop/mobile branch (single source of truth) |
layerMetadata |
Per-layer runtime state (visibility, opacity, labels) — the source of truth the layer list/legend read, and what survives basemap switches |
openWidgetId / activeFloatingWidgetId / mobileSheetWidgetId |
Which widget UI is open on each surface |
currentBasemapId |
The active basemap |
collapsedGroups / seenGroups |
Layer-list group state (kept in the store so it survives widget unmount) |
selectedFeatures / selectedFeatureIndex / selectedFeatureTrigger |
Popup state |
coordinateCaptureArmed |
Suppresses popups while the coordinates widget is capturing a click |
A recurring pattern: runtime state is the store's job; settings.ts is the source of truth only on cold boot. That's why toggling a layer survives a basemap switch — re-registration reads the store's metadata, not the settings defaults.
The merge convention¶
Config resolves kit default ← global setting ← per-item override everywhere (popups, widget headers, layer-list actions, labels, …). Almost every field is optional; an omitted key falls back rather than erroring. When adding a setting, follow this — give it a default and merge it, never require it.
The subsystem pages go deeper:
- Layers & Loaders — the loader dispatch and the shared sublayer model
- Popups — the single click/hover handler and resolution
- Legend — symbology introspection
- Theme & Storage — token → CSS var, and appId namespacing
- Widget Registry — auto-discovery and
resolveWidget