feat(home): Featured-Post-Layout, Termine-Look, Zitat-Sektion, Typo-Regelwerk
Deploy / verify (push) Successful in 1m31s
Deploy / deploy (push) Successful in 2m10s

- PostOverview: neues design "featured" (1 großer Post links, Rest kompakt
  rechts als PostCardCompact – Bild links, Text rechts, keine Nested-Links)
- CalendarCompact: Termine als Liste mit Datum-Badge + Kicker-Headline
- QuoteCarousel: dunkle Full-Bleed-Sektion, Lora-Zitat mit «», Author-Split,
  Kicker mit Sektionsnummer, Pill-Pagination + Pfeile/Swipe
- Typo-Regelwerk: Lora statt Aleo (Zitate), Headings semibold (600),
  H2/H3-Skala, "Weiterlesen"-Link-Muster, docs/typography.md
- HeadlineBlock: \n → <br> für bewusste Umbrüche
- Layout: neues spaceTop-Feld (mt-*), analog spaceBottom
- OrganisationsBlock: CTA volle Breite, Button rechts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-09 16:40:05 +02:00
parent 91e66d8d4f
commit 289cdb3951
13 changed files with 460 additions and 95 deletions
@@ -41,7 +41,7 @@
return d.toLocaleDateString("de-DE", {
weekday: "short",
day: "2-digit",
month: "short",
month: "long",
});
}
function fmtTime(d: Date): string {
@@ -51,6 +51,18 @@
return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
}
/** Titel + optional nachgestellte "(…)"-Klammer separat (Klammer gedimmt wie im Design). */
function splitTitle(raw: string): { main: string; paren: string } {
const m = raw.match(/^(.*?)\s*(\([^()]*\))\s*$/);
if (m) return { main: m[1], paren: m[2] };
return { main: raw, paren: "" };
}
function locationText(it: CalendarItemData): string {
if (!it.location) return "";
return typeof it.location === "string" ? it.location : ((it.location as { text?: string })?.text ?? "");
}
const linkHref = $derived(() => {
const lt = block.linkTo;
if (!lt) return null;
@@ -62,50 +74,60 @@
</script>
<div
class="not-prose {layoutClasses} w-full min-w-0 card-surface overflow-hidden rounded-sm border border-stein-200 bg-gradient-to-br from-wald-50/60 via-white to-white"
class="not-prose {layoutClasses} w-full min-w-0"
data-block="CalendarCompact"
data-block-type="calendar"
data-block-variant="compact"
data-block-slug={block._slug}
>
<header class="flex items-center gap-2 border-b border-stein-100 px-3 py-2">
<Icon icon="lucide:calendar" class="size-8 shrink-0 text-stein-900" aria-hidden="true" />
<h2 class="text-sm font-semibold leading-none text-stein-800">
{block.title ?? t(T.calendar_next_events)}
</h2>
</header>
<div class="mb-2 flex items-baseline justify-between gap-4">
<h3 class="mb-0">{block.title ?? t(T.calendar_next_events)}</h3>
{#if linkHref()}
<a
href={linkHref()}
class="inline-flex shrink-0 items-center gap-1 text-sm font-medium text-wald-700 no-underline hover:text-wald-800 hover:underline"
>
{t(T.calendar_all_upcoming)}
<span aria-hidden="true"></span>
</a>
{/if}
</div>
{#if upcoming.length === 0}
<p class="px-3 py-4 text-center text-xs text-stein-500">
<p class="rounded-sm border border-stein-200 px-3 py-6 text-center text-sm text-stein-500">
{t(T.calendar_no_future_events)}
</p>
{:else}
<ul class="divide-y divide-stein-100">
<ul class="divide-y divide-stein-100 overflow-hidden rounded-sm border border-stein-200 bg-white">
{#each upcoming as { it, start } (it._slug ?? it.title)}
{@const timeStr = fmtTime(start)}
{@const loc = locationText(it)}
{@const title = splitTitle(it.title ?? t(T.calendar_untitled))}
{@const base = linkHref()}
{@const itemHref = base && it._slug ? `${base}#event-${it._slug}` : null}
<li>
<svelte:element
this={itemHref ? "a" : "div"}
href={itemHref ?? undefined}
class="flex items-start gap-2.5 px-3 py-2 {itemHref ? 'transition-colors hover:bg-wald-50/40' : ''}"
class="flex items-start gap-3 px-4 py-3 no-underline {itemHref ? 'transition-colors hover:bg-stein-50' : ''}"
>
<div class="flex w-11 shrink-0 flex-col items-center rounded-sm bg-wald-50 px-1 py-1 text-wald-700">
<span class="text-[0.6rem] font-semibold uppercase leading-none">
<div class="flex w-12 shrink-0 flex-col items-center rounded-sm bg-stein-800 px-1 py-1.5 text-white">
<span class="text-[0.6rem] font-semibold uppercase leading-none tracking-wide">
{start.toLocaleDateString("de-DE", { month: "short" }).replace(".", "")}
</span>
<span class="text-base font-bold tabular-nums leading-none mt-0.5">
<span class="mt-0.5 text-lg font-bold tabular-nums leading-none">
{start.getDate()}
</span>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-xs font-semibold text-stein-800">{it.title ?? t(T.calendar_untitled)}</p>
<p class="mt-0.5 text-[0.7rem] text-stein-500">
{fmtDate(start)}{timeStr ? ` · ${timeStr}` : ""}
{#if it.location}
<span class="ml-1">· {typeof it.location === "string" ? it.location : (it.location as { text?: string })?.text ?? ""}</span>
{/if}
<p class="text-sm font-semibold leading-snug text-stein-900">
{title.main}{#if title.paren}{" "}<span class="font-normal text-stein-400">{title.paren}</span>{/if}
</p>
<p class="mt-1 flex items-center gap-1.5 text-xs text-stein-500">
<Icon icon="lucide:clock" class="size-3.5 shrink-0" aria-hidden="true" />
<span class="min-w-0 truncate">
{fmtDate(start)}{timeStr ? ` · ${timeStr}` : ""}{loc ? ` · ${loc}` : ""}
</span>
</p>
</div>
</svelte:element>
@@ -113,14 +135,4 @@
{/each}
</ul>
{/if}
{#if linkHref()}
<a
href={linkHref()}
class="flex items-center justify-center gap-1 border-t border-stein-100 bg-stein-50 px-3 py-1.5 text-xs font-medium text-wald-700 hover:bg-wald-50"
>
{t(T.calendar_all_upcoming)}
<Icon icon="lucide:arrow-right" class="size-3.5" aria-hidden="true" />
</a>
{/if}
</div>