Skip to content

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

  1. main.tsx applies the theme and UI CSS variables synchronously (so the first paint has the right colors — no flash), then mounts <AppShell>.
  2. <AppShell> validates the layout config (a same-side collision throws here), reads settings.ui.info to decide whether to auto-open the info modal, and branches on isMobile to 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.
  3. <MapView> creates the MapLibre map, registers native controls, and on the 'load' event flips isMapReady. A second effect then registers declarative layers and popup handlers.
  4. On style.load (every basemap switch, since setStyle wipes sources), layers + popups re-register. Per-layer teardowns run first so persistent listeners (e.g. FlatGeobuf's moveend) 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: