Files
windwiderstand/src/lib/components/InfoCard.svelte
T
Peter Meier 275f66fb8f
Deploy / verify (push) Successful in 1m31s
Deploy / deploy (push) Successful in 1m36s
CalendarBlock: ruhigere, editorialere UI-Überarbeitung
- Kopfleiste zusammengefasst: Suche-Pill + Themenfilter-Icon + Heute-Chip
- Monats-Grid neutralisiert: weisser Grund, Punkt-Marker statt Zahl-Badge
- Terminliste als Karten statt Zebra-Rows; Urgency in getönte Datums-Kachel
- Aktionsleiste ans Card-Ende verschoben, ruhige Text+Icon-Links

Enthält zusätzlich bestehende lokale Änderungen (Card, Header, ContactCard,
InfoCard, PostCardCompact, FilesBlock, app.css, Storybook-Fixtures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:12:44 +02:00

146 lines
4.8 KiB
Svelte

<script lang="ts">
/**
* InfoCard — Icon/Bild + Titel + Beschreibung + optionaler Link.
* 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";
import ArrowLink from "$lib/components/ArrowLink.svelte";
type Tone = "default" | "green";
let {
icon = undefined,
title,
description = undefined,
linkLabel = undefined,
linkHref = undefined,
external = false,
tone = "default",
compact = false,
href = undefined,
target = undefined,
rel = undefined,
media = undefined,
badge = undefined,
footer = undefined,
}: {
icon?: string;
title: string;
description?: string;
linkLabel?: string;
linkHref?: string;
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), s. card-hover-lift in app.css. */
const linkCls =
"card-hover-lift no-underline text-inherit 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)}
<div
class="flex shrink-0 items-center justify-center overflow-hidden rounded-full {sizeClass} {isImage
? 'border border-stein-200 bg-white'
: tone === 'green'
? 'bg-white text-wald-600'
: 'bg-wald-50 text-wald-500'}"
>
{#if isImage}
<RustyImage src={icon ?? ""} width={44} aspect="1/1" fit="contain" alt="" class="size-full object-contain p-0.5" />
{:else}
<Icon icon={icon ?? ""} class={iconSize} aria-hidden="true" />
{/if}
</div>
{/snippet}
{#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 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}
<p class="line-clamp-3 text-[13px] font-normal leading-[1.45] text-stein-500">{description}</p>
{/if}
{#if hasLink}
<div class="pt-0.5">
<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 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}
<p class="line-clamp-3 text-sm font-normal leading-[1.45] text-stein-500">{description}</p>
{/if}
</div>
{#if hasLink}
<div class="mt-auto pt-0.5">
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
</div>
{/if}
{#if footer}
<div class="mt-auto pt-1">{@render footer()}</div>
{/if}
{/if}
{/snippet}
{#if href}
<a {href} {target} {rel} class="{shell} {linkCls}">
{@render content()}
</a>
{:else}
<div class={shell}>
{@render content()}
</div>
{/if}