Files
frontend-svelte/src/entities/Font/ui/FontApplicator/FontApplicator.svelte

104 lines
2.8 KiB
Svelte

<!--
Component: FontApplicator
Loads fonts from fontshare with link tag
- Loads font only if it's not already applied
- Uses IntersectionObserver to detect when font is visible
- Adds smooth transition when font appears
-->
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import type { Snippet } from 'svelte';
import { prefersReducedMotion } from 'svelte/motion';
import { appliedFontsManager } from '../../model';
interface Props {
/**
* Font name to set
*/
name: string;
/**
* Font id to load
*/
id: string;
/** */
url: string;
/**
* Font weight
*/
weight?: number;
/**
* Variable font flag
*/
isVariable?: boolean;
/**
* Additional classes
*/
className?: string;
/**
* Children
*/
children?: Snippet;
}
let { name, id, url, weight = 400, isVariable = false, className, children }: Props = $props();
let element: Element;
// Track if the user has actually scrolled this into view
let hasEnteredViewport = $state(false);
const status = $derived(appliedFontsManager.getFontStatus(id, weight, isVariable));
$effect(() => {
if (status === 'loaded' || status === 'error') {
hasEnteredViewport = true;
return;
}
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
hasEnteredViewport = true;
// 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,
}]);
observer.unobserve(element);
}
});
if (element) observer.observe(element);
return () => observer.disconnect();
});
// The "Show" condition: Element is in view AND (Font is ready OR it errored out)
const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded'));
const transitionClasses = $derived(
prefersReducedMotion.current
? 'transition-none' // Disable CSS transitions if motion is reduced
: 'transition-all duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]',
);
</script>
<div
bind:this={element}
style:font-family={shouldReveal ? `'${name}'` : 'system-ui, -apple-system, sans-serif'}
class={cn(
transitionClasses,
// If reduced motion is on, we skip the transform/blur entirely
!shouldReveal && !prefersReducedMotion.current
&& 'opacity-50 scale-[0.95] blur-sm',
!shouldReveal && prefersReducedMotion.current && 'opacity-0', // Still hide until font is ready, but no movement
shouldReveal && 'opacity-100 scale-100 blur-0',
className,
)}
>
{@render children?.()}
</div>