@classic-homes/maps-svelte
v0.2.12
Published
Svelte MapLibre components for Classic Theme
Downloads
188
Readme
@classic-homes/maps-svelte
Svelte 5 components for MapLibre GL maps in the Classic Theme design system.
Installation
npm install @classic-homes/maps-svelte maplibre-glFeatures
- Svelte 5 Runes: Modern
$state,$derived,$effectpatterns - 7 Layer Types: Fill, Line, Circle, Symbol, FillExtrusion, Heatmap, Raster
- Controls: Navigation, Scale, Geolocation, Legend, Coordinate Display
- Clustering: Built-in cluster support with expansion utilities
- Deck.gl Integration: Optional overlay for advanced visualizations
- Accessibility: ARIA labels, keyboard navigation, reduced motion support
- Theme Integration: Light/dark themes from design tokens
Quick Start
<script lang="ts">
import { Map, MapContainer, Source, FillLayer } from '@classic-homes/maps-svelte';
import 'maplibre-gl/dist/maplibre-gl.css';
let geojsonData = $state(/* your data */);
</script>
<MapContainer class="h-[500px]">
<Map
initialCenter={[-122.4, 37.8]}
initialZoom={12}
onClick={(e) => console.log('Clicked:', e.lngLat)}
>
<Source id="lots" data={geojsonData}>
<FillLayer id="lots-fill" fillColor="#22c55e" fillOpacity={0.7} />
</Source>
</Map>
</MapContainer>Components
Map Container
<script lang="ts">
import { MapContainer, MapSkeleton, MapError, MapEmpty } from '@classic-homes/maps-svelte';
let isLoading = $state(true);
let error = $state<string | null>(null);
</script>
<MapContainer class="h-[500px]">
{#if isLoading}
<MapSkeleton />
{:else if error}
<MapError message={error} onRetry={() => reload()} />
{:else}
<Map>...</Map>
{/if}
</MapContainer>Core Map
<script lang="ts">
import { Map, useMapContext } from '@classic-homes/maps-svelte';
import { lightBasicStyle } from '@classic-homes/maps-core';
function handleClick(e) {
console.log('Clicked:', e.lngLat);
}
function handleMove(e) {
console.log('Viewport:', e.center, e.zoom);
}
</script>
<Map
initialCenter={[-122.4, 37.8]}
initialZoom={12}
initialBearing={0}
initialPitch={0}
{bounds}
fitBoundsOptions={{ padding: 50 }}
style={lightBasicStyle}
theme="light"
interactive={true}
scrollZoom={true}
dragPan={true}
onClick={handleClick}
onHover={handleHover}
onMove={handleMove}
onLoad={handleLoad}
>
<slot />
</Map>Accessing Map Context
<script lang="ts">
import { useMapContext, tryUseMapContext } from '@classic-homes/maps-svelte';
// Throws if not within Map component
const { map, loaded } = useMapContext();
// Returns null if not within Map
const context = tryUseMapContext();
</script>Data Sources
<script lang="ts">
import { Source } from '@classic-homes/maps-svelte';
</script>
<!-- GeoJSON data -->
<Source id="features" data={geojsonFeatureCollection}>
<!-- Layers go here -->
</Source>
<!-- URL source -->
<Source id="remote" data="https://example.com/data.geojson">
<!-- Layers go here -->
</Source>
<!-- With clustering -->
<Source id="points" data={pointData} cluster={true} clusterRadius={50} clusterMaxZoom={14}>
<!-- Layers go here -->
</Source>Layer Components
<script lang="ts">
import {
FillLayer,
LineLayer,
CircleLayer,
SymbolLayer,
FillExtrusionLayer,
HeatmapLayer,
} from '@classic-homes/maps-svelte';
</script>
<!-- Fill layer (polygons) -->
<FillLayer
id="polygons"
source="features"
fillColor="#22c55e"
fillOpacity={0.7}
fillOutlineColor="#166534"
onClick={handleClick}
onHover={handleHover}
/>
<!-- Line layer -->
<LineLayer id="lines" source="features" lineColor="#3b82f6" lineWidth={2} lineCap="round" />
<!-- Circle layer (points) -->
<CircleLayer
id="points"
source="features"
circleRadius={6}
circleColor="#ef4444"
circleStrokeWidth={2}
circleStrokeColor="#ffffff"
/>
<!-- Symbol layer (labels/icons) -->
<SymbolLayer
id="labels"
source="features"
textField={['get', 'name']}
textSize={12}
iconImage="marker"
/>
<!-- 3D extrusions -->
<FillExtrusionLayer
id="buildings"
source="features"
fillExtrusionHeight={['get', 'height']}
fillExtrusionColor="#94a3b8"
fillExtrusionOpacity={0.9}
/>
<!-- Heatmap -->
<HeatmapLayer
id="heat"
source="features"
heatmapWeight={1}
heatmapIntensity={0.5}
heatmapRadius={30}
/>Markers and Popups
<script lang="ts">
import {
Marker,
Popup,
PopupCard,
PopupHeader,
PopupContent,
PopupFooter,
} from '@classic-homes/maps-svelte';
</script>
<!-- Custom marker with popup -->
<Marker
lngLat={[-122.4, 37.8]}
anchor="bottom"
draggable={true}
onClick={handleClick}
onDragEnd={handleDrag}
>
<div class="custom-marker">
<MapPinIcon />
</div>
</Marker>
<!-- Popup with card styling -->
<Popup lngLat={[-122.4, 37.8]} anchor="bottom" closeOnClick={false}>
<PopupCard>
<PopupHeader>
<h3>Location Name</h3>
</PopupHeader>
<PopupContent>
<p>Description goes here</p>
</PopupContent>
<PopupFooter>
<button>View Details</button>
</PopupFooter>
</PopupCard>
</Popup>Controls
<script lang="ts">
import { Legend, LegendGroup, CoordinateDisplay } from '@classic-homes/maps-svelte';
</script>
<!-- Legend -->
<Legend
position="bottom-left"
title="Status"
items={[
{ label: 'Available', color: '#22c55e' },
{ label: 'Pending', color: '#f59e0b' },
{ label: 'Sold', color: '#ef4444' },
]}
/>
<!-- Legend group with collapsible sections -->
<LegendGroup
position="bottom-left"
legends={[
{
title: 'Status',
items: [{ label: 'Available', color: '#22c55e' }],
},
{
title: 'Price',
gradient: {
stops: [
{ value: 0, color: '#eff6ff', label: '$0' },
{ value: 100, color: '#1e40af', label: '$1M+' },
],
},
},
]}
/>
<!-- Coordinate display -->
<CoordinateDisplay position="bottom-right" precision={4} />Deck.gl Overlay
<script lang="ts">
import { DeckOverlay } from '@classic-homes/maps-svelte';
import { ScatterplotLayer } from '@deck.gl/layers';
const layers = $derived([
new ScatterplotLayer({
id: 'scatter',
data: points,
getPosition: (d) => d.coordinates,
getRadius: 100,
getFillColor: [255, 0, 0],
}),
]);
</script>
<Map>
<DeckOverlay {layers} />
</Map>Composables
useMapTheme
<script lang="ts">
import { useMapTheme } from '@classic-homes/maps-svelte';
const { theme, style, isDark, toggleTheme } = useMapTheme();
</script>
<Map style={style.current} theme={theme.current}>
<slot />
</Map>
<button onclick={toggleTheme}>
{isDark.current ? 'Light Mode' : 'Dark Mode'}
</button>useReducedMotion
<script lang="ts">
import { useReducedMotion } from '@classic-homes/maps-svelte';
const prefersReducedMotion = useReducedMotion();
function flyToLocation(lngLat) {
map.flyTo({
center: lngLat,
duration: prefersReducedMotion.current ? 0 : 1000,
});
}
</script>Cluster Utilities
<script lang="ts">
import {
isClusterFeature,
expandCluster,
getClusterBounds,
expandClusterToBounds,
} from '@classic-homes/maps-svelte';
function handleClusterClick(e) {
const feature = e.features[0];
if (isClusterFeature(feature)) {
// Option 1: Zoom to expand
expandCluster(map, 'source-id', feature.id, (zoom) => {
map.flyTo({ center: e.lngLat, zoom });
});
// Option 2: Fit to cluster bounds
expandClusterToBounds(map, 'source-id', feature, (bounds) => {
map.fitBounds(bounds, { padding: 50 });
});
}
}
</script>Reactive Updates
Svelte 5's fine-grained reactivity automatically handles prop updates:
<script lang="ts">
let data = $state(initialData);
let fillColor = $state('#22c55e');
// Data changes automatically update the source
function updateData(newData) {
data = newData;
}
// Style changes automatically update the layer
function updateColor(color) {
fillColor = color;
}
</script>
<Source id="lots" {data}>
<FillLayer id="lots-fill" {fillColor} fillOpacity={0.7} />
</Source>TypeScript
All components are fully typed:
import type {
MapContextValue,
SvelteFeatureClickEventParams,
SvelteFeatureHoverEventParams,
} from '@classic-homes/maps-svelte';
import type {
MapClickEventParams,
MapHoverEventParams,
FeatureClickEventParams,
GeoJSONFeatureCollection,
LngLat,
} from '@classic-homes/maps-core';Peer Dependencies
svelte>= 5.0.0maplibre-gl>= 4.0.0 || >= 5.0.0
License
MIT
