From e3924d43d831441d132aebad471b8c7703555c81 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 18 Feb 2026 17:36:38 +0300 Subject: [PATCH] feat(Section): add a styickyTitle feature and change the section layout --- src/shared/ui/Section/Section.svelte | 84 +++++++++++++++++++++------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/src/shared/ui/Section/Section.svelte b/src/shared/ui/Section/Section.svelte index 6e63940..9d0c7fd 100644 --- a/src/shared/ui/Section/Section.svelte +++ b/src/shared/ui/Section/Section.svelte @@ -56,13 +56,40 @@ interface Props extends Omit, 'title'> { /** * Snippet for the section content */ - children?: Snippet; + content?: Snippet<[{ className?: string }]>; + /** + * When true, the title stays fixed in view while + * scrolling through the section content. + */ + stickyTitle?: boolean; + /** + * Top offset for sticky title (e.g. header height). + * @default '0px' + */ + stickyOffset?: string; } -const { class: className, title, icon, description, index = 0, onTitleStatusChange, id, children }: Props = $props(); +const { + class: className, + title, + icon, + description, + index = 0, + onTitleStatusChange, + id, + content, + stickyTitle = false, + stickyOffset = '0px', +}: Props = $props(); let titleContainer = $state(); -const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }; +const flyParams: FlyParams = { + y: 0, + x: -50, + duration: 300, + easing: cubicOut, + opacity: 0.2, +}; // Track if the user has actually scrolled away from view let isScrolledPast = $state(false); @@ -72,18 +99,21 @@ $effect(() => { return; } let cleanup: ((index: number) => void) | undefined; - const observer = new IntersectionObserver(entries => { - const entry = entries[0]; - const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0; + const observer = new IntersectionObserver( + entries => { + const entry = entries[0]; + const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0; - if (isPast !== isScrolledPast) { - isScrolledPast = isPast; - cleanup = onTitleStatusChange?.(index, isPast, title, id); - } - }, { - // Set threshold to 0 to trigger exactly when the last pixel leaves - threshold: 0, - }); + if (isPast !== isScrolledPast) { + isScrolledPast = isPast; + cleanup = onTitleStatusChange?.(index, isPast, title, id); + } + }, + { + // Set threshold to 0 to trigger exactly when the last pixel leaves + threshold: 0, + }, + ); observer.observe(titleContainer); return () => { @@ -94,20 +124,32 @@ $effect(() => {
-
+
{#if icon} - {@render icon({ className: 'size-3 sm:size-4 stroke-foreground stroke-1 opacity-60' })} + {@render icon({ + className: 'size-3 sm:size-4 stroke-foreground stroke-1 opacity-60', +})}
{/if} + {#if description} {#snippet render({ class: className })} @@ -129,5 +171,9 @@ $effect(() => { {/if}
- {@render children?.()} + {@render content?.({ + className: stickyTitle + ? 'row-start-2 col-start-2' + : 'row-start-2 col-start-2', +})}