style: apply prettier formatting to frontend
Backend CI / Lint & vulns (push) Failing after 1m59s
Backend CI / Tests (push) Successful in 2m1s
Backend CI / Race detector (push) Failing after 4m0s

This commit is contained in:
2026-06-25 13:26:26 +01:00
parent d4664c177c
commit ad0ad253ad
74 changed files with 3927 additions and 2632 deletions
+51 -51
View File
@@ -1,28 +1,28 @@
<script lang="ts">
import { onMount, onDestroy, setContext, untrack } from "svelte";
import { writable } from "svelte/store";
import MapLibreGL from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { browser } from "$app/environment";
import { resolveMapTheme } from "./theme";
import { onMount, onDestroy, setContext, untrack } from 'svelte';
import { writable } from 'svelte/store';
import MapLibreGL from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { browser } from '$app/environment';
import { resolveMapTheme } from './theme';
const theme = writable<"light" | "dark">("light");
const theme = writable<'light' | 'dark'>('light');
// Check document class for theme (works with next-themes, etc.)
function getDocumentTheme(): "light" | "dark" | null {
if (typeof document === "undefined") return null;
if (document.documentElement.classList.contains("dark")) return "dark";
if (document.documentElement.classList.contains("light")) return "light";
function getDocumentTheme(): 'light' | 'dark' | null {
if (typeof document === 'undefined') return null;
if (document.documentElement.classList.contains('dark')) return 'dark';
if (document.documentElement.classList.contains('light')) return 'light';
return null;
}
// Get system preference
function getSystemTheme(): "light" | "dark" {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
function getSystemTheme(): 'light' | 'dark' {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
let tailwindTheme: "light" | "dark" = $state("light");
let tailwindTheme: 'light' | 'dark' = $state('light');
type MapStyleOption = string | MapLibreGL.StyleSpecification;
@@ -39,17 +39,17 @@
};
interface Props {
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
styles?: {
light?: MapStyleOption;
dark?: MapStyleOption;
};
theme?: "light" | "dark";
theme?: 'light' | 'dark';
/** Map projection type. Use `{ type: "globe" }` for 3D globe view. */
projection?: MapLibreGL.ProjectionSpecification;
center?: [number, number];
zoom?: number;
options?: Omit<MapLibreGL.MapOptions, "container" | "style">;
options?: Omit<MapLibreGL.MapOptions, 'container' | 'style'>;
/**
* Bindable reference to the underlying MapLibre map instance.
* Useful for calling map methods imperatively from the parent.
@@ -74,8 +74,8 @@
}
const defaultStyles = {
dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
light: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
dark: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
light: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
};
let {
@@ -89,7 +89,7 @@
map = $bindable(null),
viewport,
onviewportchange,
onstyleloaded,
onstyleloaded
}: Props = $props();
let mapContainer: HTMLDivElement;
@@ -111,25 +111,25 @@
center: [c.lng, c.lat],
zoom: mapInstance.getZoom(),
bearing: mapInstance.getBearing(),
pitch: mapInstance.getPitch(),
pitch: mapInstance.getPitch()
};
}
const mapStyles = $derived({
dark: styles?.dark ?? defaultStyles.dark,
light: styles?.light ?? defaultStyles.light,
light: styles?.light ?? defaultStyles.light
});
const resolvedTheme = $derived(resolveMapTheme({ explicitTheme, ambientTheme: tailwindTheme }));
const currentStyle = $derived(resolvedTheme === "light" ? mapStyles.light : mapStyles.dark);
const currentStyle = $derived(resolvedTheme === 'light' ? mapStyles.light : mapStyles.dark);
const isReady = $derived(isMounted && isLoaded && isStyleLoaded);
setContext("map", {
setContext('map', {
getMap: () => map,
isLoaded: () => hasInitiallyLoaded,
isStyleReady: () => isReady,
isStyleReady: () => isReady
});
function clearStyleTimeout() {
@@ -165,22 +165,22 @@
const observer = new MutationObserver(updateTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
attributeFilter: ['class']
});
// Also watch for system preference changes
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemChange = (e: MediaQueryListEvent) => {
// Only use system preference if no document class is set
if (!getDocumentTheme()) {
tailwindTheme = e.matches ? "dark" : "light";
tailwindTheme = e.matches ? 'dark' : 'light';
}
};
mediaQuery.addEventListener("change", handleSystemChange);
mediaQuery.addEventListener('change', handleSystemChange);
onDestroy(() => {
observer.disconnect();
mediaQuery.removeEventListener("change", handleSystemChange);
mediaQuery.removeEventListener('change', handleSystemChange);
});
}
@@ -190,13 +190,13 @@
fadeDuration: 0,
renderWorldCopies: false,
attributionControl: {
compact: true,
compact: true
},
center: viewport?.center ?? center,
zoom: viewport?.zoom ?? zoom,
bearing: viewport?.bearing ?? 0,
pitch: viewport?.pitch ?? 0,
...options,
...options
});
const styleDataHandler = () => {
@@ -229,18 +229,18 @@
onviewportchange?.(getViewport(mapInstance));
};
mapInstance.on("load", loadHandler);
mapInstance.on("styledata", styleDataHandler);
mapInstance.on("move", handleMove);
mapInstance.on('load', loadHandler);
mapInstance.on('styledata', styleDataHandler);
mapInstance.on('move', handleMove);
mapInstance.on("dragstart", () => (isInteracting = true));
mapInstance.on("dragend", () => (isInteracting = false));
mapInstance.on("zoomstart", () => (isInteracting = true));
mapInstance.on("zoomend", () => (isInteracting = false));
mapInstance.on("rotatestart", () => (isInteracting = true));
mapInstance.on("rotateend", () => (isInteracting = false));
mapInstance.on("pitchstart", () => (isInteracting = true));
mapInstance.on("pitchend", () => (isInteracting = false));
mapInstance.on('dragstart', () => (isInteracting = true));
mapInstance.on('dragend', () => (isInteracting = false));
mapInstance.on('zoomstart', () => (isInteracting = true));
mapInstance.on('zoomend', () => (isInteracting = false));
mapInstance.on('rotatestart', () => (isInteracting = true));
mapInstance.on('rotateend', () => (isInteracting = false));
mapInstance.on('pitchstart', () => (isInteracting = true));
mapInstance.on('pitchend', () => (isInteracting = false));
map = mapInstance;
});
@@ -255,7 +255,7 @@
center: viewport.center ?? current.center,
zoom: viewport.zoom ?? current.zoom,
bearing: viewport.bearing ?? current.bearing,
pitch: viewport.pitch ?? current.pitch,
pitch: viewport.pitch ?? current.pitch
};
if (
@@ -269,7 +269,7 @@
}
internalUpdate = true;
map!.once("moveend", () => {
map!.once('moveend', () => {
internalUpdate = false;
});
map.jumpTo(next);
@@ -291,12 +291,12 @@
isStyleLoaded = false;
map!.setStyle(style, { diff: true });
map!.once("styledata", () => {
map!.once('styledata', () => {
map!.jumpTo({
center: currCenter,
zoom: currZoom,
bearing: currBearing,
pitch: currPitch,
pitch: currPitch
});
});
});
@@ -330,12 +330,12 @@
{#if !isReady}
<div class="absolute inset-0 flex items-center justify-center">
<div class="flex gap-1">
<span class="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full"></span>
<span class="size-1.5 animate-pulse rounded-full bg-muted-foreground/60"></span>
<span
class="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:150ms]"
class="size-1.5 animate-pulse rounded-full bg-muted-foreground/60 [animation-delay:150ms]"
></span>
<span
class="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:300ms]"
class="size-1.5 animate-pulse rounded-full bg-muted-foreground/60 [animation-delay:300ms]"
></span>
</div>
</div>
@@ -1,5 +1,5 @@
<script lang="ts" module>
import type MapLibreGL from "maplibre-gl";
import type MapLibreGL from 'maplibre-gl';
export type MapArcDatum = {
/** Unique identifier for this arc. Required for hover state tracking. */
@@ -17,8 +17,8 @@
originalEvent: MapLibreGL.MapMouseEvent;
};
type MapArcLinePaint = NonNullable<MapLibreGL.LineLayerSpecification["paint"]>;
type MapArcLineLayout = NonNullable<MapLibreGL.LineLayerSpecification["layout"]>;
type MapArcLinePaint = NonNullable<MapLibreGL.LineLayerSpecification['paint']>;
type MapArcLineLayout = NonNullable<MapLibreGL.LineLayerSpecification['layout']>;
export type MapArcProps<T extends MapArcDatum = MapArcDatum> = {
/** Array of arcs to render. Each arc must have a unique `id`. */
@@ -50,7 +50,7 @@
</script>
<script lang="ts" generics="T extends MapArcDatum = MapArcDatum">
import { useMap } from "$lib/hooks/use-map.svelte.js";
import { useMap } from '$lib/hooks/use-map.svelte.js';
let {
data,
@@ -63,18 +63,18 @@
onclick,
onhover,
interactive = true,
beforeId,
beforeId
}: MapArcProps<T> = $props();
const DEFAULT_PAINT: NonNullable<MapLibreGL.LineLayerSpecification["paint"]> = {
"line-color": "#4285F4",
"line-width": 2,
"line-opacity": 0.85,
const DEFAULT_PAINT: NonNullable<MapLibreGL.LineLayerSpecification['paint']> = {
'line-color': '#4285F4',
'line-width': 2,
'line-opacity': 0.85
};
const DEFAULT_LAYOUT: NonNullable<MapLibreGL.LineLayerSpecification["layout"]> = {
"line-join": "round",
"line-cap": "round",
const DEFAULT_LAYOUT: NonNullable<MapLibreGL.LineLayerSpecification['layout']> = {
'line-join': 'round',
'line-cap': 'round'
};
const ARC_HIT_MIN_WIDTH = 12;
@@ -123,9 +123,9 @@
}
function mergeArcPaint(
base: NonNullable<MapLibreGL.LineLayerSpecification["paint"]>,
hover: NonNullable<MapLibreGL.LineLayerSpecification["paint"]> | undefined
): NonNullable<MapLibreGL.LineLayerSpecification["paint"]> {
base: NonNullable<MapLibreGL.LineLayerSpecification['paint']>,
hover: NonNullable<MapLibreGL.LineLayerSpecification['paint']> | undefined
): NonNullable<MapLibreGL.LineLayerSpecification['paint']> {
if (!hover) return base;
const merged: Record<string, unknown> = { ...base };
for (const [key, hoverValue] of Object.entries(hover)) {
@@ -134,32 +134,32 @@
merged[key] =
baseValue === undefined
? hoverValue
: ["case", ["boolean", ["feature-state", "hover"], false], hoverValue, baseValue];
: ['case', ['boolean', ['feature-state', 'hover'], false], hoverValue, baseValue];
}
return merged as NonNullable<MapLibreGL.LineLayerSpecification["paint"]>;
return merged as NonNullable<MapLibreGL.LineLayerSpecification['paint']>;
}
const geoJSON = $derived.by<GeoJSON.FeatureCollection<GeoJSON.LineString>>(() => ({
type: "FeatureCollection",
type: 'FeatureCollection',
features: data.map((arc) => {
const { from, to, id: arcId, ...properties } = arc;
return {
id: typeof arcId === "number" ? arcId : undefined,
type: "Feature" as const,
id: typeof arcId === 'number' ? arcId : undefined,
type: 'Feature' as const,
properties: { ...properties, _arc_id: String(arcId) },
geometry: {
type: "LineString" as const,
coordinates: buildArcCoordinates(from, to, curvature, samples),
},
type: 'LineString' as const,
coordinates: buildArcCoordinates(from, to, curvature, samples)
}
};
}),
})
}));
const mergedPaint = $derived(mergeArcPaint({ ...DEFAULT_PAINT, ...paint }, hoverPaint));
const mergedLayout = $derived({ ...DEFAULT_LAYOUT, ...layout });
const hitWidth = $derived(() => {
const w = paint?.["line-width"] ?? DEFAULT_PAINT["line-width"];
const base = typeof w === "number" ? w : ARC_HIT_MIN_WIDTH;
const w = paint?.['line-width'] ?? DEFAULT_PAINT['line-width'];
const base = typeof w === 'number' ? w : ARC_HIT_MIN_WIDTH;
return Math.max((base as number) + ARC_HIT_PADDING, ARC_HIT_MIN_WIDTH);
});
@@ -178,18 +178,18 @@
if (!map.getSource(currentSourceId)) {
map.addSource(currentSourceId, {
type: "geojson",
type: 'geojson',
data: geoJSON,
promoteId: "_arc_id",
promoteId: '_arc_id'
});
map.addLayer(
{
id: currentLayerId,
type: "line",
type: 'line',
source: currentSourceId,
layout: mergedLayout,
paint: mergedPaint,
paint: mergedPaint
},
beforeId
);
@@ -198,10 +198,10 @@
map.addLayer(
{
id: currentHitLayerId,
type: "line",
type: 'line',
source: currentSourceId,
layout: mergedLayout,
paint: { "line-color": "transparent", "line-width": hitWidth() },
paint: { 'line-color': 'transparent', 'line-width': hitWidth() }
},
beforeId
);
@@ -267,7 +267,7 @@
if (arcId) {
map.setFeatureState({ source: sourceId, id: arcId }, { hover: true });
hoveredArcId = arcId;
map.getCanvas().style.cursor = "pointer";
map.getCanvas().style.cursor = 'pointer';
if (onhover) {
const arc = getArcById(arcId);
@@ -276,7 +276,7 @@
}
}
} else {
map.getCanvas().style.cursor = "";
map.getCanvas().style.cursor = '';
if (onhover) onhover(null);
}
};
@@ -286,18 +286,18 @@
map.setFeatureState({ source: sourceId, id: hoveredArcId }, { hover: false });
hoveredArcId = null;
}
map.getCanvas().style.cursor = "";
map.getCanvas().style.cursor = '';
if (onhover) onhover(null);
};
map.on("click", targetLayer, handleClick);
map.on("mousemove", targetLayer, handleMouseMove);
map.on("mouseleave", targetLayer, handleMouseLeave);
map.on('click', targetLayer, handleClick);
map.on('mousemove', targetLayer, handleMouseMove);
map.on('mouseleave', targetLayer, handleMouseLeave);
return () => {
map.off("click", targetLayer, handleClick);
map.off("mousemove", targetLayer, handleMouseMove);
map.off("mouseleave", targetLayer, handleMouseLeave);
map.off('click', targetLayer, handleClick);
map.off('mousemove', targetLayer, handleMouseMove);
map.off('mouseleave', targetLayer, handleMouseLeave);
};
});
</script>
@@ -1,6 +1,6 @@
<script lang="ts" generics="P extends GeoJSON.GeoJsonProperties">
import { getContext } from "svelte";
import MapLibreGL from "maplibre-gl";
import { getContext } from 'svelte';
import MapLibreGL from 'maplibre-gl';
import { generateUUID } from '$lib/utils/uuid';
interface Props {
@@ -29,17 +29,17 @@
data,
clusterMaxZoom = 14,
clusterRadius = 50,
clusterColors = ["#22c55e", "#eab308", "#ef4444"],
clusterColors = ['#22c55e', '#eab308', '#ef4444'],
clusterThresholds = [100, 750],
pointColor = "#3b82f6",
pointColor = '#3b82f6',
onpointclick,
onclusterclick,
onclusterclick
}: Props = $props();
const mapCtx = getContext<{
getMap: () => MapLibreGL.Map | null;
isStyleReady: () => boolean;
}>("map");
}>('map');
const id = generateUUID();
const sourceId = $derived(`cluster-source-${id}`);
@@ -66,72 +66,72 @@
// Add clustered GeoJSON source
map.addSource(sourceId, {
type: "geojson",
type: 'geojson',
data,
cluster: true,
clusterMaxZoom,
clusterRadius,
clusterRadius
});
// Add cluster circles layer
map.addLayer({
id: clusterLayerId,
type: "circle",
type: 'circle',
source: sourceId,
filter: ["has", "point_count"],
filter: ['has', 'point_count'],
paint: {
"circle-color": [
"step",
["get", "point_count"],
'circle-color': [
'step',
['get', 'point_count'],
clusterColors[0],
clusterThresholds[0],
clusterColors[1],
clusterThresholds[1],
clusterColors[2],
clusterColors[2]
],
"circle-radius": [
"step",
["get", "point_count"],
'circle-radius': [
'step',
['get', 'point_count'],
20,
clusterThresholds[0],
30,
clusterThresholds[1],
40,
40
],
"circle-stroke-width": 1,
"circle-stroke-color": "#fff",
"circle-opacity": 0.85,
},
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
'circle-opacity': 0.85
}
});
// Add cluster count text layer
map.addLayer({
id: clusterCountLayerId,
type: "symbol",
type: 'symbol',
source: sourceId,
filter: ["has", "point_count"],
filter: ['has', 'point_count'],
layout: {
"text-field": "{point_count_abbreviated}",
"text-font": ["Open Sans"],
"text-size": 12,
'text-field': '{point_count_abbreviated}',
'text-font': ['Open Sans'],
'text-size': 12
},
paint: {
"text-color": "#fff",
},
'text-color': '#fff'
}
});
// Add unclustered point layer
map.addLayer({
id: unclusteredLayerId,
type: "circle",
type: 'circle',
source: sourceId,
filter: ["!", ["has", "point_count"]],
filter: ['!', ['has', 'point_count']],
paint: {
"circle-color": pointColor,
"circle-radius": 5,
"circle-stroke-width": 2,
"circle-stroke-color": "#fff",
},
'circle-color': pointColor,
'circle-radius': 5,
'circle-stroke-width': 2,
'circle-stroke-color': '#fff'
}
});
return () => {
@@ -151,7 +151,7 @@
const map = mapCtx.getMap();
const loaded = mapCtx.isStyleReady();
if (!loaded || !map || typeof data === "string") return;
if (!loaded || !map || typeof data === 'string') return;
const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource | undefined;
if (source) {
@@ -168,29 +168,29 @@
// Update cluster layer colors and sizes
if (map.getLayer(clusterLayerId)) {
map.setPaintProperty(clusterLayerId, "circle-color", [
"step",
["get", "point_count"],
map.setPaintProperty(clusterLayerId, 'circle-color', [
'step',
['get', 'point_count'],
clusterColors[0],
clusterThresholds[0],
clusterColors[1],
clusterThresholds[1],
clusterColors[2],
clusterColors[2]
]);
map.setPaintProperty(clusterLayerId, "circle-radius", [
"step",
["get", "point_count"],
map.setPaintProperty(clusterLayerId, 'circle-radius', [
'step',
['get', 'point_count'],
20,
clusterThresholds[0],
30,
clusterThresholds[1],
40,
40
]);
}
// Update unclustered point layer color
if (map.getLayer(unclusteredLayerId)) {
map.setPaintProperty(unclusteredLayerId, "circle-color", pointColor);
map.setPaintProperty(unclusteredLayerId, 'circle-color', pointColor);
}
});
@@ -208,7 +208,7 @@
}
) => {
const features = map.queryRenderedFeatures(e.point, {
layers: [clusterLayerId],
layers: [clusterLayerId]
});
if (!features.length) return;
@@ -225,7 +225,7 @@
const zoom = await source.getClusterExpansionZoom(clusterId);
map.easeTo({
center: coordinates,
zoom,
zoom
});
}
};
@@ -241,7 +241,7 @@
const feature = e.features[0];
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [
number,
number,
number
];
// Handle world copies
@@ -254,34 +254,34 @@
// Cursor style handlers
const handleMouseEnterCluster = () => {
map.getCanvas().style.cursor = "pointer";
map.getCanvas().style.cursor = 'pointer';
};
const handleMouseLeaveCluster = () => {
map.getCanvas().style.cursor = "";
map.getCanvas().style.cursor = '';
};
const handleMouseEnterPoint = () => {
if (onpointclick) {
map.getCanvas().style.cursor = "pointer";
map.getCanvas().style.cursor = 'pointer';
}
};
const handleMouseLeavePoint = () => {
map.getCanvas().style.cursor = "";
map.getCanvas().style.cursor = '';
};
map.on("click", clusterLayerId, handleClusterClick);
map.on("click", unclusteredLayerId, handlePointClick);
map.on("mouseenter", clusterLayerId, handleMouseEnterCluster);
map.on("mouseleave", clusterLayerId, handleMouseLeaveCluster);
map.on("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
map.on("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
map.on('click', clusterLayerId, handleClusterClick);
map.on('click', unclusteredLayerId, handlePointClick);
map.on('mouseenter', clusterLayerId, handleMouseEnterCluster);
map.on('mouseleave', clusterLayerId, handleMouseLeaveCluster);
map.on('mouseenter', unclusteredLayerId, handleMouseEnterPoint);
map.on('mouseleave', unclusteredLayerId, handleMouseLeavePoint);
return () => {
map.off("click", clusterLayerId, handleClusterClick);
map.off("click", unclusteredLayerId, handlePointClick);
map.off("mouseenter", clusterLayerId, handleMouseEnterCluster);
map.off("mouseleave", clusterLayerId, handleMouseLeaveCluster);
map.off("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
map.off("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
map.off('click', clusterLayerId, handleClusterClick);
map.off('click', unclusteredLayerId, handlePointClick);
map.off('mouseenter', clusterLayerId, handleMouseEnterCluster);
map.off('mouseleave', clusterLayerId, handleMouseLeaveCluster);
map.off('mouseenter', unclusteredLayerId, handleMouseEnterPoint);
map.off('mouseleave', unclusteredLayerId, handleMouseLeavePoint);
};
});
</script>
@@ -1,15 +1,15 @@
<script lang="ts">
import { getContext } from "svelte";
import MapLibreGL from "maplibre-gl";
import { cn } from "$lib/utils.js";
import Plus from "@lucide/svelte/icons/plus";
import Minus from "@lucide/svelte/icons/minus";
import Locate from "@lucide/svelte/icons/locate";
import Maximize from "@lucide/svelte/icons/maximize";
import Loader2 from "@lucide/svelte/icons/loader-2";
import { getContext } from 'svelte';
import MapLibreGL from 'maplibre-gl';
import { cn } from '$lib/utils.js';
import Plus from '@lucide/svelte/icons/plus';
import Minus from '@lucide/svelte/icons/minus';
import Locate from '@lucide/svelte/icons/locate';
import Maximize from '@lucide/svelte/icons/maximize';
import Loader2 from '@lucide/svelte/icons/loader-2';
interface Props {
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
showZoom?: boolean;
showCompass?: boolean;
showLocate?: boolean;
@@ -19,29 +19,29 @@
}
let {
position = "bottom-right",
position = 'bottom-right',
showZoom = true,
showCompass = false,
showLocate = false,
showFullscreen = false,
class: className,
onlocate,
onlocate
}: Props = $props();
const mapCtx = getContext<{
getMap: () => MapLibreGL.Map | null;
isLoaded: () => boolean;
}>("map");
}>('map');
let waitingForLocation = $state(false);
let compassElement: SVGSVGElement | null = $state(null);
const loaded = $derived(mapCtx.isLoaded());
const positionClasses = {
"top-left": "top-2 left-2",
"top-right": "top-2 right-2",
"bottom-left": "bottom-2 left-2",
"bottom-right": "bottom-10 right-2",
'top-left': 'top-2 left-2',
'top-right': 'top-2 right-2',
'bottom-left': 'bottom-2 left-2',
'bottom-right': 'bottom-10 right-2'
};
// Update compass rotation
@@ -57,13 +57,13 @@
compassElement.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;
};
map.on("rotate", updateRotation);
map.on("pitch", updateRotation);
map.on('rotate', updateRotation);
map.on('pitch', updateRotation);
updateRotation();
return () => {
map.off("rotate", updateRotation);
map.off("pitch", updateRotation);
map.off('rotate', updateRotation);
map.off('pitch', updateRotation);
};
});
@@ -88,23 +88,23 @@
waitingForLocation = true;
if ("geolocation" in navigator) {
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
const coords = {
longitude: position.coords.longitude,
latitude: position.coords.latitude,
latitude: position.coords.latitude
};
map.flyTo({
center: [coords.longitude, coords.latitude],
zoom: 14,
duration: 1500,
duration: 1500
});
onlocate?.(coords);
waitingForLocation = false;
},
(error) => {
console.error("Error getting location:", error);
console.error('Error getting location:', error);
waitingForLocation = false;
}
);
@@ -125,16 +125,16 @@
</script>
{#if loaded}
<div class={cn("absolute z-10 flex flex-col gap-1.5", positionClasses[position], className)}>
<div class={cn('absolute z-10 flex flex-col gap-1.5', positionClasses[position], className)}>
{#if showZoom}
<div
class="border-border bg-background [&>button:not(:last-child)]:border-border flex flex-col overflow-hidden rounded-md border shadow-sm [&>button:not(:last-child)]:border-b"
class="flex flex-col overflow-hidden rounded-md border border-border bg-background shadow-sm [&>button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border"
>
<button
onclick={handleZoomIn}
aria-label="Zoom in"
type="button"
class="hover:bg-accent dark:hover:bg-accent/40 focus-visible:ring-ring flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50"
class="flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-accent/40"
>
<Plus class="size-4" />
</button>
@@ -142,7 +142,7 @@
onclick={handleZoomOut}
aria-label="Zoom out"
type="button"
class="hover:bg-accent dark:hover:bg-accent/40 focus-visible:ring-ring flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50"
class="flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-accent/40"
>
<Minus class="size-4" />
</button>
@@ -151,13 +151,13 @@
{#if showCompass}
<div
class="border-border bg-background flex flex-col overflow-hidden rounded-md border shadow-sm"
class="flex flex-col overflow-hidden rounded-md border border-border bg-background shadow-sm"
>
<button
onclick={handleResetBearing}
aria-label="Reset bearing to north"
type="button"
class="hover:bg-accent dark:hover:bg-accent/40 focus-visible:ring-ring flex size-8 items-center justify-center transition-all focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50"
class="flex size-8 items-center justify-center transition-all hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-accent/40"
>
<svg
bind:this={compassElement}
@@ -176,13 +176,13 @@
{#if showLocate}
<div
class="border-border bg-background flex flex-col overflow-hidden rounded-md border shadow-sm"
class="flex flex-col overflow-hidden rounded-md border border-border bg-background shadow-sm"
>
<button
onclick={handleLocate}
aria-label="Find my location"
type="button"
class="hover:bg-accent dark:hover:bg-accent/40 focus-visible:ring-ring flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50"
class="flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-accent/40"
disabled={waitingForLocation}
>
{#if waitingForLocation}
@@ -196,13 +196,13 @@
{#if showFullscreen}
<div
class="border-border bg-background flex flex-col overflow-hidden rounded-md border shadow-sm"
class="flex flex-col overflow-hidden rounded-md border border-border bg-background shadow-sm"
>
<button
onclick={handleFullscreen}
aria-label="Toggle fullscreen"
type="button"
class="hover:bg-accent dark:hover:bg-accent/40 focus-visible:ring-ring flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50"
class="flex size-8 items-center justify-center transition-all first:rounded-t-md last:rounded-b-md hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-accent/40"
>
<Maximize class="size-4" />
</button>
@@ -1,22 +1,22 @@
<script lang="ts">
import { getContext, setContext, untrack } from "svelte";
import MapLibreGL, { type MarkerOptions } from "maplibre-gl";
import { getContext, setContext, untrack } from 'svelte';
import MapLibreGL, { type MarkerOptions } from 'maplibre-gl';
type Anchor =
| "center"
| "top"
| "bottom"
| "left"
| "right"
| "top-left"
| "top-right"
| "bottom-left"
| "bottom-right";
| 'center'
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
interface Props {
longitude: number;
latitude: number;
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
onclick?: (e: MouseEvent) => void;
onmouseenter?: (e: MouseEvent) => void;
onmouseleave?: (e: MouseEvent) => void;
@@ -25,10 +25,10 @@
ondragend?: (lngLat: { lng: number; lat: number }) => void;
draggable?: boolean;
anchor?: Anchor;
offset?: MarkerOptions["offset"];
offset?: MarkerOptions['offset'];
rotation?: number;
pitchAlignment?: MarkerOptions["pitchAlignment"];
rotationAlignment?: MarkerOptions["rotationAlignment"];
pitchAlignment?: MarkerOptions['pitchAlignment'];
rotationAlignment?: MarkerOptions['rotationAlignment'];
}
let {
@@ -42,17 +42,17 @@
ondrag,
ondragend,
draggable = false,
anchor = "center",
anchor = 'center',
offset,
rotation,
pitchAlignment,
rotationAlignment,
rotationAlignment
}: Props = $props();
const mapCtx = getContext<{
getMap: () => MapLibreGL.Map | null;
isLoaded: () => boolean;
}>("map");
}>('map');
let marker: MapLibreGL.Marker | null = $state(null);
let markerElement: HTMLDivElement | null = $state(null);
@@ -60,13 +60,13 @@
let isDragging = $state(false);
// Provide marker context for child components
setContext("marker", {
setContext('marker', {
getMarker: () => marker,
getElement: () => markerElement,
getMap: () => mapCtx.getMap(),
isReady: () => isReady,
isDraggable: () => draggable,
isDragging: () => isDragging,
isDragging: () => isDragging
});
// Create marker when map is ready
@@ -80,8 +80,8 @@
const lng = untrack(() => longitude);
const lat = untrack(() => latitude);
if (
typeof lng !== "number" ||
typeof lat !== "number" ||
typeof lng !== 'number' ||
typeof lat !== 'number' ||
Number.isNaN(lng) ||
Number.isNaN(lat)
) {
@@ -89,15 +89,15 @@
}
// Create container element programmatically
const container = document.createElement("div");
container.className = "cursor-pointer";
const container = document.createElement('div');
container.className = 'cursor-pointer';
markerElement = container;
// Build marker options
const markerOptions: MarkerOptions = {
element: container,
draggable,
anchor,
anchor
};
if (offset !== undefined) markerOptions.offset = offset;
@@ -111,10 +111,10 @@
marker = markerInstance;
// Mouse event listeners on the container
if (onclick) container.addEventListener("click", onclick);
if (onmouseenter) container.addEventListener("mouseenter", onmouseenter);
if (onclick) container.addEventListener('click', onclick);
if (onmouseenter) container.addEventListener('mouseenter', onmouseenter);
if (onmouseleave) {
container.addEventListener("mouseleave", (e) => {
container.addEventListener('mouseleave', (e) => {
if (!isDragging) onmouseleave(e);
});
}
@@ -136,23 +136,23 @@
};
if (draggable) {
markerInstance.on("dragstart", handleDragStart);
markerInstance.on("drag", handleDrag);
markerInstance.on("dragend", handleDragEnd);
markerInstance.on('dragstart', handleDragStart);
markerInstance.on('drag', handleDrag);
markerInstance.on('dragend', handleDragEnd);
}
isReady = true;
// Cleanup
return () => {
if (onclick) container.removeEventListener("click", onclick);
if (onmouseenter) container.removeEventListener("mouseenter", onmouseenter);
if (onmouseleave) container.removeEventListener("mouseleave", onmouseleave);
if (onclick) container.removeEventListener('click', onclick);
if (onmouseenter) container.removeEventListener('mouseenter', onmouseenter);
if (onmouseleave) container.removeEventListener('mouseleave', onmouseleave);
if (draggable) {
markerInstance.off("dragstart", handleDragStart);
markerInstance.off("drag", handleDrag);
markerInstance.off("dragend", handleDragEnd);
markerInstance.off('dragstart', handleDragStart);
markerInstance.off('drag', handleDrag);
markerInstance.off('dragend', handleDragEnd);
}
markerInstance.remove();
@@ -166,8 +166,8 @@
$effect(() => {
if (
marker &&
typeof longitude === "number" &&
typeof latitude === "number" &&
typeof longitude === 'number' &&
typeof latitude === 'number' &&
!Number.isNaN(longitude) &&
!Number.isNaN(latitude)
) {
@@ -1,18 +1,18 @@
<script lang="ts">
import { getContext } from "svelte";
import MapLibreGL, { type PopupOptions } from "maplibre-gl";
import { cn } from "$lib/utils.js";
import X from "@lucide/svelte/icons/x";
import { getContext } from 'svelte';
import MapLibreGL, { type PopupOptions } from 'maplibre-gl';
import { cn } from '$lib/utils.js';
import X from '@lucide/svelte/icons/x';
interface Props {
longitude: number;
latitude: number;
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
class?: string;
closeButton?: boolean;
onclose?: () => void;
offset?: PopupOptions["offset"];
anchor?: PopupOptions["anchor"];
offset?: PopupOptions['offset'];
anchor?: PopupOptions['anchor'];
closeOnClick?: boolean;
closeOnMove?: boolean;
focusAfterOpen?: boolean;
@@ -31,18 +31,18 @@
closeOnClick,
closeOnMove,
focusAfterOpen,
maxWidth,
maxWidth
}: Props = $props();
const mapCtx = getContext<{
getMap: () => MapLibreGL.Map | null;
isLoaded: () => boolean;
}>("map");
}>('map');
const markerCtx =
getContext<{
isDraggable?: () => boolean;
}>("marker") || {};
}>('marker') || {};
let popup: MapLibreGL.Popup | null = null;
let wrapperElement: HTMLDivElement | null = $state(null);
@@ -56,8 +56,8 @@
// Validate coordinates
if (
typeof longitude !== "number" ||
typeof latitude !== "number" ||
typeof longitude !== 'number' ||
typeof latitude !== 'number' ||
Number.isNaN(longitude) ||
Number.isNaN(latitude)
) {
@@ -65,13 +65,13 @@
}
// Create popup container
const container = document.createElement("div");
const container = document.createElement('div');
// Build popup options
const popupOptions: PopupOptions = {
offset,
closeButton: false,
className: "maplibre-popup-transparent",
className: 'maplibre-popup-transparent'
};
// If marker is draggable, preserve popup state during movement
@@ -93,14 +93,14 @@
if (maxWidth) {
popupInstance.setMaxWidth(maxWidth);
} else {
popupInstance.setMaxWidth("none");
popupInstance.setMaxWidth('none');
}
popup = popupInstance;
// Handle close event
const handleClose = () => onclose?.();
popupInstance.on("close", handleClose);
popupInstance.on('close', handleClose);
// Move content to popup container
while (wrapperElement.firstChild) {
@@ -108,7 +108,7 @@
}
return () => {
popupInstance.off("close", handleClose);
popupInstance.off('close', handleClose);
// Move content back
while (container.firstChild) {
@@ -126,8 +126,8 @@
$effect(() => {
if (
popup &&
typeof longitude === "number" &&
typeof latitude === "number" &&
typeof longitude === 'number' &&
typeof latitude === 'number' &&
!Number.isNaN(longitude) &&
!Number.isNaN(latitude)
) {
@@ -144,8 +144,8 @@
<div bind:this={wrapperElement} style="display: contents;">
<div
class={cn(
"bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md",
"animate-in fade-in-0 zoom-in-95 duration-200 ease-out",
'relative max-w-62 rounded-md border bg-popover p-3 text-popover-foreground shadow-md',
'animate-in duration-200 ease-out fade-in-0 zoom-in-95',
className
)}
>
@@ -154,7 +154,7 @@
type="button"
onclick={handleClose}
aria-label="Close popup"
class="focus-visible:ring-ring hover:bg-muted text-foreground absolute top-0.5 right-0.5 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm transition-colors focus:outline-none focus-visible:ring-2"
class="absolute top-0.5 right-0.5 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm text-foreground transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<X class="size-3.5" />
</button>
@@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from "svelte";
import MapLibreGL from "maplibre-gl";
import { getContext } from 'svelte';
import MapLibreGL from 'maplibre-gl';
import { generateUUID } from '$lib/utils/uuid';
interface Props {
@@ -28,7 +28,7 @@
let {
coordinates,
color = "#4285F4",
color = '#4285F4',
width = 3,
opacity = 0.8,
dashArray,
@@ -36,13 +36,13 @@
onmouseenter,
onmouseleave,
interactive = true,
id = generateUUID(),
id = generateUUID()
}: Props = $props();
const mapCtx = getContext<{
getMap: () => MapLibreGL.Map | null;
isStyleReady: () => boolean;
}>("map");
}>('map');
const sourceId = $derived(`route-source-${id}`);
const layerId = $derived(`route-layer-${id}`);
@@ -60,42 +60,42 @@
// Add source
map.addSource(sourceId, {
type: "geojson",
type: 'geojson',
data: {
type: "Feature",
type: 'Feature',
properties: {},
geometry: {
type: "LineString",
coordinates,
},
},
type: 'LineString',
coordinates
}
}
});
// Build paint options with transition definitions
// Use default values here - they'll be updated by the paint property effect
const paint: MapLibreGL.LineLayerSpecification['paint'] = {
"line-color": "#94a3b8", // Start with gray (unselected color)
"line-width": 5, // Start with unselected width
"line-opacity": 0.6, // Start with unselected opacity
"line-color-transition": { duration: 300, delay: 0 },
"line-width-transition": { duration: 300, delay: 0 },
"line-opacity-transition": { duration: 300, delay: 0 },
'line-color': '#94a3b8', // Start with gray (unselected color)
'line-width': 5, // Start with unselected width
'line-opacity': 0.6, // Start with unselected opacity
'line-color-transition': { duration: 300, delay: 0 },
'line-width-transition': { duration: 300, delay: 0 },
'line-opacity-transition': { duration: 300, delay: 0 }
};
if (dashArray) {
paint["line-dasharray"] = dashArray;
paint['line-dasharray'] = dashArray;
}
// Add layer
map.addLayer({
id: layerId,
type: "line",
type: 'line',
source: sourceId,
layout: {
"line-join": "round",
"line-cap": "round",
'line-join': 'round',
'line-cap': 'round'
},
paint,
paint
});
return () => {
@@ -118,12 +118,12 @@
const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource | undefined;
if (source) {
source.setData({
type: "Feature",
type: 'Feature',
properties: {},
geometry: {
type: "LineString",
coordinates,
},
type: 'LineString',
coordinates
}
});
}
});
@@ -135,12 +135,12 @@
if (!loaded || !map || !map.getLayer(layerId)) return;
map.setPaintProperty(layerId, "line-color", color);
map.setPaintProperty(layerId, "line-width", width);
map.setPaintProperty(layerId, "line-opacity", opacity);
map.setPaintProperty(layerId, 'line-color', color);
map.setPaintProperty(layerId, 'line-width', width);
map.setPaintProperty(layerId, 'line-opacity', opacity);
if (dashArray) {
map.setPaintProperty(layerId, "line-dasharray", dashArray);
map.setPaintProperty(layerId, 'line-dasharray', dashArray);
}
// Move selected routes to top (when opacity is 1, it's selected)
@@ -164,22 +164,22 @@
onclick?.();
};
const handleMouseEnter = () => {
map.getCanvas().style.cursor = "pointer";
map.getCanvas().style.cursor = 'pointer';
onmouseenter?.();
};
const handleMouseLeave = () => {
map.getCanvas().style.cursor = "";
map.getCanvas().style.cursor = '';
onmouseleave?.();
};
map.on("click", layerId, handleClick);
map.on("mouseenter", layerId, handleMouseEnter);
map.on("mouseleave", layerId, handleMouseLeave);
map.on('click', layerId, handleClick);
map.on('mouseenter', layerId, handleMouseEnter);
map.on('mouseleave', layerId, handleMouseLeave);
return () => {
map.off("click", layerId, handleClick);
map.off("mouseenter", layerId, handleMouseEnter);
map.off("mouseleave", layerId, handleMouseLeave);
map.off('click', layerId, handleClick);
map.off('mouseenter', layerId, handleMouseEnter);
map.off('mouseleave', layerId, handleMouseLeave);
};
});
</script>
@@ -1,10 +1,10 @@
<script lang="ts">
import { getContext } from "svelte";
import MapLibreGL from "maplibre-gl";
import { cn } from "$lib/utils.js";
import { getContext } from 'svelte';
import MapLibreGL from 'maplibre-gl';
import { cn } from '$lib/utils.js';
interface Props {
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
class?: string;
}
@@ -15,7 +15,7 @@
getElement: () => HTMLDivElement | null;
getMap: () => MapLibreGL.Map | null;
isReady: () => boolean;
}>("marker");
}>('marker');
let wrapperElement: HTMLDivElement | null = $state(null);
let movedContent: Node[] = [];
@@ -45,7 +45,7 @@
<!-- Hidden wrapper that holds content until marker is ready -->
<div bind:this={wrapperElement} style="display: contents;">
<div class={cn("relative cursor-pointer", className)}>
<div class={cn('relative cursor-pointer', className)}>
{#if children}
{@render children()}
{:else}
@@ -1,24 +1,24 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { cn } from '$lib/utils.js';
interface Props {
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
class?: string;
position?: "top" | "bottom";
position?: 'top' | 'bottom';
}
let { children, class: className, position = "top" }: Props = $props();
let { children, class: className, position = 'top' }: Props = $props();
const positionClasses = {
top: "bottom-full mb-1",
bottom: "top-full mt-1",
top: 'bottom-full mb-1',
bottom: 'top-full mt-1'
};
</script>
<div
class={cn(
"absolute left-1/2 -translate-x-1/2 whitespace-nowrap",
"text-foreground text-[10px] font-medium",
'absolute left-1/2 -translate-x-1/2 whitespace-nowrap',
'text-[10px] font-medium text-foreground',
positionClasses[position],
className
)}
@@ -1,15 +1,15 @@
<script lang="ts">
import { getContext } from "svelte";
import MapLibreGL, { type PopupOptions } from "maplibre-gl";
import { cn } from "$lib/utils.js";
import X from "@lucide/svelte/icons/x";
import { getContext } from 'svelte';
import MapLibreGL, { type PopupOptions } from 'maplibre-gl';
import { cn } from '$lib/utils.js';
import X from '@lucide/svelte/icons/x';
interface Props {
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
class?: string;
closeButton?: boolean;
offset?: PopupOptions["offset"];
anchor?: PopupOptions["anchor"];
offset?: PopupOptions['offset'];
anchor?: PopupOptions['anchor'];
closeOnClick?: boolean;
closeOnMove?: boolean;
focusAfterOpen?: boolean;
@@ -25,7 +25,7 @@
closeOnClick,
closeOnMove,
focusAfterOpen,
maxWidth,
maxWidth
}: Props = $props();
const markerCtx = getContext<{
@@ -35,7 +35,7 @@
isReady: () => boolean;
isDraggable?: () => boolean;
isDragging?: () => boolean;
}>("marker");
}>('marker');
let popup: MapLibreGL.Popup | null = null;
let wrapperElement: HTMLDivElement | null = $state(null);
@@ -49,13 +49,13 @@
if (!ready || !marker || !wrapperElement) return;
// Create popup container
const container = document.createElement("div");
const container = document.createElement('div');
// Build popup options
const popupOptions: PopupOptions = {
offset,
closeButton: false,
className: "maplibre-popup-transparent",
className: 'maplibre-popup-transparent'
};
if (anchor !== undefined) popupOptions.anchor = anchor;
@@ -74,7 +74,7 @@
if (maxWidth) {
popupInstance.setMaxWidth(maxWidth);
} else {
popupInstance.setMaxWidth("none");
popupInstance.setMaxWidth('none');
}
// Attach popup to marker
@@ -127,8 +127,8 @@
<div bind:this={wrapperElement} style="display: contents;">
<div
class={cn(
"bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md",
"animate-in fade-in-0 zoom-in-95 duration-200 ease-out",
'relative max-w-62 rounded-md border bg-popover p-3 text-popover-foreground shadow-md',
'animate-in duration-200 ease-out fade-in-0 zoom-in-95',
className
)}
>
@@ -137,7 +137,7 @@
type="button"
onclick={handleClose}
aria-label="Close popup"
class="focus-visible:ring-ring hover:bg-muted text-foreground absolute top-0.5 right-0.5 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm transition-colors focus:outline-none focus-visible:ring-2"
class="absolute top-0.5 right-0.5 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm text-foreground transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<X class="size-3.5" />
</button>
@@ -1,13 +1,13 @@
<script lang="ts">
import { getContext } from "svelte";
import MapLibreGL, { type PopupOptions } from "maplibre-gl";
import { cn } from "$lib/utils.js";
import { getContext } from 'svelte';
import MapLibreGL, { type PopupOptions } from 'maplibre-gl';
import { cn } from '$lib/utils.js';
interface Props {
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
class?: string;
offset?: PopupOptions["offset"];
anchor?: PopupOptions["anchor"];
offset?: PopupOptions['offset'];
anchor?: PopupOptions['anchor'];
}
let { children, class: className, offset = 16, anchor }: Props = $props();
@@ -17,7 +17,7 @@
getElement: () => HTMLDivElement | null;
getMap: () => MapLibreGL.Map | null;
isReady: () => boolean;
}>("marker");
}>('marker');
let wrapperElement: HTMLDivElement | null = $state(null);
@@ -31,21 +31,21 @@
if (!ready || !marker || !markerElement || !map || !wrapperElement) return;
// Create popup container
const container = document.createElement("div");
const container = document.createElement('div');
// Build popup options
const popupOptions: PopupOptions = {
offset,
closeOnClick: true,
closeButton: false,
className: "maplibre-popup-transparent",
className: 'maplibre-popup-transparent'
};
if (anchor !== undefined) popupOptions.anchor = anchor;
// Create popup
const popupInstance = new MapLibreGL.Popup(popupOptions)
.setMaxWidth("none")
.setMaxWidth('none')
.setDOMContent(container);
// Move content to popup container
@@ -62,12 +62,12 @@
popupInstance.remove();
};
markerElement.addEventListener("mouseenter", handleMouseEnter);
markerElement.addEventListener("mouseleave", handleMouseLeave);
markerElement.addEventListener('mouseenter', handleMouseEnter);
markerElement.addEventListener('mouseleave', handleMouseLeave);
return () => {
markerElement.removeEventListener("mouseenter", handleMouseEnter);
markerElement.removeEventListener("mouseleave", handleMouseLeave);
markerElement.removeEventListener('mouseenter', handleMouseEnter);
markerElement.removeEventListener('mouseleave', handleMouseLeave);
// Move content back
while (container.firstChild) {
@@ -82,8 +82,8 @@
<div bind:this={wrapperElement} style="display: contents;">
<div
class={cn(
"bg-foreground text-background pointer-events-none rounded-md px-2 py-1 text-xs text-balance shadow-md",
"animate-in fade-in-0 zoom-in-95 duration-200 ease-out",
'pointer-events-none rounded-md bg-foreground px-2 py-1 text-xs text-balance text-background shadow-md',
'animate-in duration-200 ease-out fade-in-0 zoom-in-95',
className
)}
>
+13 -13
View File
@@ -1,13 +1,13 @@
export { default as Map } from "./Map.svelte";
export type { MapViewport } from "./Map.svelte";
export { default as MapMarker } from "./MapMarker.svelte";
export { default as MarkerContent } from "./MarkerContent.svelte";
export { default as MarkerPopup } from "./MarkerPopup.svelte";
export { default as MarkerTooltip } from "./MarkerTooltip.svelte";
export { default as MarkerLabel } from "./MarkerLabel.svelte";
export { default as MapControls } from "./MapControls.svelte";
export { default as MapPopup } from "./MapPopup.svelte";
export { default as MapRoute } from "./MapRoute.svelte";
export { default as MapClusterLayer } from "./MapClusterLayer.svelte";
export { default as MapArc } from "./MapArc.svelte";
export type { MapArcDatum, MapArcEvent, MapArcProps } from "./MapArc.svelte";
export { default as Map } from './Map.svelte';
export type { MapViewport } from './Map.svelte';
export { default as MapMarker } from './MapMarker.svelte';
export { default as MarkerContent } from './MarkerContent.svelte';
export { default as MarkerPopup } from './MarkerPopup.svelte';
export { default as MarkerTooltip } from './MarkerTooltip.svelte';
export { default as MarkerLabel } from './MarkerLabel.svelte';
export { default as MapControls } from './MapControls.svelte';
export { default as MapPopup } from './MapPopup.svelte';
export { default as MapRoute } from './MapRoute.svelte';
export { default as MapClusterLayer } from './MapClusterLayer.svelte';
export { default as MapArc } from './MapArc.svelte';
export type { MapArcDatum, MapArcEvent, MapArcProps } from './MapArc.svelte';
+2 -2
View File
@@ -1,8 +1,8 @@
export type MapTheme = "light" | "dark";
export type MapTheme = 'light' | 'dark';
export function resolveMapTheme({
explicitTheme,
ambientTheme,
ambientTheme
}: {
explicitTheme?: MapTheme;
ambientTheme: MapTheme;