From 4c26db81db539acd2495d4adb52ec891559bad81 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 4 Jun 2026 01:07:25 +0100 Subject: [PATCH] feat(frontend): add MapLibre GL map components Reusable Svelte map component library wrapping MapLibre GL JS: Map, MapMarker, MapControls, MapPopup, MapRoute, MapClusterLayer, MapArc, and supporting components. Features theme auto-detection, controlled/uncontrolled viewport, drag support, and context API. useMap hook provides reactive access. Integrated on contact page for salon location display. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- frontend/src/lib/components/ui/map/Map.svelte | 346 ++++++++++++++++++ .../src/lib/components/ui/map/MapArc.svelte | 304 +++++++++++++++ .../components/ui/map/MapClusterLayer.svelte | 286 +++++++++++++++ .../lib/components/ui/map/MapControls.svelte | 212 +++++++++++ .../lib/components/ui/map/MapMarker.svelte | 185 ++++++++++ .../src/lib/components/ui/map/MapPopup.svelte | 176 +++++++++ .../src/lib/components/ui/map/MapRoute.svelte | 184 ++++++++++ .../components/ui/map/MarkerContent.svelte | 56 +++ .../lib/components/ui/map/MarkerLabel.svelte | 27 ++ .../lib/components/ui/map/MarkerPopup.svelte | 159 ++++++++ .../components/ui/map/MarkerTooltip.svelte | 104 ++++++ frontend/src/lib/components/ui/map/index.ts | 13 + frontend/src/lib/components/ui/map/theme.ts | 11 + frontend/src/lib/hooks/use-map.svelte.ts | 28 ++ frontend/src/routes/contact/+page.svelte | 92 +++-- 15 files changed, 2152 insertions(+), 31 deletions(-) create mode 100644 frontend/src/lib/components/ui/map/Map.svelte create mode 100644 frontend/src/lib/components/ui/map/MapArc.svelte create mode 100644 frontend/src/lib/components/ui/map/MapClusterLayer.svelte create mode 100644 frontend/src/lib/components/ui/map/MapControls.svelte create mode 100644 frontend/src/lib/components/ui/map/MapMarker.svelte create mode 100644 frontend/src/lib/components/ui/map/MapPopup.svelte create mode 100644 frontend/src/lib/components/ui/map/MapRoute.svelte create mode 100644 frontend/src/lib/components/ui/map/MarkerContent.svelte create mode 100644 frontend/src/lib/components/ui/map/MarkerLabel.svelte create mode 100644 frontend/src/lib/components/ui/map/MarkerPopup.svelte create mode 100644 frontend/src/lib/components/ui/map/MarkerTooltip.svelte create mode 100644 frontend/src/lib/components/ui/map/index.ts create mode 100644 frontend/src/lib/components/ui/map/theme.ts create mode 100644 frontend/src/lib/hooks/use-map.svelte.ts diff --git a/frontend/src/lib/components/ui/map/Map.svelte b/frontend/src/lib/components/ui/map/Map.svelte new file mode 100644 index 0000000..85eeab6 --- /dev/null +++ b/frontend/src/lib/components/ui/map/Map.svelte @@ -0,0 +1,346 @@ + + +
+ {#if !isReady} +
+
+ + + +
+
+ {/if} + {#if hasInitiallyLoaded} + {@render children?.()} + {/if} +
diff --git a/frontend/src/lib/components/ui/map/MapArc.svelte b/frontend/src/lib/components/ui/map/MapArc.svelte new file mode 100644 index 0000000..6e55209 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MapArc.svelte @@ -0,0 +1,304 @@ + + + diff --git a/frontend/src/lib/components/ui/map/MapClusterLayer.svelte b/frontend/src/lib/components/ui/map/MapClusterLayer.svelte new file mode 100644 index 0000000..3f94274 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MapClusterLayer.svelte @@ -0,0 +1,286 @@ + diff --git a/frontend/src/lib/components/ui/map/MapControls.svelte b/frontend/src/lib/components/ui/map/MapControls.svelte new file mode 100644 index 0000000..7499d50 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MapControls.svelte @@ -0,0 +1,212 @@ + + +{#if loaded} +
+ {#if showZoom} +
+ + +
+ {/if} + + {#if showCompass} +
+ +
+ {/if} + + {#if showLocate} +
+ +
+ {/if} + + {#if showFullscreen} +
+ +
+ {/if} +
+{/if} diff --git a/frontend/src/lib/components/ui/map/MapMarker.svelte b/frontend/src/lib/components/ui/map/MapMarker.svelte new file mode 100644 index 0000000..6f65954 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MapMarker.svelte @@ -0,0 +1,185 @@ + + + +{@render children?.()} diff --git a/frontend/src/lib/components/ui/map/MapPopup.svelte b/frontend/src/lib/components/ui/map/MapPopup.svelte new file mode 100644 index 0000000..5bee9c5 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MapPopup.svelte @@ -0,0 +1,176 @@ + + +
+
+ {#if closeButton} + + {/if} + {@render children?.()} +
+
+ + diff --git a/frontend/src/lib/components/ui/map/MapRoute.svelte b/frontend/src/lib/components/ui/map/MapRoute.svelte new file mode 100644 index 0000000..ba01fb1 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MapRoute.svelte @@ -0,0 +1,184 @@ + diff --git a/frontend/src/lib/components/ui/map/MarkerContent.svelte b/frontend/src/lib/components/ui/map/MarkerContent.svelte new file mode 100644 index 0000000..e735574 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MarkerContent.svelte @@ -0,0 +1,56 @@ + + + +
+
+ {#if children} + {@render children()} + {:else} + +
+ {/if} +
+
diff --git a/frontend/src/lib/components/ui/map/MarkerLabel.svelte b/frontend/src/lib/components/ui/map/MarkerLabel.svelte new file mode 100644 index 0000000..96e90c5 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MarkerLabel.svelte @@ -0,0 +1,27 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/map/MarkerPopup.svelte b/frontend/src/lib/components/ui/map/MarkerPopup.svelte new file mode 100644 index 0000000..547bf93 --- /dev/null +++ b/frontend/src/lib/components/ui/map/MarkerPopup.svelte @@ -0,0 +1,159 @@ + + +
+
+ {#if closeButton} + + {/if} + {@render children?.()} +
+
+ + diff --git a/frontend/src/lib/components/ui/map/MarkerTooltip.svelte b/frontend/src/lib/components/ui/map/MarkerTooltip.svelte new file mode 100644 index 0000000..849d5db --- /dev/null +++ b/frontend/src/lib/components/ui/map/MarkerTooltip.svelte @@ -0,0 +1,104 @@ + + +
+
+ {@render children?.()} +
+
+ + diff --git a/frontend/src/lib/components/ui/map/index.ts b/frontend/src/lib/components/ui/map/index.ts new file mode 100644 index 0000000..c7bc80f --- /dev/null +++ b/frontend/src/lib/components/ui/map/index.ts @@ -0,0 +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"; diff --git a/frontend/src/lib/components/ui/map/theme.ts b/frontend/src/lib/components/ui/map/theme.ts new file mode 100644 index 0000000..d5aa786 --- /dev/null +++ b/frontend/src/lib/components/ui/map/theme.ts @@ -0,0 +1,11 @@ +export type MapTheme = "light" | "dark"; + +export function resolveMapTheme({ + explicitTheme, + ambientTheme, +}: { + explicitTheme?: MapTheme; + ambientTheme: MapTheme; +}): MapTheme { + return explicitTheme ?? ambientTheme; +} diff --git a/frontend/src/lib/hooks/use-map.svelte.ts b/frontend/src/lib/hooks/use-map.svelte.ts new file mode 100644 index 0000000..14defb2 --- /dev/null +++ b/frontend/src/lib/hooks/use-map.svelte.ts @@ -0,0 +1,28 @@ +import { getContext } from "svelte"; +import MapLibreGL from "maplibre-gl"; + +type MapContext = { + getMap: () => MapLibreGL.Map | null; + isLoaded: () => boolean; + isStyleReady: () => boolean; +}; + +export function useMap() { + const mapCtx = getContext("map"); + + const map = $derived.by(() => mapCtx?.getMap() ?? null); + const isLoaded = $derived.by(() => mapCtx?.isLoaded() ?? false); + const isStyleReady = $derived.by(() => mapCtx?.isStyleReady() ?? false); + + return { + get map() { + return map; + }, + get isLoaded() { + return isLoaded; + }, + get isStyleReady() { + return isStyleReady; + }, + }; +} diff --git a/frontend/src/routes/contact/+page.svelte b/frontend/src/routes/contact/+page.svelte index 4535985..dccd477 100644 --- a/frontend/src/routes/contact/+page.svelte +++ b/frontend/src/routes/contact/+page.svelte @@ -1,6 +1,7 @@

Contact Me

- {#if loading} -
-
-
-
-
-
-
-
+
+
+ {#if loading} +
+
+
+
+
+
+
+
+
+ {:else if contact} + + {:else} + + {/if}
- {:else if contact} - - {:else} - - {/if} + +
+ + + +
+ + + + +
+
+ +
+

41 Pollock Walk

+

Dunfermline KY12 9DA

+
+
+
+ +
+
+