feat(page-carousel): optional leading snippet before first page

Consumers of PageCarousel can now pass a \`leading\` Snippet that
renders as the first flex child inside .fokus-track, ahead of the
page wrappers. Used on the workbench homepage for the scene header
(name + description). Scrolls with the track rather than sticking
in place — reads as an intro block, not app chrome, and doesn't
steal viewport from the cards on narrow screens.

Styled as flex-aligned, align-self:stretch so its intrinsic layout
decides the height and it centres vertically against the cards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 19:42:04 +02:00
parent 714c235798
commit 8f3ffefdf1

View file

@ -19,6 +19,11 @@
addLabel?: string;
page: Snippet<[CarouselPage, number]>;
picker?: Snippet;
/** Optional content rendered before the first page inside the same
* scroll track. Used for the scene header on the homepage. Scrolls
* with the pages (doesn't stay pinned) so it reads as an intro
* block rather than app chrome. */
leading?: Snippet;
}
let {
@ -33,6 +38,7 @@
addLabel = 'Hinzufügen',
page: pageSnippet,
picker,
leading,
}: Props = $props();
let pickerEl = $state<HTMLDivElement | null>(null);
@ -103,6 +109,9 @@
<div class="carousel-root">
<div class="fokus-track" style="--sheet-width: {defaultWidth}px" bind:this={trackEl}>
{#if leading}
<div class="leading-slot">{@render leading()}</div>
{/if}
{#each pages as p, idx (p.id)}
<div class="page-wrapper" role="listitem" data-page-id={p.id}>
{#if mountedIds.has(p.id)}
@ -163,6 +172,12 @@
.page-wrapper {
flex: 0 0 auto;
}
.leading-slot {
flex: 0 0 auto;
align-self: stretch;
display: flex;
align-items: center;
}
/* Sized stand-in for a not-yet-mounted card. Matches the card's
widthPx/heightPx so scroll position and the surrounding flex
layout stay stable while the IntersectionObserver decides which