0ab2c58eb4
- Header desktop nav: social icons grouped in a wald-tinted pill with labels, active-state (aria-current + bold), opacity hover (no per-item bg) - Mobile overlay: social links as icon+label rows with active-state - Search button: lucide:search, icon-only - Migrate all icons mdi→lucide app-wide (234 refs); brand/language logos (google, whatsapp, mastodon, telegram, language-*) stay on mdi - generate-icons: emit mdi + lucide subsets; add @iconify-json/lucide - iconify-offline: register lucide collection - scripts/icon-map|validate|apply: reusable migration tooling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
290 lines
11 KiB
Svelte
290 lines
11 KiB
Svelte
<script lang="ts">
|
||
import Icon from "@iconify/svelte";
|
||
import "$lib/iconify-offline";
|
||
import type { WindArea, TextFragment } from "$lib/windkarte";
|
||
import { useTranslate, T } from "$lib/translations";
|
||
import InfoAccordion from "$lib/components/InfoAccordion.svelte";
|
||
|
||
let {
|
||
area,
|
||
center,
|
||
cmsBase,
|
||
onclose,
|
||
}: {
|
||
area: WindArea | null;
|
||
center: [number, number] | null;
|
||
cmsBase: string;
|
||
onclose: () => void;
|
||
} = $props();
|
||
|
||
const t = useTranslate();
|
||
|
||
const STATUS_KEY: Record<string, string> = {
|
||
rechtskraeftig: T.windkarte_status_rechtskraeftig,
|
||
entwurf_2: T.windkarte_status_entwurf_2,
|
||
entwurf_3: T.windkarte_status_entwurf_3,
|
||
};
|
||
const STATUS_CLASS: Record<string, string> = {
|
||
rechtskraeftig: "bg-wald-100 text-wald-800",
|
||
entwurf_2: "bg-erde-100 text-erde-800",
|
||
entwurf_3: "bg-himmel-100 text-himmel-800",
|
||
};
|
||
|
||
function stellungnahmeHref(s: WindArea["stellungnahme"]): string | null {
|
||
if (!s) return null;
|
||
if (typeof s === "string") return `/post/${s}`;
|
||
if (s._slug) return `/post/${s._slug}`;
|
||
return null;
|
||
}
|
||
|
||
let kriterien = $state<TextFragment[]>([]);
|
||
let loadingKriterien = $state(false);
|
||
let expandedSlug = $state<string | null>(null);
|
||
|
||
$effect(() => {
|
||
const slugs = (area?.stellungnahme_kriterien ?? [])
|
||
.map((k) => (typeof k === "string" ? k : k._slug))
|
||
.filter(Boolean);
|
||
|
||
kriterien = [];
|
||
expandedSlug = null;
|
||
|
||
if (slugs.length === 0) return;
|
||
|
||
loadingKriterien = true;
|
||
Promise.all(
|
||
slugs.map((slug) =>
|
||
fetch(`${cmsBase}/api/content/text_fragment/${slug}`)
|
||
.then((r) => (r.ok ? r.json() : null))
|
||
.catch(() => null),
|
||
),
|
||
).then((results) => {
|
||
kriterien = results.filter(Boolean) as TextFragment[];
|
||
loadingKriterien = false;
|
||
});
|
||
});
|
||
|
||
let copied = $state(false);
|
||
async function copyLink() {
|
||
await navigator.clipboard.writeText(location.href);
|
||
copied = true;
|
||
setTimeout(() => (copied = false), 2000);
|
||
}
|
||
|
||
function osmHref(c: [number, number] | null): string | null {
|
||
if (!c) return null;
|
||
return `https://www.openstreetmap.org/?mlat=${c[0].toFixed(5)}&mlon=${c[1].toFixed(5)}#map=14/${c[0].toFixed(5)}/${c[1].toFixed(5)}`;
|
||
}
|
||
|
||
const exportInfo = $derived(
|
||
area
|
||
? [
|
||
`${area.gebiets_nr}${area.bezeichnung ? ` – ${area.bezeichnung}` : ''}`,
|
||
area.flaeche_ha != null ? `${t(T.windkarte_label_flaeche)}: ${area.flaeche_ha.toFixed(1)} ha` : null,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n')
|
||
: ''
|
||
);
|
||
</script>
|
||
|
||
{#if area}
|
||
<div class="pointer-events-auto flex h-full flex-col overflow-y-auto bg-stein-0 shadow-xl">
|
||
<!-- Header -->
|
||
<div class="flex items-start justify-between gap-3 border-b border-stein-200 px-5 py-4">
|
||
<div class="min-w-0">
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<span class="text-xl font-semibold text-stein-800">{area.gebiets_nr}</span>
|
||
{#if area.status && area.status !== 'rechtskraeftig'}
|
||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {STATUS_CLASS[area.status] ?? 'bg-stein-100 text-stein-600'}">
|
||
{t(STATUS_KEY[area.status] ?? area.status)}
|
||
</span>
|
||
{/if}
|
||
</div>
|
||
{#if area.bezeichnung}
|
||
<p class="mt-0.5 text-sm text-stein-500">{area.bezeichnung}</p>
|
||
{/if}
|
||
</div>
|
||
<button
|
||
onclick={onclose}
|
||
class="shrink-0 rounded-md p-1.5 text-stein-400 hover:bg-stein-100 hover:text-stein-700 transition-colors"
|
||
aria-label={t(T.windkarte_panel_close)}
|
||
>
|
||
<Icon icon="lucide:x" class="size-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="flex-1 space-y-5 px-5 py-4 text-sm">
|
||
<!-- Kenndaten -->
|
||
<div class="flex flex-col divide-y divide-stein-100 border border-stein-100 rounded-lg overflow-hidden text-sm">
|
||
{#if area.flaeche_ha != null}
|
||
<div class="flex items-center justify-between px-3 py-1.5">
|
||
<span class="flex items-center gap-1.5 text-xs text-stein-500">
|
||
<Icon icon="lucide:ruler" class="size-3.5 text-stein-400" />
|
||
{t(T.windkarte_label_flaeche)}
|
||
</span>
|
||
<span class="font-semibold text-stein-800">{area.flaeche_ha.toFixed(1)} ha</span>
|
||
</div>
|
||
{/if}
|
||
{#if area.anlagen_geplant != null}
|
||
<div class="flex items-center justify-between px-3 py-1.5">
|
||
<span class="flex items-center gap-1.5 text-xs text-stein-500">
|
||
<Icon icon="lucide:fan" class="size-3.5 text-stein-400" />
|
||
{t(T.windkarte_label_anlagen_geplant)}
|
||
</span>
|
||
<span class="flex items-center gap-1.5 font-semibold text-stein-800">
|
||
{area.anlagen_geplant}
|
||
{#if area.anlagen_geplant_hinweis}
|
||
<Icon icon="lucide:info" class="size-3.5 text-amber-700" />
|
||
{/if}
|
||
</span>
|
||
</div>
|
||
{/if}
|
||
{#if area.investor}
|
||
<div class="flex items-center justify-between px-3 py-1.5">
|
||
<span class="flex items-center gap-1.5 text-xs text-stein-500">
|
||
<Icon icon="lucide:building-2" class="size-3.5 text-stein-400" />
|
||
{t(T.windkarte_label_investor)}
|
||
</span>
|
||
<span class="font-semibold text-stein-800">{area.investor}</span>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
{#if area.anlagen_geplant_hinweis}
|
||
<InfoAccordion
|
||
title={t(T.windkarte_label_anlagen_hinweis)}
|
||
body={area.anlagen_geplant_hinweis}
|
||
variant="amber"
|
||
/>
|
||
{/if}
|
||
|
||
<!-- Gemeinden -->
|
||
{#if area.gemeinden && area.gemeinden.length > 0}
|
||
<div>
|
||
<p class="mb-2 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-stein-500">
|
||
<Icon icon="lucide:map-pin" class="size-3.5 text-stein-400" />
|
||
{t(T.windkarte_label_gemeinden)}
|
||
</p>
|
||
<div class="flex flex-wrap gap-1.5">
|
||
{#each area.gemeinden as g}
|
||
<span class="rounded-full bg-himmel-100 px-2.5 py-0.5 text-xs font-medium text-himmel-800">{g}</span>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Notizen -->
|
||
{#if area.notizen}
|
||
<div>
|
||
<p class="mb-1.5 text-xs font-medium uppercase tracking-wide text-stein-500">{t(T.windkarte_label_notizen)}</p>
|
||
<div class="text-stein-700 leading-relaxed markdown prose prose-sm prose-zinc max-w-none">{@html area.notizen}</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Stellungnahme-Kriterien -->
|
||
{#if loadingKriterien}
|
||
<div class="flex items-center gap-2 text-xs text-stein-400">
|
||
<Icon icon="lucide:loader-circle" class="size-3.5 animate-spin" />
|
||
<span>{t(T.windkarte_kriterien_loading)}</span>
|
||
</div>
|
||
{:else if kriterien.length > 0}
|
||
<div>
|
||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-stein-500">{t(T.windkarte_label_kriterien)}</p>
|
||
<div class="divide-y divide-stein-100 rounded-lg border border-stein-100 overflow-hidden">
|
||
{#each kriterien as k (k._slug)}
|
||
<div>
|
||
<button
|
||
onclick={() => { expandedSlug = expandedSlug === k._slug ? null : k._slug; }}
|
||
class="flex w-full items-start justify-between gap-2 px-3 py-2 text-left text-xs font-medium text-stein-700 hover:bg-stein-50 transition-colors"
|
||
>
|
||
<span class="min-w-0 line-clamp-2">{k.title ?? k._slug}</span>
|
||
<Icon
|
||
icon={expandedSlug === k._slug ? "lucide:chevron-up" : "lucide:chevron-down"}
|
||
class="size-3.5 shrink-0 mt-0.5 text-stein-400"
|
||
/>
|
||
</button>
|
||
{#if expandedSlug === k._slug && k.text}
|
||
<div class="px-3 pb-3 pt-1 text-xs text-stein-700 leading-relaxed markdown prose prose-xs prose-zinc max-w-none border-t border-stein-100">
|
||
{@html k.text}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Stellungnahme-Generator (interne URL) -->
|
||
{#if area.stellungnahmeUrl}
|
||
<a
|
||
href={area.stellungnahmeUrl}
|
||
class="flex items-center gap-2 rounded-lg bg-wald-700 px-4 py-3 text-sm font-semibold text-white no-underline hover:bg-wald-800 transition-colors"
|
||
>
|
||
<Icon icon="lucide:file-pen" class="size-4 shrink-0" />
|
||
Einwendung erstellen
|
||
</a>
|
||
{/if}
|
||
|
||
<!-- Stellungnahme-Vorlage (Post) -->
|
||
{#if stellungnahmeHref(area.stellungnahme)}
|
||
<a
|
||
href={stellungnahmeHref(area.stellungnahme)}
|
||
class="flex items-center gap-2 rounded-lg border border-wald-300 bg-wald-50 px-4 py-3 text-sm font-medium text-wald-700 hover:bg-wald-100 transition-colors"
|
||
>
|
||
<Icon icon="lucide:file-pen" class="size-4 shrink-0" />
|
||
{t(T.windkarte_stellungnahme_link)}
|
||
</a>
|
||
{/if}
|
||
|
||
<!-- Actions -->
|
||
<div class="flex gap-2 pt-1">
|
||
<button
|
||
onclick={copyLink}
|
||
class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 hover:bg-stein-50 transition-colors whitespace-nowrap"
|
||
>
|
||
<Icon icon={copied ? "lucide:check" : "lucide:link"} class="size-3.5 shrink-0" />
|
||
{copied ? t(T.windkarte_action_copied) : t(T.windkarte_action_copy_link)}
|
||
</button>
|
||
{#if osmHref(center)}
|
||
<a
|
||
href={osmHref(center)}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 hover:bg-stein-50 transition-colors whitespace-nowrap"
|
||
>
|
||
<Icon icon="lucide:map" class="size-3.5 shrink-0" />
|
||
{t(T.windkarte_action_osm)}
|
||
</a>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Export-Downloads -->
|
||
<div>
|
||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-stein-500">{t(T.windkarte_label_geodaten_export, { nr: area.gebiets_nr })}</p>
|
||
<div class="flex gap-2">
|
||
<a
|
||
href="/api/vorranggebiete.geojson?id={encodeURIComponent(area.gebiets_nr)}"
|
||
download
|
||
title={`${t(T.windkarte_action_download_geojson)}\n${exportInfo}`}
|
||
class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 no-underline hover:bg-stein-50 transition-colors whitespace-nowrap"
|
||
>
|
||
<Icon icon="lucide:download" class="size-3.5 shrink-0" />
|
||
GeoJSON
|
||
</a>
|
||
<a
|
||
href="/api/vorranggebiete.kml?id={encodeURIComponent(area.gebiets_nr)}"
|
||
download
|
||
title={`${t(T.windkarte_action_download_kml)}\n${exportInfo}`}
|
||
class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 no-underline hover:bg-stein-50 transition-colors whitespace-nowrap"
|
||
>
|
||
<Icon icon="lucide:download" class="size-3.5 shrink-0" />
|
||
KML
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|