feat(footer,org): Footer-Redesign + OrganisationCard auf InfoCard-Basis

Footer:
- Newsletter 2-spaltig (Titel/Desc links, Formular rechts), Desc als
  Translation-Key newsletter_inline_desc
- Link-Spalten-Überschriften als kleine Label-Kappen, Links ohne Unterstrich
- Bottom-Bar: Logo + Site-Name (data.siteName) links, Copyright/Lizenz rechts
- Site-Name-Default "Windwiderstand Thüringen" (constants.ts)

OrganisationCard:
- Neue Komponente auf InfoCard-Basis; InfoCard um href/media/badge/footer-Slots
  erweitert
- OrganisationsBlock: 4 divergierende Kartenpfade → 1 OrganisationCard
- Story + CTA-Button (mitmachen) size md + Icon

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-12 11:59:36 +02:00
parent 5a1a49a97b
commit 57f4b226f9
11 changed files with 328 additions and 184 deletions
+61 -10
View File
@@ -1,13 +1,21 @@
<script lang="ts">
/**
* InfoCard — Icon/Bild + Titel + Beschreibung + optionaler Link.
* Wiederverwendbare Kachel für Sektions-Grids (siehe SectionGrid).
* Wiederverwendbare Kachel für Sektions-Grids (siehe SectionGrid) UND als
* Basis für spezialisierte Karten (OrganisationCard) via Snippet-Slots.
*
* `icon` ist entweder ein Iconify-String ("mdi:email") ODER eine Bild-URL
* (https://… oder /…) — wird automatisch erkannt.
* `tone`: default (weiß) oder green (grün getönt, z.B. Aside-Boxen).
* `compact`: kleinere Maße + Icon inline neben dem Titel (spart Höhe).
*
* Erweiterungspunkte (halten die Basis konsistent, ohne Besonderheiten zu verlieren):
* - `href` → ganze Karte wird Link (Tile-Hover wie Card).
* - `media` → ersetzt das Standard-Icon-Badge (z.B. quadratisches Logo + Fallback).
* - `badge` → absolut oben-rechts overlay.
* - `footer` → am unteren Rand (z.B. Social-Chips).
*/
import type { Snippet } from "svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import RustyImage from "$lib/components/RustyImage.svelte";
@@ -24,6 +32,12 @@
external = false,
tone = "default",
compact = false,
href = undefined,
target = undefined,
rel = undefined,
media = undefined,
badge = undefined,
footer = undefined,
}: {
icon?: string;
title: string;
@@ -33,10 +47,25 @@
external?: boolean;
tone?: Tone;
compact?: boolean;
href?: string;
target?: string;
rel?: string;
media?: Snippet;
badge?: Snippet;
footer?: Snippet;
} = $props();
const isImage = $derived(!!icon && (/^https?:\/\//.test(icon) || icon.startsWith("/")));
const hasLink = $derived(!!linkLabel && !!linkHref);
const shell = $derived(
`relative flex h-full flex-col rounded-lg border ${compact ? "gap-2 p-4" : "gap-[14px] p-[22px]"} ${
tone === "green" ? "border-wald-100 bg-wald-50" : "border-stein-200 bg-white"
}`,
);
/* Klickbare Karte: Tile-Hover identisch zu Card.svelte (variant=tile). */
const linkCls =
"transition-[transform,box-shadow,border-color] duration-200 no-underline text-inherit hover:-translate-y-0.5 hover:shadow-md hover:border-wald-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500 focus-visible:ring-offset-2";
</script>
{#snippet iconBadge(sizeClass: string, iconSize: string)}
@@ -55,16 +84,18 @@
</div>
{/snippet}
<div
class="flex h-full flex-col rounded-lg border {compact ? 'gap-2 p-4' : 'gap-[14px] p-[22px]'} {tone ===
'green'
? 'border-wald-100 bg-wald-50'
: 'border-stein-200 bg-white'}"
>
{#snippet content()}
{#if badge}
<div class="absolute right-2 top-2 z-10">{@render badge()}</div>
{/if}
{#if compact}
<!-- Kompakt: Icon hängt links, Text-Spalte (Titel/Desc/Link) rechts bündig. -->
<div class="flex gap-2.5">
{#if icon}<div class="pt-px">{@render iconBadge("size-5", "size-3")}</div>{/if}
{#if media}
<div class="pt-px">{@render media()}</div>
{:else if icon}
<div class="pt-px">{@render iconBadge("size-5", "size-3")}</div>
{/if}
<div class="flex min-w-0 flex-1 flex-col gap-2">
<h3 class="text-sm! font-bold! leading-[1.3]! tracking-[-0.01em] text-stein-800">{title}</h3>
{#if description}
@@ -75,10 +106,17 @@
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
</div>
{/if}
{#if footer}
<div class="pt-0.5">{@render footer()}</div>
{/if}
</div>
</div>
{:else}
{#if icon}{@render iconBadge("size-11", "size-[23px]")}{/if}
{#if media}
{@render media()}
{:else if icon}
{@render iconBadge("size-11", "size-[23px]")}
{/if}
<div class="flex flex-col gap-1">
<h3 class="text-base! font-bold! leading-[1.3]! tracking-[-0.01em] text-stein-800">{title}</h3>
{#if description}
@@ -90,5 +128,18 @@
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
</div>
{/if}
{#if footer}
<div class="mt-auto pt-1">{@render footer()}</div>
{/if}
{/if}
</div>
{/snippet}
{#if href}
<a {href} {target} {rel} class="{shell} {linkCls}">
{@render content()}
</a>
{:else}
<div class={shell}>
{@render content()}
</div>
{/if}