@swisper.ai/web-map
v1.0.5
Published
Interactive MapLibre maps with selectable markers for Swisper web apps
Downloads
1,509
Readme
@swisper.ai/web-map
Interactive MapLibre maps with selectable markers for Swisper web apps. Map frames a set of points and shares selection and hover state with the MapMarkers inside it, but it doesn't store that state: your app holds the selected id and passes it back. MapContainer is the sized box around it. All text is passed in as props (the package has no i18n dependency), and components are styled with Tailwind v4 utilities against the @swisper.ai/design-system theme.
Boundary
Code belongs here if it puts a map on screen: the map surface, its markers, controls and overlays, and the engine setup it needs. Heavy map engines stay here, so consumers that never show a map don't download them. The headless scroller that keeps map cards in step with the selection belongs here. The chat-card layout that lists places beside a map, and the place cards themselves, belong in @swisper.ai/web-chat-ui.
Install
npm install @swisper.ai/web-map| Peer | Range | Why |
| ----------- | ---------- | -------------------- |
| react | >=18.0.0 | Components and hooks |
| react-dom | >=18.0.0 | Rendering |
MapLibre GL (maplibre-gl, through @vis.gl/react-maplibre) is a regular dependency, loaded lazily with the first map; its worker ships with the package and starts from a same-origin blob: URL, so your bundler needs no worker configuration. The package imports maplibre-gl/dist/maplibre-gl.css itself. It also depends on @swisper.ai/ui, whose peers (@mui/material, @mui/system, @mui/icons-material, @emotion/react, @emotion/styled) apply too.
Setup
- Load the styles (see Styles).
- If you render
MapUserLocationwith its default pin, mountTooltipProviderfrom@swisper.ai/uiabove the map: the pin shows its label in a tooltip. - Give the map a height with
MapContainer, or renderMapin a parent that already has one. MemoisefitPoints: it is compared by identity, so a new array on every render re-frames the map and undoes the user's pan and zoom.
import { Map, MapContainer, MapMarker, MapPin } from '@swisper.ai/web-map';
import { useMemo, useState } from 'react';
interface Place {
id: string;
name: string;
rating: string;
lng: number;
lat: number;
}
export function PlacesMap({ places }: { places: Place[] }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const fitPoints = useMemo(
() => places.map((place): [number, number] => [place.lng, place.lat]),
[places],
);
return (
<MapContainer>
<Map
testId="places-map"
ariaLabel="Restaurants near you"
fitPoints={fitPoints}
selectedId={selectedId}
onSelect={setSelectedId}
>
{places.map((place) => (
<MapMarker
key={place.id}
id={place.id}
longitude={place.lng}
latitude={place.lat}
label={place.name}
>
<MapPin label={place.name} value={place.rating} />
</MapMarker>
))}
</Map>
</MapContainer>
);
}Tiles come from OpenFreeMap's Liberty style (DEFAULT_MAP_STYLE_URL) unless you pass mapStyleUrl.
Styles
Pick one of the two options.
| Your app | Use |
| ----------------------------------- | --------------------------------------------------------------------------- |
| Builds its own CSS with Tailwind v4 | @import '@swisper.ai/web-map/tailwind.css'; after the design-system theme |
| Has no Tailwind | import '@swisper.ai/web-map/index.css'; once, at the app's entry |
With Tailwind, install @swisper.ai/design-system too and import its theme first:
@import '@swisper.ai/design-system/src/tailwind.css'; /* tailwindcss + theme: colors, radii, fonts */
@import '@swisper.ai/web-map/tailwind.css';@swisper.ai/web-map/tailwind.css holds only @source "./dist";, which makes Tailwind scan the built components for class names. The prebuilt index.css instead brings its own preflight, theme and every utility the @swisper.ai packages use, so loading it next to your own Tailwind build ships everything twice, and whichever file loads last wins wherever the two conflict.
API
Props and live examples are on each component's page in the Echo DS Storybook.
| Export | Why it exists |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Map | Root. Frames fitPoints (one point zooms in, none shows the world) and reports marker clicks and hover through onSelect / onHover; selection and hover are controlled by you. |
| MapContainer | Sized, rounded frame around a Map (relative, h-[400px] w-full, rounded-lg, clipped), and the box a MapPlaces strip pins itself to and sizes against (a size container named map). Override the height with className. Selection stays on Map. |
| MapMarker, useMapMarker | A selectable, hoverable point that wraps its pin in a button and passes { isSelected, isHovered } to it, as an element reading useMapMarker or a render function. |
| MapPlaces | Horizontal strip of place cards over a map. Render it after the Map inside the same MapContainer: it pins itself 24px above the container's bottom edge. selectedIndex controls the selection, or defaultSelectedIndex lets the list keep its own. The selected card scrolls to the front, and a swipe selects the card it settles on through onSelectedIndexChange. In a MapContainer narrower than 640px its side padding drops from 42px to 16px. hideOnDesktop hides the list once the container is 640px wide. |
| MapPlacesCard, MapPlacesCardMarker, MapPlacesCardMedia, MapPlacesCardContent, MapPlacesCardHeader, MapPlacesCardLeading, MapPlacesCardOrder, MapPlacesCardTitle, MapPlacesCardTrailing, MapPlacesCardMeta, MapPlacesCardMetaValue, MapPlacesCardDescription, mapPlacesCardVariants | One place in a MapPlaces strip: a surface of at least 258×70, or exactly the container's width less 96px in a MapContainer narrower than 640px, with hover and highlighted states (isHighlighted overrides), and parts for its picture, name, facts and status line. The card is not a control: lay one button over it to select the place. |
| MapPin | The default callout pin: a value that expands to value and place name on hover or selection, restyled through --map-pin-* CSS variables. |
| MapGeoPin | The route point: a pink dot, or an indigo teardrop with a flag. A route pin sets its variant and tone; an explicit prop wins. Pass color (a CSS color) to fill it with something other than the tone. |
| MapPinBubble, MapOriginPinBubble, MapDestinationPinBubble | The route callout. Origin is a pink departure fill, destination an indigo arrival fill with a flag, and tone="plain" is the white one-line waypoint pill. color replaces the fill of a tone. The detail row renders only when label or value is set. |
| MapOriginPin, MapWaypointPin, MapDestinationPin | Layout frames inside a MapMarker. Each places a MapGeoPin on the coordinate and the bubble beside or above it. Origin and waypoint markers use anchor="center"; a destination uses anchor="bottom". |
| MapJourneyLine | A line through coordinates, in travel order, inside a Map. Pass color (a CSS color, not a Tailwind class). width defaults to 3. Fewer than two points draws nothing. Put it before the markers so the stroke sits under the pins. |
| MapTools, MapToolsGroup, MapToolsFullscreen, MapToolsZoom | Bottom-right controls inside a Map. Fullscreen and zoom are MapLibre's controls, restyled into separate dark clusters. Fullscreen expands the MapContainer. |
| MapUserLocation, UserLocationPin | The user's own position; renders nothing until coordinates are known, and is not added to fitPoints for you. |
| useMapView | The map instance (mapRef) and selection state, for custom children of a Map. |
| BaseMap | The bare map with loading and error states, framing and style URL, but no selection context. |
| Layer, Marker, Source, useMap | Re-exported from @vis.gl/react-maplibre, so what you draw inside a BaseMap or Map reads the same map context as this package. |
| loadMapLib | MapLibre with its worker already set up, loaded once and shared by every map; pass it as mapLib if you render a react-maplibre Map yourself. |
| resolveMapStyleUrl, DEFAULT_MAP_STYLE_URL, DEFAULT_FIT_BOUNDS_PADDING, SINGLE_POINT_ZOOM | The defaults the components use, for maps of your own that should match. |
| getBoundsFromPoints, getBoundsCentroid, isDegenerateBounds | Helpers for framing a set of [longitude, latitude] points. |
Troubleshooting
| Symptom | Cause | Fix |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| "Invalid hook call", or a context is missing | The app resolves two copies of React | Add resolve: { dedupe: ['react', 'react-dom'] } to the Vite config |
| Components render without layout or colors | Tailwind never scans the package, because it skips node_modules by default | Import the package's tailwind.css in your Tailwind entry, or load its prebuilt index.css |
| Layout is right, but colors or radii are wrong or missing | The design-system theme isn't loaded, so theme utilities such as bg-paper-elevation-1 are empty | Import @swisper.ai/design-system/src/tailwind.css before the package's tailwind.css |
| Your own base styles (fonts, headings, resets) get overridden | The prebuilt index.css is loaded next to your own Tailwind build, and its preflight wins | Use one of the two: drop index.css and import tailwind.css |
| Spacing looks wrong after upgrading (huge gaps, tiny icons) | A number in a spacing class is pixels: gap-8 is 8px, not Tailwind's default 32px | Write the pixel value you want |
| Styles are missing or doubled, or the MUI theme doesn't apply | The app resolves two copies of @emotion/react or @mui/material | Add them to resolve.dedupe in the Vite config, next to react and react-dom |
| ERR_UNSUPPORTED_DIR_IMPORT when a test imports the package | dist/ uses extensionless imports that only a bundler resolves, and Vitest externalizes it | Set server.deps.inline: [/@swisper\.ai\//] in the Vitest config |
| The map has zero height | Map fills its parent (size-full), and the parent has no height | Wrap it in MapContainer, or give the parent an explicit height |
| The map jumps back to its starting frame whenever a marker is selected | fitPoints is a new array on every render, and each new array is framed again | Memoise fitPoints |
| Clicking the map doesn't clear the selection | Map never clears it; selection is controlled by the parent | Set your selected id to null where you want to deselect |
| VITE_MAP_STYLE_URL is ignored | The library reads no env vars (Vite would have baked them in when the library was built) | Pass mapStyleUrl |
| Tooltip must be used within TooltipProvider | MapUserLocation's default pin shows its label in a tooltip | Mount TooltipProvider from @swisper.ai/ui above the map |
| The map stays blank under a strict CSP | MapLibre's worker starts from a blob: URL, and tiles load from the style's host | Allow blob: in worker-src, and the style, tile and font hosts in connect-src |
| Bundler error importing maplibre-gl/dist/maplibre-gl.css | The package imports MapLibre's stylesheet, and the bundler doesn't handle CSS imports from dependencies | Enable CSS imports for node_modules (Vite does by default) |
| useMapView must be used within a Map component, or MapMarker throws | MapMarker and useMapView read the Map context | Render them inside a Map; inside a bare BaseMap, use Marker instead |
