feat(carousel): 2 cols tablet, 3 cols desktop; equal-height cards
Deploy / verify (push) Successful in 54s
Deploy / deploy (push) Successful in 1m4s

- PostOverviewBlock carousel: md:2 cols, lg:3 cols via snap-scroll
- PostCard: add class prop for external styling
- Carousel cards: h-full for equal height

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-16 14:54:36 +02:00
parent 41dfbb2930
commit 19f3948f48
2 changed files with 48 additions and 12 deletions
+3 -1
View File
@@ -22,12 +22,14 @@
href,
tagBase = "/posts/tag",
commentCount = null,
class: className = "",
}: {
post: PostWithImage;
href: string;
tagBase?: string;
/** Approved comment count. `null` = unknown/loading, hide badge. `0` = also hide. */
commentCount?: number | null;
class?: string;
} = $props();
const rawImg = $derived(post._rawImageUrl);
@@ -52,7 +54,7 @@
<Card
{href}
layout="post"
class={isPastEvent ? "opacity-70 grayscale" : ""}
class="{isPastEvent ? 'opacity-70 grayscale' : ''} {className}"
>
<div class="relative aspect-3/2 w-full overflow-hidden bg-stein-100">
{#if rawImg}
@@ -1,7 +1,8 @@
<script lang="ts">
import { marked } from "$lib/markdown-safe";
import PostCard from "../PostCard.svelte";
import Carousel from "$lib/components/Carousel.svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { PostOverviewBlockData } from "$lib/block-types";
import type { PostEntry } from "$lib/cms";
@@ -21,6 +22,13 @@
const posts = $derived((block.postsResolved ?? []) as (PostEntry & { _resolvedImageUrl?: string })[]);
const design = $derived(block.design || "cards");
const tagBase = "/posts/tag";
let scroller: HTMLDivElement | undefined = $state();
function scrollByPage(dir: 1 | -1) {
if (!scroller) return;
scroller.scrollBy({ left: scroller.clientWidth * dir * 0.9, behavior: "smooth" });
}
// Hide the entire block (headline, intro, footer link) when there are no
// posts and no intro text — otherwise an empty section with just a heading
// sits on the page.
@@ -74,16 +82,42 @@
{/each}
</div>
{:else if design === "carousel" && posts.length > 0}
<Carousel count={posts.length} ariaLabel={block.headline ?? "Beiträge"}>
{#snippet item(i)}
<PostCard
post={posts[i]}
href={postHref(posts[i])}
tagBase={tagBase}
commentCount={commentCounts[postSlugForComments(posts[i])] ?? null}
/>
{/snippet}
</Carousel>
<div class="relative">
<div
bind:this={scroller}
class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory pb-2 [scrollbar-width:thin]"
>
{#each posts as post}
<div class="shrink-0 snap-start flex flex-col w-full md:w-[calc((100%-1rem)/2)] lg:w-[calc((100%-2rem)/3)]">
<PostCard
post={post}
href={postHref(post)}
tagBase={tagBase}
commentCount={commentCounts[postSlugForComments(post)] ?? null}
class="h-full"
/>
</div>
{/each}
</div>
{#if posts.length > 1}
<button
type="button"
onclick={() => scrollByPage(-1)}
aria-label="Zurück"
class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2 size-7 items-center justify-center rounded-full bg-white/90 border border-stein-200 shadow-sm hover:bg-white hover:border-wald-700 transition"
>
<Icon icon="mdi:chevron-left" class="size-4" aria-hidden="true" />
</button>
<button
type="button"
onclick={() => scrollByPage(1)}
aria-label="Weiter"
class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2 size-7 items-center justify-center rounded-full bg-white/90 border border-stein-200 shadow-sm hover:bg-white hover:border-wald-700 transition"
>
<Icon icon="mdi:chevron-right" class="size-4" aria-hidden="true" />
</button>
{/if}
</div>
{/if}
{#if posts.length > 0}