Compare commits
15 Commits
d749f86edc
...
2f45dc3620
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f45dc3620 | ||
|
|
d282448c53 | ||
|
|
f2e8de1d1d | ||
|
|
cee2a80c41 | ||
|
|
8b02333c01 | ||
|
|
0e85851cfd | ||
|
|
7dce7911c0 | ||
|
|
5e3929575d | ||
|
|
d3297d519f | ||
|
|
21d8273967 | ||
|
|
cdb2c355c0 | ||
|
|
3423eebf77 | ||
|
|
08d474289b | ||
|
|
2e6fc0e858 | ||
|
|
173816b5c0 |
@@ -67,6 +67,7 @@
|
||||
"vitest-browser-svelte": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/svelte-query": "^6.0.14"
|
||||
"@tanstack/svelte-query": "^6.0.14",
|
||||
"lenis": "^1.3.17"
|
||||
}
|
||||
}
|
||||
|
||||
13
src/app/types/ambient.d.ts
vendored
13
src/app/types/ambient.d.ts
vendored
@@ -35,3 +35,16 @@ declare module '*.jpg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly DEV: boolean;
|
||||
readonly PROD: boolean;
|
||||
readonly MODE: string;
|
||||
// Add other env variables you use
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
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
|
||||
- Reacts to font load status to show/hide content
|
||||
- Adds smooth transition when font appears
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getFontUrl } from '$entities/Font/lib';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { prefersReducedMotion } from 'svelte/motion';
|
||||
@@ -34,46 +33,23 @@ interface Props {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { font, weight = 400, className, children }: Props = $props();
|
||||
let element: Element;
|
||||
let {
|
||||
font,
|
||||
weight = 400,
|
||||
className,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
// Track if the user has actually scrolled this into view
|
||||
let hasEnteredViewport = $state(false);
|
||||
const status = $derived(appliedFontsManager.getFontStatus(font.id, weight, font.features.isVariable));
|
||||
|
||||
$effect(() => {
|
||||
if (status === 'loaded' || status === 'error') {
|
||||
hasEnteredViewport = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
hasEnteredViewport = true;
|
||||
const url = getFontUrl(font, weight);
|
||||
// Touch ensures it's in the queue.
|
||||
// It's safe to call this even if VirtualList called it
|
||||
// (Manager dedupes based on key)
|
||||
if (url) {
|
||||
appliedFontsManager.touch([{
|
||||
id: font.id,
|
||||
const status = $derived(
|
||||
appliedFontsManager.getFontStatus(
|
||||
font.id,
|
||||
weight,
|
||||
name: font.name,
|
||||
url,
|
||||
isVariable: font.features.isVariable,
|
||||
}]);
|
||||
}
|
||||
font.features.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'));
|
||||
// The "Show" condition: Font is loaded OR it errored out OR it's a noTouch preview (like in search)
|
||||
const shouldReveal = $derived(status === 'loaded' || status === 'error');
|
||||
|
||||
const transitionClasses = $derived(
|
||||
prefersReducedMotion.current
|
||||
@@ -83,12 +59,14 @@ const transitionClasses = $derived(
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
style:font-family={shouldReveal ? `'${font.name}'` : 'system-ui, -apple-system, sans-serif'}
|
||||
style:font-family={shouldReveal
|
||||
? `'${font.name}'`
|
||||
: 'system-ui, -apple-system, sans-serif'}
|
||||
class={cn(
|
||||
transitionClasses,
|
||||
// If reduced motion is on, we skip the transform/blur entirely
|
||||
!shouldReveal && !prefersReducedMotion.current
|
||||
!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',
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<!--
|
||||
Component: FontListItem
|
||||
Displays a font item and manages its animations
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { type UnifiedFont } from '../../model';
|
||||
|
||||
interface Props {
|
||||
@@ -31,51 +26,14 @@ interface Props {
|
||||
children: Snippet<[font: UnifiedFont]>;
|
||||
}
|
||||
|
||||
const { font, isFullyVisible, isPartiallyVisible, proximity, children }: Props = $props();
|
||||
|
||||
let timeoutId = $state<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Create a spring for smooth scale animation
|
||||
const scale = new Spring(1, {
|
||||
stiffness: 0.3,
|
||||
damping: 0.7,
|
||||
});
|
||||
|
||||
// Springs react to the virtualizer's computed state
|
||||
const bloom = new Spring(0, {
|
||||
stiffness: 0.15,
|
||||
damping: 0.6,
|
||||
});
|
||||
|
||||
// Sync spring to proximity for a "Lens" effect
|
||||
$effect(() => {
|
||||
bloom.target = isPartiallyVisible ? 1 : 0;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function animateSelection() {
|
||||
scale.target = 0.98;
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
scale.target = 1;
|
||||
}, 150);
|
||||
}
|
||||
const { font, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('pb-1 will-change-transform')}
|
||||
style:opacity={bloom.current}
|
||||
style:transform="
|
||||
scale({0.92 + (bloom.current * 0.08)})
|
||||
translateY({(1 - bloom.current) * 10}px)
|
||||
"
|
||||
class={cn(
|
||||
'pb-1 will-change-transform transition-transform duration-200 ease-out',
|
||||
'hover:scale-[0.98]', // Simple CSS hover effect
|
||||
)}
|
||||
>
|
||||
{@render children?.(font)}
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@ const letterSpacing = $derived(controlManager.spacing);
|
||||
class="
|
||||
w-full h-full rounded-xl sm:rounded-2xl
|
||||
flex flex-col
|
||||
backdrop-blur-md bg-background-80
|
||||
bg-background-80
|
||||
border border-border-muted
|
||||
shadow-[0_1px_3px_rgba(0,0,0,0.04)]
|
||||
relative overflow-hidden
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import Lenis from 'lenis';
|
||||
import {
|
||||
getContext,
|
||||
setContext,
|
||||
} from 'svelte';
|
||||
|
||||
const LENIS_KEY = Symbol('lenis');
|
||||
|
||||
export function createLenisContext() {
|
||||
let lenis = $state<Lenis | null>(null);
|
||||
|
||||
return {
|
||||
get lenis() {
|
||||
return lenis;
|
||||
},
|
||||
setLenis(instance: Lenis) {
|
||||
lenis = instance;
|
||||
},
|
||||
destroyLenis() {
|
||||
lenis?.destroy();
|
||||
lenis = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function setLenisContext(context: ReturnType<typeof createLenisContext>) {
|
||||
setContext(LENIS_KEY, context);
|
||||
}
|
||||
|
||||
export function getLenisContext() {
|
||||
return getContext<ReturnType<typeof createLenisContext>>(LENIS_KEY);
|
||||
}
|
||||
@@ -120,9 +120,11 @@ export function createVirtualizer<T>(
|
||||
// By wrapping the getter in $derived, we track everything inside it
|
||||
const options = $derived(optionsGetter());
|
||||
|
||||
// This derivation now tracks: count, measuredSizes, AND the data array itself
|
||||
// This derivation now tracks: count, _version (for measuredSizes updates), AND the data array itself
|
||||
const offsets = $derived.by(() => {
|
||||
const count = options.count;
|
||||
// Implicit dependency on version signal
|
||||
const v = _version;
|
||||
const result = new Float64Array(count);
|
||||
let accumulated = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -144,6 +146,8 @@ export function createVirtualizer<T>(
|
||||
// We MUST read options.data here so Svelte knows to re-run
|
||||
// this derivation when the items array is replaced!
|
||||
const { count, data } = options;
|
||||
// Implicit dependency
|
||||
const v = _version;
|
||||
if (count === 0 || containerHeight === 0 || !data) return [];
|
||||
|
||||
const overscan = options.overscan ?? 5;
|
||||
@@ -318,6 +322,9 @@ export function createVirtualizer<T>(
|
||||
|
||||
let measurementBuffer: Record<number, number> = {};
|
||||
let frameId: number | null = null;
|
||||
// Signal to trigger updates when mutating measuredSizes in place
|
||||
let _version = $state(0);
|
||||
|
||||
/**
|
||||
* Svelte action to measure individual item elements for dynamic height support.
|
||||
*
|
||||
@@ -334,18 +341,25 @@ export function createVirtualizer<T>(
|
||||
const height = entry.borderBoxSize[0]?.blockSize ?? node.offsetHeight;
|
||||
|
||||
if (!isNaN(index)) {
|
||||
// Accessing the version ensures we have the latest state if needed,
|
||||
// though here we just read the raw object.
|
||||
const oldHeight = measuredSizes[index];
|
||||
|
||||
// Only update if the height difference is significant (> 0.5px)
|
||||
// This prevents "jitter" from focus rings or sub-pixel border changes
|
||||
if (oldHeight === undefined || Math.abs(oldHeight - height) > 0.5) {
|
||||
// Stuff the measurement into a temporary buffer
|
||||
// Stuff the measurement into a temporary buffer to batch updates
|
||||
measurementBuffer[index] = height;
|
||||
|
||||
// Schedule a single update for the next animation frame
|
||||
if (frameId === null) {
|
||||
frameId = requestAnimationFrame(() => {
|
||||
measuredSizes = { ...measuredSizes, ...measurementBuffer };
|
||||
// Reset the buffer
|
||||
// Mutation in place for performance
|
||||
Object.assign(measuredSizes, measurementBuffer);
|
||||
|
||||
// Trigger reactivity
|
||||
_version += 1;
|
||||
|
||||
// Reset buffer
|
||||
measurementBuffer = {};
|
||||
frameId = null;
|
||||
});
|
||||
|
||||
@@ -42,3 +42,9 @@ export {
|
||||
type ResponsiveManager,
|
||||
responsiveManager,
|
||||
} from './createResponsiveManager/createResponsiveManager.svelte';
|
||||
|
||||
export {
|
||||
createLenisContext,
|
||||
getLenisContext,
|
||||
setLenisContext,
|
||||
} from './createScrollContext/createScrollContext.svelte';
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
createDebouncedState,
|
||||
createEntityStore,
|
||||
createFilter,
|
||||
createLenisContext,
|
||||
createPersistentStore,
|
||||
createResponsiveManager,
|
||||
createTypographyControl,
|
||||
@@ -13,11 +14,13 @@ export {
|
||||
type EntityStore,
|
||||
type Filter,
|
||||
type FilterModel,
|
||||
getLenisContext,
|
||||
type LineData,
|
||||
type PersistentStore,
|
||||
type Property,
|
||||
type ResponsiveManager,
|
||||
responsiveManager,
|
||||
setLenisContext,
|
||||
type TypographyControl,
|
||||
type VirtualItem,
|
||||
type Virtualizer,
|
||||
@@ -27,10 +30,12 @@ export {
|
||||
export {
|
||||
buildQueryString,
|
||||
clampNumber,
|
||||
debounce,
|
||||
getDecimalPlaces,
|
||||
roundToStepPrecision,
|
||||
smoothScroll,
|
||||
splitArray,
|
||||
throttle,
|
||||
} from './utils';
|
||||
|
||||
export { springySlideFade } from './transitions';
|
||||
|
||||
@@ -13,3 +13,4 @@ export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces';
|
||||
export { roundToStepPrecision } from './roundToStepPrecision/roundToStepPrecision';
|
||||
export { smoothScroll } from './smoothScroll/smoothScroll';
|
||||
export { splitArray } from './splitArray/splitArray';
|
||||
export { throttle } from './throttle/throttle';
|
||||
|
||||
32
src/shared/lib/utils/throttle/throttle.ts
Normal file
32
src/shared/lib/utils/throttle/throttle.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Throttle function execution to a maximum frequency.
|
||||
*
|
||||
* @param fn Function to throttle.
|
||||
* @param wait Maximum time between function calls.
|
||||
* @returns Throttled function.
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
wait: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let lastCall = 0;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastCall = now - lastCall;
|
||||
|
||||
if (timeSinceLastCall >= wait) {
|
||||
lastCall = now;
|
||||
fn(...args);
|
||||
} else {
|
||||
// Schedule for end of wait period
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
lastCall = Date.now();
|
||||
fn(...args);
|
||||
timeoutId = null;
|
||||
}, wait - timeSinceLastCall);
|
||||
}
|
||||
};
|
||||
}
|
||||
78
src/shared/ui/SmoothScroll/SmoothScroll.svelte
Normal file
78
src/shared/ui/SmoothScroll/SmoothScroll.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
createLenisContext,
|
||||
setLenisContext,
|
||||
} from '$shared/lib';
|
||||
import Lenis from 'lenis';
|
||||
import type { LenisOptions } from 'lenis';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
// Lenis options - all optional with sensible defaults
|
||||
duration?: number;
|
||||
easing?: (t: number) => number;
|
||||
smoothWheel?: boolean;
|
||||
wheelMultiplier?: number;
|
||||
touchMultiplier?: number;
|
||||
infinite?: boolean;
|
||||
orientation?: 'vertical' | 'horizontal';
|
||||
gestureOrientation?: 'vertical' | 'horizontal' | 'both';
|
||||
}
|
||||
|
||||
let {
|
||||
children,
|
||||
duration = 1.2,
|
||||
easing = t => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
smoothWheel = true,
|
||||
wheelMultiplier = 1,
|
||||
touchMultiplier = 2,
|
||||
infinite = false,
|
||||
orientation = 'vertical',
|
||||
gestureOrientation = 'vertical',
|
||||
}: Props = $props();
|
||||
|
||||
const lenisContext = createLenisContext();
|
||||
setLenisContext(lenisContext);
|
||||
|
||||
onMount(() => {
|
||||
const lenisOptions: LenisOptions = {
|
||||
duration,
|
||||
easing,
|
||||
smoothWheel,
|
||||
wheelMultiplier,
|
||||
touchMultiplier,
|
||||
infinite,
|
||||
orientation,
|
||||
gestureOrientation,
|
||||
// Prevent jitter with virtual scroll
|
||||
prevent: (node: HTMLElement) => {
|
||||
// Don't smooth scroll inside elements with data-lenis-prevent
|
||||
return node.hasAttribute('data-lenis-prevent');
|
||||
},
|
||||
};
|
||||
|
||||
const lenis = new Lenis(lenisOptions);
|
||||
|
||||
lenisContext.setLenis(lenis);
|
||||
|
||||
// RAF loop
|
||||
function raf(time: number) {
|
||||
lenis.raf(time);
|
||||
requestAnimationFrame(raf);
|
||||
}
|
||||
|
||||
requestAnimationFrame(raf);
|
||||
|
||||
// Expose to window for debugging (only in dev)
|
||||
if (import.meta.env?.DEV) {
|
||||
(window as any).lenis = lenis;
|
||||
}
|
||||
|
||||
return () => {
|
||||
lenisContext.destroyLenis();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -10,6 +10,7 @@
|
||||
-->
|
||||
<script lang="ts" generics="T">
|
||||
import { createVirtualizer } from '$shared/lib';
|
||||
import { throttle } from '$shared/lib/utils';
|
||||
import { ScrollArea } from '$shared/shadcn/ui/scroll-area';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -154,10 +155,20 @@ $effect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const throttledVisibleChange = throttle((visibleItems: T[]) => {
|
||||
onVisibleItemsChange?.(visibleItems);
|
||||
}, 150); // 150ms debounce
|
||||
|
||||
const throttledNearBottom = throttle((lastVisibleIndex: number) => {
|
||||
onNearBottom?.(lastVisibleIndex);
|
||||
}, 200); // 200ms debounce
|
||||
|
||||
$effect(() => {
|
||||
const visibleItems = virtualizer.items.map(item => items[item.index]);
|
||||
onVisibleItemsChange?.(visibleItems);
|
||||
throttledVisibleChange(visibleItems);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Trigger onNearBottom when user scrolls near the end of loaded items (within 5 items)
|
||||
if (virtualizer.items.length > 0 && onNearBottom) {
|
||||
const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1];
|
||||
@@ -165,7 +176,7 @@ $effect(() => {
|
||||
const itemsRemaining = items.length - lastVisibleItem.index;
|
||||
|
||||
if (itemsRemaining <= 5) {
|
||||
onNearBottom(lastVisibleItem.index);
|
||||
throttledNearBottom(lastVisibleItem.index);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -180,6 +191,7 @@ $effect(() => {
|
||||
data-index={item.index}
|
||||
class="absolute top-0 left-0 w-full will-change-transform"
|
||||
style:transform="translateY({item.start}px)"
|
||||
data-lenis-prevent
|
||||
>
|
||||
{#if item.index < items.length}
|
||||
{@render children({
|
||||
@@ -212,7 +224,6 @@ $effect(() => {
|
||||
data-index={item.index}
|
||||
class="absolute top-0 left-0 w-full will-change-transform"
|
||||
style:transform="translateY({item.start}px)"
|
||||
animate:flip={{ delay: 0, duration: 300, easing: quintOut }}
|
||||
>
|
||||
{#if item.index < items.length}
|
||||
{@render children({
|
||||
|
||||
@@ -13,4 +13,5 @@ export { default as SearchBar } from './SearchBar/SearchBar.svelte';
|
||||
export { default as Section } from './Section/Section.svelte';
|
||||
export { default as Skeleton } from './Skeleton/Skeleton.svelte';
|
||||
export { default as Slider } from './Slider/Slider.svelte';
|
||||
export { default as SmoothScroll } from './SmoothScroll/SmoothScroll.svelte';
|
||||
export { default as VirtualList } from './VirtualList/VirtualList.svelte';
|
||||
|
||||
@@ -78,8 +78,6 @@ class ComparisonStore {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fontsReady = false;
|
||||
|
||||
const weight = this.#typography.weight;
|
||||
const size = this.#typography.renderedSize;
|
||||
const fontAName = this.#fontA?.name;
|
||||
@@ -87,11 +85,25 @@ class ComparisonStore {
|
||||
|
||||
if (!fontAName || !fontBName) return;
|
||||
|
||||
const fontAString = `${weight} ${size}px "${fontAName}"`;
|
||||
const fontBString = `${weight} ${size}px "${fontBName}"`;
|
||||
|
||||
// Check if already loaded to avoid UI flash
|
||||
const isALoaded = document.fonts.check(fontAString);
|
||||
const isBLoaded = document.fonts.check(fontBString);
|
||||
|
||||
if (isALoaded && isBLoaded) {
|
||||
this.#fontsReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fontsReady = false;
|
||||
|
||||
try {
|
||||
// Step 1: Load fonts into memory
|
||||
await Promise.all([
|
||||
document.fonts.load(`${weight} ${size}px "${fontAName}"`),
|
||||
document.fonts.load(`${weight} ${size}px "${fontBName}"`),
|
||||
document.fonts.load(fontAString),
|
||||
document.fonts.load(fontBString),
|
||||
]);
|
||||
|
||||
// Step 2: Wait for browser to be ready to render
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
Renders a character with particular styling based on proximity, isPast, weight, fontAName, and fontBName.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { appliedFontsManager } from '$entities/Font';
|
||||
import { getFontUrl } from '$entities/Font/lib';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { comparisonStore } from '../../../model';
|
||||
|
||||
@@ -28,33 +26,6 @@ let { char, proximity, isPast }: Props = $props();
|
||||
const fontA = $derived(comparisonStore.fontA);
|
||||
const fontB = $derived(comparisonStore.fontB);
|
||||
const typography = $derived(comparisonStore.typography);
|
||||
|
||||
$effect(() => {
|
||||
if (!fontA || !fontB) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlA = getFontUrl(fontA, typography.weight);
|
||||
const urlB = getFontUrl(fontB, typography.weight);
|
||||
|
||||
if (!urlA || !urlB) {
|
||||
return;
|
||||
}
|
||||
|
||||
appliedFontsManager.touch([{
|
||||
id: fontA.id,
|
||||
weight: typography.weight,
|
||||
name: fontA.name,
|
||||
url: urlA,
|
||||
isVariable: fontA.features.isVariable,
|
||||
}, {
|
||||
id: fontB.id,
|
||||
weight: typography.weight,
|
||||
name: fontB.name,
|
||||
url: urlB,
|
||||
isVariable: fontB.features.isVariable,
|
||||
}]);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if fontA && fontB}
|
||||
@@ -67,13 +38,18 @@ $effect(() => {
|
||||
style:font-weight={typography.weight}
|
||||
style:font-size={`${typography.renderedSize}px`}
|
||||
style:transform="
|
||||
scale({1 + proximity * 0.3})
|
||||
translateY({-proximity * 12}px)
|
||||
rotateY({proximity * 25 * (isPast ? -1 : 1)}deg)
|
||||
scale({1 + proximity * 0.3}) translateY({-proximity * 12}px) rotateY({proximity *
|
||||
25 *
|
||||
(isPast ? -1 : 1)}deg)
|
||||
"
|
||||
style:filter="brightness({1 + proximity * 0.2}) contrast({1 + proximity * 0.1})"
|
||||
style:text-shadow={proximity > 0.5 ? '0 0 15px rgba(99,102,241,0.3)' : 'none'}
|
||||
style:will-change={proximity > 0 ? 'transform, font-family, color' : 'auto'}
|
||||
style:filter="brightness({1 + proximity * 0.2}) contrast({1 +
|
||||
proximity * 0.1})"
|
||||
style:text-shadow={proximity > 0.5
|
||||
? '0 0 15px rgba(99,102,241,0.3)'
|
||||
: 'none'}
|
||||
style:will-change={proximity > 0
|
||||
? 'transform, font-family, color'
|
||||
: 'auto'}
|
||||
>
|
||||
{char === ' ' ? '\u00A0' : char}
|
||||
</span>
|
||||
@@ -84,7 +60,7 @@ span {
|
||||
/*
|
||||
Optimize for performance and smooth transitions.
|
||||
step-end logic is effectively handled by binary font switching in JS.
|
||||
*/
|
||||
*/
|
||||
transition:
|
||||
font-family 0.15s ease-out,
|
||||
color 0.2s ease-out,
|
||||
|
||||
@@ -20,11 +20,16 @@ interface Props {
|
||||
container: HTMLElement;
|
||||
}
|
||||
|
||||
let { sliderPos, isDragging, typographyControls = $bindable<HTMLDivElement | null>(null), container }: Props = $props();
|
||||
let {
|
||||
sliderPos,
|
||||
isDragging,
|
||||
typographyControls = $bindable<HTMLDivElement | null>(null),
|
||||
container,
|
||||
}: Props = $props();
|
||||
|
||||
const fontA = $derived(comparisonStore.fontA);
|
||||
const fontB = $derived(comparisonStore.fontB);
|
||||
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
||||
|
||||
const weight = $derived(comparisonStore.typography.weight);
|
||||
|
||||
const responsive = getContext<ResponsiveManager>('responsive');
|
||||
@@ -41,8 +46,18 @@ $effect(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontAConfig = { id: fontA.id, name: fontA.name, url: fontAUrl, weight: weight };
|
||||
const fontBConfig = { id: fontB.id, name: fontB.name, url: fontBUrl, weight: weight };
|
||||
const fontAConfig = {
|
||||
id: fontA.id,
|
||||
name: fontA.name,
|
||||
url: fontAUrl,
|
||||
weight: weight,
|
||||
};
|
||||
const fontBConfig = {
|
||||
id: fontB.id,
|
||||
name: fontB.name,
|
||||
url: fontBUrl,
|
||||
weight: weight,
|
||||
};
|
||||
|
||||
appliedFontsManager.touch([fontAConfig, fontBConfig]);
|
||||
});
|
||||
@@ -73,7 +88,6 @@ $effect(() => {
|
||||
{/snippet}
|
||||
</Drawer>
|
||||
{:else}
|
||||
{#if !isLoading}
|
||||
<div class="absolute top-3 sm:top-6 left-3 sm:left-6 z-50">
|
||||
<TypographyControls
|
||||
{sliderPos}
|
||||
@@ -82,11 +96,8 @@ $effect(() => {
|
||||
containerWidth={container?.clientWidth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isLoading}
|
||||
<div class="absolute bottom-3 sm:bottom-6 md:bottom-8 inset-x-3 sm:inset-x-6 md:inset-x-12">
|
||||
<SelectComparedFonts {sliderPos} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TypographyMenu,
|
||||
controlManager,
|
||||
} from '$features/SetupFont';
|
||||
import { throttle } from '$shared/lib/utils';
|
||||
|
||||
let text = $state('The quick brown fox jumps over the lazy dog...');
|
||||
let wrapper = $state<HTMLDivElement | null>(null);
|
||||
@@ -23,7 +24,9 @@ let innerHeight = $state(0);
|
||||
// Is the component above the middle of the viewport?
|
||||
let isAboveMiddle = $state(false);
|
||||
|
||||
const isLoading = $derived(unifiedFontStore.isFetching || unifiedFontStore.isLoading);
|
||||
const isLoading = $derived(
|
||||
unifiedFontStore.isFetching || unifiedFontStore.isLoading,
|
||||
);
|
||||
|
||||
/**
|
||||
* Load more fonts by moving to the next page
|
||||
@@ -62,14 +65,14 @@ const displayRange = $derived.by(() => {
|
||||
return `Showing ${loadedCount} of ${total} fonts`;
|
||||
});
|
||||
|
||||
function checkPosition() {
|
||||
const checkPosition = throttle(() => {
|
||||
if (!wrapper) return;
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const viewportMiddle = innerHeight / 2;
|
||||
|
||||
isAboveMiddle = rect.top < viewportMiddle;
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
@@ -95,7 +98,12 @@ function checkPosition() {
|
||||
proximity,
|
||||
index,
|
||||
})}
|
||||
<FontListItem {font} {isFullyVisible} {isPartiallyVisible} {proximity}>
|
||||
<FontListItem
|
||||
{font}
|
||||
{isFullyVisible}
|
||||
{isPartiallyVisible}
|
||||
{proximity}
|
||||
>
|
||||
<FontSampler {font} bind:text {index} />
|
||||
</FontListItem>
|
||||
{/snippet}
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -2459,6 +2459,7 @@ __metadata:
|
||||
dprint: "npm:^0.50.2"
|
||||
jsdom: "npm:^27.4.0"
|
||||
lefthook: "npm:^2.0.13"
|
||||
lenis: "npm:^1.3.17"
|
||||
oxlint: "npm:^1.35.0"
|
||||
playwright: "npm:^1.57.0"
|
||||
storybook: "npm:^10.1.11"
|
||||
@@ -2849,6 +2850,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lenis@npm:^1.3.17":
|
||||
version: 1.3.17
|
||||
resolution: "lenis@npm:1.3.17"
|
||||
peerDependencies:
|
||||
"@nuxt/kit": ">=3.0.0"
|
||||
react: ">=17.0.0"
|
||||
vue: ">=3.0.0"
|
||||
peerDependenciesMeta:
|
||||
"@nuxt/kit":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
checksum: 10c0/c268da36d5711677b239c7d173bc52775276df08f86f7f89f305c4e02ba4055d8c50ea69125d16c94bb1e1999ccd95f654237d11c6647dc5fdf63aa90515fbfb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lightningcss-android-arm64@npm:1.30.2":
|
||||
version: 1.30.2
|
||||
resolution: "lightningcss-android-arm64@npm:1.30.2"
|
||||
|
||||
Reference in New Issue
Block a user