275f66fb8f
- 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>
146 lines
4.8 KiB
Svelte
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}
|