@fyit/crouton-maps
v0.1.0
Published
Map integration layer for Nuxt Crouton with Mapbox support
Maintainers
Readme
@fyit/crouton-maps
Map integration layer for Nuxt Crouton with Mapbox support
Add interactive maps to your Nuxt Crouton applications with this powerful, easy-to-use layer built on Mapbox GL JS.
Features
- 🗺️ Mapbox GL JS Integration - Beautiful, performant vector maps
- 🎯 Easy Geocoding - Convert addresses to coordinates and vice versa
- 📍 Marker Management - Simple marker and popup components
- 🔐 Secure API Keys - Runtime config for safe key management
- 🎨 Nuxt UI 4 Styling - Seamless integration with Crouton design system
- 📦 TypeScript First - Full type safety out of the box
- 🔌 Extensible - Built for future provider support (Google Maps, Leaflet, etc.)
Installation
pnpm add @fyit/crouton-mapsSetup
1. Add to your Nuxt config
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'@fyit/crouton',
'@fyit/crouton-maps'
],
runtimeConfig: {
public: {
mapbox: {
accessToken: process.env.MAPBOX_TOKEN,
// Optional defaults
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749], // [lng, lat]
zoom: 12
}
}
}
})2. Add your Mapbox token
Create a .env file:
MAPBOX_TOKEN=pk.eyJ1IjoieW91ciIsImEiOiJ0b2tlbiJ9...Get a free Mapbox token: https://account.mapbox.com/access-tokens/
Usage
Basic Map
<script setup lang="ts">
const center = ref<[number, number]>([-122.4194, 37.7749])
const zoom = ref(12)
const handleMapLoad = (map: any) => {
console.log('Map loaded!', map)
}
</script>
<template>
<CroutonMapMap
:center="center"
:zoom="zoom"
height="500px"
@load="handleMapLoad"
/>
</template>Map with Markers
<script setup lang="ts">
const mapInstance = ref(null)
const markers = [
{ position: [-122.4194, 37.7749], name: 'San Francisco' },
{ position: [-118.2437, 34.0522], name: 'Los Angeles' },
{ position: [-122.6765, 45.5231], name: 'Portland' }
]
const handleMapLoad = (map: any) => {
mapInstance.value = map
}
</script>
<template>
<CroutonMapMap
:center="[-120, 37]"
:zoom="6"
height="600px"
@load="handleMapLoad"
>
<template #default="{ map }">
<CroutonMapMarker
v-for="marker in markers"
:key="marker.name"
:map="map"
:position="marker.position"
:popup-content="`<h3>${marker.name}</h3>`"
color="red"
/>
</template>
</CroutonMapMap>
</template>Geocoding Example
<script setup lang="ts">
const { geocode, reverseGeocode, loading, error } = useGeocode()
const address = ref('1600 Amphitheatre Parkway, Mountain View, CA')
const result = ref(null)
const searchAddress = async () => {
result.value = await geocode(address.value)
}
const searchCoordinates = async () => {
result.value = await reverseGeocode([-122.0840575, 37.4220656])
}
</script>
<template>
<div>
<UInput v-model="address" placeholder="Enter an address" />
<UButton @click="searchAddress" :loading="loading">
Search
</UButton>
<div v-if="result">
<p>{{ result.address }}</p>
<p>Coordinates: {{ result.coordinates }}</p>
</div>
<div v-if="error">
<p class="text-red-500">{{ error }}</p>
</div>
</div>
</template>Interactive Map with Geocoding
<script setup lang="ts">
const { geocode, loading } = useGeocode()
const mapInstance = ref(null)
const center = ref<[number, number]>([-122.4194, 37.7749])
const markerPosition = ref<[number, number]>([-122.4194, 37.7749])
const searchQuery = ref('')
const handleSearch = async () => {
const result = await geocode(searchQuery.value)
if (result && mapInstance.value) {
center.value = result.coordinates
markerPosition.value = result.coordinates
mapInstance.value.flyTo({
center: result.coordinates,
zoom: 14
})
}
}
</script>
<template>
<div class="space-y-4">
<div class="flex gap-2">
<UInput
v-model="searchQuery"
placeholder="Search for a place..."
class="flex-1"
@keyup.enter="handleSearch"
/>
<UButton @click="handleSearch" :loading="loading">
Search
</UButton>
</div>
<CroutonMapMap
:center="center"
:zoom="12"
height="500px"
@load="(map) => mapInstance = map"
>
<template #default="{ map }">
<CroutonMapMarker
:map="map"
:position="markerPosition"
color="red"
/>
</template>
</CroutonMapMap>
</div>
</template>Components
<CroutonMapMap>
Main map container component.
Props:
id- Map container ID (auto-generated)center- Initial center coordinates[lng, lat]zoom- Initial zoom level (default: 12)style- Mapbox style URLheight- Container height (default: '400px')width- Container width (default: '100%')class- Additional CSS classes
Events:
@load- Emitted when map loads, receives map instance@error- Emitted on error, receives error message
Slots:
default- Scoped slot with{ map }for adding markers/popups
<CroutonMapMarker>
Map marker component.
Props:
map- Map instance (required)position- Marker position[lng, lat](required)color- Marker coloroptions- Mapbox MarkerOptionspopupContent- HTML content for popup
Events:
@click- Marker clicked@dragStart- Drag started@drag- Dragging@dragEnd- Drag ended
<CroutonMapPopup>
Popup component.
Props:
map- Map instance (required)position- Popup position[lng, lat](required)closeButton- Show close button (default: true)closeOnClick- Close on map click (default: true)maxWidth- Max width (default: '240px')
Events:
@open- Popup opened@close- Popup closed
Slots:
default- Popup content
Composables
useMap()
Core map management composable.
const { map, isLoaded, error, initialize, destroy, resize } = useMap()
await initialize({
container: 'map-container',
center: [-122.4194, 37.7749],
zoom: 12
})useMarker()
Marker management composable.
const { marker, addMarker, removeMarker, setPosition, togglePopup } = useMarker()
addMarker({
map: mapInstance,
position: [-122.4194, 37.7749],
popup: { content: '<h3>Hello</h3>' }
})useGeocode()
Geocoding composable.
const { geocode, reverseGeocode, loading, error } = useGeocode()
// Address → Coordinates
const result = await geocode('1600 Amphitheatre Parkway')
// Coordinates → Address
const result = await reverseGeocode([-122.0840575, 37.4220656])useMapConfig()
Access map configuration.
const { accessToken, style, center, zoom } = useMapConfig()Mapbox Styles
Using Style Presets
The package provides convenient presets for all Mapbox default styles:
<script setup lang="ts">
import { MAPBOX_STYLES, useMapboxStyles } from '#imports'
// Option 1: Use constants directly
const style = MAPBOX_STYLES.dark
// Option 2: Use the composable
const { styles, getStyle } = useMapboxStyles()
const style = styles.satellite
// Option 3: Helper function (works with presets or custom URLs)
const style = getStyle('outdoors') // Returns preset URL
const custom = getStyle('mapbox://styles/username/custom-id') // Returns as-is
</script>
<template>
<CroutonMapsMap :style="style" />
</template>Available Style Presets
standard- New Mapbox Standard style (recommended) - fully customizable 3Dstreets- Classic streets style (default)outdoors- Topographic with hiking trailslight- Minimalist light themedark- Dark theme for night modesatellite- Satellite imagery onlysatelliteStreets- Satellite with street labelsnavigationDay- Optimized for daytime navigationnavigationNight- Optimized for nighttime navigation
Custom Styles
You can also use custom styles from Mapbox Studio:
<CroutonMapsMap
style="mapbox://styles/your-username/your-custom-style-id"
/>Setting a Default Style
Set a default style for your entire app in nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
public: {
mapbox: {
accessToken: process.env.MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/dark-v11' // or use MAPBOX_STYLES constant
}
}
}
})API Limits
Mapbox Free Tier:
- 50,000 map loads/month
- 100,000 geocoding requests/month
- Unlimited static maps
For higher limits, see: https://www.mapbox.com/pricing/
TypeScript Support
All components and composables are fully typed. Import types from the package:
import type {
MapConfig,
MapInstance,
MarkerInstance,
GeocodeResult,
UseMapOptions
} from '@fyit/crouton-maps'Future Roadmap
- 🗺️ Additional map providers (Google Maps, Leaflet, MapLibre)
- 🎨 Custom marker components
- 📊 Heatmap support
- 🛣️ Route visualization
- 🌍 Clustering for large datasets
- 🔍 Advanced search with autocomplete
License
MIT
Contributing
Issues and PRs welcome at https://github.com/pmcp/nuxt-crouton
Built with ❤️ by FYIT
