feat(windkarte): Karten-UX Verbesserungen
- Polygon-Labels ab Zoom 13 mit zoom-proportionaler Schriftgröße - Hover-Highlight, Deselect per Hintergrundklick - Mobile Bottom-Sheet / Desktop Side-Panel (CSS-only) - Share-Link via URL-Hash (#W-10), auto-select on load - Share-Button + OSM-Link im Panel - Reset-Button auf Karte - Buffer-Spinner während Turf-Berechnung - Leaflet-Attribution nach links (kollidierte mit Panel) - fitBounds mit asymmetrischem Padding + maxZoom 14 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,21 +16,37 @@
|
||||
|
||||
let areas = $state<WindArea[]>([]);
|
||||
let selectedArea = $state<WindArea | null>(null);
|
||||
let selectedCenter = $state<[number, number] | null>(null);
|
||||
let mapReady = $state(false);
|
||||
let initialGebietsNr = $state<string | null>(null);
|
||||
let resetMap: (() => void) | null = null;
|
||||
|
||||
// Dynamic import: WindkarteMap needs window/document
|
||||
const MapComponent = browser
|
||||
? import("$lib/components/WindkarteMap.svelte").then((m) => m.default)
|
||||
: null;
|
||||
|
||||
function handleSelect(area: WindArea | null, center: [number, number] | null = null) {
|
||||
selectedArea = area;
|
||||
selectedCenter = center;
|
||||
if (browser) {
|
||||
if (area) {
|
||||
history.replaceState(null, "", location.pathname + location.search + "#" + encodeURIComponent(area.gebiets_nr));
|
||||
} else {
|
||||
history.replaceState(null, "", location.pathname + location.search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) initialGebietsNr = decodeURIComponent(hash);
|
||||
|
||||
try {
|
||||
const slugs = (block.areas ?? [])
|
||||
.map((a) => (typeof a === "string" ? a : a._slug))
|
||||
.filter(Boolean);
|
||||
|
||||
if (slugs.length > 0) {
|
||||
// Fetch only the explicitly referenced areas in parallel
|
||||
const results = await Promise.all(
|
||||
slugs.map((slug) =>
|
||||
fetch(`${CMS_BASE}/api/content/wind_area/${slug}`, {
|
||||
@@ -40,7 +56,6 @@
|
||||
);
|
||||
areas = results.filter((r): r is WindArea => r !== null);
|
||||
} else {
|
||||
// Fallback: all published areas
|
||||
const res = await fetch(`${CMS_BASE}/api/content/wind_area?limit=100`, {
|
||||
signal: AbortSignal.timeout(6_000),
|
||||
});
|
||||
@@ -68,16 +83,30 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden rounded-xl border border-stein-200 shadow-sm"
|
||||
style="height: 520px;"
|
||||
>
|
||||
<div class="map-container relative overflow-hidden rounded-xl border border-stein-200 shadow-sm">
|
||||
<!-- Reset button -->
|
||||
{#if resetMap}
|
||||
<button
|
||||
onclick={() => { resetMap?.(); handleSelect(null); }}
|
||||
class="pointer-events-auto absolute top-2.5 right-2.5 z-[500] rounded-md bg-white/90 p-1.5 shadow-md text-stein-500 hover:text-stein-800 hover:bg-white transition-colors"
|
||||
aria-label="Übersicht zurücksetzen"
|
||||
title="Übersicht zurücksetzen"
|
||||
>
|
||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if browser && MapComponent && mapReady}
|
||||
{#await MapComponent then Map}
|
||||
<Map
|
||||
{areas}
|
||||
geojsonUrl={GEOJSON_URL}
|
||||
onselect={(area) => { selectedArea = area; }}
|
||||
{initialGebietsNr}
|
||||
onselect={handleSelect}
|
||||
onready={(ctrl) => { resetMap = ctrl.reset; }}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
@@ -86,13 +115,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Sliding panel -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 z-[500] w-72 transition-transform duration-300 ease-in-out {selectedArea ? 'translate-x-0' : 'translate-x-full'}"
|
||||
>
|
||||
<!-- Panel: bottom-sheet on mobile, side-panel on sm+ -->
|
||||
<div class="panel-slide {selectedArea ? 'open' : ''}" style="pointer-events: none; z-index: 500;">
|
||||
<WindAreaPanel
|
||||
area={selectedArea}
|
||||
onclose={() => { selectedArea = null; }}
|
||||
center={selectedCenter}
|
||||
onclose={() => handleSelect(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,6 +135,57 @@
|
||||
<span class="inline-block size-2.5 rounded-sm" style="background:#b08a52; opacity:.7"></span>
|
||||
2. Entwurf 2026
|
||||
</span>
|
||||
<span class="ml-auto">Geodaten: © GDI-Th, dl-de/by-2-0</span>
|
||||
<span class="flex items-center gap-x-3 gap-y-1 flex-wrap text-stein-300">
|
||||
<span class="text-stein-400">Abstände:</span>
|
||||
{#each [{ color: "#e35651", label: "200 m" }, { color: "#b08a52", label: "600 m" }, { color: "#436e85", label: "1 km" }] as ring}
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="dashed-line" style="border-color:{ring.color}"></span>
|
||||
{ring.label}
|
||||
</span>
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.map-container {
|
||||
height: clamp(360px, 58vh, 580px);
|
||||
}
|
||||
|
||||
/* Panel: mobile = bottom sheet, sm+ = right side panel */
|
||||
.panel-slide {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 72%;
|
||||
transform: translateY(100%);
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
.panel-slide.open {
|
||||
transform: translateY(0);
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.panel-slide {
|
||||
top: 0;
|
||||
left: auto;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
width: 18rem;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.panel-slide.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dashed-line {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 0;
|
||||
border-top: 2px dashed;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user