feat(Section): component redesign
This commit is contained in:
@@ -11,7 +11,9 @@ import {
|
|||||||
type FlyParams,
|
type FlyParams,
|
||||||
fly,
|
fly,
|
||||||
} from 'svelte/transition';
|
} from 'svelte/transition';
|
||||||
import { Footnote } from '..';
|
|
||||||
|
import SectionHeader from './SectionHeader/SectionHeader.svelte';
|
||||||
|
import SectionTitle from './SectionTitle/SectionTitle.svelte';
|
||||||
|
|
||||||
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
||||||
/**
|
/**
|
||||||
@@ -23,17 +25,15 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|||||||
*/
|
*/
|
||||||
class?: string;
|
class?: string;
|
||||||
/**
|
/**
|
||||||
* Snippet for a title itself
|
* Title of the section
|
||||||
*/
|
*/
|
||||||
title?: Snippet<[{ className?: string }]>;
|
title: string;
|
||||||
/**
|
|
||||||
* Snippet for a title icon
|
|
||||||
*/
|
|
||||||
icon?: Snippet<[{ className?: string }]>;
|
|
||||||
/**
|
/**
|
||||||
* Snippet for a title description
|
* Snippet for a title description
|
||||||
*/
|
*/
|
||||||
description?: Snippet<[{ className?: string }]>;
|
description?: Snippet<[{ className?: string }]>;
|
||||||
|
headerTitle?: string;
|
||||||
|
headerSubtitle?: string;
|
||||||
/**
|
/**
|
||||||
* Index of the section
|
* Index of the section
|
||||||
*/
|
*/
|
||||||
@@ -57,32 +57,20 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|||||||
* Snippet for the section content
|
* Snippet for the section content
|
||||||
*/
|
*/
|
||||||
content?: Snippet<[{ className?: string }]>;
|
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 {
|
const {
|
||||||
class: className,
|
class: className,
|
||||||
title,
|
title,
|
||||||
icon,
|
headerTitle,
|
||||||
|
headerSubtitle,
|
||||||
description,
|
description,
|
||||||
index = 0,
|
index = 0,
|
||||||
onTitleStatusChange,
|
onTitleStatusChange,
|
||||||
id,
|
id,
|
||||||
content,
|
content,
|
||||||
stickyTitle = false,
|
|
||||||
stickyOffset = '0px',
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let titleContainer = $state<HTMLElement>();
|
|
||||||
const flyParams: FlyParams = {
|
const flyParams: FlyParams = {
|
||||||
y: 0,
|
y: 0,
|
||||||
x: -50,
|
x: -50,
|
||||||
@@ -90,90 +78,19 @@ const flyParams: FlyParams = {
|
|||||||
easing: cubicOut,
|
easing: cubicOut,
|
||||||
opacity: 0.2,
|
opacity: 0.2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track if the user has actually scrolled away from view
|
|
||||||
let isScrolledPast = $state(false);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!titleContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let cleanup: ((index: number) => void) | undefined;
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(titleContainer);
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
cleanup?.(index);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
{id}
|
{id}
|
||||||
class={cn(
|
class="w-full max-w-7xl mx-auto px-4 md:px-6 pb-32 md:pb-48"
|
||||||
'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}
|
in:fly={flyParams}
|
||||||
out:fly={flyParams}
|
out:fly={flyParams}
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
bind:this={titleContainer}
|
{#if headerTitle}
|
||||||
class={cn(
|
<SectionHeader title={headerTitle} subtitle={headerSubtitle} index={index} />
|
||||||
'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',
|
|
||||||
})}
|
|
||||||
<div class="w-px h-2.5 sm:h-3 bg-border-subtle"></div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if description}
|
|
||||||
<Footnote>
|
|
||||||
{#snippet render({ class: className })}
|
|
||||||
{@render description({ className })}
|
|
||||||
{/snippet}
|
|
||||||
</Footnote>
|
|
||||||
{:else if typeof index === 'number'}
|
|
||||||
<Footnote>
|
|
||||||
Component_{String(index).padStart(3, '0')}
|
|
||||||
</Footnote>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<SectionTitle text={title} />
|
||||||
</div>
|
</div>
|
||||||
|
{@render content?.({})}
|
||||||
{#if title}
|
|
||||||
{@render title({
|
|
||||||
className:
|
|
||||||
'text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-semibold tracking-tighter text-foreground leading-[0.9]',
|
|
||||||
})}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{@render content?.({
|
|
||||||
className: stickyTitle
|
|
||||||
? 'row-start-2 col-start-2'
|
|
||||||
: 'row-start-2 col-start-2',
|
|
||||||
})}
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user