Route Layer
mapuiDeclarative GeoJSON line string layer with directional dash arrays, casing outlines, and waypoints.
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.jsonnpx shadcn-vue@latest add @uipkge/mapInstalls to:app/components/ui/map/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
accessTokenMapbox access token. | string | — | optional |
variantNamed Mapbox basemap variant preset ('streets' | 'outdoors' | 'satellite' | 'satellite-streets' | 'light' | 'dark' | 'navigation-day' | 'navigation-night' | 'standard' | 'muted' | 'default'). | 'default''muted''streets''outdoors''light''dark''satellite''satellite-streets''navigation-day''navigation-night''standard' | default | optional |
sizeHeight preset. Omit to size via `class` (blocks typically pass `size-full`). | 'default''sm''lg''xl''full' | — | optional |
mapStyleOverride the base style. Defaults to variant or theme-aware light/dark style. | string | — | optional |
centerInitial [lng, lat]. | [number, number] | — | optional |
zoom | number | — | optional |
pitchCamera pitch tilt angle in degrees (0 - 85). | number | — | optional |
bearingCamera bearing rotation angle in degrees (0 - 360). | number | — | optional |
buildings3dAutomatically add 3D extruded building footprints. | boolean | — | optional |
terrain3dEnable 3D digital elevation model terrain mesh. | boolean | — | optional |
projection'mercator' (flat) or 'globe'. | string | — | optional |
navigationShow the zoom/compass control. | boolean | — | optional |
navigationPositionPlacement position of the navigation control ('top-left' | 'top-right' | 'bottom-left' | 'bottom-right'). | 'top-left''top-right''bottom-left''bottom-right' | — | optional |
showCompassExplicitly show/hide compass in navigation control. Defaults to true when pitch > 0. | boolean | — | optional |
showZoomExplicitly show/hide zoom buttons in navigation control. Defaults to true. | boolean | — | optional |
fullscreenShow the HTML5 fullscreen toggle control button. | boolean | — | optional |
fullscreenPositionPlacement position of the fullscreen control ('top-left' | 'top-right' | 'bottom-left' | 'bottom-right'). Defaults to 'top-right'. | 'top-left''top-right''bottom-left''bottom-right' | — | optional |
mutedRepaint the basemap to a quiet, desaturated canvas. | boolean | — | optional |
class | string | — | optional |
npm dependencies
Files installed (6)
app/components/ui/map/Map.vue15.3 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. * * Supports all official Mapbox variants via `variant`: * - `streets`: Mapbox Streets v12 navigation & POI basemap * - `outdoors`: Mapbox Outdoors v12 topographic & hiking contours * - `light`: Mapbox Light v11 monochromatic * - `dark`: Mapbox Dark v11 monochromatic * - `satellite`: Mapbox Satellite v9 high-res aerial photography * - `satellite-streets`: Mapbox Satellite Streets v12 hybrid satellite + street labels * - `navigation-day`: Turn-by-turn navigation day palette * - `navigation-night`: Turn-by-turn navigation dark HUD palette * - `standard`: Mapbox Standard with dynamic 3D lighting & landmark models * - `muted`: Desaturated theme-aware minimal canvas for data dashboards * - `default`: Theme-aware light/dark basemap * * Additional 3D features: * - `pitch`: Camera tilt angle (0 - 85 degrees) * - `bearing`: Camera rotation angle (0 - 360 degrees) * - `buildings3d`: Adds 3D extruded building footprint layer * - `terrain3d`: Enables 3D digital elevation model (DEM) terrain * * Requires a Mapbox access token (https://account.mapbox.com). */ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import mapboxgl from 'mapbox-gl' import { MapboxMap } from '@studiometa/vue-mapbox-gl' import { cn } from '@/lib/utils' import { mapVariants, MAPBOX_STYLES, type MapVariant, type MapVariants } from './map.variants' import 'mapbox-gl/dist/mapbox-gl.css' import './map.css' const DEFAULT_MAPBOX_TOKEN = '' const props = withDefaults( defineProps<{ /** Mapbox access token. */ accessToken?: string /** Named Mapbox basemap variant preset ('streets' | 'outdoors' | 'satellite' | 'satellite-streets' | 'light' | 'dark' | 'navigation-day' | 'navigation-night' | 'standard' | 'muted' | 'default'). */ variant?: MapVariant /** Height preset. Omit to size via `class` (blocks typically pass `size-full`). */ size?: MapVariants['size'] /** Override the base style. Defaults to variant or theme-aware light/dark style. */ mapStyle?: string /** Initial [lng, lat]. */ center?: [number, number] zoom?: number /** Camera pitch tilt angle in degrees (0 - 85). */ pitch?: number /** Camera bearing rotation angle in degrees (0 - 360). */ bearing?: number /** Automatically add 3D extruded building footprints. */ buildings3d?: boolean /** Enable 3D digital elevation model terrain mesh. */ terrain3d?: boolean /** 'mercator' (flat) or 'globe'. */ projection?: string /** Show the zoom/compass control. */ navigation?: boolean /** Placement position of the navigation control ('top-left' | 'top-right' | 'bottom-left' | 'bottom-right'). */ navigationPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' /** Explicitly show/hide compass in navigation control. Defaults to true when pitch > 0. */ showCompass?: boolean /** Explicitly show/hide zoom buttons in navigation control. Defaults to true. */ showZoom?: boolean /** Show the HTML5 fullscreen toggle control button. */ fullscreen?: boolean /** Placement position of the fullscreen control ('top-left' | 'top-right' | 'bottom-left' | 'bottom-right'). Defaults to 'top-right'. */ fullscreenPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' /** Repaint the basemap to a quiet, desaturated canvas. */ muted?: boolean class?: string }>(), { accessToken: () => (typeof process !== 'undefined' && (process.env?.NEXT_PUBLIC_MAPBOX_TOKEN || process.env?.PUBLIC_MAPBOX_TOKEN)) || (typeof import.meta !== 'undefined' && (import.meta as any).env?.PUBLIC_MAPBOX_TOKEN) || DEFAULT_MAPBOX_TOKEN, variant: 'default', mapStyle: undefined, center: () => [0, 20], zoom: 1.4, pitch: 0, bearing: 0, buildings3d: false, terrain3d: false, projection: 'mercator', navigation: true, navigationPosition: 'bottom-right', showCompass: undefined, showZoom: true, fullscreen: false, fullscreenPosition: 'top-right', muted: false, }, ) const resolvedToken = computed(() => { // Explicit empty string opts out of the demo fallback — used by "missing token" stories. if (props.accessToken === '') return '' return ( props.accessToken || (typeof process !== 'undefined' && (process.env?.NEXT_PUBLIC_MAPBOX_TOKEN || process.env?.PUBLIC_MAPBOX_TOKEN)) || (typeof import.meta !== 'undefined' && (import.meta as any).env?.PUBLIC_MAPBOX_TOKEN) || DEFAULT_MAPBOX_TOKEN ) }) const emit = defineEmits<{ (e: 'created', map: mapboxgl.Map): void }>() const htmlDark = ref(false) const isDark = () => htmlDark.value const isMuted = computed(() => props.muted || props.variant === 'muted') const resolvedStyle = computed(() => { if (props.mapStyle) return props.mapStyle if ( props.variant && props.variant !== 'default' && props.variant !== 'muted' && MAPBOX_STYLES[props.variant as keyof typeof MAPBOX_STYLES] ) { return MAPBOX_STYLES[props.variant as keyof typeof MAPBOX_STYLES] } return htmlDark.value ? '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) const inView = ref(false) const containerRef = ref<HTMLDivElement | null>(null) let resizeObserver: ResizeObserver | null = null let intersectionObserver: IntersectionObserver | null = null let themeObserver: MutationObserver | null = null onMounted(() => { const root = document.documentElement const syncTheme = () => { htmlDark.value = root.classList.contains('dark') } syncTheme() themeObserver = new MutationObserver(syncTheme) themeObserver.observe(root, { attributes: true, attributeFilter: ['class'] }) mounted.value = true if (!containerRef.value) { inView.value = true return } if (typeof IntersectionObserver !== 'undefined') { // Latch: the observer only defers the first paint. Un-setting it on scroll // would tear the WebGL map down and rebuild it (fresh style fetch, camera // reset, markers re-laid-out against a stale canvas size) every time the // block leaves the viewport. intersectionObserver = new IntersectionObserver( ([entry]) => { if (!entry.isIntersecting) return inView.value = true intersectionObserver?.disconnect() intersectionObserver = null }, { rootMargin: '160px', threshold: 0.01 }, ) intersectionObserver.observe(containerRef.value) } else { inView.value = true } if (typeof ResizeObserver !== 'undefined') { resizeObserver = new ResizeObserver(() => { map?.resize() }) resizeObserver.observe(containerRef.value) } }) onUnmounted(() => { resizeObserver?.disconnect() resizeObserver = null intersectionObserver?.disconnect() intersectionObserver = null themeObserver?.disconnect() themeObserver = null }) 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 dark = isDark() const P = dark ? { land: 'rgb(24,24,27)', water: 'rgb(39,39,42)', use: 'rgb(30,30,34)', road: 'rgb(45,46,52)', label: 'rgb(161,161,170)', halo: 'rgb(24,24,27)', 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 apply3DBuildings() { if (!map || !props.buildings3d) return if (map.getLayer('3d-buildings')) return const layers = map.getStyle()?.layers let labelLayerId: string | undefined if (layers) { for (let i = 0; i < layers.length; i++) { if (layers[i].type === 'symbol' && (layers[i].layout as any)?.['text-field']) { labelLayerId = layers[i].id break } } } try { map.addLayer( { id: '3d-buildings', source: 'composite', 'source-layer': 'building', filter: ['==', 'extrude', 'true'], type: 'fill-extrusion', minzoom: 14, paint: { 'fill-extrusion-color': isDark() ? '#27272a' : '#d4d4d8', 'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 14, 0, 14.05, ['get', 'height']], 'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], 14, 0, 14.05, ['get', 'min_height']], 'fill-extrusion-opacity': 0.8, }, }, labelLayerId, ) } catch { /* building layer not present in raster styles */ } } function apply3DTerrain() { if (!map || !props.terrain3d) return try { if (!map.getSource('mapbox-dem')) { map.addSource('mapbox-dem', { type: 'raster-dem', url: 'mapbox://mapbox.mapbox-terrain-dem-v1', tileSize: 512, maxzoom: 14, }) } map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 }) } catch { /* terrain not supported in current projection */ } } let navControl: mapboxgl.NavigationControl | null = null let fullscreenControl: mapboxgl.FullscreenControl | null = null function updateNavControl() { if (!map) return if (navControl) { try { map.removeControl(navControl) } catch { /* ignore */ } navControl = null } if (props.navigation) { const showCompass = props.showCompass !== undefined ? props.showCompass : (props.pitch ?? 0) > 0 const showZoom = props.showZoom ?? true navControl = new mapboxgl.NavigationControl({ showCompass, showZoom }) map.addControl(navControl, props.navigationPosition ?? 'bottom-right') } } function updateFullscreenControl() { if (!map) return if (fullscreenControl) { try { map.removeControl(fullscreenControl) } catch { /* ignore */ } fullscreenControl = null } if (props.fullscreen) { fullscreenControl = new mapboxgl.FullscreenControl() map.addControl(fullscreenControl, props.fullscreenPosition ?? 'top-right') } } function onCreated(instance: mapboxgl.Map) { map = instance // The ResizeObserver's first callback runs before this instance exists, so a // container that settled its height during mount would leave the canvas -- // and every marker laid out against it -- at the stale size. map.resize() try { ;(map as any).setProjection(props.projection) } catch { /* older api */ } if (props.pitch) { try { map.setPitch(props.pitch) } catch { /* ignore */ } } if (props.bearing) { try { map.setBearing(props.bearing) } catch { /* ignore */ } } updateNavControl() updateFullscreenControl() const ready = () => { if (isMuted.value) applyMuted() if (props.buildings3d) apply3DBuildings() if (props.terrain3d) apply3DTerrain() emit('created', map!) } if (map.isStyleLoaded()) ready() else map.once('load', ready) map.on('style.load', () => { if (isMuted.value) applyMuted() if (props.buildings3d) apply3DBuildings() if (props.terrain3d) apply3DTerrain() }) } // <script setup> components are closed by default, so a template ref on <Map> // would hand back an instance with nothing on it. react-map-gl's ref exposes // the map's own methods, so mirror that: expose the mapbox-gl Map itself, plus // the camera helpers blocks reach for directly off the ref. defineExpose({ /** The underlying mapbox-gl Map, or null until it is created. */ get map() { return map }, getMap: () => map, // Option types are read off the Map class so they track mapbox-gl's own // signatures instead of drifting against renamed exports. flyTo: (...args: Parameters<mapboxgl.Map['flyTo']>) => map?.flyTo(...args), easeTo: (...args: Parameters<mapboxgl.Map['easeTo']>) => map?.easeTo(...args), jumpTo: (...args: Parameters<mapboxgl.Map['jumpTo']>) => map?.jumpTo(...args), fitBounds: (...args: Parameters<mapboxgl.Map['fitBounds']>) => map?.fitBounds(...args), resize: () => map?.resize(), }) watch( () => props.pitch, (newPitch) => { if (map && newPitch !== undefined) { map.setPitch(newPitch) } }, ) watch( () => props.bearing, (newBearing) => { if (map && newBearing !== undefined) { map.setBearing(newBearing) } }, ) watch( () => [props.navigation, props.navigationPosition, props.showCompass, props.showZoom], () => { updateNavControl() }, ) watch( () => [props.fullscreen, props.fullscreenPosition], () => { updateFullscreenControl() }, ) </script> <template> <div ref="containerRef" data-uipkge data-slot="map" :data-variant="variant" :data-muted="isMuted" :class="cn(mapVariants({ variant, ...(size ? { size } : {}) }), props.class)" > <MapboxMap v-if="mounted && inView && resolvedToken" class="size-full" :access-token="resolvedToken" :map-style="resolvedStyle" :center="center" :zoom="zoom" :attribution-control="false" @mb-created="onCreated" > <slot /> </MapboxMap> <div v-else-if="mounted && !resolvedToken" class="text-muted-foreground flex size-full flex-col items-center justify-center gap-1 px-6 text-center text-sm" > <span class="text-foreground font-medium">Mapbox token required</span> <span>Pass an access token to render the map.</span> </div> </div> </template>app/components/ui/map/MapSource.vue2.2 kB
<script setup lang="ts"> import { onMounted, onUnmounted, provide, unref, watch } from 'vue' import { useMap } from '@studiometa/vue-mapbox-gl' import type { AnySourceData, GeoJSONSource } from 'mapbox-gl' const props = defineProps<{ id: string type?: string data?: any options?: AnySourceData }>() const { map } = useMap() provide('uipkge-map-source-id', props.id) function getSourceOptions(): AnySourceData { if (props.options) return props.options return { type: (props.type || 'geojson') as any, data: props.data, } } watch( () => props.data, (newData) => { const mapInstance = unref(map) if (!mapInstance || !newData) return try { const src = mapInstance.getSource(props.id) as GeoJSONSource | undefined if (src && typeof src.setData === 'function') { src.setData(newData) } } catch { // Ignored if source not yet ready or re-rendering } }, { deep: true }, ) function addOrUpdateSource() { const mapInstance = unref(map) if (!mapInstance) return try { if (mapInstance.getSource(props.id)) { const src = mapInstance.getSource(props.id) as GeoJSONSource | undefined if (src && typeof src.setData === 'function' && props.data) { src.setData(props.data) } return } mapInstance.addSource(props.id, getSourceOptions()) } catch { // Retry on next frame if style is still configuring } } onMounted(() => { const mapInstance = unref(map) if (!mapInstance) return if (mapInstance.isStyleLoaded && mapInstance.isStyleLoaded()) { addOrUpdateSource() } else { mapInstance.once('style.load', addOrUpdateSource) mapInstance.once('load', addOrUpdateSource) } }) onUnmounted(() => { const mapInstance = unref(map) if (!mapInstance) return try { if (mapInstance.getSource(props.id)) { const layers = mapInstance.getStyle()?.layers || [] for (const l of layers) { if ('source' in l && l.source === props.id) { mapInstance.removeLayer(l.id) } } mapInstance.removeSource(props.id) } } catch { // Ignored during map destruction } }) </script> <template> <div :id="id" style="display: contents"> <slot /> </div> </template>app/components/ui/map/MapLayer.vue2.8 kB
<script setup lang="ts"> import { inject, onMounted, onUnmounted, unref, watch } from 'vue' import { useMap } from '@studiometa/vue-mapbox-gl' import type { AnyLayer } from 'mapbox-gl' const props = defineProps<{ id: string /** Optional: `options` carries its own `type`, so only the flat form needs it. */ type?: string source?: string paint?: Record<string, any> layout?: Record<string, any> beforeId?: string options?: any }>() const parentSourceId = inject<string | undefined>('uipkge-map-source-id', undefined) const { map } = useMap() function getLayerConfig(): AnyLayer { if (props.options) return { ...props.options, id: props.id } return { id: props.id, type: props.type as any, source: props.source || parentSourceId, paint: props.paint || {}, layout: props.layout || {}, } as AnyLayer } function addLayerSafe() { const mapInstance = unref(map) if (!mapInstance) return try { if (mapInstance.getLayer(props.id)) { mapInstance.removeLayer(props.id) } const config = getLayerConfig() if (config.source && !mapInstance.getSource(config.source as string)) { // Source not ready yet, wait for sourcedata event const checkAndAdd = () => { try { if (!mapInstance.getLayer(props.id) && mapInstance.getSource(config.source as string)) { mapInstance.addLayer(config, props.beforeId) mapInstance.off('sourcedata', checkAndAdd) } } catch { // Ignored } } mapInstance.on('sourcedata', checkAndAdd) return } mapInstance.addLayer(config, props.beforeId) } catch { // Retry if style loading } } watch( () => props.paint, (newPaint) => { const mapInstance = unref(map) if (!mapInstance || !newPaint) return try { if (mapInstance.getLayer(props.id)) { for (const [key, value] of Object.entries(newPaint)) { // Keys come from a caller-supplied record; mapbox generics the name // against a closed union of every paint property, which a plain // string cannot satisfy. ;(mapInstance.setPaintProperty as (layer: string, name: string, value: unknown) => void)(props.id, key, value) } } } catch { // Ignored } }, { deep: true }, ) onMounted(() => { const mapInstance = unref(map) if (!mapInstance) return if (mapInstance.isStyleLoaded && mapInstance.isStyleLoaded()) { addLayerSafe() } else { mapInstance.once('style.load', addLayerSafe) mapInstance.once('load', addLayerSafe) } }) onUnmounted(() => { const mapInstance = unref(map) if (!mapInstance) return try { if (mapInstance.getLayer(props.id)) { mapInstance.removeLayer(props.id) } } catch { // Ignored } }) </script> <template> <div :id="id" style="display: none" /> </template>app/components/ui/map/map.variants.ts1.5 kB
import { cva, type VariantProps } from 'class-variance-authority' export type MapVariant = | 'default' | 'muted' | 'streets' | 'outdoors' | 'light' | 'dark' | 'satellite' | 'satellite-streets' | 'navigation-day' | 'navigation-night' | 'standard' export const MAPBOX_STYLES: Record<Exclude<MapVariant, 'default' | 'muted'>, string> = { streets: 'mapbox://styles/mapbox/streets-v12', outdoors: 'mapbox://styles/mapbox/outdoors-v12', light: 'mapbox://styles/mapbox/light-v11', dark: 'mapbox://styles/mapbox/dark-v11', satellite: 'mapbox://styles/mapbox/satellite-v9', 'satellite-streets': 'mapbox://styles/mapbox/satellite-streets-v12', 'navigation-day': 'mapbox://styles/mapbox/navigation-day-v1', 'navigation-night': 'mapbox://styles/mapbox/navigation-night-v1', standard: 'mapbox://styles/mapbox/standard', } export const mapVariants = cva('relative size-full overflow-hidden bg-muted', { variants: { variant: { default: '', muted: 'data-[muted=true]:grayscale-[25%]', streets: '', outdoors: '', light: '', dark: '', satellite: '', 'satellite-streets': '', 'navigation-day': '', 'navigation-night': '', standard: '', }, size: { default: 'h-96 w-full', sm: 'h-64 w-full', lg: 'h-[500px] w-full', xl: 'h-[650px] w-full', full: 'size-full', }, }, defaultVariants: { variant: 'default', }, }) export type MapVariants = VariantProps<typeof mapVariants>app/components/ui/map/index.ts0.9 kB
import type Map from './Map.vue' export { default as Map } from './Map.vue' /** Handle returned by a template ref on <Map> — mirrors react-map-gl's MapRef. * Carries the camera helpers plus `map` / `getMap()` for the raw mapbox-gl Map. */ export type MapRef = InstanceType<typeof Map> export { mapVariants, MAPBOX_STYLES, type MapVariant, type MapVariants } from './map.variants' export { default as MapSource } from './MapSource.vue' export { default as MapLayer } from './MapLayer.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, MapboxNavigationControl as MapNavigationControl, MapboxFullscreenControl as MapFullscreenControl, } from '@studiometa/vue-mapbox-gl'app/components/ui/map/map.css1.7 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); } .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(var(--opacity-muted, 0.7)); } .dark .mapboxgl-ctrl-group { background: var(--card, #18181b); border-color: var(--border, #27272a); } .dark .mapboxgl-ctrl-group button { background: transparent; } .dark .mapboxgl-ctrl-group button:hover { background: var(--muted, #27272a); } .dark .mapboxgl-ctrl-group button + button { border-top-color: var(--border, #27272a); } .dark .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: invert(1) brightness(1.2); } .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: var(--spacing-2) var(--spacing-3); background: var(--popover); color: var(--popover-foreground); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md); } .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-route-layer.json