No description
Find a file
autocommit aec9add2d1
Some checks failed
Publish / publish (push) Failing after 0s
chore: add .gitignore, remove node_modules/dist/.turbo from tracking
2026-04-20 01:12:39 -07:00
.forgejo/workflows chore: initial package split from monorepo 2026-04-20 01:10:39 -07:00
src chore: initial package split from monorepo 2026-04-20 01:10:39 -07:00
.gitignore chore: add .gitignore, remove node_modules/dist/.turbo from tracking 2026-04-20 01:12:39 -07:00
eslint.config.js chore: initial package split from monorepo 2026-04-20 01:10:39 -07:00
package.json chore: initial package split from monorepo 2026-04-20 01:10:39 -07:00
README.md chore: initial package split from monorepo 2026-04-20 01:10:39 -07:00
tsconfig.json chore: initial package split from monorepo 2026-04-20 01:10:39 -07:00

@lilith/ui-map

Self-hosted MapLibre GL map components for creator discovery. Fully self-hostable with PMTiles support, no external API keys required.

Features

  • Self-hosted capable - uses OpenStreetMap tiles or PMTiles for fully local operation
  • Creator markers with avatars and popups
  • Neighborhood boundaries with hover effects and click handlers
  • Clustering - automatic grouping of nearby markers when zoomed out
  • Radius overlay - visual circle showing search radius
  • Navigation controls - zoom and compass
  • Viewport tracking - get bounds on pan/zoom for lazy loading
  • Custom popups - render custom content on marker click

Installation

pnpm add @lilith/ui-map

Peer Dependencies

{
  "react": ">=18.0.0",
  "react-dom": ">=18.0.0",
  "styled-components": ">=6.0.0"
}

Usage

Basic Map

import { MapView, type MapMarker } from '@lilith/ui-map';

const markers: MapMarker[] = [
  {
    id: '1',
    location: { lat: 40.7128, lng: -74.006 },
    label: 'Jane Doe',
    avatarUrl: 'https://example.com/avatar.jpg',
  },
  {
    id: '2',
    location: { lat: 40.7282, lng: -73.7949 },
    label: 'John Smith',
  },
];

function CreatorMap() {
  return (
    <MapView
      markers={markers}
      center={{ lat: 40.7128, lng: -74.006 }}
      zoom={12}
      onMarkerClick={(marker) => console.log('Clicked:', marker)}
    />
  );
}

With Custom Popup

import { MapView } from '@lilith/ui-map';

<MapView
  markers={creators}
  renderPopup={(creator) => (
    <div>
      <img src={creator.avatarUrl} alt={creator.label} />
      <h3>{creator.label}</h3>
      <button onClick={() => viewProfile(creator.id)}>
        View Profile
      </button>
    </div>
  )}
/>

With Neighborhood Boundaries

import { MapView, type NeighborhoodBoundary } from '@lilith/ui-map';

const neighborhoods: NeighborhoodBoundary[] = [
  {
    id: 'soho',
    name: 'SoHo',
    slug: 'soho',
    coordinates: [[[-74.005, 40.722], [-73.997, 40.722], /* ... */]],
    creatorCount: 42,
    centroid: { lat: 40.723, lng: -74.001 },
  },
];

<MapView
  markers={creators}
  neighborhoods={neighborhoods}
  onNeighborhoodClick={(neighborhood) => {
    router.push(`/explore/${neighborhood.slug}`);
  }}
  neighborhoodColor="#10b981"
  neighborhoodHighlightColor="#059669"
/>

With Radius Circle

<MapView
  markers={creators}
  center={{ lat: 40.7128, lng: -74.006 }}
  radiusMiles={5}
  markerColor="#3b82f6"
/>

With Clustering

<MapView
  markers={creators}
  enableClustering={true}
  clusterRadius={120}
  clusterMaxZoom={14}
/>

Viewport Change Tracking

Track map viewport for lazy loading markers:

import { MapView, type ViewportBounds } from '@lilith/ui-map';

function LazyLoadMap() {
  const loadCreators = async (bounds: ViewportBounds, zoom: number) => {
    const { minLat, maxLat, minLng, maxLng } = bounds;
    const creators = await api.getCreatorsInBounds(bounds);
    setMarkers(creators);
  };

  return (
    <MapView
      markers={markers}
      onViewportChange={loadCreators}
    />
  );
}

Self-Hosted PMTiles

Use locally hosted map tiles:

import {
  MapView,
  registerPMTilesProtocol,
  createPMTilesStyle,
  PMTILES_SOURCES,
} from '@lilith/ui-map';

// Register PMTiles protocol (call once on app init)
registerPMTilesProtocol();

// Create style for local tiles
const localStyle = createPMTilesStyle(PMTILES_SOURCES.nyc);

<MapView
  markers={creators}
  mapStyle={localStyle}
/>

Using Different Map Styles

import { MapView, ONLINE_STYLES, getMapStyle } from '@lilith/ui-map';

// Use predefined online styles
<MapView mapStyle={ONLINE_STYLES.positron} markers={markers} />
<MapView mapStyle={ONLINE_STYLES.darkMatter} markers={markers} />

// Or get style dynamically
const style = getMapStyle('positron');
<MapView mapStyle={style} markers={markers} />

API Reference

MapViewProps

Prop Type Default Description
markers MapMarker[] [] Array of markers to display
center MapLocation First marker or NYC Map center coordinates
zoom number 12 Initial zoom level
radiusMiles number - Radius circle overlay
onMarkerClick (marker) => void - Marker click handler
renderPopup (marker) => ReactNode - Custom popup renderer
mapStyle string CARTO Positron MapLibre style URL
minHeight string '600px' Minimum map height
markerColor string '#3b82f6' Marker pin color
neighborhoods NeighborhoodBoundary[] - Neighborhood polygons
onNeighborhoodClick (neighborhood) => void - Neighborhood click handler
neighborhoodColor string '#10b981' Neighborhood fill color
neighborhoodHighlightColor string '#059669' Hover highlight color
enableClustering boolean true Enable marker clustering
clusterRadius number 120 Cluster radius in pixels
clusterMaxZoom number 14 Max zoom for clustering
onViewportChange (bounds, zoom) => void - Viewport change handler

MapMarker

interface MapMarker {
  id: string;
  location: MapLocation;
  label?: string;
  avatarUrl?: string;
}

MapLocation

interface MapLocation {
  lat: number;
  lng: number;
}

NeighborhoodBoundary

interface NeighborhoodBoundary {
  id: string;
  name: string;
  slug: string;
  coordinates: [number, number][][]; // GeoJSON polygon coordinates
  creatorCount?: number;
  centroid?: MapLocation;
}

ViewportBounds

interface ViewportBounds {
  minLat: number;
  maxLat: number;
  minLng: number;
  maxLng: number;
}

PMTiles Utilities

registerPMTilesProtocol

Register the PMTiles protocol handler for MapLibre:

import { registerPMTilesProtocol } from '@lilith/ui-map';

// Call once during app initialization
registerPMTilesProtocol();

createPMTilesStyle

Create a MapLibre style for PMTiles source:

import { createPMTilesStyle, PMTILES_SOURCES } from '@lilith/ui-map';

const style = createPMTilesStyle(PMTILES_SOURCES.nyc);
// or with custom URL
const style = createPMTilesStyle('https://example.com/tiles.pmtiles');

Map Styles

import { ONLINE_STYLES, LOCAL_PMTILES, getMapStyle } from '@lilith/ui-map';

// Online styles (CDN-hosted)
ONLINE_STYLES.positron      // Light theme
ONLINE_STYLES.darkMatter    // Dark theme
ONLINE_STYLES.voyager       // Colorful

// Local PMTiles regions
LOCAL_PMTILES.nyc
LOCAL_PMTILES.la
LOCAL_PMTILES.sf

Types

import type {
  MapViewProps,
  MapMarker,
  MapLocation,
  NeighborhoodBoundary,
  ViewportBounds,
  PMTilesRegion,
  OnlineStyle,
  LocalRegion,
} from '@lilith/ui-map';

Browser Requirements

  • WebGL support required for MapLibre GL
  • Modern browsers (Chrome 80+, Firefox 75+, Safari 14+)

Attribution

The default map style uses OpenStreetMap data. Include proper attribution:

  • OpenStreetMap contributors
  • CARTO (for default basemap style)

License

MIT