Compare commits

...

15 Commits

Author SHA1 Message Date
Peter Meier cd362fb811 windkarte: Rechtskräftig default hidden, remove Entwurf-2-voraussichtlich, 2km/5km rings on by default
Deploy / verify (push) Successful in 58s
Deploy / deploy (push) Successful in 1m6s
- Hide status badge for rechtskraeftig in WindAreaPanel
- Default hiddenStatuses=["rechtskraeftig"] so polygons start hidden
- Fix: call restyleAll after geojsonLayer init so initial hidden state applies
- Default hiddenRings=[] so 2km+5km buffer rings show on selection
- Remove entwurf_2_voraussichtlich from legend
- Lower maxZoom 12→11 for more context on feature select

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:26:19 +02:00
Peter Meier 4a72142555 WindAreaPanel: compact key-value rows instead of cards
Label left, value right, divided rows. No more padded bg cards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:18:38 +02:00
Peter Meier 823adde1fa WindAreaPanel: stats single column layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:18:06 +02:00
Peter Meier f55f409158 WindAreaPanel: render notizen as HTML with prose styles
Was output as escaped plain text. Field contains HTML from the CMS editor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:16:46 +02:00
Peter Meier 250865893e FilesBlock: icon size-4 → size-5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:46:52 +02:00
Peter Meier 07b6317b94 FilesBlock: mt-0.5 on icon for top alignment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:46:38 +02:00
Peter Meier 07f17e880e FilesBlock: top-align icon with text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:45:48 +02:00
Peter Meier baa4b9aae4 FilesBlock: allow description to wrap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:45:17 +02:00
Peter Meier 61e01c127f FilesBlock: size as inline badge after filename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:44:46 +02:00
Peter Meier 887a6bda3e FilesBlock: remove default link underline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:43:57 +02:00
Peter Meier 774ac893ec FilesBlock: description on second line below filename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:43:10 +02:00
Peter Meier 1284be9c42 FilesBlock: size label smaller + no-wrap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:42:32 +02:00
Peter Meier 325f95487f FilesBlock: move size under icon, drop redundant ext label
Icon already communicates file type. Size now sits below the icon
in a fixed-width left column. Ext text removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:41:40 +02:00
Peter Meier 1825b7d9fe FilesBlock: compact single-row layout
Reduce padding, smaller icon, filename + ext/size on one line.
Description hidden on mobile, truncated on desktop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:39:09 +02:00
Peter Meier 4b9214dbaf Fix FilesBlock: external URLs bypass /cms-files proxy
getCmsFileUrl() returned /cms-files?src=... for all URLs including
external ones. resolveCmsSource() rejects foreign hosts (SSRF guard),
so external file links 404'd. External http(s) URLs now returned directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:36:43 +02:00
5 changed files with 41 additions and 35 deletions
+15 -15
View File
@@ -54,7 +54,7 @@
<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}
{#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>
@@ -76,29 +76,29 @@
<!-- Body -->
<div class="flex-1 space-y-5 px-5 py-4 text-sm">
<!-- Kenndaten -->
<div class="grid grid-cols-2 gap-3">
<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="rounded-lg bg-stein-50 p-3">
<p class="text-xs text-stein-500">{t(T.windkarte_label_flaeche)}</p>
<p class="mt-0.5 font-semibold text-stein-800">{area.flaeche_ha.toFixed(1)} ha</p>
<div class="flex items-center justify-between px-3 py-1.5">
<span class="text-xs text-stein-500">{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="rounded-lg bg-stein-50 p-3">
<p class="text-xs text-stein-500">{t(T.windkarte_label_anlagen_geplant)}</p>
<p class="mt-0.5 font-semibold text-stein-800">{area.anlagen_geplant}</p>
<div class="flex items-center justify-between px-3 py-1.5">
<span class="text-xs text-stein-500">{t(T.windkarte_label_anlagen_geplant)}</span>
<span class="font-semibold text-stein-800">{area.anlagen_geplant}</span>
</div>
{/if}
{#if area.hoehe_max_m != null}
<div class="rounded-lg bg-stein-50 p-3">
<p class="text-xs text-stein-500">{t(T.windkarte_label_hoehe_max)}</p>
<p class="mt-0.5 font-semibold text-stein-800">{area.hoehe_max_m} m</p>
<div class="flex items-center justify-between px-3 py-1.5">
<span class="text-xs text-stein-500">{t(T.windkarte_label_hoehe_max)}</span>
<span class="font-semibold text-stein-800">{area.hoehe_max_m} m</span>
</div>
{/if}
{#if area.investor}
<div class="rounded-lg bg-stein-50 p-3 col-span-2">
<p class="text-xs text-stein-500">{t(T.windkarte_label_investor)}</p>
<p class="mt-0.5 font-semibold text-stein-800">{area.investor}</p>
<div class="flex items-center justify-between px-3 py-1.5">
<span class="text-xs text-stein-500">{t(T.windkarte_label_investor)}</span>
<span class="font-semibold text-stein-800">{area.investor}</span>
</div>
{/if}
</div>
@@ -119,7 +119,7 @@
{#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>
<p class="text-stein-700 leading-relaxed">{area.notizen}</p>
<div class="text-stein-700 leading-relaxed markdown prose prose-sm prose-zinc max-w-none">{@html area.notizen}</div>
</div>
{/if}
+5 -3
View File
@@ -9,7 +9,7 @@
onready = null,
initialGebietsNr = null,
hiddenStatuses = [],
hiddenRings = ["2km", "5km"],
hiddenRings = [],
}: {
areas: WindArea[];
onselect: (area: WindArea | null, center: [number, number] | null) => void;
@@ -52,7 +52,7 @@
let selectedSlug = $state<string | null>(null);
let geojsonLayer: import("leaflet").GeoJSON | null = null;
let buffering = $state(false);
let _hiddenRings: string[] = ["2km", "5km"];
let _hiddenRings: string[] = [];
$effect(() => {
_hiddenRings = hiddenRings;
@@ -164,7 +164,7 @@
map!.fitBounds((layer as import("leaflet").Polygon).getBounds(), {
paddingTopLeft: [40, 40],
paddingBottomRight: [desktop ? 320 : 40, desktop ? 40 : 300],
maxZoom: 12,
maxZoom: 11,
});
}
}
@@ -278,6 +278,8 @@
},
}).addTo(map);
restyleAll(selectedSlug);
// Labels: show from zoom 13, scale font proportionally with zoom
const syncLabels = () => {
if (!labelLayer || !map) return;
+12 -12
View File
@@ -116,26 +116,26 @@
{#if resolved.length === 0}
<p class="text-sm text-stein-500">Keine Dateien verfügbar.</p>
{:else}
<ul class="flex flex-col gap-2 list-none! p-0 m-0! ml-0">
<ul class="flex flex-col gap-1 list-none! p-0 m-0! ml-0">
{#each resolved as f}
<li class="list-none! ml-0">
<a
href={f.href}
class="group flex items-start gap-3 rounded-xs border border-stein-200 bg-white px-3 py-2 hover:border-stein-400 hover:bg-stein-50 transition-colors"
class="group flex items-start gap-2 rounded-xs border border-stein-200 bg-white px-2.5 py-1.5 hover:border-stein-400 hover:bg-stein-50 transition-colors no-underline"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={f.iconName} class="size-6 shrink-0 text-stein-600" aria-hidden="true" />
<span class="flex flex-col min-w-0">
<span class="font-medium text-stein-900 group-hover:underline truncate">{f.label}</span>
{#if f.description}
<span class="text-sm text-stein-600">{f.description}</span>
{/if}
<span class="text-xs text-stein-500">
{#if f.ext}<span class="uppercase">{f.ext}</span>{/if}
{#if f.ext && f.sizeLabel} · {/if}
{#if f.sizeLabel}{f.sizeLabel}{/if}
<Icon icon={f.iconName} class="size-5 shrink-0 text-stein-500 mt-0.5" aria-hidden="true" />
<span class="flex-1 min-w-0 flex flex-col">
<span class="flex items-center gap-1.5 min-w-0">
<span class="text-sm text-stein-900 group-hover:underline truncate">{f.label}</span>
{#if f.sizeLabel}
<span class="shrink-0 text-[.6rem] tabular-nums whitespace-nowrap bg-stein-100 text-stein-500 px-1 py-px rounded-xs">{f.sizeLabel}</span>
{/if}
</span>
{#if f.description}
<span class="text-xs text-stein-400">{f.description}</span>
{/if}
</span>
</a>
</li>
@@ -24,8 +24,8 @@ let areas = $state<WindArea[]>([]);
let mapReady = $state(false);
let initialGebietsNr = $state<string | null>(null);
let resetMap: (() => void) | null = null;
let hiddenStatuses = $state<string[]>([]);
let hiddenRings = $state<string[]>(["2km", "5km"]);
let hiddenStatuses = $state<string[]>(["rechtskraeftig"]);
let hiddenRings = $state<string[]>([]);
let mapActive = $state(false);
function toggleStatus(status: string) {
@@ -171,9 +171,8 @@ let areas = $state<WindArea[]>([]);
<!-- Legend -->
<div class="mt-2 flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-stein-400">
{#each [
{ status: "rechtskraeftig", color: "#2d7a45", label: t(T.windkarte_legend_rechtskraeftig) },
{ status: "entwurf_2", color: "#b08a52", label: t(T.windkarte_legend_entwurf_2) },
{ status: "entwurf_2_voraussichtlich", color: "#d4752a", label: t(T.windkarte_legend_entwurf_2_voraussichtlich) },
{ status: "rechtskraeftig", color: "#2d7a45", label: t(T.windkarte_legend_rechtskraeftig) },
{ status: "entwurf_2", color: "#b08a52", label: t(T.windkarte_legend_entwurf_2) },
] as item}
<button
type="button"
+5
View File
@@ -62,8 +62,13 @@ export interface CmsFileUrlOptions {
/**
* Lokale Datei-URL: /cms-files?src=…&name=…&dl=…
* `src` = volle Datei-URL oder CMS-Asset-Pfad (`/api/assets/...`).
* Externe http(s)-URLs werden direkt zurückgegeben — der /cms-files-Proxy
* würde sie wegen SSRF-Schutz sowieso ablehnen.
*/
export function getCmsFileUrl(src: string, opts: CmsFileUrlOptions = {}): string {
if (src.startsWith('http://') || src.startsWith('https://')) {
return src;
}
const params = new URLSearchParams();
params.set('src', src);
if (opts.filename) params.set('name', opts.filename);