Resolved: light

Maps & geo

A CUSTOM STD EXTENSION — @phauna/ds ships no map component, so this is the house map layer, built on react-map-gl/maplibre over a keyless OpenStreetMap raster style (no Mapbox/MapTiler API key, same approach as the portal EventMap). MapView is the themed wrapper: DS surface/border tokens for the chrome, sensible EventMap defaults, layers as children. Compose accent MapMarkers, a TerritoryLayer (a GeoJSON boundary polygon in the accent color), and a ClusterLayer (member geography clustered with MapLibre's built-in GeoJSON clustering) inside it. The label-only LocationChip and DistrictBadge are server-safe — they render a place or a legislative district with DS tokens and no map at all. The whole layer is kept small and token-compatible with @phauna/ds conventions so it could be contributed upstream.

import path
The interactive map components are client-only — import from "@/components/ds/maps" (NOT the server-safe ds/index). The chips (LocationChip / DistrictBadge) are server-safe and import from "@/components/ds".
keyless tiles
OSM_STYLE is a raster style backed by tile.openstreetmap.org — no API key. © OpenStreetMap attribution is mandatory and shown via the compact attribution control. Override with the mapStyle prop.
theme-aware paint
MapMarkers + the chips use the DS token utility classes directly (HTML/SVG). The WebGL layers (TerritoryLayer / ClusterLayer) can't read CSS vars, so they resolve --color-accent/--color-good at runtime via useTokenColors and re-skin when the theme flips.
real geo data
src/lib/geo/* + geo-teams.ts are METADATA (division/state/metro keys, CBSA codes, predicates) — not polygon GeoJSON. The TerritoryLayer demo uses an inline sample polygon; wire sourced boundary GeoJSON into TerritoryLayer's `data` for production.
client-only
MapView/MapMarkers/TerritoryLayer/ClusterLayer mount maplibre-gl (WebGL) so they're 'use client'. This page is a server component; the maps live in the colocated ./MapsDemo island.

MapView + markers

The themed MapView wrapper with a couple of accent MapMarkers. Each marker is an HTML pin anchored at its tip, so it uses the --color-accent token directly. Pan/zoom the map; flip the theme in the top bar and the chrome (border, surface) re-skins.

MapView (longitude/latitude/zoom/height) + MapMarkers (accent pins, anchored at the tip)
MapView
Props: longitude, latitude, zoom (def 11), height (def 320 — px number or CSS length), interactive (def true), showNavigation, mapStyle override, aria-label, className. Layers go in as children.
MapMarkers
markers: { id, longitude, latitude, label? }[]; optional onMarkerClick(marker). Renders an accent MapPin anchored at its tip per marker; empty list renders nothing.
interactive={false}
Disables all gestures (drag/scroll/zoom) and hides the nav control — a static locator. See the static example at the bottom.

TerritoryLayer — boundary polygon

A GeoJSON Polygon/MultiPolygon drawn as a translucent accent fill plus a solid accent outline — the shape of a congressional district, a metro, or a state. This demo feeds it an INLINE sample rectangle over central Austin (the geo backbone is metadata, not boundary GeoJSON yet); swap real boundary geometry into the `data` prop in production.

TerritoryLayer id + data (Polygon Feature) — accent fill + stroke via tokens. (Sample polygon; wire real boundaries.)
TerritoryLayer
Props: id (unique Source id), data (a Polygon/MultiPolygon Feature or FeatureCollection), fillOpacity (def 0.15), lineWidth (def 2). Mounts a Source + a fill Layer + a line Layer.
token paint
Fill + stroke resolve --color-accent at runtime (WebGL can't read CSS vars) and re-resolve on theme change, so the boundary matches the accent everywhere else.
no auto-fit
The layer draws the polygon but doesn't move the camera — set the MapView's center/zoom to the territory's bounds yourself.

ClusterLayer — member geography

A cloud of points (here ~120 synthetic member coordinates around Austin) clustered with MapLibre's built-in GeoJSON clustering. Dense areas collapse into sized, labelled accent bubbles; zoom in and the clusters split until single points show as small green dots. Pass raw { longitude, latitude }[] — the layer wraps them into the GeoJSON the source needs.

ClusterLayer id + points ({longitude,latitude}[]) — accent bubbles sized by count, good-tone lone dots. Zoom to split.
ClusterLayer
Props: id (unique Source id), points: { longitude, latitude }[], clusterMaxZoom (def 14), clusterRadius (def 50, px). Mounts a clustering Source + cluster-circle, cluster-count, and unclustered-point layers.
sizing
Cluster circles step-scale their radius by point_count (bigger = denser); the count label shows the abbreviated total. Lone points render as small --color-good dots.
scale
Clustering is GPU-side, so tens of thousands of points stay smooth — feed it the full member coordinate set, not a pre-aggregated one.

LocationChip & DistrictBadge (server-safe)

The label-only half of the layer — small presentational chips with no map and no client directive, so they drop straight into server-rendered rosters, tables, and cards. LocationChip renders a 'city, ST' place pill with an accent pin; DistrictBadge renders a monospace legislative-district token (congressional / state senate / state house), optionally state-qualified. Both are pure DS tokens and theme-aware.

Austin, TXSan Francisco Bay Area, CABrooklynWYBoston, MA
LocationChip — city / state (renders whichever is present)
TX-CD-12WY-CD-ALCA-SD-3HD 48
DistrictBadge — congressional (CD), state senate (SD), state house (HD); optionally state-qualified
LocationChip
Props: city?, state?, hideIcon?, + span passthrough. Renders "City, ST" / "City" / "ST"; renders nothing when both are empty. A muted surface pill with an accent pin.
DistrictBadge
Props: kind ('congressional' | 'state-upper' | 'state-lower'), number (string | number, e.g. 'AL' for at-large), state?, + span passthrough. Renders e.g. "TX-CD-12" / "CD 12" in mono on an accent-soft fill, with a hover title.
server-safe
No 'use client', no map, no hooks — exported from "@/components/ds" (not ds/maps). Use them freely in server components alongside, or entirely without, a map.

Static locator

interactive={false} turns MapView into a non-pannable display — gestures off, no nav control — for a fixed location card or a small inline locator next to an address.

MapView interactive={false} + a single marker — a static locator (no drag/scroll/zoom)
interactive
false → all gestures disabled and the nav control hidden (showNavigation defaults to interactive). The map still renders tiles + layers, it just doesn't respond to input.