Map
Vue data-displayA thin, theme-aware Mapbox GL JS wrapper (built on @studiometa/vue-mapbox-gl). Pass an access token and drop MapMarker / MapPopup / MapLayer into the slot to build any map — fleet boards, journey maps, store locators. The base style follows light/dark automatically, an opt-in `muted` prop desaturates the basemap so overlaid data is the only colour, and `@created` hands you the raw map instance for custom layers and fitBounds.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/map.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/map.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/map.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/map.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/map
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
accessToken Mapbox access token. | string | — | required |
mapStyle Override the base style. Defaults to a theme-aware light/dark style. | string | undefined | optional |
center Initial [lng, lat]. | [number, number] | () => [0 | optional |
zoom | number | — | optional |
projection 'mercator' (flat) or 'globe'. | string | — | optional |
navigation Show the zoom/compass control. | boolean | — | optional |
muted Repaint the basemap to a quiet, desaturated canvas. | boolean | — | optional |
Dependencies
Used by
Files (3)
-
app/components/ui/map/Map.vue 5.5 kB
<script setup lang="ts"> /** * Map — a thin, theme-aware wrapper around @studiometa/vue-mapbox-gl's * MapboxMap. Pass an `access-token`; drop `<MapMarker>` / `<MapPopup>` / * `<MapLayer>` (re-exported from this package) or any @studiometa child into * the default slot to build any kind of map. * * - The base style follows the app theme (light-v11 / dark-v11) unless you pass * an explicit `map-style`. * - `muted` repaints the basemap to a quiet, desaturated canvas (graphite in * dark, light-grey in light) so overlaid data is the only colour on the map — * ideal for dashboards, fleet boards and journey maps. * - `@created` hands you the raw mapbox-gl Map instance for imperative work * (custom layers, fitBounds, controls). * * Requires a Mapbox access token (https://account.mapbox.com). */ import { computed, onMounted, ref } from 'vue' import mapboxgl from 'mapbox-gl' import { MapboxMap } from '@studiometa/vue-mapbox-gl' import 'mapbox-gl/dist/mapbox-gl.css' const props = withDefaults( defineProps<{ /** Mapbox access token. */ accessToken: string /** Override the base style. Defaults to a theme-aware light/dark style. */ mapStyle?: string /** Initial [lng, lat]. */ center?: [number, number] zoom?: number /** 'mercator' (flat) or 'globe'. */ projection?: string /** Show the zoom/compass control. */ navigation?: boolean /** Repaint the basemap to a quiet, desaturated canvas. */ muted?: boolean }>(), { mapStyle: undefined, center: () => [0, 20], zoom: 1.4, projection: 'mercator', navigation: true, muted: false, }, ) const emit = defineEmits<{ (e: 'created', map: mapboxgl.Map): void }>() const { theme } = useTheme() const isDark = () => theme.value === 'dark' || (theme.value === 'system' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches) const resolvedStyle = computed( () => props.mapStyle ?? (isDark() ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/light-v11'), ) // Render the map only on the client — Mapbox needs the DOM, and rendering it on // the server would hydrate-mismatch. The bg-muted box is the SSR placeholder. const mounted = ref(false) onMounted(() => { mounted.value = true }) let map: mapboxgl.Map | null = null /** Desaturate the basemap to a quiet canvas. Each op is guarded — style layer * ids drift between style versions and the wrong property for a layer type * throws. */ function applyMuted() { if (!map) return let styleLayers: any[] try { styleLayers = map.getStyle()?.layers ?? [] } catch { return // style not loaded yet } const P = isDark() ? { land: 'rgb(23,24,29)', water: 'rgb(17,18,22)', use: 'rgb(31,32,38)', road: 'rgb(50,52,60)', label: 'rgb(150,152,165)', halo: 'rgb(23,24,29)', admin: 'rgb(50,52,60)' } : { land: 'rgb(246,247,249)', water: 'rgb(226,230,235)', use: 'rgb(238,240,243)', road: 'rgb(221,224,229)', label: 'rgb(120,124,134)', halo: 'rgb(246,247,249)', admin: 'rgb(213,216,221)' } for (const l of styleLayers) { const id = l.id try { if (id === 'background' || id === 'land') map.setPaintProperty(id, 'background-color', P.land) else if (/water/.test(id) && l.type === 'fill') map.setPaintProperty(id, 'fill-color', P.water) else if (/landuse|landcover|national-park/.test(id) && l.type === 'fill') map.setPaintProperty(id, 'fill-color', P.use) else if (/^road-(motorway|trunk|primary)/.test(id) && l.type === 'line') { map.setPaintProperty(id, 'line-color', P.road) map.setPaintProperty(id, 'line-opacity', 0.55) } else if (/^road-(secondary|tertiary|street|minor|service|path|pedestrian)/.test(id)) { map.setLayoutProperty(id, 'visibility', 'none') } else if (/poi|transit|airport|natural-point|water-point|waterway-label|building/.test(id)) { map.setLayoutProperty(id, 'visibility', 'none') } else if (/road-label|settlement-major-label|settlement-minor-label|state-label/.test(id) && l.type === 'symbol') { map.setPaintProperty(id, 'text-color', P.label) map.setPaintProperty(id, 'text-halo-color', P.halo) map.setPaintProperty(id, 'text-opacity', 0.6) } else if (id === 'admin-1-boundary' && l.type === 'line') { map.setPaintProperty(id, 'line-color', P.admin) map.setPaintProperty(id, 'line-opacity', 0.6) } else if (id === 'admin-1-boundary-bg') map.setPaintProperty(id, 'line-opacity', 0) } catch { /* skip */ } } } function onCreated(instance: mapboxgl.Map) { map = instance try { ;(map as any).setProjection(props.projection) } catch { /* older api */ } if (props.navigation) map.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'bottom-right') // Emit once the style has loaded so the handler can safely add sources/layers. // (A theme restyle wipes custom layers — re-add them in your own // `map.on('style.load')` if you need them to survive a theme switch.) const ready = () => { if (props.muted) applyMuted() emit('created', map!) } if (map.isStyleLoaded()) ready() else map.once('load', ready) map.on('style.load', () => { if (props.muted) applyMuted() }) } </script> <template> <div class="bg-muted size-full overflow-hidden"> <MapboxMap v-if="mounted" class="size-full" :access-token="accessToken" :map-style="resolvedStyle" :center="center" :zoom="zoom" :attribution-control="false" @mb-created="onCreated" > <slot /> </MapboxMap> </div> </template> -
app/components/ui/map/index.ts 0.5 kB
export { default as Map } from './Map.vue' // Re-exported from @studiometa/vue-mapbox-gl under Map* names so consumers get // the whole map toolkit from one import. Place these inside <Map>'s slot — they // inject the map instance from the wrapping MapboxMap. export { MapboxMarker as MapMarker, MapboxPopup as MapPopup, MapboxLayer as MapLayer, MapboxSource as MapSource, MapboxNavigationControl as MapNavigationControl, } from '@studiometa/vue-mapbox-gl' -
app/components/ui/map/map.css 1.4 kB
/** * Map chrome — re-skins Mapbox GL's controls + popups to the design-system * tokens so they sit naturally on light and dark surfaces. Global (Mapbox * renders this chrome outside the component subtree). Import once, e.g. in your * root CSS: `@import './components/ui/map/map.css';` */ .mapboxgl-ctrl-group { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.08)); } .mapboxgl-ctrl-group button { background: transparent; } .mapboxgl-ctrl-group button + button { border-top-color: var(--border); } .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: invert(var(--map-ctrl-invert, 0)) opacity(0.7); } .dark .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: invert(1) opacity(0.7); } .mapboxgl-ctrl-attrib { background: color-mix(in oklab, var(--card) 80%, transparent); color: var(--muted-foreground); } .mapboxgl-ctrl-attrib a { color: var(--muted-foreground); } .mapboxgl-popup-content { padding: 8px 12px; background: var(--popover); color: var(--popover-foreground); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 0.12)); } .mapboxgl-popup-close-button { color: var(--muted-foreground); } .mapboxgl-popup-tip { border-top-color: var(--popover); border-bottom-color: var(--popover); }
Raw manifest: https://uipkge.dev/r/vue/map.json