feat(Section): add a styickyTitle feature and change the section layout
This commit is contained in:
@@ -56,13 +56,40 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, '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<HTMLElement>();
|
||||
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(() => {
|
||||
</script>
|
||||
|
||||
<section
|
||||
id={id}
|
||||
{id}
|
||||
class={cn(
|
||||
'flex flex-col',
|
||||
'col-span-2 grid grid-cols-subgrid',
|
||||
stickyTitle ? 'gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12' : 'grid-rows-[max-content_1fr]',
|
||||
className,
|
||||
)}
|
||||
in:fly={flyParams}
|
||||
out:fly={flyParams}
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:gap-3" bind:this={titleContainer}>
|
||||
<div
|
||||
bind:this={titleContainer}
|
||||
class={cn(
|
||||
'flex flex-col gap-2 sm:gap-3',
|
||||
stickyTitle && 'self-start',
|
||||
)}
|
||||
style:position={stickyTitle ? 'sticky' : undefined}
|
||||
style:top={stickyTitle ? stickyOffset : undefined}
|
||||
>
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
{#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',
|
||||
})}
|
||||
<div class="w-px h-2.5 sm:h-3 bg-border-subtle"></div>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<Footnote>
|
||||
{#snippet render({ class: className })}
|
||||
@@ -129,5 +171,9 @@ $effect(() => {
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{@render children?.()}
|
||||
{@render content?.({
|
||||
className: stickyTitle
|
||||
? 'row-start-2 col-start-2'
|
||||
: 'row-start-2 col-start-2',
|
||||
})}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user