npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

npm license

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

  1. Load the styles (see Styles).
  2. If you render MapUserLocation with its default pin, mount TooltipProvider from @swisper.ai/ui above the map: the pin shows its label in a tooltip.
  3. Give the map a height with MapContainer, or render Map in a parent that already has one. Memoise fitPoints: 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 |

License

Apache-2.0