2026-01-18 12:57:56 +03:00
|
|
|
<!--
|
|
|
|
|
Component: FontApplicator
|
|
|
|
|
Loads fonts from fontshare with link tag
|
2026-01-18 15:55:07 +03:00
|
|
|
- Loads font only if it's not already applied
|
|
|
|
|
- Uses IntersectionObserver to detect when font is visible
|
2026-01-18 16:56:53 +03:00
|
|
|
- Adds smooth transition when font appears
|
2026-01-18 12:57:56 +03:00
|
|
|
-->
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
|
|
|
import type { Snippet } from 'svelte';
|
2026-01-22 15:33:38 +03:00
|
|
|
import { prefersReducedMotion } from 'svelte/motion';
|
2026-01-18 15:55:07 +03:00
|
|
|
import { appliedFontsManager } from '../../model';
|
2026-01-18 12:57:56 +03:00
|
|
|
|
|
|
|
|
interface Props {
|
2026-01-18 15:55:07 +03:00
|
|
|
/**
|
|
|
|
|
* Font name to set
|
|
|
|
|
*/
|
2026-01-18 12:57:56 +03:00
|
|
|
name: string;
|
2026-01-18 15:55:07 +03:00
|
|
|
/**
|
|
|
|
|
* Font id to load
|
|
|
|
|
*/
|
2026-01-18 12:57:56 +03:00
|
|
|
id: string;
|
2026-02-02 12:10:38 +03:00
|
|
|
|
|
|
|
|
url: string;
|
2026-01-20 14:21:07 +03:00
|
|
|
/**
|
|
|
|
|
* Font weight
|
|
|
|
|
*/
|
|
|
|
|
weight?: number;
|
2026-02-05 11:44:16 +03:00
|
|
|
|
|
|
|
|
isVariable?: boolean;
|
2026-01-18 15:55:07 +03:00
|
|
|
/**
|
|
|
|
|
* Additional classes
|
|
|
|
|
*/
|
2026-01-18 12:57:56 +03:00
|
|
|
className?: string;
|
2026-01-18 15:55:07 +03:00
|
|
|
/**
|
|
|
|
|
* Children
|
|
|
|
|
*/
|
2026-01-18 12:57:56 +03:00
|
|
|
children?: Snippet;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 11:44:16 +03:00
|
|
|
let { name, id, url, weight = 400, isVariable = false, className, children }: Props = $props();
|
2026-01-18 12:57:56 +03:00
|
|
|
let element: Element;
|
|
|
|
|
|
2026-01-18 16:56:53 +03:00
|
|
|
// Track if the user has actually scrolled this into view
|
|
|
|
|
let hasEnteredViewport = $state(false);
|
2026-02-05 11:44:16 +03:00
|
|
|
const status = $derived(appliedFontsManager.getFontStatus(id, weight, isVariable));
|
2026-01-18 16:56:53 +03:00
|
|
|
|
2026-01-18 12:57:56 +03:00
|
|
|
$effect(() => {
|
2026-02-05 11:44:16 +03:00
|
|
|
if (status === 'loaded' || status === 'error') {
|
|
|
|
|
hasEnteredViewport = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:57:56 +03:00
|
|
|
const observer = new IntersectionObserver(entries => {
|
|
|
|
|
if (entries[0].isIntersecting) {
|
2026-01-18 16:56:53 +03:00
|
|
|
hasEnteredViewport = true;
|
|
|
|
|
|
2026-02-05 11:44:16 +03:00
|
|
|
// Touch ensures it's in the queue.
|
|
|
|
|
// It's safe to call this even if VirtualList called it
|
|
|
|
|
// (Manager dedupes based on key)
|
|
|
|
|
appliedFontsManager.touch([{
|
|
|
|
|
id,
|
|
|
|
|
weight,
|
|
|
|
|
name,
|
|
|
|
|
url,
|
|
|
|
|
isVariable,
|
|
|
|
|
}]);
|
|
|
|
|
|
2026-01-18 16:56:53 +03:00
|
|
|
observer.unobserve(element);
|
2026-01-18 12:57:56 +03:00
|
|
|
}
|
|
|
|
|
});
|
2026-02-05 11:44:16 +03:00
|
|
|
|
|
|
|
|
if (element) observer.observe(element);
|
2026-01-18 12:57:56 +03:00
|
|
|
return () => observer.disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-18 16:56:53 +03:00
|
|
|
// The "Show" condition: Element is in view AND (Font is ready OR it errored out)
|
|
|
|
|
const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error'));
|
|
|
|
|
|
|
|
|
|
const transitionClasses = $derived(
|
2026-01-22 15:33:38 +03:00
|
|
|
prefersReducedMotion.current
|
2026-01-18 16:56:53 +03:00
|
|
|
? 'transition-none' // Disable CSS transitions if motion is reduced
|
|
|
|
|
: 'transition-all duration-700 ease-[cubic-bezier(0.22,1,0.36,1)]',
|
|
|
|
|
);
|
2026-01-18 12:57:56 +03:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
bind:this={element}
|
2026-02-05 11:44:16 +03:00
|
|
|
style:font-family={shouldReveal ? `'${name}'` : 'sans-serif'}
|
2026-01-18 12:57:56 +03:00
|
|
|
class={cn(
|
2026-01-18 16:56:53 +03:00
|
|
|
transitionClasses,
|
|
|
|
|
// If reduced motion is on, we skip the transform/blur entirely
|
2026-01-22 15:33:38 +03:00
|
|
|
!shouldReveal && !prefersReducedMotion.current
|
|
|
|
|
&& 'opacity-0 translate-y-8 scale-[0.98] blur-sm',
|
|
|
|
|
!shouldReveal && prefersReducedMotion.current && 'opacity-0', // Still hide until font is ready, but no movement
|
2026-01-18 16:56:53 +03:00
|
|
|
shouldReveal && 'opacity-100 translate-y-0 scale-100 blur-0',
|
2026-01-18 12:57:56 +03:00
|
|
|
className,
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{@render children?.()}
|
|
|
|
|
</div>
|