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, href,
tagBase = "/posts/tag", tagBase = "/posts/tag",
commentCount = null, commentCount = null,
class: className = "",
}: { }: {
post: PostWithImage; post: PostWithImage;
href: string; href: string;
tagBase?: string; tagBase?: string;
/** Approved comment count. `null` = unknown/loading, hide badge. `0` = also hide. */ /** Approved comment count. `null` = unknown/loading, hide badge. `0` = also hide. */
commentCount?: number | null; commentCount?: number | null;
class?: string;
} = $props(); } = $props();
const rawImg = $derived(post._rawImageUrl); const rawImg = $derived(post._rawImageUrl);
@@ -52,7 +54,7 @@
<Card <Card
{href} {href}
layout="post" 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"> <div class="relative aspect-3/2 w-full overflow-hidden bg-stein-100">
{#if rawImg} {#if rawImg}
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { marked } from "$lib/markdown-safe"; import { marked } from "$lib/markdown-safe";
import PostCard from "../PostCard.svelte"; 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 { getBlockLayoutClasses } from "$lib/block-layout";
import type { PostOverviewBlockData } from "$lib/block-types"; import type { PostOverviewBlockData } from "$lib/block-types";
import type { PostEntry } from "$lib/cms"; import type { PostEntry } from "$lib/cms";
@@ -21,6 +22,13 @@
const posts = $derived((block.postsResolved ?? []) as (PostEntry & { _resolvedImageUrl?: string })[]); const posts = $derived((block.postsResolved ?? []) as (PostEntry & { _resolvedImageUrl?: string })[]);
const design = $derived(block.design || "cards"); const design = $derived(block.design || "cards");
const tagBase = "/posts/tag"; 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 // 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 // posts and no intro text — otherwise an empty section with just a heading
// sits on the page. // sits on the page.
@@ -74,16 +82,42 @@
{/each} {/each}
</div> </div>
{:else if design === "carousel" && posts.length > 0} {:else if design === "carousel" && posts.length > 0}
<Carousel count={posts.length} ariaLabel={block.headline ?? "Beiträge"}> <div class="relative">
{#snippet item(i)} <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 <PostCard
post={posts[i]} post={post}
href={postHref(posts[i])} href={postHref(post)}
tagBase={tagBase} tagBase={tagBase}
commentCount={commentCounts[postSlugForComments(posts[i])] ?? null} commentCount={commentCounts[postSlugForComments(post)] ?? null}
class="h-full"
/> />
{/snippet} </div>
</Carousel> {/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}
{#if posts.length > 0} {#if posts.length > 0}